Série: ocpp
python
442 linhas
· Atualizado 2025-10-10
code_generator.py
ocpp/homework/ocpp_many/scripts/v21/code_generator.py
import re
from typing import List, Union
from json_schema_parser import (
Any,
Array,
Boolean,
Enum,
Integer,
Number,
Object,
String,
)
# Tab's are 4 spaces.
TAB = " "
def generate_calls(calls: List[Object]) -> str:
"""Generate the dataclasses that represent all OCPP 2.1 Calls."""
return _generate_messages(calls)
def generate_call_results(call_results: List[Object]) -> str:
"""Generate the dataclasses that represent all OCPP 2.1 CallResults."""
return _generate_messages(call_results)
def generate_enums(messages: List[Object]) -> str:
"""Generate all Enum that `messages` rely on directly or indirectly."""
dependencies = _all_dependencies(messages)
enums = []
calls = sorted(messages, key=lambda x: x.name)
seen_calls = []
for dependency in dependencies:
if not isinstance(dependency, Enum):
continue
if dependency in enums:
continue
enums.append(dependency)
# Order enums alphabetically
enums = sorted(enums, key=lambda x: x.name)
code = """try:
# breaking change introduced in python 3.11
from enum import StrEnum
except ImportError: # pragma: no cover
from enum import Enum # pragma: no cover
class StrEnum(str, Enum): # pragma: no cover
pass # pragma: no cover"""
if calls:
code += f'\n\n\nclass Action(StrEnum):\n{
TAB}"""An Action is a required part of a Call message."""\n'
for call in calls:
if call.name not in seen_calls:
code += f'\n{TAB}{_camel_to_snake_case(call.name)} = "{
call.name}"'
seen_calls.append(call.name)
for enum in enums:
lines = _generate_code_for_enum(enum)
code += "\n\n\n" + lines
code += "\n"
return code
def generate_datatypes(messages: List[Object]) -> str:
"""Generate all custom types that OCPP 2.1 Calls and CallResults rely on."""
objects = []
dependencies = _all_dependencies(messages)
for dependency in dependencies:
if not isinstance(dependency, Object):
continue
if dependency in objects:
continue
objects.append(dependency)
objects = sorted(objects, key=lambda x: x.name)
code = ""
code += "from __future__ import annotations\n"
code += "from typing import List, Optional\n"
code += "from dataclasses import dataclass\n"
imports = _generate_import_statement_for_external_enums(objects)
if imports != "":
code += "\n"
code += imports
for object in objects:
code += "\n\n" + _generate_code_for_object(object)
return code
def _generate_messages(messages: List[Object]) -> str:
objects = sorted(messages, key=lambda x: x.name)
code = ""
code += "from dataclasses import dataclass\n"
code += "from typing import Any, List, Optional\n"
imports = _generate_import_statement_for_external_datatypes(objects)
if imports != "":
code += "\n"
code += imports
imports = _generate_import_statement_for_external_enums(objects)
if imports != "":
code += "\n"
code += imports
for object in objects:
code += "\n\n" + _generate_code_for_object(object)
return code
def _all_dependencies(
containers: List[Union[Object, Array]],
) -> List[Union[Object, Enum, Array]]:
"""Return a list of `Object`s`, `Enum`s and `Array`s `containers` rely on."""
visited_dependencies = []
while True:
containers = _direct_dependencies(containers)
visited_dependencies += containers
if len(containers) == 0:
break
return visited_dependencies
def _direct_dependencies(
containers: List[Union[Object, Array]],
) -> List[Union[Object, Enum, Array]]:
"""Return a list with all direct dependencies of `containers`.
This function iterates over every `container`s properties. When a property is of type `Object` or `Enum`, these types
are included in the return value. `Array`s holding `Object`s or `Enum`s are included too.
Different `Object`s might rely on the same dependency. Any duplicate dependencies are removed.
`Object`s relying on each other aren't in the return value either.
"""
dependencies = []
for container in containers:
if isinstance(container, Array):
if (
isinstance(container.type, Object)
or isinstance(container.type, Enum)
or isinstance(container.type, Array)
):
dependencies.append(container.type)
continue
if isinstance(container, Enum):
continue
for property in container.properties.values():
if (
isinstance(property.type, Object)
or isinstance(property.type, Enum)
or isinstance(property.type, Array)
):
dependencies.append(property.type)
return dependencies
def _generate_code_for_objects(objects: List[Object]) -> str:
# Order enums alphabetically
objects = sorted(objects, key=lambda x: x.name)
code = ""
for object in objects:
code += "\n\n\n" + _generate_code_for_object(object)
return code
def _generate_code_for_object(object: Object) -> str:
lines = [
"@dataclass",
f"class {object.name}:",
]
if len(object.properties) == 0:
lines.append(f"{TAB}pass")
return "\n".join(lines)
property_names = sorted(
object.properties.keys(), key=lambda x: _camel_to_snake_case(x)
)
skipped_properties = []
for name in property_names:
property = object.properties[name]
if not property.required or property.has_default_value():
skipped_properties.append(name)
continue
lines.append(
f"{TAB}{_camel_to_snake_case(name)}: {
_get_python_type(property.type)}"
)
for name in skipped_properties:
property = object.properties[name]
if isinstance(property.type, Any):
lines.append(
f"{TAB}{_camel_to_snake_case(name)}: {
_get_python_type(property.type)} = None"
)
continue
if not property.has_default_value():
lines.append(
f"{TAB}{_camel_to_snake_case(name)}: Optional[{
_get_python_type(property.type)}] = None"
)
continue
default = property.default
if isinstance(property.type, Enum):
default = f"{property.type.name}.{
_camel_to_snake_case(property.default)}"
if isinstance(property.type, String):
default = f'"{property.default}"'
lines.append(
f"{TAB}{_camel_to_snake_case(name)}: {
_get_python_type(property.type)} = {default}"
)
return "\n".join(lines) + "\n"
def _get_python_type(value) -> str:
"""Return a Python type hint as a `str` for given object."""
if isinstance(value, Any):
return "Any"
if isinstance(value, String):
return "str"
if isinstance(value, Integer):
return "int"
if isinstance(value, Number):
return "float"
if isinstance(value, Array):
value.type
return f"List[{_get_python_type(value.type)}]"
if isinstance(value, Object):
return value.name
if isinstance(value, Enum):
if _has_potential_naming_collision_with_a_message(value.name):
return f"enums.{value.name}"
return value.name
if isinstance(value, Boolean):
return "bool"
else:
raise RuntimeError(value)
def _generate_code_for_enum(value: Enum) -> str:
lines = [
f"class {value.name}(StrEnum):",
]
for variant in value.variants:
lines.append(f'{TAB}{_camel_to_snake_case(variant)} = "{variant}"')
return "\n".join(lines)
def _generate_code_for_action(object: Object) -> str:
lines = [
f"{TAB}{_camel_to_snake_case(object.name)} = {object.name}",
]
return "\n".join(lines)
def _generate_import_statement_for_external_enums(objects: List[Object]) -> str:
"""Generate an import statement for all `Enum`s that the `objects` rely on."""
enums = _external_enums_required(objects)
enums = sorted(enums, key=lambda x: x.name)
code = ""
if len(enums) == 0:
return code
for enum in enums:
if _has_potential_naming_collision_with_a_message(enum.name):
code += "from ocpp.v21 import enums\n"
break
enums = [
enum
for enum in enums
if not _has_potential_naming_collision_with_a_message(enum.name)
]
if len(enums) == 0:
return code
code += "from ocpp.v21.enums import (\n"
for enum in enums:
code += f"{TAB}{enum.name},\n"
code += ")\n"
return code
def _generate_import_statement_for_external_datatypes(objects: List[Object]) -> str:
"""Generate an import statement for all datatypes that the `objects` rely on."""
objects = _external_datatypes_required(objects)
objects = sorted(objects, key=lambda x: x.name)
if len(objects) == 0:
return ""
code = "from ocpp.v21.datatypes import (\n"
for object in objects:
if object.name == "data" and len(object.properties) == 0:
continue
code += f"{TAB}{object.name},\n"
code += ")\n"
return code
def _external_enums_required(objects: List[Object]) -> List[Enum]:
"""Return a list of all `Enum`s that `objects` depend on directly.
The list is sorted alphabetically and without duplicates. These enums are
not in scope of the `objects` and must be imported."""
enums = []
for dependency in _direct_dependencies(objects):
if isinstance(dependency, Array):
dependency = dependency.type
if not isinstance(dependency, Enum):
continue
if dependency in enums:
continue
enums.append(dependency)
return enums
def _external_datatypes_required(objects: List[Object]) -> List[Object]:
"""Return a list of all `Object`s that `objects` depend on directly."""
filtered_objects = []
for dependency in _direct_dependencies(objects):
if isinstance(dependency, Array):
dependency = dependency.type
if not isinstance(dependency, Object):
continue
if dependency in objects:
continue
if dependency in filtered_objects:
continue
filtered_objects.append(dependency)
return filtered_objects
def _has_potential_naming_collision_with_a_message(name: str) -> bool:
"""A few request and responses rely on enums that collide with message names.
For example, the Reset request relies on the Reset enum. Without handling these naming collisions, the code for the Reset request looks like this:
from ocpp.v21.enums import (
Reset,
)
class Reset(StrEnum):
type: Reset
This is wrong. Instead, the code should look like this:
from ocpp.v21 import enums
class Reset(StrEnum):
type: enums.Reset
It's hard to find these exceptional cases dynamically. So I opted to hard code all
enums that share a name with a call or call result.
"""
return name in [
"GetCertificateStatus",
"IdToken",
"Reset",
"TransactionEvent",
"VPN",
]
def _camel_to_snake_case(value: str) -> str:
# Make sure "_" is inserted between "iso" and "15118".
value = value.replace("iso15118", "iso_15118")
# Prevent "V2X" from becoming "v2_x".
value = value.replace("V2X", "V2x")
# Prevent "V2G" from becoming "v2_g".
value = value.replace("V2G", "V2g")
# Prevent "prioritizedEMAIDs" from becoming "prioritized_emaid_s".
value = value.replace("EMAID", "Emaid")
# quick for measurands like "CUrrent.Import"
value = value.replace(".", "")
# This fix prevents "V2X" from becomming "v2_x".
value = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", value)
value = re.sub("([a-z0-9])([A-Z])(?=\\S)", r"\1_\2", value).lower()
# fix for s309-1_p-16a = "s309-1P-16A"
return value.replace("-", "_")
Artigos relacionados
ocpp
python
Atualizado 2026-02-03
central_system.py
central_system.py — python source code from the ocpp learning materials (ocpp/homework/central_system.py).
Ler artigo →
ocpp
python
Atualizado 2026-02-03
charge_point.py
charge_point.py — python source code from the ocpp learning materials (ocpp/homework/ocpp/charge_point.py).
Ler artigo →
ocpp
python
Atualizado 2026-02-03
exceptions.py
exceptions.py — python source code from the ocpp learning materials (ocpp/homework/ocpp/exceptions.py).
Ler artigo →
ocpp
python
Atualizado 2026-02-03
messages.py
messages.py — python source code from the ocpp learning materials (ocpp/homework/ocpp/messages.py).
Ler artigo →
ocpp
python
Atualizado 2026-02-03
routing.py
routing.py — python source code from the ocpp learning materials (ocpp/homework/ocpp/routing.py).
Ler artigo →
ocpp
python
Atualizado 2026-02-03
__init__.py
__init__.py — python source code from the ocpp learning materials (ocpp/homework/ocpp/v16/__init__.py).
Ler artigo →