From de1893133e822dd961ab542364b710440f77a459 Mon Sep 17 00:00:00 2001 From: "Federico M. Facca" Date: Mon, 16 Oct 2023 22:30:08 +0200 Subject: [PATCH] support single file generation for enum and schema Signed-off-by: Federico M. Facca --- modernpython/Base.py | 17 +++- modernpython/enum_header.py | 2 + modernpython/langPack.py | 81 ++++++++++++------- modernpython/schema_header.py | 67 +++++++++++++++ .../templates/cimpy_class_template.mustache | 18 +---- .../templates/pydantic_enum_template.mustache | 3 +- 6 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 modernpython/enum_header.py create mode 100644 modernpython/schema_header.py diff --git a/modernpython/Base.py b/modernpython/Base.py index 70a5417d..b6581b84 100644 --- a/modernpython/Base.py +++ b/modernpython/Base.py @@ -79,11 +79,26 @@ class DataclassConfig: # pylint: disable=too-few-public-methods # By default with pydantic extra arguments given to a dataclass are silently ignored. # This matches the default behaviour by failing noisily. - extra = "ignore" + extra = "forbid" populate_by_name = True defer_build = True from_attributes = True +class GeoDataclassConfig: # pylint: disable=too-few-public-methods + """ + Used to configure pydantic dataclasses. + + See doc at + https://docs.pydantic.dev/latest/usage/model_config/#options + """ + + # By default with pydantic extra arguments given to a dataclass are silently ignored. + # This matches the default behaviour by failing noisily. + extra = "ignore" + populate_by_name = True + defer_build = True + from_attributes = True + arbitrary_types_allowed=True # Default namespaces used by CGMES. NAMESPACES = { diff --git a/modernpython/enum_header.py b/modernpython/enum_header.py new file mode 100644 index 00000000..c860071f --- /dev/null +++ b/modernpython/enum_header.py @@ -0,0 +1,2 @@ +from enum import Enum, IntEnum + diff --git a/modernpython/langPack.py b/modernpython/langPack.py index 01a97aa1..2236e147 100644 --- a/modernpython/langPack.py +++ b/modernpython/langPack.py @@ -33,6 +33,8 @@ def location(version): template_files = [{"filename": "cimpy_class_template.mustache", "ext": ".py"}] enum_template_files = [{"filename": "pydantic_enum_template.mustache", "ext": ".py"}] +required_profiles = ["EQ", "GL"] #temporary + def get_class_location(class_name, class_map, version): # Check if the current class has a parent class if class_map[class_name].superClass(): @@ -53,7 +55,7 @@ def _compute_data_type(attribute): if "range" in attribute: # return "'"+attribute["range"].split("#")[1]+"'" - return "'"+attribute["range"].split("#")[1]+"'" + return attribute["range"].split("#")[1] if "dataType" in attribute and "class_name" in attribute: # for whatever weird reason String is not created as class from CIMgen if is_primitive_class(attribute["class_name"]) or attribute["class_name"] == "String": @@ -84,7 +86,7 @@ def _compute_data_type(attribute): if is_cim_data_type_class(attribute["class_name"]): return "float" # this is for example the case for 'StreetAddress.streetDetail' - return "'"+attribute["dataType"].split("#")[1]+"'" + return attribute["dataType"].split("#")[1] def _ends_with_s(attribute_name): return attribute_name.endswith("s") @@ -252,6 +254,12 @@ def has_unit_attribute(attributes): return True return False +def is_required_profile(class_origin): + for origin in class_origin: + if origin["origin"] in required_profiles: + return True + return False + def run_template(version_path, class_details): if ( # Primitives are never used in the in memory representation but only for @@ -260,6 +268,7 @@ def run_template(version_path, class_details): # Datatypes based on primitives are never used in the in memory # representation but only for the schema or class_details["is_a_cim_data_type"] == True + or class_details["class_name"] == 'PositionPoint' ): return elif class_details["has_instances"] == True: @@ -269,47 +278,59 @@ def run_template(version_path, class_details): def run_template_enum(version_path, class_details, templates): for template_info in templates: - class_file = os.path.join(version_path, class_details["class_name"] + template_info["ext"]) + class_file = os.path.join(version_path, "enum" + template_info["ext"]) if not os.path.exists(class_file): with open(class_file, "w", encoding="utf-8") as file: - template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"]) - class_details["setInstances"] = _set_instances - with open(template_path, encoding="utf-8") as f: - args = { - "data": class_details, - "template": f, - "partials_dict": partials, - } - output = chevron.render(**args) - file.write(output) + header_file_path = os.path.join( + os.getcwd(), "modernpython", "enum_header.py" + ) + header_file = open(header_file_path, "r") + file.write(header_file.read()) + with open(class_file, "a", encoding="utf-8") as file: + template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"]) + class_details["setInstances"] = _set_instances + with open(template_path, encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) def run_template_schema(version_path, class_details, templates): for template_info in templates: - class_file = os.path.join(version_path, class_details["class_name"] + template_info["ext"]) + class_file = os.path.join(version_path, "schema" + template_info["ext"]) if not os.path.exists(class_file): with open(class_file, "w", encoding="utf-8") as file: - template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"]) - class_details["setDefault"] = _set_default - class_details["setType"] = _set_type - class_details["setImports"] = _set_imports - class_details["setValidator"] = _set_validator - class_details["setNormalizedName"] = _set_normalized_name - with open(template_path, encoding="utf-8") as f: - args = { - "data": class_details, - "template": f, - "partials_dict": partials, - } - output = chevron.render(**args) - file.write(output) + schema_file_path = os.path.join( + os.getcwd(), "modernpython", "schema_header.py" + ) + schema_file = open(schema_file_path, "r") + file.write(schema_file.read()) + with open(class_file, "a", encoding="utf-8") as file: + template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"]) + class_details["setDefault"] = _set_default + class_details["setType"] = _set_type + class_details["setImports"] = _set_imports + class_details["setValidator"] = _set_validator + class_details["setNormalizedName"] = _set_normalized_name + with open(template_path, encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) def _create_init(path): init_file = path + "/__init__.py" with open(init_file, "w", encoding="utf-8") as init: - init.write("# pylint: disable=too-many-lines,missing-module-docstring\n") - + #init.write("# pylint: disable=too-many-lines,missing-module-docstring\n") + pass # creates the Base class file, all classes inherit from this class def _create_base(path): diff --git a/modernpython/schema_header.py b/modernpython/schema_header.py new file mode 100644 index 00000000..5ba4122b --- /dev/null +++ b/modernpython/schema_header.py @@ -0,0 +1,67 @@ +from __future__ import annotations +import uuid +from functools import cached_property +from pydantic import ConfigDict, Field, field_validator, computed_field +from geoalchemy2.shape import to_shape +from geoalchemy2.elements import WKBElement +from shapely.geometry import Point +from datetime import date, datetime, time +from typing import Optional, Iterator, List +from pydantic.dataclasses import dataclass +from .Base import DataclassConfig, GeoDataclassConfig, Profile, Base +from .util import cyclic_references_validator +from .enum import * + +@dataclass(config=GeoDataclassConfig) +class PositionPoint(Base): + """ + Set of spatial coordinates that determine a point, defined in the coordinate system specified in 'Location.CoordinateSystem'. Use a single position point instance to desribe a point-oriented location. Use a sequence of position points to describe a line-oriented object (physical location of non-point oriented objects like cables or lines), or area of an object (like a substation or a geographical zone - in this case, have first and last position point with the same values). + + :Location: Location described by this position point. + :sequenceNumber: Zero-relative sequence number of this point within a series of points. + :xPosition: X axis position. + :yPosition: Y axis position. + :zPosition: (if applicable) Z axis position. + """ + + location: "Location" = Field(alias="Location", in_profiles = [Profile.GL, ]) + sequenceNumber: Optional[int] = Field(default=None, in_profiles = [Profile.GL, ]) + point: Point = Field( + repr=False, in_profiles = [Profile.GL, ] + ) # we introduce this field compared to CIM definition because we want to store a proper geometry "point" in the database + + @computed_field + @property + def xPosition(self) -> str: + return str(self.point.x) + + @computed_field + @property + def yPosition(self) -> str: + return str(self.point.y) + + @computed_field + @property + def zPosition(self) -> str: + return str(self.point.z) + + @cached_property + def possible_profiles(self)->set[Profile]: + """ + A resource can be used by multiple profiles. This is the set of profiles + where this element can be found. + """ + return { Profile.GL, } + + # Pydantic needs help to map GeoAlchemy classes to Shapely + @field_validator("point", mode="before") + def validate_point_format(cls, v): + if isinstance(v, Point): + return v + elif isinstance(v, WKBElement): + point = to_shape(v) + if point.geom_type != "Point": + raise ValueError("must be a Point") + return Point(point) + else: + raise ValueError("must be a Point or a WKBElement") diff --git a/modernpython/templates/cimpy_class_template.mustache b/modernpython/templates/cimpy_class_template.mustache index d121e6fd..b84aaffb 100644 --- a/modernpython/templates/cimpy_class_template.mustache +++ b/modernpython/templates/cimpy_class_template.mustache @@ -1,18 +1,8 @@ + """ Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen """ -from __future__ import annotations -from functools import cached_property -from typing import Optional, List -from pydantic import Field, field_validator -import uuid -from datetime import date, datetime, time -from pydantic.dataclasses import dataclass, rebuild_dataclass -from .Base import DataclassConfig, Profile -from .util import cyclic_references_validator -from .{{sub_class_of}} import {{sub_class_of}} - @dataclass(config=DataclassConfig) class {{class_name}}({{sub_class_of}}): """ @@ -37,8 +27,6 @@ class {{class_name}}({{sub_class_of}}): {{^attributes}} # No attributes defined for this class. {{/attributes}} - - @cached_property def possible_profiles(self)->set[Profile]: """ @@ -46,7 +34,3 @@ class {{class_name}}({{sub_class_of}}): where this element can be found. """ return { {{#class_origin}}Profile.{{origin}}, {{/class_origin}} } - -{{#setImports}}{{attributes}}{{/setImports}} - -rebuild_dataclass({{class_name}}) diff --git a/modernpython/templates/pydantic_enum_template.mustache b/modernpython/templates/pydantic_enum_template.mustache index 52770cd6..1ad7d9f6 100644 --- a/modernpython/templates/pydantic_enum_template.mustache +++ b/modernpython/templates/pydantic_enum_template.mustache @@ -1,9 +1,8 @@ + """ Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen """ -from enum import Enum - class {{class_name}}(str,Enum): '''