Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pydantic code generation #5

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f4029b8
base pydantic code generator
chicco785 Jul 28, 2023
a89ad7a
fix __init__.py import
chicco785 Jul 29, 2023
d92eadc
Automated Black fmt fixes
Jul 29, 2023
b3d7f6c
docs(release_notes): update RELEASE_NOTES.md
chicco785 Jul 29, 2023
2d2faeb
support cyclic reference serialization
chicco785 Jul 31, 2023
b2779cc
add pydantic to tests
chicco785 Jul 31, 2023
03a514d
Automated Black fmt fixes
Jul 31, 2023
267849c
docs(release_notes): update RELEASE_NOTES.md
chicco785 Jul 31, 2023
de05e72
fix import issue
chicco785 Jul 31, 2023
d6ac0fb
improve datatype support
chicco785 Jul 31, 2023
ab3a612
fix multiplicity support in validator generation
chicco785 Jul 31, 2023
bfcef79
add schema and pydantic to docker build
chicco785 Jul 31, 2023
65aa211
Automated Black fmt fixes
Jul 31, 2023
d7021fd
fix Enum for CgmesProfile
chicco785 Jul 31, 2023
169f3bf
generates only EQ profile (tested in CIM-Manager)
chicco785 Aug 2, 2023
860ab89
fix rendering for usage of &quote; in CMGES3.0
chicco785 Aug 2, 2023
91f5338
fix copy file path
chicco785 Aug 2, 2023
d9cdc71
docs(release_notes): update RELEASE_NOTES.md
chicco785 Aug 2, 2023
35d00f0
Automated Black fmt fixes
Aug 2, 2023
87eacd0
add support for GL profile
chicco785 Aug 2, 2023
bf33749
fix model for PositionPoint
chicco785 Aug 2, 2023
02d5e61
fix model for PositionPoint
chicco785 Aug 2, 2023
2ab84c6
Update langPack.py
chicco785 Aug 2, 2023
c733ad6
Automated Black fmt fixes
Aug 2, 2023
d927b76
improve multiplicity handling
chicco785 Aug 2, 2023
8ca17e0
fix representation configuration for point
chicco785 Aug 2, 2023
4e18810
Automated Black fmt fixes
Aug 2, 2023
74e6030
use string as mrid while default value is uuid (serialized as a string)
chicco785 Aug 3, 2023
f19cd4a
revert change
chicco785 Aug 3, 2023
3c00f69
Automated Black fmt fixes
Aug 3, 2023
4916422
docs(release_notes): update RELEASE_NOTES.md
chicco785 Aug 3, 2023
335848a
support defaults
chicco785 Aug 3, 2023
230d157
Automated Black fmt fixes
Aug 3, 2023
e7f77c9
docs(release_notes): update RELEASE_NOTES.md
chicco785 Sep 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Automated Black fmt fixes
Signed-off-by: Bot <[email protected]>
Bot authored and chicco785 committed Sep 13, 2023

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
commit d92eadce7d7ed9ca529244068a2e20b673d6ca1e
5 changes: 3 additions & 2 deletions pydantic/Base.py
Original file line number Diff line number Diff line change
@@ -13,16 +13,17 @@ class CgmesProfileEnum(IntEnum):
TP_DB = 7
ED_BD = 8


class Base(BaseModel):
"""
Base Class for CIM
"""

class Config:
@staticmethod
def schema_extra(schema: dict, _):
props = {}
for k, v in schema.get('properties', {}).items():
for k, v in schema.get("properties", {}).items():
if not v.get("hidden", False):
props[k] = v
schema["properties"] = props
52 changes: 32 additions & 20 deletions pydantic/PositionPoint.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
from __future__ import annotations
from .Base import Base, CgmesProfileEnum

from pydantic import (
ConfigDict,
computed_field,
field_validator,
Field
)
from pydantic import ConfigDict, computed_field, field_validator, Field

from geoalchemy2.shape import to_shape
from geoalchemy2.elements import WKBElement
@@ -16,24 +11,41 @@
from geoalchemy2.shape import to_shape
from .Location import Location


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 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.
"""

possibleProfileList: dict = Field(default={'class': [CgmesProfileEnum.GL, ],
'Location': [CgmesProfileEnum.GL, ],
'sequenceNumber': [CgmesProfileEnum.GL, ],
'xPosition': [CgmesProfileEnum.GL, ],
'yPosition': [CgmesProfileEnum.GL, ],
'zPosition': [CgmesProfileEnum.GL, ],
}, hidden=True)
possibleProfileList: dict = Field(
default={
"class": [
CgmesProfileEnum.GL,
],
"Location": [
CgmesProfileEnum.GL,
],
"sequenceNumber": [
CgmesProfileEnum.GL,
],
"xPosition": [
CgmesProfileEnum.GL,
],
"yPosition": [
CgmesProfileEnum.GL,
],
"zPosition": [
CgmesProfileEnum.GL,
],
},
hidden=True,
)

Location: List[Location]
sequenceNumber: Optional[int]
158 changes: 95 additions & 63 deletions pydantic/langPack.py
Original file line number Diff line number Diff line change
@@ -4,8 +4,10 @@
import glob
import shutil
import sys

logger = logging.getLogger(__name__)


# This makes sure we have somewhere to write the classes, and
# creates a couple of files the python implementation needs.
# cgmes_profile_info details which uri belongs in each profile.
@@ -17,39 +19,45 @@ def setup(version_path, cgmes_profile_info):
_create_init(version_path)
_copy_files(version_path)


def location(version):
return "pydantic." + version + ".Base";
return "pydantic." + version + ".Base"


base = {"base_class": "Base", "class_location": location}

base = {
"base_class": "Base",
"class_location": location
}
template_files = [{"filename": "pydantic_class_template.mustache", "ext": ".py"}]
enum_template_files = [{"filename": "pydantic_enum_template.mustache", "ext": ".py"}]

template_files=[ { "filename": "pydantic_class_template.mustache", "ext": ".py" } ]
enum_template_files = [ { "filename": "pydantic_enum_template.mustache", "ext": ".py" } ]

def get_class_location(class_name, class_map, version):
# Check if the current class has a parent class
if class_map[class_name].superClass():
if class_map[class_name].superClass() in class_map:
return 'pydantic.' + version + "." + class_map[class_name].superClass()
elif class_map[class_name].superClass() == 'Base' or class_map[class_name].superClass() == None:
return "pydantic." + version + "." + class_map[class_name].superClass()
elif (
class_map[class_name].superClass() == "Base"
or class_map[class_name].superClass() == None
):
return location(version)
else:
return location(version)


partials = {}


# called by chevron, text contains the label {{dataType}}, which is evaluated by the renderer (see class template)
def _set_instances(text, render):
instance = eval(render(text))
if "label" in instance:
value = instance['label'] + ' = "' + instance['label'] + '"'
if 'comment' in instance:
value += ' #' + instance['comment']
value = instance["label"] + ' = "' + instance["label"] + '"'
if "comment" in instance:
value += " #" + instance["comment"]
return value
else:
return ''
return ""


# called by chevron, text contains the label {{dataType}}, which is evaluated by the renderer (see class template)
def _set_imports(text, render):
@@ -58,51 +66,58 @@ def _set_imports(text, render):
try:
res = eval(rendered)
finally:
result = ''
classes = set ()
result = ""
classes = set()
if res:
for val in res:
if "range" in val:
classes.add(val['range'].split('#')[1])
classes.add(val["range"].split("#")[1])
for val in classes:
result += "from ."+val+" import "+val+"\n"
result += "from ." + val + " import " + val + "\n"
return result


# called by chevron, text contains the label {{dataType}}, which is evaluated by the renderer (see class template)
def _set_default(text, render):
attribute = eval(render(text))

if "range" in attribute and "isFixed" in attribute:
return ' = ' + attribute['range'].split('#')[1] + '.' + attribute['isFixed']
if "multiplicity" in attribute and attribute['multiplicity'] in ['M:0..n'] and "range" in attribute:
return ' = None'
return " = " + attribute["range"].split("#")[1] + "." + attribute["isFixed"]
if (
"multiplicity" in attribute
and attribute["multiplicity"] in ["M:0..n"]
and "range" in attribute
):
return " = None"
if "range" not in attribute and "isFixed" in attribute:
return ' = "' + attribute['isFixed'] + '"'
return ''
return ' = "' + attribute["isFixed"] + '"'
return ""


def _compute_data_type(attribute):
if 'label' in attribute and attribute['label'] == 'mRID':
return 'uuid.UUID'
if "label" in attribute and attribute["label"] == "mRID":
return "uuid.UUID"

if "dataType" in attribute:
if attribute['dataType'].startswith('#'):
datatype = attribute['dataType'].split('#')[1]
if (datatype == 'Integer' or datatype == 'integer'):
return 'int'
if (datatype == 'Boolean'):
return 'bool'
if (datatype == 'String'):
return 'str'
if (datatype == 'DateTime'):
return 'str'
if (datatype == 'Date'):
return 'str'
if (datatype == 'String'):
return 'str'
if attribute["dataType"].startswith("#"):
datatype = attribute["dataType"].split("#")[1]
if datatype == "Integer" or datatype == "integer":
return "int"
if datatype == "Boolean":
return "bool"
if datatype == "String":
return "str"
if datatype == "DateTime":
return "str"
if datatype == "Date":
return "str"
if datatype == "String":
return "str"
else:
return "float"
if "range" in attribute:
return attribute['range'].split('#')[1]
return attribute["range"].split("#")[1]


# called by chevron, text contains the label {{dataType}}, which is evaluated by the renderer (see class template)
def _set_data_type(text, render):
@@ -111,71 +126,88 @@ def _set_data_type(text, render):
datatype = _compute_data_type(attribute)

if "multiplicity" in attribute:
multiplicity = attribute['multiplicity']
if multiplicity in ['M:1..1', '']:
multiplicity = attribute["multiplicity"]
if multiplicity in ["M:1..1", ""]:
return datatype
if multiplicity in ['M:0..1']:
return 'Optional['+datatype+']'
elif multiplicity in ['M:0..n'] or 'M:0..' in multiplicity:
return 'Optional[List['+datatype+']]'
elif multiplicity in ['M:1', 'M:1..n'] or 'M:1..' in multiplicity:
return 'List['+datatype+']'
if multiplicity in ["M:0..1"]:
return "Optional[" + datatype + "]"
elif multiplicity in ["M:0..n"] or "M:0.." in multiplicity:
return "Optional[List[" + datatype + "]]"
elif multiplicity in ["M:1", "M:1..n"] or "M:1.." in multiplicity:
return "List[" + datatype + "]"
else:
return 'List['+datatype+']'
return "List[" + datatype + "]"
else:
return datatype


def set_enum_classes(new_enum_classes):
return


def set_float_classes(new_float_classes):
return


def run_template(version_path, class_details):
if class_details['class_name'] in ['Float','Integer','String','Boolean','Date']:
if class_details["class_name"] in ["Float", "Integer", "String", "Boolean", "Date"]:
templates = []
elif class_details['has_instances'] == True:
elif class_details["has_instances"] == True:
templates = enum_template_files
else:
templates = template_files


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, class_details["class_name"] + template_info["ext"]
)
if not os.path.exists(class_file):
with open(class_file, 'w') as file:
template_path = os.path.join(os.getcwd(), 'pydantic/templates', template_info["filename"])
class_details['setDefault'] = _set_default
class_details['setDataType'] = _set_data_type
class_details['setImports'] = _set_imports
class_details['setInstances'] = _set_instances
with open(class_file, "w") as file:
template_path = os.path.join(
os.getcwd(), "pydantic/templates", template_info["filename"]
)
class_details["setDefault"] = _set_default
class_details["setDataType"] = _set_data_type
class_details["setImports"] = _set_imports
class_details["setInstances"] = _set_instances
with open(template_path) as f:
args = {
'data': class_details,
'template': f,
'partials_dict': partials
"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'):
with open(init_file, "w"):
pass


# creates the Base class file, all classes inherit from this class
def _copy_files(path):
shutil.copy("pydantic/Base.py", path + "/Base.py")
shutil.copy("pydantic/PositionPoint.py", path + "/PositionPoint.py")
shutil.copy("pydantic/util.py", path + "/util.py")


def resolve_headers(path):
filenames = glob.glob(path + "/*.py")
include_names = []
for filename in filenames:
include_names.append(os.path.splitext(os.path.basename(filename))[0])
with open(path + "/__init__.py", "w") as header_file:
for include_name in include_names:
header_file.write("from " + "." + include_name + " import " + include_name + " as " + include_name + "\n")
header_file.write(
"from "
+ "."
+ include_name
+ " import "
+ include_name
+ " as "
+ include_name
+ "\n"
)
header_file.close()
1 change: 1 addition & 0 deletions pydantic/util.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
)
from typing import Iterator


def is_recursion_validation_error(exc: ValidationError) -> bool:
errors = exc.errors()
return len(errors) == 1 and errors[0]["type"] == "recursion_loop"