Skip to content

Commit 49ff7f6

Browse files
authored
hexagonal architecture td/metadata (#181)
* update docstrings for ty and ruff * update imports and do unit tests * fix typing and bugs * ruff & ty * ruff and ty again * update doc * change top level folder * introduce top level file for injection of metadata generation formats * import annotations * fix tests * ruff, ty and change signature of to_affordance to to_metadata
1 parent d421711 commit 49ff7f6

43 files changed

Lines changed: 1502 additions & 590 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-pipeline.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,11 @@ jobs:
8383
ruff check --config ruff.toml hololinked/serializers
8484
ruff check --config ruff.toml hololinked/schema_validators
8585
ruff check --config ruff.toml hololinked/storage
86+
ruff check --config ruff.toml hololinked/metadata/td
8687
ruff check --config ruff.toml hololinked/serialization.py
8788
ruff check --config ruff.toml hololinked/schemas.py
8889
ruff check --config ruff.toml hololinked/persistence.py
90+
ruff check --config ruff.toml hololinked/ddl.py
8991
9092
- name: run ty type checker
9193
if: matrix.tool == 'ty'
@@ -95,9 +97,11 @@ jobs:
9597
ty check hololinked/serializers
9698
ty check hololinked/schema_validators
9799
ty check hololinked/storage
100+
ty check hololinked/metadata/td
98101
ty check hololinked/serialization.py
99102
ty check hololinked/schemas.py
100103
ty check hololinked/persistence.py
104+
ty check hololinked/ddl.py
101105
102106
scan:
103107
name: security scan (${{ matrix.tool }})

hololinked/client/abstractions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
import structlog
3737

3838
from hololinked.constants import Operations
39-
from hololinked.td import ActionAffordance, EventAffordance, PropertyAffordance
40-
from hololinked.td.forms import Form
39+
from hololinked.metadata.td import ActionAffordance, EventAffordance, PropertyAffordance
40+
from hololinked.metadata.td.forms import Form
4141
from hololinked.utils import get_current_async_loop
4242

4343

hololinked/client/factory.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
)
2828
from hololinked.constants import ZMQ_TRANSPORTS
2929
from hololinked.core import Thing
30-
from hololinked.td.interaction_affordance import (
30+
from hololinked.metadata.td.interaction_affordance import (
3131
ActionAffordance,
3232
EventAffordance,
3333
PropertyAffordance,
@@ -164,7 +164,7 @@ def zmq(
164164

165165
# Fetch the TD
166166
Thing.get_thing_model # noqa: B018 # type: Action
167-
FetchTDAffordance = Thing.get_thing_model.to_affordance()
167+
FetchTDAffordance = Thing.get_thing_model.to_metadata()
168168
FetchTDAffordance.override_defaults(name="get_thing_description", thing_id=thing_id)
169169
FetchTD = ZMQAction(
170170
resource=FetchTDAffordance,
@@ -193,7 +193,7 @@ def zmq(
193193

194194
# add properties
195195
for name in TD.get("properties", []):
196-
affordance = cast(PropertyAffordance, PropertyAffordance.from_TD(name, TD))
196+
affordance = PropertyAffordance.from_TD(name, TD)
197197
consumed_property = ZMQProperty(
198198
resource=affordance,
199199
sync_client=sync_zmq_client,
@@ -213,7 +213,7 @@ def zmq(
213213
ClientFactory.add_event(object_proxy, consumed_observable)
214214
# add actions
215215
for action in TD.get("actions", []):
216-
affordance = cast(ActionAffordance, ActionAffordance.from_TD(action, TD))
216+
affordance = ActionAffordance.from_TD(action, TD)
217217
consumed_action = ZMQAction(
218218
resource=affordance,
219219
sync_client=sync_zmq_client,
@@ -226,7 +226,7 @@ def zmq(
226226
ClientFactory.add_action(object_proxy, consumed_action)
227227
# add events
228228
for event in TD.get("events", []):
229-
affordance = cast(EventAffordance, EventAffordance.from_TD(event, TD))
229+
affordance = EventAffordance.from_TD(event, TD)
230230
consumed_event = ZMQEvent(
231231
resource=affordance,
232232
owner_inst=object_proxy,
@@ -395,7 +395,7 @@ def http(url: str, **kwargs) -> ObjectProxy:
395395
object_proxy = ObjectProxy(id, td=TD, logger=logger, security=security, **kwargs)
396396

397397
for name in TD.get("properties", []):
398-
affordance = cast(PropertyAffordance, PropertyAffordance.from_TD(name, TD))
398+
affordance = PropertyAffordance.from_TD(name, TD)
399399
consumed_property = HTTPProperty(
400400
resource=affordance,
401401
sync_client=req_rep_sync_client,
@@ -418,7 +418,7 @@ def http(url: str, **kwargs) -> ObjectProxy:
418418
)
419419
ClientFactory.add_event(object_proxy, consumed_event)
420420
for action in TD.get("actions", []):
421-
affordance = cast(ActionAffordance, ActionAffordance.from_TD(action, TD))
421+
affordance = ActionAffordance.from_TD(action, TD)
422422
consumed_action = HTTPAction(
423423
resource=affordance,
424424
sync_client=req_rep_sync_client,
@@ -430,7 +430,7 @@ def http(url: str, **kwargs) -> ObjectProxy:
430430
)
431431
ClientFactory.add_action(object_proxy, consumed_action)
432432
for event in TD.get("events", []):
433-
affordance = cast(EventAffordance, EventAffordance.from_TD(event, TD))
433+
affordance = EventAffordance.from_TD(event, TD)
434434
consumed_event = HTTPEvent(
435435
resource=affordance,
436436
sync_client=sse_sync_client,

hololinked/client/http/consumed_interactions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
)
2222
from hololinked.client.exceptions import raise_local_exception
2323
from hololinked.constants import Operations
24-
from hololinked.td.forms import Form
25-
from hololinked.td.interaction_affordance import (
24+
from hololinked.metadata.td.forms import Form
25+
from hololinked.metadata.td.interaction_affordance import (
2626
ActionAffordance,
2727
EventAffordance,
2828
PropertyAffordance,

hololinked/client/mqtt/consumed_interactions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
from hololinked import Serializers
1313
from hololinked.client.abstractions import SSE, ConsumedThingEvent
1414
from hololinked.core.interfaces import BaseSerializer # noqa: F401
15-
from hololinked.td.forms import Form
16-
from hololinked.td.interaction_affordance import EventAffordance, PropertyAffordance
15+
from hololinked.metadata.td.forms import Form
16+
from hololinked.metadata.td.interaction_affordance import (
17+
EventAffordance,
18+
PropertyAffordance,
19+
)
1720

1821

1922
class MQTTConsumer(ConsumedThingEvent): # noqa: D101

hololinked/client/zmq/consumed_interactions.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,26 @@
1717
ConsumedThingEvent,
1818
ConsumedThingProperty,
1919
)
20-
from hololinked.client.exceptions import raise_local_exception
21-
22-
from ...constants import Operations
23-
from ...core import Action, Thing # noqa: F401
24-
from ...core.zmq.brokers import (
20+
from hololinked.client.exceptions import ReplyNotArrivedError, raise_local_exception
21+
from hololinked.constants import Operations
22+
from hololinked.core import Action, Thing # noqa: F401
23+
from hololinked.core.zmq.brokers import (
2524
AsyncEventConsumer,
2625
AsyncZMQClient,
2726
BreakLoop,
2827
EventConsumer,
2928
SyncZMQClient,
3029
)
31-
from ...core.zmq.message import (
30+
from hololinked.core.zmq.message import (
3231
EMPTY_BYTE,
3332
ERROR,
3433
INVALID_MESSAGE,
3534
TIMEOUT,
3635
ResponseMessage,
3736
)
38-
from ...core.zmq.payloads import SerializableData
39-
from ...td import ActionAffordance, EventAffordance, PropertyAffordance
40-
from ...td.forms import Form
41-
from ..exceptions import ReplyNotArrivedError
37+
from hololinked.core.zmq.payloads import SerializableData
38+
from hololinked.metadata.td import ActionAffordance, EventAffordance, PropertyAffordance
39+
from hololinked.metadata.td.forms import Form
4240

4341

4442
__error_message_types__ = [TIMEOUT, ERROR, INVALID_MESSAGE]
@@ -604,7 +602,7 @@ def __init__(
604602
timeout for execution of action or property read/write
605603
"""
606604
action = Thing._set_properties # type: Action
607-
resource = action.to_affordance(Thing)
605+
resource = action.to_metadata(Thing)
608606
resource._thing_id = owner_inst.thing_id
609607
super().__init__(
610608
resource=resource,
@@ -645,7 +643,7 @@ def __init__(
645643
timeout for execution of action or property read/write
646644
"""
647645
action = Thing._get_properties # type: Action
648-
resource = action.to_affordance(Thing)
646+
resource = action.to_metadata(Thing)
649647
resource._thing_id = owner_inst.thing_id
650648
super().__init__(
651649
resource=resource,

hololinked/core/actions.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
"""Concrete definition of an Action. Implemention of async and sync versions, action decorator."""
2+
3+
from __future__ import annotations
4+
15
import warnings
26

37
from enum import Enum
48
from inspect import getfullargspec, iscoroutinefunction
59
from types import FunctionType, MethodType
6-
from typing import Any
10+
from typing import TYPE_CHECKING, Any
711

812
import jsonschema
913

@@ -12,6 +16,7 @@
1216
from hololinked import SchemaValidatorClasses
1317
from hololinked.constants import JSON
1418
from hololinked.core.exceptions import StateMachineError
19+
from hololinked.core.interfaces.metadata import ActionMetadata
1520
from hololinked.param.parameterized import ParameterizedFunction
1621
from hololinked.utils import (
1722
get_input_model_from_signature,
@@ -24,6 +29,11 @@
2429
from .dataklasses import ActionInfoValidator
2530

2631

32+
if TYPE_CHECKING:
33+
from hololinked.core.interfaces import ActionMetadata
34+
from hololinked.core.thing import Thing
35+
36+
2737
class Action:
2838
"""
2939
Object that models an action.
@@ -91,7 +101,7 @@ def execution_info(self, value: ActionInfoValidator) -> None:
91101
raise TypeError("execution_info must be of type ActionInfoValidator")
92102
self._execution_info = value # type: ActionInfoValidator
93103

94-
def to_affordance(self, owner_inst=None):
104+
def to_metadata(self, owner_inst: Thing | None = None, format: str = "wot") -> ActionMetadata:
95105
"""
96106
Generates a `ActionAffordance` TD fragment for this Action.
97107
@@ -105,9 +115,12 @@ def to_affordance(self, owner_inst=None):
105115
ActionAffordance
106116
the affordance TD fragment for this action
107117
"""
108-
from ..td import ActionAffordance
118+
from hololinked.ddl import MetadataFormats
109119

110-
return ActionAffordance.generate(self, owner_inst or self.owner)
120+
return MetadataFormats.get(format).action.from_descriptor(
121+
self,
122+
owner_inst or self.owner,
123+
)
111124

112125

113126
class BoundAction:
@@ -132,7 +145,7 @@ def __init__(self, obj: FunctionType, descriptor: Action, owner_inst, owner) ->
132145

133146
def __post_init__(self):
134147
# never called, neither possible to call, only type hinting
135-
from .thing import Thing, ThingMeta
148+
from .thing import ThingMeta
136149

137150
# owner class and instance
138151
self.owner: ThingMeta
@@ -145,6 +158,7 @@ def __post_init__(self):
145158
def validate_call(self, args, kwargs: dict[str, Any]) -> None:
146159
"""
147160
Validate the call to the action, like payload, state machine state etc.
161+
148162
Errors are raised as exceptions.
149163
150164
Parameters
@@ -153,6 +167,13 @@ def validate_call(self, args, kwargs: dict[str, Any]) -> None:
153167
positional arguments to the action
154168
kwargs: dict
155169
keyword arguments to the action
170+
171+
Raises
172+
------
173+
StateMachineError
174+
if the action cannot be executed in the current state of the owning thing
175+
RuntimeError
176+
if the action explicity accepts only keyword arguments but some positional arguments are given
156177
"""
157178
if self.execution_info.isparameterized and len(args) > 0:
158179
raise RuntimeError("parameterized functions cannot have positional arguments")
@@ -202,7 +223,7 @@ def __getattribute__(self, name):
202223
return self.obj.__doc__
203224
return super().__getattribute__(name)
204225

205-
def to_affordance(self):
226+
def to_metadata(self, owner_inst: Thing | None = None, format: str = "wot") -> ActionMetadata:
206227
"""
207228
Generates a `ActionAffordance` TD fragment for this Action.
208229
@@ -216,17 +237,19 @@ def to_affordance(self):
216237
ActionAffordance
217238
the affordance TD fragment for this action
218239
"""
219-
return Action.to_affordance(self.descriptor, self.owner_inst or self.owner)
240+
return Action.to_metadata(self.descriptor, owner_inst or self.owner_inst or self.owner, format=format)
220241

221242

222243
class BoundSyncAction(BoundAction):
223244
"""
224-
non async(io) action call. The call is passed to the method as-it-is to allow local
245+
Non-async(io) action call.
246+
247+
The call is passed to the method as-it-is to allow local
225248
invocation without state machine checks. Use `external_call` to have validation.
226249
"""
227250

228251
def external_call(self, *args, **kwargs):
229-
"""Validated call to the action with state machine and payload checks"""
252+
"""Validated call to the action with state machine and payload checks."""
230253
self.validate_call(args, kwargs)
231254
return self.__call__(*args, **kwargs)
232255

@@ -238,12 +261,14 @@ def __call__(self, *args, **kwargs):
238261

239262
class BoundAsyncAction(BoundAction):
240263
"""
241-
async(io) action call. The call is passed to the method as-it-is to allow local
264+
async(io) action call.
265+
266+
The call is passed to the method as-it-is to allow local
242267
invocation without state machine checks. Use `external_call` to have validation.
243268
"""
244269

245270
async def external_call(self, *args, **kwargs):
246-
"""Validated call to the action with state machine and payload checks"""
271+
"""Validated call to the action with state machine and payload checks."""
247272
self.validate_call(args, kwargs)
248273
return await self.__call__(*args, **kwargs)
249274

@@ -263,8 +288,9 @@ def action(
263288
**kwargs,
264289
) -> Action:
265290
"""
266-
Decorate on your methods to make them accessible remotely or create 'actions' out of them. When used with hardware,
267-
actions generally command the hardware to do something.
291+
Decorate on your methods to make them accessible remotely or create 'actions' out of them.
292+
293+
When used with hardware, actions generally command the hardware to do something.
268294
269295
Parameters
270296
----------
@@ -298,6 +324,13 @@ def action(
298324
Action
299325
returns the callable object wrapped in an `Action` object. When accessed at instance level,
300326
a `BoundSyncAction` or `BoundAsyncAction` object is returned.
327+
328+
Raises
329+
------
330+
TypeError
331+
if the decorated object is not a function or method, or if the input/output schema is of invalid type
332+
ValueError
333+
if the decorated function is a dunder method, or if unknown keyword arguments are provided
301334
"""
302335

303336
def inner(obj):

0 commit comments

Comments
 (0)