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.
- rtls_sdk/__init__.py +123 -0
- rtls_sdk/_auth.py +266 -0
- rtls_sdk/_client.py +419 -0
- rtls_sdk/_envelope.py +74 -0
- rtls_sdk/_http.py +145 -0
- rtls_sdk/_logging.py +143 -0
- rtls_sdk/_pagination.py +235 -0
- rtls_sdk/_query.py +114 -0
- rtls_sdk/_time.py +84 -0
- rtls_sdk/compounds/__init__.py +19 -0
- rtls_sdk/compounds/auth.py +100 -0
- rtls_sdk/compounds/context.py +159 -0
- rtls_sdk/compounds/groups.py +126 -0
- rtls_sdk/compounds/nodes.py +176 -0
- rtls_sdk/compounds/reports.py +339 -0
- rtls_sdk/compounds/system.py +43 -0
- rtls_sdk/compounds/tags.py +404 -0
- rtls_sdk/compounds/users.py +143 -0
- rtls_sdk/compounds/zones.py +203 -0
- rtls_sdk/errors.py +238 -0
- rtls_sdk/models/__init__.py +73 -0
- rtls_sdk/models/_base.py +46 -0
- rtls_sdk/models/alarm.py +23 -0
- rtls_sdk/models/anchor.py +25 -0
- rtls_sdk/models/area.py +28 -0
- rtls_sdk/models/association.py +48 -0
- rtls_sdk/models/bulk.py +80 -0
- rtls_sdk/models/company.py +32 -0
- rtls_sdk/models/csv_blob.py +40 -0
- rtls_sdk/models/floorplan.py +42 -0
- rtls_sdk/models/group.py +20 -0
- rtls_sdk/models/heatmap.py +37 -0
- rtls_sdk/models/import_result.py +42 -0
- rtls_sdk/models/node.py +28 -0
- rtls_sdk/models/notification.py +38 -0
- rtls_sdk/models/position.py +66 -0
- rtls_sdk/models/project.py +26 -0
- rtls_sdk/models/pws.py +38 -0
- rtls_sdk/models/report.py +34 -0
- rtls_sdk/models/session_context.py +81 -0
- rtls_sdk/models/site.py +31 -0
- rtls_sdk/models/subscriber.py +68 -0
- rtls_sdk/models/system.py +97 -0
- rtls_sdk/models/system_health.py +36 -0
- rtls_sdk/models/tag.py +48 -0
- rtls_sdk/models/tag_template.py +24 -0
- rtls_sdk/models/user.py +121 -0
- rtls_sdk/models/zone.py +27 -0
- rtls_sdk/models/zone_event.py +21 -0
- rtls_sdk/py.typed +0 -0
- rtls_sdk/resources/__init__.py +49 -0
- rtls_sdk/resources/_base.py +63 -0
- rtls_sdk/resources/alarms.py +108 -0
- rtls_sdk/resources/anchors.py +147 -0
- rtls_sdk/resources/areas.py +78 -0
- rtls_sdk/resources/associations.py +157 -0
- rtls_sdk/resources/auth.py +63 -0
- rtls_sdk/resources/companies.py +79 -0
- rtls_sdk/resources/context.py +50 -0
- rtls_sdk/resources/events.py +149 -0
- rtls_sdk/resources/floorplans.py +283 -0
- rtls_sdk/resources/groups.py +99 -0
- rtls_sdk/resources/logger.py +40 -0
- rtls_sdk/resources/messaging.py +51 -0
- rtls_sdk/resources/nodes.py +157 -0
- rtls_sdk/resources/notifications.py +55 -0
- rtls_sdk/resources/projects.py +67 -0
- rtls_sdk/resources/reports.py +180 -0
- rtls_sdk/resources/sites.py +115 -0
- rtls_sdk/resources/subscribers.py +110 -0
- rtls_sdk/resources/system.py +125 -0
- rtls_sdk/resources/tags.py +370 -0
- rtls_sdk/resources/users.py +275 -0
- rtls_sdk/resources/zones.py +199 -0
- rtls_sdk-0.2.0.dist-info/METADATA +141 -0
- rtls_sdk-0.2.0.dist-info/RECORD +78 -0
- rtls_sdk-0.2.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|
rtls_sdk/models/_base.py
ADDED
|
@@ -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"]
|
rtls_sdk/models/alarm.py
ADDED
|
@@ -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"]
|
rtls_sdk/models/area.py
ADDED
|
@@ -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"]
|