labthings-fastapi 0.0.17__tar.gz → 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/PKG-INFO +3 -3
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/pyproject.toml +3 -3
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/__init__.py +0 -2
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/actions.py +1 -7
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/base_descriptor.py +1 -1
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/logs.py +10 -4
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/outputs/blob.py +1 -36
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/__init__.py +10 -5
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/cli.py +7 -1
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/utilities/__init__.py +4 -9
- labthings_fastapi-0.0.17/src/labthings_fastapi/client/in_server.py +0 -344
- labthings_fastapi-0.0.17/src/labthings_fastapi/dependencies/__init__.py +0 -15
- labthings_fastapi-0.0.17/src/labthings_fastapi/dependencies/blocking_portal.py +0 -77
- labthings_fastapi-0.0.17/src/labthings_fastapi/dependencies/invocation.py +0 -160
- labthings_fastapi-0.0.17/src/labthings_fastapi/dependencies/metadata.py +0 -92
- labthings_fastapi-0.0.17/src/labthings_fastapi/dependencies/raw_thing.py +0 -130
- labthings_fastapi-0.0.17/src/labthings_fastapi/dependencies/thing.py +0 -58
- labthings_fastapi-0.0.17/src/labthings_fastapi/dependencies/thing_server.py +0 -96
- labthings_fastapi-0.0.17/src/labthings_fastapi/deps.py +0 -30
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/.gitignore +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/LICENSE +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/README.md +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/client/__init__.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/endpoints.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/example_things/__init__.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/exceptions.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/invocation_contexts.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/invocations.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/middleware/__init__.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/middleware/url_for.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/notifications.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/outputs/__init__.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/outputs/mjpeg_stream.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/properties.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/py.typed +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/config_model.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/fallback.html.jinja +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/fallback.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/testing.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/__init__.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/_model.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/td-json-schema-validation.json +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/validation.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_server_interface.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_slots.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/types/__init__.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/types/numpy.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/utilities/introspection.py +0 -0
- {labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/websockets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labthings-fastapi
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: An implementation of LabThings using FastAPI
|
|
5
5
|
Project-URL: Homepage, https://github.com/labthings/labthings-fastapi
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/labthings/labthings-fastapi/issues
|
|
@@ -11,11 +11,11 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
13
|
Requires-Dist: anyio~=4.0
|
|
14
|
-
Requires-Dist: fastapi[all]
|
|
14
|
+
Requires-Dist: fastapi[all]~=0.135.0
|
|
15
15
|
Requires-Dist: httpx
|
|
16
16
|
Requires-Dist: jsonschema
|
|
17
17
|
Requires-Dist: numpy>=1.20
|
|
18
|
-
Requires-Dist: pydantic~=2.
|
|
18
|
+
Requires-Dist: pydantic~=2.12
|
|
19
19
|
Requires-Dist: typing-extensions
|
|
20
20
|
Requires-Dist: zeroconf>=0.28.0
|
|
21
21
|
Provides-Extra: dev
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "labthings-fastapi"
|
|
3
|
-
version = "0.0
|
|
3
|
+
version = "0.1.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Richard Bowman", email="richard.bowman@cantab.net" },
|
|
6
6
|
]
|
|
@@ -13,13 +13,13 @@ classifiers = [
|
|
|
13
13
|
"Operating System :: OS Independent",
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"pydantic ~= 2.
|
|
16
|
+
"pydantic ~= 2.12",
|
|
17
17
|
"numpy>=1.20",
|
|
18
18
|
"jsonschema",
|
|
19
19
|
"typing_extensions",
|
|
20
20
|
"anyio ~=4.0",
|
|
21
21
|
"httpx",
|
|
22
|
-
"fastapi[all]
|
|
22
|
+
"fastapi[all]~=0.135.0",
|
|
23
23
|
"zeroconf >=0.28.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
@@ -25,7 +25,6 @@ from .thing_server_interface import ThingServerInterface
|
|
|
25
25
|
from .properties import property, setting, DataProperty, DataSetting
|
|
26
26
|
from .actions import action
|
|
27
27
|
from .endpoints import endpoint
|
|
28
|
-
from . import deps
|
|
29
28
|
from . import outputs
|
|
30
29
|
from .outputs import blob
|
|
31
30
|
from .server import ThingServer, cli
|
|
@@ -53,7 +52,6 @@ __all__ = [
|
|
|
53
52
|
"action",
|
|
54
53
|
"thing_slot",
|
|
55
54
|
"endpoint",
|
|
56
|
-
"deps",
|
|
57
55
|
"outputs",
|
|
58
56
|
"blob",
|
|
59
57
|
"ThingServer",
|
|
@@ -48,7 +48,6 @@ from .base_descriptor import (
|
|
|
48
48
|
from .logs import add_thing_log_destination
|
|
49
49
|
from .utilities import model_to_dict, wrap_plain_types_in_rootmodel
|
|
50
50
|
from .invocations import InvocationModel, InvocationStatus
|
|
51
|
-
from .dependencies.invocation import NonWarningInvocationID
|
|
52
51
|
from .exceptions import (
|
|
53
52
|
InvocationCancelledError,
|
|
54
53
|
InvocationError,
|
|
@@ -352,7 +351,6 @@ class ActionManager:
|
|
|
352
351
|
self,
|
|
353
352
|
action: ActionDescriptor,
|
|
354
353
|
thing: Thing,
|
|
355
|
-
id: uuid.UUID,
|
|
356
354
|
input: Any,
|
|
357
355
|
dependencies: dict[str, Any],
|
|
358
356
|
) -> Invocation:
|
|
@@ -366,8 +364,6 @@ class ActionManager:
|
|
|
366
364
|
:param thing: is the object on which we are running the ``action``, i.e.
|
|
367
365
|
it is supplied to the function wrapped by ``action`` as the ``self``
|
|
368
366
|
argument.
|
|
369
|
-
:param id: is a `uuid.UUID` used to identify the invocation, for example
|
|
370
|
-
when polling its status via HTTP.
|
|
371
367
|
:param input: is a `pydantic.BaseModel` representing the body of the HTTP
|
|
372
368
|
request that invoked the action. It is supplied to the function as
|
|
373
369
|
keyword arguments.
|
|
@@ -381,7 +377,7 @@ class ActionManager:
|
|
|
381
377
|
thing=thing,
|
|
382
378
|
input=input,
|
|
383
379
|
dependencies=dependencies,
|
|
384
|
-
id=
|
|
380
|
+
id=uuid.uuid4(),
|
|
385
381
|
)
|
|
386
382
|
self.append_invocation(thread)
|
|
387
383
|
thread.start()
|
|
@@ -821,7 +817,6 @@ class ActionDescriptor(
|
|
|
821
817
|
# the function to the decorator.
|
|
822
818
|
def start_action(
|
|
823
819
|
body: Any, # This annotation will be overwritten below.
|
|
824
|
-
id: NonWarningInvocationID,
|
|
825
820
|
background_tasks: BackgroundTasks,
|
|
826
821
|
**dependencies: Any,
|
|
827
822
|
) -> InvocationModel:
|
|
@@ -831,7 +826,6 @@ class ActionDescriptor(
|
|
|
831
826
|
thing=thing,
|
|
832
827
|
input=body,
|
|
833
828
|
dependencies=dependencies,
|
|
834
|
-
id=id,
|
|
835
829
|
)
|
|
836
830
|
background_tasks.add_task(action_manager.expire_invocations)
|
|
837
831
|
return action.response()
|
{labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/base_descriptor.py
RENAMED
|
@@ -247,7 +247,7 @@ class BaseDescriptorInfo(
|
|
|
247
247
|
encountered directly by someone using LabThings, except as a base class for
|
|
248
248
|
`.Action`\ , `.Property` and others.
|
|
249
249
|
|
|
250
|
-
LabThings uses descriptors to represent the :ref:`
|
|
250
|
+
LabThings uses descriptors to represent the :ref:`wot_affordances` of a `.Thing`\ .
|
|
251
251
|
However, passing descriptors around isn't very elegant for two reasons:
|
|
252
252
|
|
|
253
253
|
* Holding references to Descriptor objects can confuse static type checkers.
|
|
@@ -79,11 +79,11 @@ class DequeByInvocationIDHandler(logging.Handler):
|
|
|
79
79
|
pass # If there's no destination for a particular log, ignore it.
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def configure_thing_logger() -> None:
|
|
82
|
+
def configure_thing_logger(level: int | None = None) -> None:
|
|
83
83
|
"""Set up the logger for thing instances.
|
|
84
84
|
|
|
85
|
-
We always set the logger for thing instances to level INFO
|
|
86
|
-
is currently used to relay progress to the client.
|
|
85
|
+
We always set the logger for thing instances to level INFO by default,
|
|
86
|
+
as this is currently used to relay progress to the client.
|
|
87
87
|
|
|
88
88
|
This function will collect logs on a per-invocation
|
|
89
89
|
basis by adding a `.DequeByInvocationIDHandler` to the log. Only one
|
|
@@ -93,8 +93,14 @@ def configure_thing_logger() -> None:
|
|
|
93
93
|
a filter to add invocation ID is not possible. Instead, we attach a filter to
|
|
94
94
|
the handler, which filters all the records that propagate to it (i.e. anything
|
|
95
95
|
that starts with ``labthings_fastapi.things``).
|
|
96
|
+
|
|
97
|
+
:param level: the logging level to use. If not specified, we use INFO.
|
|
96
98
|
"""
|
|
97
|
-
|
|
99
|
+
if level is not None:
|
|
100
|
+
THING_LOGGER.setLevel(level)
|
|
101
|
+
else:
|
|
102
|
+
THING_LOGGER.setLevel(logging.INFO)
|
|
103
|
+
|
|
98
104
|
if not any(
|
|
99
105
|
isinstance(h, DequeByInvocationIDHandler) for h in THING_LOGGER.handlers
|
|
100
106
|
):
|
|
@@ -93,7 +93,6 @@ from typing import (
|
|
|
93
93
|
Literal,
|
|
94
94
|
Mapping,
|
|
95
95
|
)
|
|
96
|
-
from warnings import warn
|
|
97
96
|
from weakref import WeakValueDictionary
|
|
98
97
|
from tempfile import TemporaryDirectory
|
|
99
98
|
import uuid
|
|
@@ -615,7 +614,7 @@ class Blob:
|
|
|
615
614
|
"""
|
|
616
615
|
return core_schema.no_info_wrap_validator_function(
|
|
617
616
|
cls._validate,
|
|
618
|
-
BlobModel.
|
|
617
|
+
BlobModel.__pydantic_core_schema__,
|
|
619
618
|
serialization=core_schema.wrap_serializer_function_ser_schema(
|
|
620
619
|
cls._serialize,
|
|
621
620
|
is_field_serializer=False,
|
|
@@ -881,40 +880,6 @@ class Blob:
|
|
|
881
880
|
)
|
|
882
881
|
|
|
883
882
|
|
|
884
|
-
def blob_type(media_type: str) -> type[Blob]:
|
|
885
|
-
r"""Create a `.Blob` subclass for a given media type.
|
|
886
|
-
|
|
887
|
-
This convenience function may confuse static type checkers, so it is usually
|
|
888
|
-
clearer to make a subclass instead, e.g.:
|
|
889
|
-
|
|
890
|
-
.. code-block:: python
|
|
891
|
-
|
|
892
|
-
class MyImageBlob(Blob):
|
|
893
|
-
media_type = "image/png"
|
|
894
|
-
|
|
895
|
-
:param media_type: the media type that the new `.Blob` subclass will use.
|
|
896
|
-
|
|
897
|
-
:return: a subclass of `.Blob` with the specified media type.
|
|
898
|
-
|
|
899
|
-
:raise ValueError: if the media type contains ``'`` or ``\``.
|
|
900
|
-
"""
|
|
901
|
-
warn(
|
|
902
|
-
"`blob_type` is deprecated and will be removed in v0.1.0. "
|
|
903
|
-
"Create a subclass of `Blob` instead.",
|
|
904
|
-
DeprecationWarning,
|
|
905
|
-
stacklevel=2,
|
|
906
|
-
)
|
|
907
|
-
if "'" in media_type or "\\" in media_type:
|
|
908
|
-
raise ValueError("media_type must not contain single quotes or backslashes")
|
|
909
|
-
return type(
|
|
910
|
-
f"{media_type.replace('/', '_')}_blob",
|
|
911
|
-
(Blob,),
|
|
912
|
-
{
|
|
913
|
-
"media_type": media_type,
|
|
914
|
-
},
|
|
915
|
-
)
|
|
916
|
-
|
|
917
|
-
|
|
918
883
|
router = APIRouter()
|
|
919
884
|
"""A FastAPI router for BlobData download endpoints."""
|
|
920
885
|
|
{labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/__init__.py
RENAMED
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from typing import Any, AsyncGenerator, Optional, TypeVar
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
import os
|
|
13
|
+
import logging
|
|
13
14
|
|
|
14
15
|
from fastapi import FastAPI, Request
|
|
15
16
|
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -27,7 +28,6 @@ from ..logs import configure_thing_logger
|
|
|
27
28
|
from ..thing import Thing
|
|
28
29
|
from ..thing_server_interface import ThingServerInterface
|
|
29
30
|
from ..thing_description._model import ThingDescription
|
|
30
|
-
from ..dependencies.thing_server import _thing_servers # noqa: F401
|
|
31
31
|
from .config_model import (
|
|
32
32
|
ThingsConfig,
|
|
33
33
|
ThingServerConfig,
|
|
@@ -66,6 +66,7 @@ class ThingServer:
|
|
|
66
66
|
things: ThingsConfig,
|
|
67
67
|
settings_folder: Optional[str] = None,
|
|
68
68
|
application_config: Optional[Mapping[str, Any]] = None,
|
|
69
|
+
debug: bool = False,
|
|
69
70
|
) -> None:
|
|
70
71
|
r"""Initialise a LabThings server.
|
|
71
72
|
|
|
@@ -86,9 +87,12 @@ class ThingServer:
|
|
|
86
87
|
application. This is not processed by LabThings. Each `.Thing` can access
|
|
87
88
|
application. This is not processed by LabThings. Each `.Thing` can access
|
|
88
89
|
this via the Thing-Server interface.
|
|
90
|
+
:param debug: If ``True``, set the log level for `.Thing` instances to
|
|
91
|
+
DEBUG.
|
|
89
92
|
"""
|
|
90
93
|
self.startup_failure: dict | None = None
|
|
91
|
-
|
|
94
|
+
# Note: this is safe to call multiple times.
|
|
95
|
+
configure_thing_logger(logging.DEBUG if debug else None)
|
|
92
96
|
self._config = ThingServerConfig(
|
|
93
97
|
things=things,
|
|
94
98
|
settings_folder=settings_folder,
|
|
@@ -105,22 +109,23 @@ class ThingServer:
|
|
|
105
109
|
self.blocking_portal: Optional[BlockingPortal] = None
|
|
106
110
|
self.startup_status: dict[str, str | dict] = {"things": {}}
|
|
107
111
|
global _thing_servers # noqa: F824
|
|
108
|
-
_thing_servers.add(self)
|
|
109
112
|
# The function calls below create and set up the Things.
|
|
110
113
|
self._things = self._create_things()
|
|
111
114
|
self._connect_things()
|
|
112
115
|
self._attach_things_to_server()
|
|
113
116
|
|
|
114
117
|
@classmethod
|
|
115
|
-
def from_config(cls, config: ThingServerConfig) -> Self:
|
|
118
|
+
def from_config(cls, config: ThingServerConfig, debug: bool = False) -> Self:
|
|
116
119
|
r"""Create a ThingServer from a configuration model.
|
|
117
120
|
|
|
118
121
|
This is equivalent to ``ThingServer(**dict(config))``\ .
|
|
119
122
|
|
|
120
123
|
:param config: The configuration parameters for the server.
|
|
124
|
+
:param debug: If ``True``, set the log level for `.Thing` instances to
|
|
125
|
+
DEBUG.
|
|
121
126
|
:return: A `.ThingServer` configured as per the model.
|
|
122
127
|
"""
|
|
123
|
-
return cls(**dict(config))
|
|
128
|
+
return cls(**dict(config), debug=debug)
|
|
124
129
|
|
|
125
130
|
def _set_cors_middleware(self) -> None:
|
|
126
131
|
"""Configure the server to allow requests from other origins.
|
|
@@ -56,6 +56,11 @@ def get_default_parser() -> ArgumentParser:
|
|
|
56
56
|
default=5000,
|
|
57
57
|
help="Bind socket to this port. If 0, an available port will be picked.",
|
|
58
58
|
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--debug",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Enable debug logging.",
|
|
63
|
+
)
|
|
59
64
|
return parser
|
|
60
65
|
|
|
61
66
|
|
|
@@ -149,10 +154,11 @@ def serve_from_cli(
|
|
|
149
154
|
option is not specified.
|
|
150
155
|
"""
|
|
151
156
|
args = parse_args(argv)
|
|
157
|
+
|
|
152
158
|
try:
|
|
153
159
|
config, server = None, None
|
|
154
160
|
config = config_from_args(args)
|
|
155
|
-
server = ThingServer.from_config(config)
|
|
161
|
+
server = ThingServer.from_config(config, True if args.debug else False)
|
|
156
162
|
if dry_run:
|
|
157
163
|
return server
|
|
158
164
|
uvicorn.run(server.app, host=args.host, port=args.port)
|
{labthings_fastapi-0.0.17 → labthings_fastapi-0.1.0}/src/labthings_fastapi/utilities/__init__.py
RENAMED
|
@@ -6,7 +6,6 @@ from typing import Any, Dict, Generic, Iterable, TYPE_CHECKING, Optional, TypeVa
|
|
|
6
6
|
from weakref import WeakSet
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field, RootModel, create_model
|
|
8
8
|
from pydantic.dataclasses import dataclass
|
|
9
|
-
from pydantic_core import SchemaError
|
|
10
9
|
|
|
11
10
|
from labthings_fastapi.exceptions import UnsupportedConstraintError
|
|
12
11
|
from .introspection import EmptyObject
|
|
@@ -129,7 +128,7 @@ def wrap_plain_types_in_rootmodel(
|
|
|
129
128
|
:raises UnsupportedConstraintError: if constraints are provided for an
|
|
130
129
|
unsuitable type, for example `allow_inf_nan` for an `int` property, or
|
|
131
130
|
any constraints for a `BaseModel` subclass.
|
|
132
|
-
:raises
|
|
131
|
+
:raises RuntimeError: if other errors prevent Pydantic from creating a schema
|
|
133
132
|
for the generated model.
|
|
134
133
|
"""
|
|
135
134
|
try: # This needs to be a `try` as basic types are not classes
|
|
@@ -148,13 +147,9 @@ def wrap_plain_types_in_rootmodel(
|
|
|
148
147
|
root=(model, Field(**constraints)),
|
|
149
148
|
__base__=LabThingsRootModelWrapper,
|
|
150
149
|
)
|
|
151
|
-
except
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
key = error["loc"][-1]
|
|
155
|
-
raise UnsupportedConstraintError(
|
|
156
|
-
f"Constraint {key} is not supported for type {model!r}."
|
|
157
|
-
) from e
|
|
150
|
+
except RuntimeError as e:
|
|
151
|
+
if "Unable to apply constraint" in str(e):
|
|
152
|
+
raise UnsupportedConstraintError(str(e)) from e
|
|
158
153
|
raise e
|
|
159
154
|
|
|
160
155
|
|
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
"""A mock client that uses a Thing directly.
|
|
2
|
-
|
|
3
|
-
When `.Thing` objects interact on the server, it can be very useful to
|
|
4
|
-
use an interface that is identical to the `.ThingClient` used to access
|
|
5
|
-
the same `.Thing` remotely. This means that code can run either on the
|
|
6
|
-
server or on a client, e.g. in a Jupyter notebook where it is much
|
|
7
|
-
easier to debug. See :ref:`things_from_things` for more detail.
|
|
8
|
-
|
|
9
|
-
Currently `.DirectThingClient` is not a subclass of `.ThingClient`,
|
|
10
|
-
that may need to change. It's a good idea to create a
|
|
11
|
-
`.DirectThingClient` at module level, so that type hints work.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
from functools import wraps
|
|
18
|
-
import inspect
|
|
19
|
-
import logging
|
|
20
|
-
from typing import Any, Mapping, Optional, Union
|
|
21
|
-
from warnings import warn
|
|
22
|
-
from pydantic import BaseModel
|
|
23
|
-
from ..actions import ActionDescriptor
|
|
24
|
-
|
|
25
|
-
from ..properties import BaseProperty
|
|
26
|
-
from ..utilities import attributes
|
|
27
|
-
from . import PropertyClientDescriptor
|
|
28
|
-
from ..thing import Thing
|
|
29
|
-
from ..dependencies.thing_server import find_thing_server
|
|
30
|
-
from fastapi import Request
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
__all__ = ["DirectThingClient", "direct_thing_client_class"]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class DirectThingClient:
|
|
37
|
-
"""A wrapper for `.Thing` that is a work-a-like for `.ThingClient`.
|
|
38
|
-
|
|
39
|
-
This class is used to create a class that works like `.ThingClient`
|
|
40
|
-
but does not communicate over HTTP. Instead, it wraps a `.Thing` object
|
|
41
|
-
and calls its methods directly.
|
|
42
|
-
|
|
43
|
-
It is not yet 100% identical to `.ThingClient`, in particular `.ThingClient`
|
|
44
|
-
returns a lot of data directly as deserialised from JSON, while this class
|
|
45
|
-
generally returns `pydantic.BaseModel` instances, without serialisation.
|
|
46
|
-
|
|
47
|
-
`.DirectThingClient` is generally not used on its own, but is subclassed
|
|
48
|
-
(often dynamically) to add the actions and properties of a particular
|
|
49
|
-
`.Thing`.
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
__globals__ = globals() # "bake in" globals so dependency injection works
|
|
53
|
-
thing_class: type[Thing]
|
|
54
|
-
"""The class of the underlying `.Thing` we are wrapping."""
|
|
55
|
-
thing_name: str
|
|
56
|
-
"""The name of the Thing on the server."""
|
|
57
|
-
|
|
58
|
-
def __init__(self, request: Request, **dependencies: Mapping[str, Any]) -> None:
|
|
59
|
-
r"""Wrap a `.Thing` so it works like a `.ThingClient`.
|
|
60
|
-
|
|
61
|
-
This class is designed to be used as a FastAPI dependency, and will
|
|
62
|
-
retrieve a `.Thing` based on its ``thing_path`` attribute.
|
|
63
|
-
Finding the Thing by class may also be an option in the future.
|
|
64
|
-
|
|
65
|
-
:param request: This is a FastAPI dependency to access the
|
|
66
|
-
`fastapi.Request` object, allowing access to various resources.
|
|
67
|
-
:param \**dependencies: Further arguments will be added
|
|
68
|
-
dynamically by subclasses, by duplicating this method and
|
|
69
|
-
manipulating its signature. Adding arguments with annotated
|
|
70
|
-
type hints instructs FastAPI to inject dependency arguments,
|
|
71
|
-
such as access to other `.Things`.
|
|
72
|
-
"""
|
|
73
|
-
warn(
|
|
74
|
-
"`DirectThingClient` is deprecated and will be removed in v0.1.0. Use "
|
|
75
|
-
"`lt.thing_slot` instead.",
|
|
76
|
-
DeprecationWarning,
|
|
77
|
-
stacklevel=2,
|
|
78
|
-
)
|
|
79
|
-
server = find_thing_server(request.app)
|
|
80
|
-
self._wrapped_thing = server.things[self.thing_name]
|
|
81
|
-
self._request = request
|
|
82
|
-
self._dependencies = dependencies
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def property_descriptor(
|
|
86
|
-
property_name: str,
|
|
87
|
-
model: Union[type, BaseModel],
|
|
88
|
-
description: Optional[str] = None,
|
|
89
|
-
readable: bool = True,
|
|
90
|
-
writeable: bool = True,
|
|
91
|
-
property_path: Optional[str] = None,
|
|
92
|
-
) -> PropertyClientDescriptor:
|
|
93
|
-
"""Create a correctly-typed descriptor that gets and/or sets a property.
|
|
94
|
-
|
|
95
|
-
.. todo::
|
|
96
|
-
This is copy-pasted from labthings_fastapi.client.__init__.property_descriptor
|
|
97
|
-
TODO: refactor this into a shared function.
|
|
98
|
-
|
|
99
|
-
Create a descriptor object that wraps a property. This is for use on
|
|
100
|
-
a `.DirectThingClient` subclass.
|
|
101
|
-
|
|
102
|
-
:param property_name: should be the name of the property (i.e. the
|
|
103
|
-
name it takes in the thing description, and also the name it is
|
|
104
|
-
assigned to in the class).
|
|
105
|
-
:param model: the Python ``type`` or a ``pydantic.BaseModel`` that
|
|
106
|
-
represents the datatype of the property.
|
|
107
|
-
:param description: text to use for a docstring.
|
|
108
|
-
:param readable: whether the property may be read (i.e. has ``__get__``).
|
|
109
|
-
:param writeable: whether the property may be written to.
|
|
110
|
-
:param property_path: the URL of the ``getproperty`` and ``setproperty``
|
|
111
|
-
HTTP endpoints. Currently these must both be the same. These are
|
|
112
|
-
relative to the ``base_url``, i.e. the URL of the Thing Description.
|
|
113
|
-
|
|
114
|
-
:return: a descriptor allowing access to the specified property.
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
class P(PropertyClientDescriptor):
|
|
118
|
-
name = property_name
|
|
119
|
-
type = model
|
|
120
|
-
path = property_path or property_name
|
|
121
|
-
|
|
122
|
-
def __get__(
|
|
123
|
-
self: PropertyClientDescriptor,
|
|
124
|
-
obj: Optional[DirectThingClient] = None,
|
|
125
|
-
_objtype: Optional[type[DirectThingClient]] = None,
|
|
126
|
-
) -> Any:
|
|
127
|
-
if obj is None:
|
|
128
|
-
return self
|
|
129
|
-
return getattr(obj._wrapped_thing, self.name)
|
|
130
|
-
|
|
131
|
-
def __set__(
|
|
132
|
-
self: PropertyClientDescriptor, obj: DirectThingClient, value: Any
|
|
133
|
-
) -> None:
|
|
134
|
-
setattr(obj._wrapped_thing, self.name, value)
|
|
135
|
-
|
|
136
|
-
def set_readonly(
|
|
137
|
-
self: PropertyClientDescriptor, obj: DirectThingClient, value: Any
|
|
138
|
-
) -> None:
|
|
139
|
-
raise AttributeError("This property is read-only.")
|
|
140
|
-
|
|
141
|
-
if readable:
|
|
142
|
-
__get__.__annotations__["return"] = model
|
|
143
|
-
P.__get__ = __get__ # type: ignore[attr-defined]
|
|
144
|
-
if writeable:
|
|
145
|
-
__set__.__annotations__["value"] = model
|
|
146
|
-
P.__set__ = __set__ # type: ignore[attr-defined]
|
|
147
|
-
else:
|
|
148
|
-
set_readonly.__annotations__["value"] = model
|
|
149
|
-
P.__set__ = set_readonly # type: ignore[attr-defined]
|
|
150
|
-
if description:
|
|
151
|
-
P.__doc__ = description
|
|
152
|
-
return P()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class DependencyNameClashError(KeyError):
|
|
156
|
-
"""A dependency argument name is used inconsistently.
|
|
157
|
-
|
|
158
|
-
A current limitation of `.DirectThingClient` is that the dependency
|
|
159
|
-
arguments (see :ref:`dependencies`) are collected together in a single
|
|
160
|
-
dictionary. This makes the assumption that, if a name is reused, it is
|
|
161
|
-
reused for the same dependency.
|
|
162
|
-
|
|
163
|
-
When names are reused, we check if the values match. If not, this
|
|
164
|
-
exception is raised.
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
def __init__(self, name: str, existing: type, new: type) -> None:
|
|
168
|
-
"""Create a DependencyNameClashError.
|
|
169
|
-
|
|
170
|
-
See class docstring for an explanation of the error.
|
|
171
|
-
|
|
172
|
-
:param name: the name of the clashing dependencies.
|
|
173
|
-
:param existing: the dependency type annotation in the dictionary.
|
|
174
|
-
:param new: the clashing type annotation.
|
|
175
|
-
"""
|
|
176
|
-
super().__init__(
|
|
177
|
-
f"{self.__doc__}\n\n"
|
|
178
|
-
f"This clash is with name: {name}.\n"
|
|
179
|
-
f"Its value is currently {existing}, which clashes with {new}."
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def add_action(
|
|
184
|
-
attrs: dict[str, Any],
|
|
185
|
-
dependencies: list[inspect.Parameter],
|
|
186
|
-
name: str,
|
|
187
|
-
action: ActionDescriptor,
|
|
188
|
-
) -> None:
|
|
189
|
-
"""Generate an action method and adds it to an attrs dict.
|
|
190
|
-
|
|
191
|
-
FastAPI Dependencies are appended to the `dependencies` list.
|
|
192
|
-
This list should later be converted to type hints on the class
|
|
193
|
-
initialiser, so that FastAPI supplies the dependencies when
|
|
194
|
-
the `.DirectThingClient` is initialised.
|
|
195
|
-
|
|
196
|
-
:param attrs: the attributes of a soon-to-be-created `.DirectThingClient`
|
|
197
|
-
subclass. This will be passed to `type()` to create the subclass.
|
|
198
|
-
We will add the action method to this dictionary.
|
|
199
|
-
:param dependencies: lists the dependency parameters that will be
|
|
200
|
-
injected by FastAPI as arguments to the class ``__init__``.
|
|
201
|
-
Any dependency parameters of the supplied ``action`` should be
|
|
202
|
-
added to this list.
|
|
203
|
-
:param name: the name of the action. Should be the name of the
|
|
204
|
-
attribute, i.e. we will set ``attrs[name]``, and also match
|
|
205
|
-
the ``name`` in the supplied action descriptor.
|
|
206
|
-
:param action: an `.ActionDescriptor` to be wrapped.
|
|
207
|
-
|
|
208
|
-
:raise DependencyNameClashError: if dependencies are inconsistent.
|
|
209
|
-
"""
|
|
210
|
-
|
|
211
|
-
@wraps(action.func)
|
|
212
|
-
def action_method(self: DirectThingClient, **kwargs: Any) -> Any:
|
|
213
|
-
dependency_kwargs = {
|
|
214
|
-
param.name: self._dependencies[param.name]
|
|
215
|
-
for param in action.dependency_params
|
|
216
|
-
}
|
|
217
|
-
kwargs_and_deps = {**kwargs, **dependency_kwargs}
|
|
218
|
-
return getattr(self._wrapped_thing, name)(**kwargs_and_deps)
|
|
219
|
-
|
|
220
|
-
attrs[name] = action_method
|
|
221
|
-
# We collect up all the dependencies, so that we can
|
|
222
|
-
# resolve them when we create the client.
|
|
223
|
-
for param in action.dependency_params:
|
|
224
|
-
included = False
|
|
225
|
-
for existing_param in dependencies:
|
|
226
|
-
if existing_param.name == param.name:
|
|
227
|
-
# Currently, each name may only have one annotation, across
|
|
228
|
-
# all actions - this is a limitation we should fix.
|
|
229
|
-
if existing_param.annotation != param.annotation:
|
|
230
|
-
raise DependencyNameClashError(
|
|
231
|
-
param.name, existing_param.annotation, param.annotation
|
|
232
|
-
)
|
|
233
|
-
included = True
|
|
234
|
-
if not included:
|
|
235
|
-
dependencies.append(param)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def add_property(
|
|
239
|
-
attrs: dict[str, Any], property_name: str, property: BaseProperty
|
|
240
|
-
) -> None:
|
|
241
|
-
"""Add a property to a DirectThingClient subclass.
|
|
242
|
-
|
|
243
|
-
We create a new descriptor using `.property_descriptor` and add it
|
|
244
|
-
to the ``attrs`` dictionary as ``property_name``.
|
|
245
|
-
|
|
246
|
-
:param attrs: the attributes of a soon-to-be-created `.DirectThingClient`
|
|
247
|
-
subclass. This will be passed to `type()` to create the subclass.
|
|
248
|
-
We will add the property to this dictionary.
|
|
249
|
-
:param property_name: the name of the property. Should be the name of the
|
|
250
|
-
attribute, i.e. we will set ``attrs[name]``.
|
|
251
|
-
:param property: a `.PropertyDescriptor` to be wrapped.
|
|
252
|
-
"""
|
|
253
|
-
attrs[property_name] = property_descriptor(
|
|
254
|
-
property_name,
|
|
255
|
-
property.model,
|
|
256
|
-
description=property.description,
|
|
257
|
-
writeable=not property.readonly,
|
|
258
|
-
readable=True,
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def direct_thing_client_class(
|
|
263
|
-
thing_class: type[Thing],
|
|
264
|
-
thing_name: str,
|
|
265
|
-
actions: Optional[list[str]] = None,
|
|
266
|
-
) -> type[DirectThingClient]:
|
|
267
|
-
r"""Create a DirectThingClient from a Thing class and a path.
|
|
268
|
-
|
|
269
|
-
This is a class, not an instance: it's designed to be a FastAPI dependency.
|
|
270
|
-
|
|
271
|
-
:param thing_class: The `.Thing` subclass that will be wrapped.
|
|
272
|
-
:param thing_name: The name of the `.Thing` on the server.
|
|
273
|
-
:param actions: An optional list giving a subset of actions that will be
|
|
274
|
-
accessed. If this is specified, it may reduce the number of FastAPI
|
|
275
|
-
dependencies we need.
|
|
276
|
-
|
|
277
|
-
:return: a subclass of `DirectThingClient` with attributes that match the
|
|
278
|
-
properties and actions of ``thing_class``. The ``__init__`` method
|
|
279
|
-
will have annotations that instruct FastAPI to supply all the
|
|
280
|
-
dependencies needed by its actions.
|
|
281
|
-
|
|
282
|
-
This class may be used as a FastAPI dependency: see :ref:`things_from_things`.
|
|
283
|
-
"""
|
|
284
|
-
warn(
|
|
285
|
-
"`direct_thing_client_class` is deprecated and will be removed in v0.1.0. "
|
|
286
|
-
"Use `lt.thing_slot` instead.",
|
|
287
|
-
DeprecationWarning,
|
|
288
|
-
stacklevel=3, # This is called from `direct_thing_client_dependency` so we
|
|
289
|
-
# need stacklevel=3 to point to user code.
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
def init_proxy(
|
|
293
|
-
self: DirectThingClient, request: Request, **dependencies: Mapping[str, Any]
|
|
294
|
-
) -> None:
|
|
295
|
-
r"""Initialise a DirectThingClient (this docstring will be replaced).
|
|
296
|
-
|
|
297
|
-
:param self: The DirectThingClient instance we're initialising.
|
|
298
|
-
:param request: a FastAPI Request option (will be supplied by FastAPI).
|
|
299
|
-
:param \**dependencies: Other keyword arguments will be saved as
|
|
300
|
-
dependencies. FastAPI will look at the signature (which we will
|
|
301
|
-
manipulate below) to determine these.
|
|
302
|
-
"""
|
|
303
|
-
# NB this definition isimportant, as we must modify its signature.
|
|
304
|
-
# Inheriting __init__ means we'll accidentally modify the signature
|
|
305
|
-
# of `DirectThingClient` with bad results.
|
|
306
|
-
DirectThingClient.__init__(self, request, **dependencies)
|
|
307
|
-
|
|
308
|
-
init_proxy.__doc__ = f"""Initialise a client for {thing_class}"""
|
|
309
|
-
|
|
310
|
-
# Using a class definition gets confused by the scope of the function
|
|
311
|
-
# arguments - this is equivalent to a class definition but all the
|
|
312
|
-
# arguments are evaluated in the right scope.
|
|
313
|
-
client_attrs = {
|
|
314
|
-
"thing_class": thing_class,
|
|
315
|
-
"thing_name": thing_name,
|
|
316
|
-
"__doc__": f"A client for {thing_class} named {thing_name}",
|
|
317
|
-
"__init__": init_proxy,
|
|
318
|
-
}
|
|
319
|
-
dependencies: list[inspect.Parameter] = []
|
|
320
|
-
for name, item in attributes(thing_class):
|
|
321
|
-
if isinstance(item, BaseProperty):
|
|
322
|
-
add_property(client_attrs, name, item)
|
|
323
|
-
elif isinstance(item, ActionDescriptor):
|
|
324
|
-
if actions is None or name in actions:
|
|
325
|
-
add_action(client_attrs, dependencies, name, item)
|
|
326
|
-
else:
|
|
327
|
-
continue # Ignore actions that aren't in the list
|
|
328
|
-
else:
|
|
329
|
-
for affordance in ["property", "action", "event"]:
|
|
330
|
-
if hasattr(item, f"{affordance}_affordance"):
|
|
331
|
-
logging.warning(
|
|
332
|
-
f"DirectThingClient doesn't support custom affordances, "
|
|
333
|
-
f"ignoring {name}"
|
|
334
|
-
)
|
|
335
|
-
# This block of code makes dependencies show up in __init__ so
|
|
336
|
-
# they get resolved. It's more or less copied from the `action` descriptor.
|
|
337
|
-
sig = inspect.signature(init_proxy)
|
|
338
|
-
params = [p for p in sig.parameters.values() if p.name != "dependencies"]
|
|
339
|
-
init_proxy.__signature__ = sig.replace( # type: ignore[attr-defined]
|
|
340
|
-
parameters=params + dependencies
|
|
341
|
-
)
|
|
342
|
-
return type(
|
|
343
|
-
f"{thing_class.__name__}DirectClient", (DirectThingClient,), client_attrs
|
|
344
|
-
)
|