-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Generation of graphql schema from transformation rules * added tests * added tests * added repair of names * bump version * Update CHANGELOG.md Co-authored-by: Anders Albert <[email protected]> * Update cognite/neat/core/extractors/rules_to_graphql.py Co-authored-by: Anders Albert <[email protected]> * Fix name of fixture * Update cognite/neat/core/extractors/rules_to_graphql.py Co-authored-by: Anders Albert <[email protected]> * Update cognite/neat/core/extractors/rules_to_graphql.py Co-authored-by: Anders Albert <[email protected]> * Update cognite/neat/core/extractors/rules_to_graphql.py Co-authored-by: Anders Albert <[email protected]> * moved test to docstring * switch from dataframe to dict/pydantic, added data model validation * moving things around * simpler logic * Update cognite/neat/core/extractors/rules_to_graphql.py Co-authored-by: Anders Albert <[email protected]> * blacked --------- Co-authored-by: Anders Albert <[email protected]>
- Loading branch information
Showing
12 changed files
with
282 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
__version__ = "0.11.1" | ||
__version__ = "0.11.2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import logging | ||
import re | ||
import warnings | ||
|
||
from graphql import GraphQLError, GraphQLField, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema | ||
from graphql import assert_name as assert_graphql_name | ||
from graphql import print_schema | ||
|
||
from cognite.neat.core.data_classes.transformation_rules import DATA_TYPE_MAPPING, Property, TransformationRules | ||
|
||
|
||
def get_invalid_names(entity_names: set) -> set: | ||
"""Returns a set of invalid entity names""" | ||
invalid_names = set() | ||
for entity_name in entity_names: | ||
try: | ||
assert_graphql_name(entity_name) | ||
except GraphQLError: | ||
invalid_names.add(entity_name) | ||
return invalid_names | ||
|
||
|
||
def repair_name(name: str, entity_type: str, fix_casing: bool = False) -> str: | ||
""" | ||
Repairs an entity name to conform to GraphQL naming convention | ||
>>> repair_name("wind-speed", "property") | ||
'windspeed' | ||
>>> repair_name("Wind.Speed", "property", True) | ||
'windSpeed' | ||
>>> repair_name("windSpeed", "class", True) | ||
'WindSpeed' | ||
>>> repair_name("22windSpeed", "class") | ||
'_22windSpeed' | ||
""" | ||
|
||
# Remove any non GraphQL compliant characters | ||
repaired_string = re.sub(r"[^_a-zA-Z0-9/_]", "", name) | ||
|
||
# Name must start with a letter or underscore | ||
if repaired_string[0].isdigit(): | ||
repaired_string = f"_{repaired_string}" | ||
|
||
if not fix_casing: | ||
return repaired_string | ||
# Property names must be camelCase | ||
if entity_type == "property" and repaired_string[0].isupper(): | ||
return repaired_string[0].lower() + repaired_string[1:] | ||
# Class names must be PascalCase | ||
elif entity_type == "class" and repaired_string[0].islower(): | ||
return repaired_string[0].upper() + repaired_string[1:] | ||
else: | ||
return repaired_string | ||
|
||
|
||
def _remove_query_type(schema_string: str) -> str: | ||
"""Removes unnecessary Query types to conform to Cognite's GraphQL API""" | ||
lines = schema_string.split("\n") | ||
|
||
for _i, line in enumerate(lines): | ||
if "}" in line: | ||
break | ||
|
||
return "\n".join(lines[_i + 2 :]) | ||
|
||
|
||
def _get_graphql_schema_string(schema: GraphQLSchema) -> str: | ||
return _remove_query_type(print_schema(schema)) | ||
|
||
|
||
def rules2graphql_schema( | ||
transformation_rules: TransformationRules, | ||
stop_on_exception: bool = False, | ||
fix_casing: bool = False, | ||
) -> str: | ||
"""Generates a GraphQL schema from an instance of TransformationRules | ||
Parameters | ||
---------- | ||
transformation_rules : TransformationRules | ||
TransformationRules object | ||
stop_on_exception : bool, optional | ||
Stop on any exception, by default False | ||
fix_casing : bool, optional | ||
Whether to attempt to fix casing of entity names, by default False | ||
Returns | ||
------- | ||
str | ||
GraphQL schema string | ||
""" | ||
invalid_names: set = get_invalid_names(transformation_rules.get_entity_names()) | ||
data_model_issues: set = transformation_rules.check_data_model_definitions() | ||
|
||
if invalid_names and stop_on_exception: | ||
msg = "Entity names must only contain [_a-zA-Z0-9] characters and can start only with [_a-zA-Z]" | ||
logging.error(f"{msg}, following entities {invalid_names} do not follow these rules!") | ||
raise GraphQLError(f"{msg}, following entities {invalid_names} do not follow these rules!") | ||
elif invalid_names and not stop_on_exception: | ||
msg = "Entity names must only contain [_a-zA-Z0-9] characters and can start only with [_a-zA-Z]" | ||
logging.warn( | ||
f"{msg}, following entities {invalid_names} do not follow these rules! Attempting to repair names..." | ||
) | ||
warnings.warn( | ||
f"{msg}, following entities {invalid_names} do not follow these rules! Attempting to repair names...", | ||
stacklevel=2, | ||
) | ||
|
||
if data_model_issues and stop_on_exception: | ||
msg = " ".join(data_model_issues) | ||
logging.error(msg) | ||
raise ValueError(msg) | ||
elif data_model_issues and not stop_on_exception: | ||
msg = " ".join(data_model_issues) | ||
msg += " Redefinitions will be skipped!" | ||
logging.warn(msg) | ||
warnings.warn( | ||
msg, | ||
stacklevel=2, | ||
) | ||
|
||
def _define_fields(property_definitions: list[Property]) -> dict[str, GraphQLField]: | ||
gql_field_definitions = {} | ||
for property_ in property_definitions: | ||
property_name = repair_name(property_.property_name, "property", fix_casing=fix_casing) # type: ignore | ||
|
||
if property_name in gql_field_definitions: | ||
logging.warn(f"Property {property_name} being redefined... skipping!") | ||
warnings.warn(f"Property {property_name} being redefined... skipping!", stacklevel=2) | ||
continue | ||
|
||
# Node attribute | ||
if property_.property_type == "DatatypeProperty": | ||
value_type_gql = DATA_TYPE_MAPPING[property_.expected_value_type]["GraphQL"] | ||
|
||
# Case: Mandatory, single value | ||
if property_.min_count and property_.max_count == 1: | ||
value = GraphQLNonNull(value_type_gql) | ||
# Case: Mandatory, multiple value | ||
elif property_.min_count and property_.max_count != 1: | ||
value = GraphQLNonNull(GraphQLList(GraphQLNonNull(value_type_gql))) | ||
# Case: Optional, single value | ||
elif property_.max_count == 1: | ||
value = value_type_gql | ||
# Case: Optional, multiple value | ||
else: | ||
value = GraphQLList(value_type_gql) | ||
|
||
# Node edge | ||
else: | ||
value = gql_type_definitions[repair_name(property_.expected_value_type, "class", fix_casing=fix_casing)] | ||
is_one_to_many_edge = not (property_.min_count and property_.max_count == 1) | ||
if is_one_to_many_edge: | ||
value = GraphQLList(value) | ||
gql_field_definitions[property_name] = GraphQLField(value) | ||
|
||
return gql_field_definitions | ||
|
||
gql_type_definitions: dict = {} | ||
for class_, properties in transformation_rules.get_classes_with_properties().items(): | ||
gql_type_definitions[repair_name(class_, "class", fix_casing=fix_casing)] = GraphQLObjectType( | ||
repair_name(class_, "class", fix_casing=fix_casing), | ||
lambda properties=properties: _define_fields(properties), | ||
) | ||
|
||
# Needs this so we are able to generate the schema string | ||
query_schema = GraphQLSchema( | ||
query=GraphQLObjectType( | ||
"Query", lambda: {type_name: GraphQLField(type_def) for type_name, type_def in gql_type_definitions.items()} | ||
) | ||
) | ||
return _get_graphql_schema_string(query_schema) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[tool.poetry] | ||
name = "cognite-neat" | ||
version = "0.11.1" | ||
version = "0.11.2" | ||
readme = "README.md" | ||
description = "Knowledge graph transformation" | ||
authors = ["Nikola Vasiljevic <[email protected]>", | ||
|
@@ -54,6 +54,7 @@ fastapi = "^0.95" | |
schedule = "^1" | ||
python-multipart = "^0.0.6" | ||
oxrdflib = {version = "^0.3.3", extras = ["oxigraph"]} | ||
graphql-core = "^3.2.3" | ||
|
||
[tool.poetry.dev-dependencies] | ||
twine = "*" | ||
|
Oops, something went wrong.