labthings-fastapi 0.0.16__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.
Files changed (50) hide show
  1. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/PKG-INFO +3 -3
  2. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/pyproject.toml +3 -3
  3. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/__init__.py +0 -2
  4. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/actions.py +1 -7
  5. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/base_descriptor.py +48 -33
  6. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/logs.py +10 -4
  7. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/outputs/blob.py +1 -36
  8. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/__init__.py +10 -5
  9. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/cli.py +7 -1
  10. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/utilities/__init__.py +4 -9
  11. labthings_fastapi-0.0.16/src/labthings_fastapi/client/in_server.py +0 -344
  12. labthings_fastapi-0.0.16/src/labthings_fastapi/dependencies/__init__.py +0 -15
  13. labthings_fastapi-0.0.16/src/labthings_fastapi/dependencies/blocking_portal.py +0 -77
  14. labthings_fastapi-0.0.16/src/labthings_fastapi/dependencies/invocation.py +0 -160
  15. labthings_fastapi-0.0.16/src/labthings_fastapi/dependencies/metadata.py +0 -92
  16. labthings_fastapi-0.0.16/src/labthings_fastapi/dependencies/raw_thing.py +0 -130
  17. labthings_fastapi-0.0.16/src/labthings_fastapi/dependencies/thing.py +0 -58
  18. labthings_fastapi-0.0.16/src/labthings_fastapi/dependencies/thing_server.py +0 -96
  19. labthings_fastapi-0.0.16/src/labthings_fastapi/deps.py +0 -30
  20. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/.gitignore +0 -0
  21. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/LICENSE +0 -0
  22. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/README.md +0 -0
  23. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/client/__init__.py +0 -0
  24. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/endpoints.py +0 -0
  25. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/example_things/__init__.py +0 -0
  26. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/exceptions.py +0 -0
  27. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/invocation_contexts.py +0 -0
  28. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/invocations.py +0 -0
  29. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/middleware/__init__.py +0 -0
  30. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/middleware/url_for.py +0 -0
  31. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/notifications.py +0 -0
  32. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/outputs/__init__.py +0 -0
  33. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/outputs/mjpeg_stream.py +0 -0
  34. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/properties.py +0 -0
  35. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/py.typed +0 -0
  36. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/config_model.py +0 -0
  37. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/fallback.html.jinja +0 -0
  38. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/server/fallback.py +0 -0
  39. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/testing.py +0 -0
  40. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing.py +0 -0
  41. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/__init__.py +0 -0
  42. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/_model.py +0 -0
  43. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/td-json-schema-validation.json +0 -0
  44. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_description/validation.py +0 -0
  45. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_server_interface.py +0 -0
  46. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/thing_slots.py +0 -0
  47. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/types/__init__.py +0 -0
  48. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/types/numpy.py +0 -0
  49. {labthings_fastapi-0.0.16 → labthings_fastapi-0.1.0}/src/labthings_fastapi/utilities/introspection.py +0 -0
  50. {labthings_fastapi-0.0.16 → 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.16
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]>=0.115.0
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.10.6
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.16"
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.10.6",
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]>=0.115.0",
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=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()
@@ -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:`affordances` of a `.Thing`\ .
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.
@@ -335,22 +335,6 @@ class BaseDescriptorInfo(
335
335
  descriptor = self.get_descriptor()
336
336
  return descriptor.__get__(self.owning_object_or_error())
337
337
 
338
- def set(self, value: Value) -> None:
339
- """Set the value of the descriptor.
340
-
341
- This method may only be called if the DescriptorInfo object is bound to a
342
- `.Thing` instance. It will raise an error if called on a class.
343
-
344
- :param value: the new value.
345
-
346
- :raises NotBoundToInstanceError: if called on an unbound info object.
347
- """
348
- if not self.is_bound:
349
- msg = f"We can't set the value of {self.name} when called on a class."
350
- raise NotBoundToInstanceError(msg)
351
- descriptor = self.get_descriptor()
352
- descriptor.__set__(self.owning_object_or_error(), value)
353
-
354
338
  def __eq__(self, other: Any) -> bool:
355
339
  """Determine if this object is equal to another one.
356
340
 
@@ -404,6 +388,11 @@ class BaseDescriptor(Generic[Owner, Value]):
404
388
  assert p.name == "my_prop"
405
389
  assert p.title == "My Property."
406
390
  assert p.description.startswith("This is")
391
+
392
+ `.BaseDescriptor` is a "non-data descriptor" (meaning it doesn't implement
393
+ ``__set__``). This allows it to be overwritten by assigning to an object's
394
+ attribute, which can be useful in test code. This can easily be changed in
395
+ subclasses by implementing ``__set__``\ .
407
396
  """
408
397
 
409
398
  def __init__(self) -> None:
@@ -593,21 +582,6 @@ class BaseDescriptor(Generic[Owner, Value]):
593
582
  "See BaseDescriptor.__instance_get__ for details."
594
583
  )
595
584
 
596
- def __set__(self, obj: Owner, value: Value) -> None:
597
- """Mark the `BaseDescriptor` as a data descriptor.
598
-
599
- Even for read-only descriptors, it's important to define a ``__set__`` method.
600
- The presence of this method prevents Python overwriting the descriptor when
601
- a value is assigned. This base implementation returns an `AttributeError` to
602
- signal that the descriptor is read-only. Overriding it with a method that
603
- does not raise an exception will allow the descriptor to be written to.
604
-
605
- :param obj: The object on which to set the value.
606
- :param value: The value to set the descriptor to.
607
- :raises AttributeError: always, as this is read-only by default.
608
- """
609
- raise AttributeError("This attribute is read-only.")
610
-
611
585
  def _descriptor_info(
612
586
  self, info_class: type[DescriptorInfoT], obj: Owner | None = None
613
587
  ) -> DescriptorInfoT:
@@ -669,9 +643,35 @@ class FieldTypedBaseDescriptorInfo(
669
643
  """The type of the descriptor's value."""
670
644
  return self.get_descriptor().value_type
671
645
 
646
+ def set(self, value: Value) -> None:
647
+ """Set the value of the descriptor.
648
+
649
+ This method may only be called if the DescriptorInfo object is bound to a
650
+ `.Thing` instance. It will raise an error if called on a class.
651
+
652
+ :param value: the new value.
653
+
654
+ :raises NotBoundToInstanceError: if called on an unbound info object.
655
+ """
656
+ if not self.is_bound:
657
+ msg = f"We can't set the value of {self.name} when called on a class."
658
+ raise NotBoundToInstanceError(msg)
659
+ descriptor = self.get_descriptor()
660
+ descriptor.__set__(self.owning_object_or_error(), value)
661
+
672
662
 
673
663
  class FieldTypedBaseDescriptor(Generic[Owner, Value], BaseDescriptor[Owner, Value]):
674
- """A BaseDescriptor that determines its type like a dataclass field."""
664
+ r"""A `.BaseDescriptor` that determines its type like a dataclass field.
665
+
666
+ This adds two things to `.BaseDescriptor`\ :
667
+
668
+ 1. Descriptors inheriting from this class will inspect the type annotations of
669
+ their owning class when determining ``value_type``\ .
670
+ 2. This class and its children will be "data descriptors" because there is a
671
+ stub implementation of ``__set__``\ . This means that the attribute may not
672
+ be assigned to (unless ``__set__`` is overridden). This is the behaviour
673
+ that `builtins.property` has.
674
+ """
675
675
 
676
676
  def __init__(self) -> None:
677
677
  """Initialise the FieldTypedBaseDescriptor.
@@ -852,6 +852,21 @@ class FieldTypedBaseDescriptor(Generic[Owner, Value], BaseDescriptor[Owner, Valu
852
852
  """
853
853
  return self._descriptor_info(FieldTypedBaseDescriptorInfo, owner)
854
854
 
855
+ def __set__(self, obj: Owner, value: Value) -> None:
856
+ """Mark the `BaseDescriptor` as a data descriptor.
857
+
858
+ Even for read-only descriptors, it's important to define a ``__set__`` method.
859
+ The presence of this method prevents Python overwriting the descriptor when
860
+ a value is assigned. This base implementation returns an `AttributeError` to
861
+ signal that the descriptor is read-only. Overriding it with a method that
862
+ does not raise an exception will allow the descriptor to be written to.
863
+
864
+ :param obj: The object on which to set the value.
865
+ :param value: The value to set the descriptor to.
866
+ :raises AttributeError: always, as this is read-only by default.
867
+ """
868
+ raise AttributeError("This attribute is read-only.")
869
+
855
870
 
856
871
  class DescriptorInfoCollection(
857
872
  Mapping[str, DescriptorInfoT],
@@ -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, as this
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
- THING_LOGGER.setLevel(logging.INFO)
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.__get_pydantic_core_schema__(BlobModel, handler),
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
 
@@ -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
- configure_thing_logger() # Note: this is safe to call multiple times.
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)
@@ -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 SchemaError: if other errors prevent Pydantic from creating a schema
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 SchemaError as e:
152
- for error in e.errors():
153
- if error["loc"][-1] in constraints:
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