rtls-sdk 0.2.0__py3-none-any.whl

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 (78) hide show
  1. rtls_sdk/__init__.py +123 -0
  2. rtls_sdk/_auth.py +266 -0
  3. rtls_sdk/_client.py +419 -0
  4. rtls_sdk/_envelope.py +74 -0
  5. rtls_sdk/_http.py +145 -0
  6. rtls_sdk/_logging.py +143 -0
  7. rtls_sdk/_pagination.py +235 -0
  8. rtls_sdk/_query.py +114 -0
  9. rtls_sdk/_time.py +84 -0
  10. rtls_sdk/compounds/__init__.py +19 -0
  11. rtls_sdk/compounds/auth.py +100 -0
  12. rtls_sdk/compounds/context.py +159 -0
  13. rtls_sdk/compounds/groups.py +126 -0
  14. rtls_sdk/compounds/nodes.py +176 -0
  15. rtls_sdk/compounds/reports.py +339 -0
  16. rtls_sdk/compounds/system.py +43 -0
  17. rtls_sdk/compounds/tags.py +404 -0
  18. rtls_sdk/compounds/users.py +143 -0
  19. rtls_sdk/compounds/zones.py +203 -0
  20. rtls_sdk/errors.py +238 -0
  21. rtls_sdk/models/__init__.py +73 -0
  22. rtls_sdk/models/_base.py +46 -0
  23. rtls_sdk/models/alarm.py +23 -0
  24. rtls_sdk/models/anchor.py +25 -0
  25. rtls_sdk/models/area.py +28 -0
  26. rtls_sdk/models/association.py +48 -0
  27. rtls_sdk/models/bulk.py +80 -0
  28. rtls_sdk/models/company.py +32 -0
  29. rtls_sdk/models/csv_blob.py +40 -0
  30. rtls_sdk/models/floorplan.py +42 -0
  31. rtls_sdk/models/group.py +20 -0
  32. rtls_sdk/models/heatmap.py +37 -0
  33. rtls_sdk/models/import_result.py +42 -0
  34. rtls_sdk/models/node.py +28 -0
  35. rtls_sdk/models/notification.py +38 -0
  36. rtls_sdk/models/position.py +66 -0
  37. rtls_sdk/models/project.py +26 -0
  38. rtls_sdk/models/pws.py +38 -0
  39. rtls_sdk/models/report.py +34 -0
  40. rtls_sdk/models/session_context.py +81 -0
  41. rtls_sdk/models/site.py +31 -0
  42. rtls_sdk/models/subscriber.py +68 -0
  43. rtls_sdk/models/system.py +97 -0
  44. rtls_sdk/models/system_health.py +36 -0
  45. rtls_sdk/models/tag.py +48 -0
  46. rtls_sdk/models/tag_template.py +24 -0
  47. rtls_sdk/models/user.py +121 -0
  48. rtls_sdk/models/zone.py +27 -0
  49. rtls_sdk/models/zone_event.py +21 -0
  50. rtls_sdk/py.typed +0 -0
  51. rtls_sdk/resources/__init__.py +49 -0
  52. rtls_sdk/resources/_base.py +63 -0
  53. rtls_sdk/resources/alarms.py +108 -0
  54. rtls_sdk/resources/anchors.py +147 -0
  55. rtls_sdk/resources/areas.py +78 -0
  56. rtls_sdk/resources/associations.py +157 -0
  57. rtls_sdk/resources/auth.py +63 -0
  58. rtls_sdk/resources/companies.py +79 -0
  59. rtls_sdk/resources/context.py +50 -0
  60. rtls_sdk/resources/events.py +149 -0
  61. rtls_sdk/resources/floorplans.py +283 -0
  62. rtls_sdk/resources/groups.py +99 -0
  63. rtls_sdk/resources/logger.py +40 -0
  64. rtls_sdk/resources/messaging.py +51 -0
  65. rtls_sdk/resources/nodes.py +157 -0
  66. rtls_sdk/resources/notifications.py +55 -0
  67. rtls_sdk/resources/projects.py +67 -0
  68. rtls_sdk/resources/reports.py +180 -0
  69. rtls_sdk/resources/sites.py +115 -0
  70. rtls_sdk/resources/subscribers.py +110 -0
  71. rtls_sdk/resources/system.py +125 -0
  72. rtls_sdk/resources/tags.py +370 -0
  73. rtls_sdk/resources/users.py +275 -0
  74. rtls_sdk/resources/zones.py +199 -0
  75. rtls_sdk-0.2.0.dist-info/METADATA +141 -0
  76. rtls_sdk-0.2.0.dist-info/RECORD +78 -0
  77. rtls_sdk-0.2.0.dist-info/WHEEL +4 -0
  78. rtls_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,203 @@
1
+ """Compound zone workflows.
2
+
3
+ Static zones go through the raw path. Dynamic (buffer) zones bound to a
4
+ trackable need the additional rebinding plumbing this module provides.
5
+
6
+ Step-name conventions (stable):
7
+
8
+ - ``create``: ``create_zone``, ``set_trackable_binding``.
9
+ - ``update``: ``clear_old_binding``, ``set_new_binding``, ``update_zone``.
10
+ - ``delete``: ``clear_binding``, ``delete_zone``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from ..errors import PartialFailureError, RtlsError
19
+ from ..models import Zone
20
+ from .tags import UNSET, _Unset
21
+
22
+ if TYPE_CHECKING:
23
+ from .._client import RtlsClient
24
+
25
+
26
+ logger = logging.getLogger("rtls_sdk.compounds.zones")
27
+
28
+
29
+ def create_zone(
30
+ client: RtlsClient,
31
+ name: str,
32
+ *,
33
+ dynamic: bool = False,
34
+ trackable_uid: str | None = None,
35
+ **fields: Any,
36
+ ) -> Zone:
37
+ """Create a zone, binding it to a trackable if requested.
38
+
39
+ For static zones the compound collapses to a single
40
+ ``zones.create_raw`` call. For dynamic zones with a ``trackable_uid``
41
+ the SDK additionally writes ``self_zone_uid`` onto the trackable
42
+ after the zone is created.
43
+
44
+ Rollback: if the trackable update fails after the zone is created,
45
+ the SDK best-effort deletes the orphan zone before raising.
46
+ """
47
+ if dynamic:
48
+ fields = {**fields, "dynamic": True}
49
+ if trackable_uid:
50
+ fields["trackable_object_uid"] = trackable_uid
51
+
52
+ zone = client.zones.create_raw(name=name, **fields)
53
+ completed = ["create_zone"]
54
+
55
+ if dynamic and trackable_uid:
56
+ try:
57
+ client.tags.update_raw(trackable_uid, self_zone_uid=zone.uid)
58
+ except RtlsError as exc:
59
+ try:
60
+ client.zones.delete_raw(zone.uid)
61
+ except RtlsError as rollback_exc:
62
+ logger.warning("rollback of created zone %s failed: %s", zone.uid, rollback_exc)
63
+ raise PartialFailureError(
64
+ f"zones.create: set_trackable_binding failed: {exc}",
65
+ completed_steps=completed,
66
+ failed_step="set_trackable_binding",
67
+ cause=exc,
68
+ partial_result={"zone_uid": zone.uid, "trackable_uid": trackable_uid},
69
+ ) from exc
70
+ completed.append("set_trackable_binding")
71
+
72
+ return zone
73
+
74
+
75
+ def update_zone(
76
+ client: RtlsClient,
77
+ uid: str,
78
+ *,
79
+ trackable_uid: str | None | _Unset = UNSET,
80
+ **fields: Any,
81
+ ) -> Zone:
82
+ """Update a zone, re-binding the trackable if needed.
83
+
84
+ Pass ``trackable_uid=...`` (default) to leave any current binding
85
+ untouched. Pass ``trackable_uid=None`` to clear the binding;
86
+ ``trackable_uid="t-x"`` to rebind.
87
+
88
+ The rebinding sequence is intentionally three steps:
89
+
90
+ 1. Load the existing zone to discover the current binding.
91
+ 2. If changed: clear old binding (``self_zone_uid=None`` on old tag).
92
+ 3. If new binding requested: set new (``self_zone_uid=zone.uid`` on
93
+ new tag).
94
+ 4. ``zones.update_raw`` for whatever field changes the caller passed.
95
+
96
+ If step 3 fails after step 2 succeeded, the SDK best-effort restores
97
+ the old binding before raising. If that restore also fails, the old
98
+ trackable is left dangling and the exception's ``partial_result``
99
+ names what needs manual reconciliation.
100
+ """
101
+ completed: list[str] = []
102
+
103
+ # Step 1: discover current binding if a change is requested.
104
+ current_trackable: str | None = None
105
+ if not isinstance(trackable_uid, _Unset):
106
+ try:
107
+ current = client.zones.get(uid)
108
+ current_trackable = current.trackable_object_uid
109
+ except RtlsError as exc:
110
+ raise PartialFailureError(
111
+ f"zones.update: failed to load current binding: {exc}",
112
+ completed_steps=completed,
113
+ failed_step="load_zone",
114
+ cause=exc,
115
+ ) from exc
116
+
117
+ # Step 2: clear old binding if we're rebinding.
118
+ if current_trackable and current_trackable != trackable_uid:
119
+ try:
120
+ client.tags.update_raw(current_trackable, self_zone_uid=None)
121
+ except RtlsError as exc:
122
+ raise PartialFailureError(
123
+ f"zones.update: clear_old_binding failed: {exc}",
124
+ completed_steps=completed,
125
+ failed_step="clear_old_binding",
126
+ cause=exc,
127
+ partial_result={"zone_uid": uid, "old_trackable_uid": current_trackable},
128
+ ) from exc
129
+ completed.append("clear_old_binding")
130
+
131
+ # Step 3: set new binding.
132
+ if trackable_uid and trackable_uid != current_trackable:
133
+ try:
134
+ client.tags.update_raw(trackable_uid, self_zone_uid=uid)
135
+ except RtlsError as exc:
136
+ # Best-effort restore of old binding.
137
+ if current_trackable:
138
+ try:
139
+ client.tags.update_raw(current_trackable, self_zone_uid=uid)
140
+ except RtlsError as restore_exc:
141
+ logger.warning(
142
+ "zones.update: restore of old binding on %s failed: %s",
143
+ current_trackable,
144
+ restore_exc,
145
+ )
146
+ raise PartialFailureError(
147
+ f"zones.update: set_new_binding failed: {exc}",
148
+ completed_steps=completed,
149
+ failed_step="set_new_binding",
150
+ cause=exc,
151
+ partial_result={
152
+ "zone_uid": uid,
153
+ "old_trackable_uid": current_trackable,
154
+ "new_trackable_uid": trackable_uid,
155
+ },
156
+ ) from exc
157
+ completed.append("set_new_binding")
158
+
159
+ # Step 4: write field updates.
160
+ try:
161
+ zone = client.zones.update_raw(uid, **fields)
162
+ except RtlsError as exc:
163
+ raise PartialFailureError(
164
+ f"zones.update: update_zone failed: {exc}",
165
+ completed_steps=completed,
166
+ failed_step="update_zone",
167
+ cause=exc,
168
+ ) from exc
169
+ completed.append("update_zone")
170
+ return zone
171
+
172
+
173
+ def delete_zone(client: RtlsClient, uid: str) -> None:
174
+ """Delete a zone, clearing any dynamic-trackable binding first.
175
+
176
+ Step names: ``load_zone``, ``clear_binding``, ``delete_zone``.
177
+
178
+ ``clear_binding`` failure is logged but does not abort the delete —
179
+ the orphan trackable.``self_zone_uid`` is the lesser problem than
180
+ leaving the zone behind.
181
+ """
182
+ try:
183
+ zone = client.zones.get(uid)
184
+ except RtlsError:
185
+ # If we can't fetch it, fall through to the delete — the server
186
+ # will return 404 there if the zone doesn't exist.
187
+ client.zones.delete_raw(uid)
188
+ return
189
+
190
+ if zone.dynamic and zone.trackable_object_uid:
191
+ try:
192
+ client.tags.update_raw(zone.trackable_object_uid, self_zone_uid=None)
193
+ except RtlsError as exc:
194
+ logger.warning(
195
+ "zones.delete: clear_binding on tag %s failed: %s",
196
+ zone.trackable_object_uid,
197
+ exc,
198
+ )
199
+
200
+ client.zones.delete_raw(uid)
201
+
202
+
203
+ __all__ = ["create_zone", "delete_zone", "update_zone"]
rtls_sdk/errors.py ADDED
@@ -0,0 +1,238 @@
1
+ """Exception hierarchy for the RTLS SDK.
2
+
3
+ Every method in the public API raises at most one type from this module on
4
+ failure. HTTP status codes are mapped to typed exceptions at the response
5
+ boundary so user code never has to inspect ``response.status_code``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ class RtlsError(Exception):
14
+ """Base for every SDK exception.
15
+
16
+ Attributes
17
+ ----------
18
+ status_code
19
+ HTTP status code, or ``None`` for transport-level failures.
20
+ request_url
21
+ The URL the SDK attempted to call. Includes the path; query string
22
+ may be present.
23
+ request_method
24
+ Uppercase HTTP method (``"GET"``, ``"POST"``, …).
25
+ response_body
26
+ Parsed response body with secrets redacted, or ``None`` if the
27
+ server returned no parseable body.
28
+ error_envelope
29
+ The server's ``error`` or ``message`` field if either was present.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ message: str,
35
+ *,
36
+ status_code: int | None = None,
37
+ request_url: str | None = None,
38
+ request_method: str | None = None,
39
+ response_body: Any = None,
40
+ error_envelope: str | None = None,
41
+ ) -> None:
42
+ super().__init__(message)
43
+ self.status_code = status_code
44
+ self.request_url = request_url
45
+ self.request_method = request_method
46
+ self.response_body = response_body
47
+ self.error_envelope = error_envelope
48
+
49
+
50
+ class ConnectionError(RtlsError):
51
+ """Transport-level failure: DNS, connection refused, read timeout, TLS."""
52
+
53
+
54
+ class AuthenticationError(RtlsError):
55
+ """401 after the SDK already tried to re-authenticate, or no credentials.
56
+
57
+ For ``from_env()`` and explicit-credential clients this means the
58
+ username/password is invalid (or the token was revoked). For
59
+ bring-your-own-token clients this means the token is invalid or expired;
60
+ the caller must acquire a fresh token and reconstruct the client.
61
+ """
62
+
63
+
64
+ class PermissionDenied(RtlsError):
65
+ """403. The authenticated user lacks permission for the requested action."""
66
+
67
+
68
+ class NotFound(RtlsError):
69
+ """404. The requested resource does not exist (or is hidden by scope)."""
70
+
71
+
72
+ class Conflict(RtlsError):
73
+ """409. The request conflicts with current server state (e.g. duplicate MAC)."""
74
+
75
+
76
+ class ValidationError(RtlsError):
77
+ """422. The request body failed server-side validation.
78
+
79
+ ``field_errors`` is populated when the server returned a structured
80
+ field-level error response. The shape varies by endpoint; consult the
81
+ raised exception's ``response_body`` for the raw form.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ message: str,
87
+ *,
88
+ field_errors: dict[str, Any] | None = None,
89
+ **kwargs: Any,
90
+ ) -> None:
91
+ super().__init__(message, **kwargs)
92
+ self.field_errors: dict[str, Any] = field_errors or {}
93
+
94
+
95
+ class RateLimited(RtlsError):
96
+ """429 or 423. Server rate-limit hit.
97
+
98
+ ``retry_after_seconds`` is parsed from the ``Retry-After`` header when
99
+ present. The SDK does not auto-retry; the caller decides whether to
100
+ sleep and try again.
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ message: str,
106
+ *,
107
+ retry_after_seconds: float | None = None,
108
+ **kwargs: Any,
109
+ ) -> None:
110
+ super().__init__(message, **kwargs)
111
+ self.retry_after_seconds = retry_after_seconds
112
+
113
+
114
+ class ServerError(RtlsError):
115
+ """5xx. The server failed. Retry at the caller's discretion."""
116
+
117
+
118
+ class PartialFailureError(RtlsError):
119
+ """A compound multi-call operation failed partway through.
120
+
121
+ Raised by compound SDK methods (M4+) when an intermediate step succeeds
122
+ and a subsequent step fails. The exception carries enough context for
123
+ the caller to reason about the inconsistent state and decide whether
124
+ to roll back further manually.
125
+
126
+ Attributes
127
+ ----------
128
+ completed_steps
129
+ Stable step-name strings for each call that succeeded before the
130
+ failure. Documented per-method.
131
+ failed_step
132
+ Stable step-name string identifying the call that raised.
133
+ cause
134
+ The underlying exception that caused the failure. Also chained via
135
+ ``raise ... from cause``.
136
+ partial_result
137
+ Identifiers for any partially-created resources, so the caller can
138
+ clean up manually. Stable keys documented per-method.
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ message: str,
144
+ *,
145
+ completed_steps: list[str],
146
+ failed_step: str,
147
+ cause: RtlsError,
148
+ partial_result: dict[str, Any] | None = None,
149
+ **kwargs: Any,
150
+ ) -> None:
151
+ super().__init__(message, **kwargs)
152
+ self.completed_steps = completed_steps
153
+ self.failed_step = failed_step
154
+ self.cause = cause
155
+ self.partial_result: dict[str, Any] = partial_result or {}
156
+
157
+
158
+ class ReportError(RtlsError):
159
+ """Base for report-specific failures."""
160
+
161
+
162
+ class ReportFailed(ReportError):
163
+ """The server reported an error in the report's status payload."""
164
+
165
+
166
+ class ReportTimeout(ReportError):
167
+ """The SDK's polling deadline elapsed before the report completed.
168
+
169
+ ``report_uid`` carries the in-flight report's identifier so the caller
170
+ can resume polling later by calling ``reports.get(report_uid)``.
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ message: str,
176
+ *,
177
+ report_uid: str | None = None,
178
+ **kwargs: Any,
179
+ ) -> None:
180
+ super().__init__(message, **kwargs)
181
+ self.report_uid = report_uid
182
+
183
+
184
+ _STATUS_TO_EXC: dict[int, type[RtlsError]] = {
185
+ 401: AuthenticationError,
186
+ 403: PermissionDenied,
187
+ 404: NotFound,
188
+ 409: Conflict,
189
+ 422: ValidationError,
190
+ 423: RateLimited,
191
+ 429: RateLimited,
192
+ }
193
+
194
+
195
+ def exception_for_status(status_code: int) -> type[RtlsError]:
196
+ """Map an HTTP status code to the SDK exception class to raise."""
197
+ if status_code in _STATUS_TO_EXC:
198
+ return _STATUS_TO_EXC[status_code]
199
+ if 500 <= status_code < 600:
200
+ return ServerError
201
+ return RtlsError
202
+
203
+
204
+ def parse_error_envelope(body: Any) -> str | None:
205
+ """Tolerate both documented server error envelope shapes.
206
+
207
+ The server returns either ``{"success": false, "error": "..."}`` or
208
+ ``{"message": "..."}`` depending on the endpoint (RESEARCH §2). Return
209
+ whichever string is present, or ``None``.
210
+ """
211
+ if not isinstance(body, dict):
212
+ return None
213
+ err = body.get("error")
214
+ if isinstance(err, str):
215
+ return err
216
+ msg = body.get("message")
217
+ if isinstance(msg, str):
218
+ return msg
219
+ return None
220
+
221
+
222
+ __all__ = [
223
+ "AuthenticationError",
224
+ "Conflict",
225
+ "ConnectionError",
226
+ "NotFound",
227
+ "PartialFailureError",
228
+ "PermissionDenied",
229
+ "RateLimited",
230
+ "ReportError",
231
+ "ReportFailed",
232
+ "ReportTimeout",
233
+ "RtlsError",
234
+ "ServerError",
235
+ "ValidationError",
236
+ "exception_for_status",
237
+ "parse_error_envelope",
238
+ ]
@@ -0,0 +1,73 @@
1
+ """Public model surface for the RTLS SDK."""
2
+
3
+ from .alarm import Alarm
4
+ from .anchor import Anchor
5
+ from .area import Area
6
+ from .association import Association
7
+ from .bulk import BulkFailure, BulkResult
8
+ from .company import Company
9
+ from .csv_blob import CsvBlob
10
+ from .floorplan import Floorplan
11
+ from .group import Group
12
+ from .heatmap import Heatmap, HeatmapInput
13
+ from .import_result import ImportResult
14
+ from .node import Node
15
+ from .notification import NotificationProfile, NotificationType
16
+ from .position import Position
17
+ from .project import Project
18
+ from .pws import PwsEvent, PwsReport
19
+ from .report import Report, ReportType
20
+ from .session_context import SessionContext
21
+ from .site import Site
22
+ from .subscriber import Subscriber, SubscriberAddresses, SystemSubscriber
23
+ from .system import Capabilities, Connection, Host, Uptime, Version, WsHost
24
+ from .system_health import SystemHealth
25
+ from .tag import Tag
26
+ from .tag_template import TagTemplate
27
+ from .user import Role, User, UserFlags
28
+ from .zone import Zone
29
+ from .zone_event import ZoneEvent
30
+
31
+ __all__ = [
32
+ "Alarm",
33
+ "Anchor",
34
+ "Area",
35
+ "Association",
36
+ "BulkFailure",
37
+ "BulkResult",
38
+ "Capabilities",
39
+ "Company",
40
+ "Connection",
41
+ "CsvBlob",
42
+ "Floorplan",
43
+ "Group",
44
+ "Heatmap",
45
+ "HeatmapInput",
46
+ "Host",
47
+ "ImportResult",
48
+ "Node",
49
+ "NotificationProfile",
50
+ "NotificationType",
51
+ "Position",
52
+ "Project",
53
+ "PwsEvent",
54
+ "PwsReport",
55
+ "Report",
56
+ "ReportType",
57
+ "Role",
58
+ "SessionContext",
59
+ "Site",
60
+ "Subscriber",
61
+ "SubscriberAddresses",
62
+ "SystemHealth",
63
+ "SystemSubscriber",
64
+ "Tag",
65
+ "TagTemplate",
66
+ "Uptime",
67
+ "User",
68
+ "UserFlags",
69
+ "Version",
70
+ "WsHost",
71
+ "Zone",
72
+ "ZoneEvent",
73
+ ]
@@ -0,0 +1,46 @@
1
+ """Base pydantic model used by every SDK model.
2
+
3
+ Models are frozen (immutable from the user's perspective — there's no
4
+ ``model.save()`` pattern in this SDK). They accept extra keys to remain
5
+ forward-compatible when the server adds fields, and expose the original
6
+ wire payload via ``.raw`` so callers can read newly-added fields without
7
+ waiting for an SDK release.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, ConfigDict
15
+ from typing_extensions import Self
16
+
17
+
18
+ class _BaseModel(BaseModel):
19
+ """Shared base — frozen, alias-tolerant, extra-tolerant."""
20
+
21
+ model_config = ConfigDict(
22
+ frozen=True,
23
+ populate_by_name=True,
24
+ extra="allow",
25
+ )
26
+
27
+ @classmethod
28
+ def from_wire(cls, payload: dict[str, Any]) -> Self:
29
+ """Build an instance from a raw server payload, capturing the
30
+ original dict on ``.raw`` for forward-compat field access."""
31
+ instance = cls.model_validate(payload)
32
+ # frozen=True blocks normal assignment; bypass via object.__setattr__.
33
+ object.__setattr__(instance, "_raw", payload)
34
+ return instance
35
+
36
+ @property
37
+ def raw(self) -> dict[str, Any]:
38
+ """Return the original server payload that built this model.
39
+
40
+ Use this only to read fields the SDK doesn't know about yet —
41
+ the shape is unsupported and may break across server versions.
42
+ """
43
+ return getattr(self, "_raw", {})
44
+
45
+
46
+ __all__ = ["_BaseModel"]
@@ -0,0 +1,23 @@
1
+ """Alarm model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._base import _BaseModel
6
+
7
+
8
+ class Alarm(_BaseModel):
9
+ """An alarm event raised by the server.
10
+
11
+ Shape is consistent across the active / period / area / site endpoints
12
+ — the ``format=`` query parameter switches between JSON (returns
13
+ Alarm) and CSV (raw bytes, surfaced by report methods, not here).
14
+ """
15
+
16
+ uid: str | None = None
17
+ alarm_type_name: str | None = None
18
+ location_uid: str | None = None
19
+ sublocation_uid: str | None = None
20
+ trackable_object_uid: str | None = None
21
+
22
+
23
+ __all__ = ["Alarm"]
@@ -0,0 +1,25 @@
1
+ """WIN anchor model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._base import _BaseModel
6
+
7
+
8
+ class Anchor(_BaseModel):
9
+ """A WIN (wireless indoor navigation) anchor — fixed-position beacon.
10
+
11
+ Fields beyond ``uid`` / ``name`` / ``mac_address`` / ``sublocation_uid``
12
+ are optional placement metadata returned on create / read.
13
+ """
14
+
15
+ uid: str
16
+ name: str | None = None
17
+ mac_address: str | None = None
18
+ sublocation_uid: str | None = None
19
+ position_x: float | None = None
20
+ position_y: float | None = None
21
+ position_z: float | None = None
22
+ rotation: float | None = None
23
+
24
+
25
+ __all__ = ["Anchor"]
@@ -0,0 +1,28 @@
1
+ """Area (sublocation) model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._base import _BaseModel
6
+
7
+
8
+ class Area(_BaseModel):
9
+ """A sublocation inside a site.
10
+
11
+ Areas (also called sublocations) are the geographic units that zones,
12
+ positions, and trackables are scoped to. Fields beyond ``uid`` /
13
+ ``name`` / ``location_uid`` are optional placement metadata returned
14
+ on create / read.
15
+ """
16
+
17
+ uid: str
18
+ name: str | None = None
19
+ location_uid: str | None = None
20
+ description: str | None = None
21
+ position_x: float | None = None
22
+ position_y: float | None = None
23
+ width: float | None = None
24
+ height: float | None = None
25
+ rotation: float | None = None
26
+
27
+
28
+ __all__ = ["Area"]
@@ -0,0 +1,48 @@
1
+ """Trackable-association model.
2
+
3
+ The wire envelope differs between ``GET /trackable_objects_associations/``
4
+ (``{trackable_objects_associations: [...]}``) and ``POST /…/`` create
5
+ (``{association: ...}``). The SDK uses one ``Association`` model for both.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from pydantic import model_validator
14
+
15
+ from .._time import from_epoch_ms
16
+ from ._base import _BaseModel
17
+
18
+
19
+ class Association(_BaseModel):
20
+ """A binding between a hardware MAC address and a trackable.
21
+
22
+ Associations are time-bounded: a trackable can wear different physical
23
+ tags over its lifetime. ``closed_at`` is ``None`` on an open binding.
24
+ """
25
+
26
+ uid: str | None = None
27
+ mac_address: str | None = None
28
+ obj_uid: str | None = None
29
+ opened_at: datetime | None = None
30
+ closed_at: datetime | None = None
31
+
32
+ @model_validator(mode="before")
33
+ @classmethod
34
+ def _rewrite_aliases(cls, data: Any) -> Any:
35
+ if not isinstance(data, dict):
36
+ return data
37
+ out = dict(data)
38
+ for src, dst in (("start", "opened_at"), ("end", "closed_at")):
39
+ if dst not in out and src in out and out[src] is not None:
40
+ value = out[src]
41
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
42
+ out[dst] = from_epoch_ms(value)
43
+ else:
44
+ out[dst] = value
45
+ return out
46
+
47
+
48
+ __all__ = ["Association"]