Skip to content

Commit 6da45fe

Browse files
authored
Hexagonal architecture for schema validators (JSON schema and pydantic) (#179)
* introduce base class and move serializer registry one level top * complete implementation
1 parent ec2c343 commit 6da45fe

33 files changed

Lines changed: 378 additions & 167 deletions

.github/workflows/ci-pipeline.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,19 @@ jobs:
8181
source .venv/bin/activate
8282
ruff check --config ruff.toml hololinked/client
8383
ruff check --config ruff.toml hololinked/serializers
84+
ruff check --config ruff.toml hololinked/schema_validators
85+
ruff check --config ruff.toml hololinked/serialization.py
86+
ruff check --config ruff.toml hololinked/schemas.py
8487
8588
- name: run ty type checker
8689
if: matrix.tool == 'ty'
8790
run: |
8891
source .venv/bin/activate
8992
ty check hololinked/client
9093
ty check hololinked/serializers
94+
ty check hololinked/schema_validators
95+
ty check hololinked/serialization.py
96+
ty check hololinked/schemas.py
9197
9298
scan:
9399
name: security scan (${{ matrix.tool }})

hololinked/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@
33
__version__ = "0.4.0"
44

55
from .config import global_config # noqa
6-
import hololinked.core # noqa: F401
6+
from .serialization import Serializers as Serializers
7+
from .schemas import JSONSchema as JSONSchema, SchemaValidatorClasses as SchemaValidatorClasses
8+
9+
import hololinked.core # noqa: F401 # this one is lazy for most part
710
import hololinked.serializers # noqa: F401
11+
import hololinked.schema_validators # noqa: F401

hololinked/client/factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from paho.mqtt.client import CallbackAPIVersion, MQTTMessage, MQTTProtocolVersion
1414
from paho.mqtt.client import Client as PahoMQTTClient
1515

16+
from hololinked import Serializers
1617
from hololinked.client.abstractions import (
1718
ConsumedThingAction,
1819
ConsumedThingEvent,
@@ -25,7 +26,7 @@
2526
OAuthDirectAccessGrant,
2627
)
2728
from hololinked.constants import ZMQ_TRANSPORTS
28-
from hololinked.core import Serializers, Thing
29+
from hololinked.core import Thing
2930
from hololinked.td.interaction_affordance import (
3031
ActionAffordance,
3132
EventAffordance,

hololinked/client/http/consumed_interactions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import httpx
1313
import structlog
1414

15+
from hololinked import Serializers
1516
from hololinked.client.abstractions import (
1617
SSE,
1718
ConsumedThingAction,
@@ -20,7 +21,6 @@
2021
)
2122
from hololinked.client.exceptions import raise_local_exception
2223
from hololinked.constants import Operations
23-
from hololinked.core import Serializers
2424
from hololinked.td.forms import Form
2525
from hololinked.td.interaction_affordance import (
2626
ActionAffordance,

hololinked/client/mqtt/consumed_interactions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from paho.mqtt.client import Client as PahoMQTTClient
1010
from paho.mqtt.client import MQTTMessage
1111

12+
from hololinked import Serializers
1213
from hololinked.client.abstractions import SSE, ConsumedThingEvent
13-
from hololinked.core import Serializers
1414
from hololinked.core.interfaces import BaseSerializer # noqa: F401
1515
from hololinked.td.forms import Form
1616
from hololinked.td.interaction_affordance import EventAffordance, PropertyAffordance

hololinked/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# types
99
JSONSerializable = typing.Union[str, int, float, bool, None, typing.Dict[str, typing.Any], typing.List]
1010
JSON = typing.Dict[str, JSONSerializable]
11+
JSONSchema = typing.Dict[str, str | typing.Dict[str, typing.Any] | typing.List[typing.Dict[str, typing.Any]]]
1112

1213
byte_types = (bytes, bytearray, memoryview)
1314

hololinked/core/__init__.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,57 @@
44
State machines, meta classes, descriptor registries and the concrete implementation of how an operation is executed
55
is also included here.
66
"""
7-
# Order of import is reflected in this file to avoid circular imports
8-
9-
from .thing import * # noqa
10-
from .events import * # noqa
11-
from .actions import * # noqa
12-
from .property import * # noqa
13-
from .state_machine import StateMachine as StateMachine
14-
from .meta import ThingMeta as ThingMeta
15-
from .serializer_registry import Serializers as Serializers
7+
8+
from typing import TYPE_CHECKING
9+
10+
# Interfaces must be available to register adappters
11+
from .interfaces import BaseSchemaValidator as BaseSchemaValidator
12+
from .interfaces import BaseSerializer as BaseSerializer
13+
14+
15+
__all__ = [
16+
"BaseSchemaValidator",
17+
"BaseSerializer",
18+
"action",
19+
"Action",
20+
"Event",
21+
"ThingMeta",
22+
"Property",
23+
"StateMachine",
24+
"Thing",
25+
]
26+
27+
# Submodules that use SchemaValidatorClasses / Serializers are loaded lazily so
28+
# that hololinked/__init__.py can finish registering the adapters (schema_validators,
29+
# serializers) before any class body in meta.py / actions.py / property.py executes.
30+
_lazy: dict[str, tuple[str, str]] = {
31+
"action": (".actions", "action"),
32+
"Action": (".actions", "Action"),
33+
"Event": (".events", "Event"),
34+
"ThingMeta": (".meta", "ThingMeta"),
35+
"Property": (".property", "Property"),
36+
"StateMachine": (".state_machine", "StateMachine"),
37+
"Thing": (".thing", "Thing"),
38+
}
39+
40+
41+
def __getattr__(name: str):
42+
if name in _lazy:
43+
import importlib
44+
45+
module_path, attr = _lazy[name]
46+
mod = importlib.import_module(module_path, package=__name__)
47+
val = getattr(mod, attr)
48+
globals()[name] = val # cache so subsequent access skips __getattr__
49+
return val
50+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
51+
52+
53+
if TYPE_CHECKING:
54+
from .actions import Action as Action
55+
from .actions import action as action
56+
from .events import Event as Event
57+
from .meta import ThingMeta as ThingMeta
58+
from .property import Property as Property
59+
from .state_machine import StateMachine as StateMachine
60+
from .thing import Thing as Thing

hololinked/core/actions.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,34 @@
99

1010
from pydantic import BaseModel, RootModel
1111

12-
from ..constants import JSON
13-
from ..param.parameterized import ParameterizedFunction
14-
from ..schema_validators.validators import JSONSchemaValidator, PydanticSchemaValidator
15-
from ..utils import (
12+
from hololinked import SchemaValidatorClasses
13+
from hololinked.constants import JSON
14+
from hololinked.core.exceptions import StateMachineError
15+
from hololinked.param.parameterized import ParameterizedFunction
16+
from hololinked.utils import (
1617
get_input_model_from_signature,
1718
get_return_type_from_signature,
1819
has_async_def,
1920
isclassmethod,
2021
issubklass,
2122
)
23+
2224
from .dataklasses import ActionInfoValidator
23-
from .exceptions import StateMachineError
2425

2526

2627
class Action:
2728
"""
2829
Object that models an action.
30+
2931
These actions are unbound and return a bound action when accessed using the owning object.
3032
"""
3133

3234
__slots__ = ["obj", "owner", "_execution_info"]
3335

3436
def __init__(self, obj: MethodType) -> None:
3537
"""
38+
Initialize an Action.
39+
3640
Parameters
3741
----------
3842
obj: MethodType
@@ -69,13 +73,13 @@ def __call__(self, *args, **kwargs):
6973

7074
@property
7175
def name(self) -> str:
72-
"""name of the action"""
76+
"""Name of the action."""
7377
return self.obj.__name__
7478

7579
@property
7680
def execution_info(self) -> ActionInfoValidator:
7781
"""
78-
internal dataclass that holds all information about the action
82+
Internal dataclass that holds all information about the action.
7983
8084
TODO: this can be refactored
8185
"""
@@ -107,9 +111,7 @@ def to_affordance(self, owner_inst=None):
107111

108112

109113
class BoundAction:
110-
"""
111-
A bound action - base class for both sync and async methods.
112-
"""
114+
"""A bound action, base class for both sync and async methods."""
113115

114116
__slots__ = [
115117
"obj",
@@ -173,14 +175,14 @@ def validate_call(self, args, kwargs: dict[str, Any]) -> None:
173175

174176
@property
175177
def name(self) -> str:
176-
"""name of the action"""
178+
"""Name of the action."""
177179
return self.obj.__name__
178180

179181
def __call__(self, *args, **kwargs):
180182
raise NotImplementedError("call must be implemented by subclass")
181183

182184
def external_call(self, *args, **kwargs):
183-
"""validated call to the action with state machine and payload checks"""
185+
"""Validated call to the action with state machine and payload checks."""
184186
raise NotImplementedError("external_call must be implemented by subclass")
185187

186188
def __str__(self):
@@ -224,7 +226,7 @@ class BoundSyncAction(BoundAction):
224226
"""
225227

226228
def external_call(self, *args, **kwargs):
227-
"""validated call to the action with state machine and payload checks"""
229+
"""Validated call to the action with state machine and payload checks"""
228230
self.validate_call(args, kwargs)
229231
return self.__call__(*args, **kwargs)
230232

@@ -241,7 +243,7 @@ class BoundAsyncAction(BoundAction):
241243
"""
242244

243245
async def external_call(self, *args, **kwargs):
244-
"""validated call to the action with state machine and payload checks"""
246+
"""Validated call to the action with state machine and payload checks"""
245247
self.validate_call(args, kwargs)
246248
return await self.__call__(*args, **kwargs)
247249

@@ -358,9 +360,9 @@ def inner(obj):
358360
)
359361
if input_schema:
360362
if isinstance(input_schema, dict):
361-
execution_info_validator.schema_validator = JSONSchemaValidator(input_schema)
363+
execution_info_validator.schema_validator = SchemaValidatorClasses.json_schema(input_schema)
362364
elif issubklass(input_schema, (BaseModel, RootModel)):
363-
execution_info_validator.schema_validator = PydanticSchemaValidator(input_schema)
365+
execution_info_validator.schema_validator = SchemaValidatorClasses.pydantic(input_schema)
364366
else:
365367
raise TypeError(
366368
"input schema must be a JSON schema or a Pydantic model, got {}".format(type(input_schema))

hololinked/core/dataklasses.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99

1010
from pydantic import BaseModel, RootModel
1111

12-
from ..constants import USE_OBJECT_NAME
13-
from ..param.parameterized import ParameterizedMetaclass
14-
from ..param.parameters import Boolean, ClassSelector, Parameter, String, Tuple
15-
from ..schema_validators import BaseSchemaValidator
16-
from ..utils import issubklass
12+
from hololinked.constants import USE_OBJECT_NAME
13+
from hololinked.core.interfaces import BaseSchemaValidator
14+
from hololinked.param.parameterized import ParameterizedMetaclass
15+
from hololinked.param.parameters import Boolean, ClassSelector, Parameter, String, Tuple
16+
from hololinked.utils import issubklass
1717

1818

1919
# TODO, this class will be removed in future and merged directly into the corresponding object

hololinked/core/exceptions.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,31 @@
1-
class BreakInnerLoop(Exception):
2-
"""raise to break an inner loop"""
1+
"""Exception classes."""
2+
33

4-
pass
4+
class BreakInnerLoop(Exception):
5+
"""Raise to break an inner loop."""
56

67

78
class BreakAllLoops(Exception):
8-
"""raise to exit all loops"""
9-
10-
pass
9+
"""Raise to exit all loops."""
1110

1211

1312
class BreakLoop(Exception):
14-
"""raise and catch to exit a loop from within another function or method"""
15-
16-
pass
13+
"""Raise and catch to exit a loop from within another function or method."""
1714

1815

1916
class BreakFlow(Exception):
20-
"""raised to break the flow of the program"""
21-
22-
pass
17+
"""Raise to break the flow of the program."""
2318

2419

2520
# TODO - remove unused and reduce number of definitions
2621

2722

2823
class StateMachineError(Exception):
29-
"""raise to show errors while calling actions or writing properties in wrong state"""
30-
31-
pass
24+
"""Raise to show errors while calling actions or writing properties in wrong state."""
3225

3326

3427
class DatabaseError(Exception):
35-
"""raise to show database related errors"""
28+
"""Raise to show database related errors."""
3629

3730

3831
__all__ = ["BreakInnerLoop", "BreakAllLoops", "BreakLoop", "BreakFlow", "StateMachineError", "DatabaseError"]

0 commit comments

Comments
 (0)