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,125 @@
|
|
|
1
|
+
"""System / host resource sub-client.
|
|
2
|
+
|
|
3
|
+
These endpoints expose server-health diagnostics. They share an
|
|
4
|
+
``RtlsClient`` with the rest of the SDK but use simple object responses
|
|
5
|
+
(no envelope wrapping).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..compounds import system as _compound
|
|
13
|
+
from ..models import Connection, Host, SystemHealth, Uptime, Version, WsHost
|
|
14
|
+
from ._base import ResourceAPI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SystemAPI(ResourceAPI):
|
|
18
|
+
"""Host info, uptime, connections, and server version metadata.
|
|
19
|
+
|
|
20
|
+
The aggregated :meth:`health` view (fan-out across host + uptime +
|
|
21
|
+
connections) is implemented in M5; M2 only exposes the individual
|
|
22
|
+
accessors.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def host(self) -> Host:
|
|
26
|
+
"""Return the server's hostname + status snapshot."""
|
|
27
|
+
response = self._get("/api/v2/host")
|
|
28
|
+
return Host.from_wire(_as_dict(response.json()))
|
|
29
|
+
|
|
30
|
+
def uptime(self) -> list[Uptime]:
|
|
31
|
+
"""Return per-service uptime rows."""
|
|
32
|
+
response = self._get("/api/v2/host/uptime")
|
|
33
|
+
return [Uptime.from_wire(_as_dict(item)) for item in _as_list(response.json())]
|
|
34
|
+
|
|
35
|
+
def connections(self) -> list[Connection]:
|
|
36
|
+
"""Return active server connections."""
|
|
37
|
+
response = self._get("/api/v2/host/connections")
|
|
38
|
+
return [Connection.from_wire(_as_dict(item)) for item in _as_list(response.json())]
|
|
39
|
+
|
|
40
|
+
def version(self) -> Version:
|
|
41
|
+
"""Return server version info from ``GET /version``.
|
|
42
|
+
|
|
43
|
+
Note: ``/version`` is one of the few endpoints not under the
|
|
44
|
+
``/api/v2/`` prefix.
|
|
45
|
+
"""
|
|
46
|
+
response = self._get("/version")
|
|
47
|
+
return Version.from_wire(_as_dict(response.json()))
|
|
48
|
+
|
|
49
|
+
def beta_version(self) -> Version:
|
|
50
|
+
"""Return the beta-channel version info."""
|
|
51
|
+
response = self._get("/api/v2/beta/version")
|
|
52
|
+
return Version.from_wire(_as_dict(response.json()))
|
|
53
|
+
|
|
54
|
+
def health(self) -> SystemHealth:
|
|
55
|
+
"""Aggregate host + uptime + connections in one snapshot.
|
|
56
|
+
|
|
57
|
+
Each sub-call is independent: a failure on one sets that field
|
|
58
|
+
to ``None`` and records the exception in
|
|
59
|
+
``SystemHealth.errors``, leaving the rest of the snapshot
|
|
60
|
+
intact. ``partial=True`` when any sub-call failed.
|
|
61
|
+
|
|
62
|
+
Sequential in v1 (sync SDK); a v2 async surface would fan these
|
|
63
|
+
out in parallel.
|
|
64
|
+
"""
|
|
65
|
+
return _compound.collect_system_health(self._client)
|
|
66
|
+
|
|
67
|
+
def ws_host(self) -> WsHost:
|
|
68
|
+
"""Return the WebSocket hostname the server expects clients to dial.
|
|
69
|
+
|
|
70
|
+
This is the single REST hop needed before opening a WS connection
|
|
71
|
+
(deferred to v2).
|
|
72
|
+
"""
|
|
73
|
+
response = self._get("/api/v2/wshost/")
|
|
74
|
+
return WsHost.from_wire(_as_dict(response.json()))
|
|
75
|
+
|
|
76
|
+
def helper_link(self) -> str | None:
|
|
77
|
+
"""URL of the help portal (deployment-specific).
|
|
78
|
+
|
|
79
|
+
Returned by ``GET /info/helper``. Body shape is loose across
|
|
80
|
+
deployments; this method surfaces whichever URL-shaped string
|
|
81
|
+
the server returned, or ``None`` if absent. Used by
|
|
82
|
+
:meth:`ContextAPI.load` (M6).
|
|
83
|
+
"""
|
|
84
|
+
response = self._get("/api/v2/info/helper")
|
|
85
|
+
return _extract_url(response.json())
|
|
86
|
+
|
|
87
|
+
def monitor_link(self) -> str | None:
|
|
88
|
+
"""URL of the site-monitor portal (deployment-specific).
|
|
89
|
+
|
|
90
|
+
Returned by ``GET /info/site_monitor``. Same loose-shape
|
|
91
|
+
conventions as :meth:`helper_link`.
|
|
92
|
+
"""
|
|
93
|
+
response = self._get("/api/v2/info/site_monitor")
|
|
94
|
+
return _extract_url(response.json())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _extract_url(payload: Any) -> str | None:
|
|
98
|
+
"""Pick a URL-shaped string out of a loose info-endpoint payload."""
|
|
99
|
+
if isinstance(payload, str):
|
|
100
|
+
return payload
|
|
101
|
+
if isinstance(payload, dict):
|
|
102
|
+
for key in ("url", "link", "href", "value", "data"):
|
|
103
|
+
value = payload.get(key)
|
|
104
|
+
if isinstance(value, str) and value:
|
|
105
|
+
return value
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _as_dict(payload: Any) -> dict[str, Any]:
|
|
110
|
+
if isinstance(payload, dict):
|
|
111
|
+
return payload
|
|
112
|
+
return {"data": payload}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _as_list(payload: Any) -> list[Any]:
|
|
116
|
+
if isinstance(payload, list):
|
|
117
|
+
return list(payload)
|
|
118
|
+
if isinstance(payload, dict):
|
|
119
|
+
for key in ("uptime", "connections", "data"):
|
|
120
|
+
if key in payload and isinstance(payload[key], list):
|
|
121
|
+
return list(payload[key])
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["SystemAPI"]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Trackable / tag resource sub-client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._envelope import unwrap_resource, unwrap_resource_single
|
|
9
|
+
from .._time import to_epoch_ms
|
|
10
|
+
from ..compounds import tags as _compound
|
|
11
|
+
from ..compounds.tags import UNSET, _Unset
|
|
12
|
+
from ..errors import NotFound
|
|
13
|
+
from ..models import BulkResult, Position, Tag, TagTemplate
|
|
14
|
+
from ._base import ResourceAPI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TagsAPI(ResourceAPI):
|
|
18
|
+
"""List, get, and (later) mutate trackables.
|
|
19
|
+
|
|
20
|
+
The wire name for trackables is ``trackable_objects`` — exposed as
|
|
21
|
+
``tags`` here because "tag" is what operators actually call them.
|
|
22
|
+
|
|
23
|
+
Note on method order: ``list`` is defined last in this class so that
|
|
24
|
+
mypy resolves ``list[Tag]`` annotations in other methods to the
|
|
25
|
+
builtin ``list`` and not to ``TagsAPI.list``. (Python class-scope
|
|
26
|
+
shadowing limitation, even with PEP-563 deferred annotations.)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def get(self, uid: str) -> Tag:
|
|
30
|
+
"""Fetch a single trackable by uid.
|
|
31
|
+
|
|
32
|
+
The server returns ``{trackableObjects: [t]}`` even for single
|
|
33
|
+
gets — a one-element list wrapper. The SDK unwraps it. An empty
|
|
34
|
+
wrapper (meaning the uid isn't valid) raises :class:`NotFound`.
|
|
35
|
+
"""
|
|
36
|
+
response = self._get(f"/api/v2/trackable_objects/{uid}")
|
|
37
|
+
payload = response.json()
|
|
38
|
+
try:
|
|
39
|
+
item = unwrap_resource_single(payload, "trackableObjects")
|
|
40
|
+
except ValueError as exc:
|
|
41
|
+
raise NotFound(
|
|
42
|
+
f"trackable {uid!r} not found",
|
|
43
|
+
status_code=response.status_code,
|
|
44
|
+
request_url=str(response.request.url),
|
|
45
|
+
request_method=response.request.method,
|
|
46
|
+
) from exc
|
|
47
|
+
return Tag.from_wire(item)
|
|
48
|
+
|
|
49
|
+
def list_active_for_period(
|
|
50
|
+
self,
|
|
51
|
+
start: datetime,
|
|
52
|
+
end: datetime,
|
|
53
|
+
*,
|
|
54
|
+
sublocation_uid: str,
|
|
55
|
+
) -> list[Tag]:
|
|
56
|
+
"""Trackables active in ``[start, end]`` inside one sublocation.
|
|
57
|
+
|
|
58
|
+
Timestamps go on the wire as epoch milliseconds.
|
|
59
|
+
"""
|
|
60
|
+
params = {
|
|
61
|
+
"sublocation_uid": sublocation_uid,
|
|
62
|
+
"from": str(to_epoch_ms(start, arg_name="start")),
|
|
63
|
+
"to": str(to_epoch_ms(end, arg_name="end")),
|
|
64
|
+
}
|
|
65
|
+
response = self._get("/api/v2/trackable_objects/active_for_period", params=params)
|
|
66
|
+
items = unwrap_resource(response.json(), "trackableObjects")
|
|
67
|
+
return [Tag.from_wire(item) for item in items]
|
|
68
|
+
|
|
69
|
+
def list_existed_for_period(self, start: datetime, end: datetime) -> list[Tag]:
|
|
70
|
+
"""Trackables that existed at any point in ``[start, end]``."""
|
|
71
|
+
params = {
|
|
72
|
+
"from": str(to_epoch_ms(start, arg_name="start")),
|
|
73
|
+
"to": str(to_epoch_ms(end, arg_name="end")),
|
|
74
|
+
}
|
|
75
|
+
response = self._get("/api/v2/trackable_objects/existed_for_period", params=params)
|
|
76
|
+
items = unwrap_resource(response.json(), "trackableObjects")
|
|
77
|
+
return [Tag.from_wire(item) for item in items]
|
|
78
|
+
|
|
79
|
+
def list_templates(self) -> list[TagTemplate]:
|
|
80
|
+
"""Return all tag-creation templates available on the server.
|
|
81
|
+
|
|
82
|
+
Server-side templates are pre-baked configurations operators reuse
|
|
83
|
+
when creating new trackables.
|
|
84
|
+
"""
|
|
85
|
+
response = self._get("/api/v2/trackable_objects/templates")
|
|
86
|
+
payload = response.json()
|
|
87
|
+
# The wire envelope key is ``trackable_templates`` (snake_case)
|
|
88
|
+
# rather than the camelCase used by ``trackableObjects``.
|
|
89
|
+
items = unwrap_resource(payload, "trackable_templates")
|
|
90
|
+
return [TagTemplate.from_wire(item) for item in items]
|
|
91
|
+
|
|
92
|
+
def last_position(self, uid: str) -> Position | None:
|
|
93
|
+
"""Return the last cached position for a trackable, if known.
|
|
94
|
+
|
|
95
|
+
The response envelope is ``{trackablePositionFromState}`` — a
|
|
96
|
+
single position object, not a list (RESEARCH §3). Returns
|
|
97
|
+
``None`` if the server has no cached position.
|
|
98
|
+
"""
|
|
99
|
+
response = self._get(f"/api/v2/trackable_objects/position_from_state/{uid}")
|
|
100
|
+
payload = response.json()
|
|
101
|
+
if not isinstance(payload, dict):
|
|
102
|
+
return None
|
|
103
|
+
for key in ("trackablePositionFromState", "trackable_position_from_state"):
|
|
104
|
+
if key in payload and isinstance(payload[key], dict):
|
|
105
|
+
return Position.from_wire(payload[key])
|
|
106
|
+
# Some deployments return the position object directly.
|
|
107
|
+
if "x" in payload or "ts" in payload:
|
|
108
|
+
return Position.from_wire(payload)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def create_raw(self, name: str, **fields: Any) -> Tag:
|
|
112
|
+
"""Create a tag (single REST call, no zone / group plumbing).
|
|
113
|
+
|
|
114
|
+
**Advanced.** Most users want :meth:`create` (added in M4) which
|
|
115
|
+
also handles attached-zone and group binding automatically.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
name
|
|
120
|
+
Required. The tag's name.
|
|
121
|
+
**fields
|
|
122
|
+
Any other field the server's ``createTrackObject`` schema
|
|
123
|
+
accepts: ``display_name``, ``description``, ``type``,
|
|
124
|
+
``category``, ``position_update_interval``, ``self_zone_uid``,
|
|
125
|
+
``buffer``, ``tag_config``, ``acc_config``, ``bt_config``,
|
|
126
|
+
``sensor_config``, ``user_params``, ``custom_info``,
|
|
127
|
+
``t2t_no_alarm_uid``. Unknown fields are stripped silently
|
|
128
|
+
by the server's Joi default.
|
|
129
|
+
|
|
130
|
+
Notes
|
|
131
|
+
-----
|
|
132
|
+
**Tags do NOT carry a `mac_address`.** The MAC belongs to a
|
|
133
|
+
:class:`Node` (the hardware), bound to a tag via an
|
|
134
|
+
:class:`Association` (`trackable_objects_associations`). Passing
|
|
135
|
+
``mac_address=`` here is a silent no-op — the server drops it.
|
|
136
|
+
If your workflow is "I have a MAC and want a tag bound to it,"
|
|
137
|
+
call :meth:`NodesAPI.create` + :meth:`AssociationsAPI.create`
|
|
138
|
+
in sequence. A compound ``tags.create_with_node`` that wraps
|
|
139
|
+
the saga is planned for a future milestone.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
Tag
|
|
144
|
+
The created tag as returned by the server.
|
|
145
|
+
"""
|
|
146
|
+
body: dict[str, Any] = {"name": name, **fields}
|
|
147
|
+
response = self._post("/api/v2/trackable_objects", json=body)
|
|
148
|
+
payload = response.json()
|
|
149
|
+
try:
|
|
150
|
+
item = unwrap_resource_single(payload, "trackableObjects")
|
|
151
|
+
except ValueError:
|
|
152
|
+
item = payload if isinstance(payload, dict) else {}
|
|
153
|
+
return Tag.from_wire(item)
|
|
154
|
+
|
|
155
|
+
def update_raw(self, uid: str, **fields: Any) -> Tag:
|
|
156
|
+
"""Update a trackable (single REST call).
|
|
157
|
+
|
|
158
|
+
**Advanced.** Most users want :meth:`update` (added in M4) which
|
|
159
|
+
diff-applies zone/group changes alongside the field updates.
|
|
160
|
+
"""
|
|
161
|
+
response = self._put(f"/api/v2/trackable_objects/{uid}", json=fields)
|
|
162
|
+
payload = response.json()
|
|
163
|
+
try:
|
|
164
|
+
item = unwrap_resource_single(payload, "trackableObjects")
|
|
165
|
+
except ValueError:
|
|
166
|
+
item = payload if isinstance(payload, dict) else {}
|
|
167
|
+
return Tag.from_wire(item)
|
|
168
|
+
|
|
169
|
+
def delete_raw(self, uid: str) -> None:
|
|
170
|
+
"""Delete a trackable (single REST call, no zone-cascade).
|
|
171
|
+
|
|
172
|
+
**Advanced.** Most users want :meth:`delete` (added in M4) which
|
|
173
|
+
also deletes the trackable's attached zone if any.
|
|
174
|
+
"""
|
|
175
|
+
self._delete(f"/api/v2/trackable_objects/{uid}")
|
|
176
|
+
|
|
177
|
+
def replace_groups(self, uid: str, group_uids: list[str], *, no_notify: bool = True) -> None:
|
|
178
|
+
"""Replace the set of groups a trackable belongs to.
|
|
179
|
+
|
|
180
|
+
Two server paths exist:
|
|
181
|
+
|
|
182
|
+
- ``PUT /trackable_objects/{uid}/groups`` with a bare-array body
|
|
183
|
+
(per RESEARCH §5 quirk #15 and the JS reference client).
|
|
184
|
+
- ``PUT /trackable_objects/{uid}`` with ``{"groups": [...]}`` in
|
|
185
|
+
the body (the standard tag-update endpoint).
|
|
186
|
+
|
|
187
|
+
The live test server treats the dedicated ``/groups`` endpoint
|
|
188
|
+
as a silent no-op (returns 204, doesn't persist) — only the
|
|
189
|
+
tag-update path actually writes membership. The SDK uses the
|
|
190
|
+
tag-update path because it's the one that works.
|
|
191
|
+
|
|
192
|
+
``no_notify`` is accepted for API stability but ignored on the
|
|
193
|
+
tag-update path; the server doesn't broadcast a group-change
|
|
194
|
+
event from there.
|
|
195
|
+
"""
|
|
196
|
+
del no_notify # kept for signature stability; no analog on the working path
|
|
197
|
+
self._put(f"/api/v2/trackable_objects/{uid}", json={"groups": group_uids})
|
|
198
|
+
|
|
199
|
+
def create(
|
|
200
|
+
self,
|
|
201
|
+
name: str,
|
|
202
|
+
*,
|
|
203
|
+
attached_zone: dict[str, Any] | None = None,
|
|
204
|
+
groups: list[str] | None = None,
|
|
205
|
+
**fields: Any,
|
|
206
|
+
) -> Tag:
|
|
207
|
+
"""Create a tag, optionally attaching a zone and group memberships.
|
|
208
|
+
|
|
209
|
+
One call collapses up to four REST round-trips (create tag,
|
|
210
|
+
create bound zone, replace groups, update tag with
|
|
211
|
+
``self_zone_uid``). On any sub-step failure the SDK attempts to
|
|
212
|
+
roll back the created tag and zone; if a rollback fails, the
|
|
213
|
+
raised :class:`PartialFailureError` carries the orphaned uid in
|
|
214
|
+
its ``partial_result`` so the caller can clean up.
|
|
215
|
+
|
|
216
|
+
Step names (for ``PartialFailureError.failed_step``):
|
|
217
|
+
``create_tag``, ``create_zone``, ``replace_groups``,
|
|
218
|
+
``update_tag_with_zone_uid``.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
name
|
|
223
|
+
Required tag name.
|
|
224
|
+
attached_zone
|
|
225
|
+
Optional zone spec dict — ``shape``, geometry, etc. The SDK
|
|
226
|
+
stamps ``trackable_object_uid`` and ``dynamic=True`` onto
|
|
227
|
+
it automatically.
|
|
228
|
+
groups
|
|
229
|
+
Optional list of group uids the tag should belong to.
|
|
230
|
+
**fields
|
|
231
|
+
Other tag fields the ``createTrackObject`` schema accepts
|
|
232
|
+
(``display_name``, ``type``, ``category``, etc.). **Not
|
|
233
|
+
`mac_address`** — see Notes below.
|
|
234
|
+
|
|
235
|
+
Notes
|
|
236
|
+
-----
|
|
237
|
+
Tags do NOT carry a ``mac_address``. The MAC belongs to a
|
|
238
|
+
:class:`Node`, bound via an :class:`Association`. See the
|
|
239
|
+
Notes on :meth:`create_raw` for how to bind hardware to a tag.
|
|
240
|
+
|
|
241
|
+
Examples
|
|
242
|
+
--------
|
|
243
|
+
>>> tag = client.tags.create(
|
|
244
|
+
... name="Forklift 7",
|
|
245
|
+
... attached_zone={"shape": "circle", "geometry": {"radius": 3000}},
|
|
246
|
+
... groups=["g-fleet"],
|
|
247
|
+
... )
|
|
248
|
+
"""
|
|
249
|
+
return _compound.create_tag(
|
|
250
|
+
self._client,
|
|
251
|
+
name,
|
|
252
|
+
attached_zone=attached_zone,
|
|
253
|
+
groups=groups,
|
|
254
|
+
**fields,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def update(
|
|
258
|
+
self,
|
|
259
|
+
uid: str,
|
|
260
|
+
*,
|
|
261
|
+
attached_zone: dict[str, Any] | None | _Unset = UNSET,
|
|
262
|
+
groups: list[str] | None | _Unset = UNSET,
|
|
263
|
+
**fields: Any,
|
|
264
|
+
) -> Tag:
|
|
265
|
+
"""Diff-driven update of a tag's fields, attached zone, and groups.
|
|
266
|
+
|
|
267
|
+
``attached_zone`` / ``groups`` accept three forms:
|
|
268
|
+
|
|
269
|
+
- default (``...`` — sentinel): leave unchanged.
|
|
270
|
+
- ``None``: remove (delete bound zone / clear groups).
|
|
271
|
+
- dict / list: set or replace.
|
|
272
|
+
|
|
273
|
+
Step names: ``load_current_tag``, ``delete_attached_zone``,
|
|
274
|
+
``create_attached_zone``, ``update_attached_zone``,
|
|
275
|
+
``replace_groups``, ``update_tag``.
|
|
276
|
+
|
|
277
|
+
Unlike :meth:`create`, partial updates cannot be unwound
|
|
278
|
+
cleanly. On failure the SDK raises :class:`PartialFailureError`
|
|
279
|
+
listing the ``completed_steps`` so the caller can reconcile.
|
|
280
|
+
"""
|
|
281
|
+
return _compound.update_tag(
|
|
282
|
+
self._client,
|
|
283
|
+
uid,
|
|
284
|
+
attached_zone=attached_zone,
|
|
285
|
+
groups=groups,
|
|
286
|
+
**fields,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def delete(self, target: Tag | str | list[Tag | str]) -> BulkResult[None] | None:
|
|
290
|
+
"""Delete one or many tags, cascading to attached zones.
|
|
291
|
+
|
|
292
|
+
- Single uid (``str``) or :class:`Tag`: returns ``None``, raises
|
|
293
|
+
:class:`PartialFailureError` on failure.
|
|
294
|
+
- List: returns :class:`BulkResult` with per-item errors; an
|
|
295
|
+
error on one row does NOT abort the others.
|
|
296
|
+
|
|
297
|
+
Passing :class:`Tag` instances skips the per-tag GET that would
|
|
298
|
+
otherwise be needed to discover ``self_zone_uid``.
|
|
299
|
+
|
|
300
|
+
Step names (single-target variant): ``load_tag``,
|
|
301
|
+
``delete_attached_zone``, ``delete_tag``.
|
|
302
|
+
"""
|
|
303
|
+
if isinstance(target, list):
|
|
304
|
+
return _compound.bulk_delete_tags(self._client, target)
|
|
305
|
+
_compound.delete_tag(self._client, target)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
def bulk_update(self, tag_uids: list[str], changes: dict[str, Any]) -> BulkResult[Tag]:
|
|
309
|
+
"""Apply the same updates to many tags, continue-on-error.
|
|
310
|
+
|
|
311
|
+
Each tag is updated via :meth:`update` (so ``attached_zone`` and
|
|
312
|
+
``groups`` inside ``changes`` get the same diff semantics as the
|
|
313
|
+
single-tag form). Failures land in ``BulkResult.failures``.
|
|
314
|
+
"""
|
|
315
|
+
return _compound.bulk_update_tags(self._client, tag_uids, dict(changes))
|
|
316
|
+
|
|
317
|
+
def list_active_in_period(
|
|
318
|
+
self,
|
|
319
|
+
sublocation_uid: str,
|
|
320
|
+
start: datetime,
|
|
321
|
+
end: datetime,
|
|
322
|
+
) -> list[Tag]:
|
|
323
|
+
"""Tags active in ``[start, end]`` inside one sublocation.
|
|
324
|
+
|
|
325
|
+
Inner-joins ``trackable_objects/active_for_period`` with the
|
|
326
|
+
active-association list — the union the JS reference client
|
|
327
|
+
manually composes (RESEARCH §4 "Load trackables with active
|
|
328
|
+
associations for a period").
|
|
329
|
+
"""
|
|
330
|
+
return _compound.list_active_in_period(self._client, sublocation_uid, start, end)
|
|
331
|
+
|
|
332
|
+
def list(self, *, include_removed: bool = False, company_scope: bool = False) -> list[Tag]:
|
|
333
|
+
"""Return active trackables in the current project scope.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
include_removed
|
|
338
|
+
Include soft-deleted trackables. Defaults to ``False``.
|
|
339
|
+
company_scope
|
|
340
|
+
If ``True``, list across the whole company instead of the
|
|
341
|
+
currently-scoped project.
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
list[Tag]
|
|
346
|
+
Possibly empty.
|
|
347
|
+
|
|
348
|
+
Examples
|
|
349
|
+
--------
|
|
350
|
+
>>> tags = client.tags.list()
|
|
351
|
+
>>> [t.name for t in tags]
|
|
352
|
+
['Forklift 7', 'Pallet jack 12']
|
|
353
|
+
"""
|
|
354
|
+
if company_scope:
|
|
355
|
+
path = "/api/v2/trackable_objects/all"
|
|
356
|
+
params = {"include_removed": "true"} if include_removed else None
|
|
357
|
+
elif include_removed:
|
|
358
|
+
path = "/api/v2/trackable_objects/all"
|
|
359
|
+
params = {"include_removed": "true"}
|
|
360
|
+
else:
|
|
361
|
+
path = "/api/v2/trackable_objects"
|
|
362
|
+
params = None
|
|
363
|
+
|
|
364
|
+
response = self._get(path, params=params)
|
|
365
|
+
payload = response.json()
|
|
366
|
+
items = unwrap_resource(payload, "trackableObjects")
|
|
367
|
+
return [Tag.from_wire(item) for item in items]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
__all__ = ["TagsAPI"]
|