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
rtls_sdk/models/bulk.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""BulkResult — return type for bulk operations.
|
|
2
|
+
|
|
3
|
+
A ``BulkResult[T]`` is the SDK's standard "I tried N things and N1
|
|
4
|
+
succeeded and N2 failed" shape (DESIGN §2.3 / §5.4). Used by every bulk
|
|
5
|
+
method that follows the continue-on-error semantic (the M3 default —
|
|
6
|
+
RESEARCH §9 R1).
|
|
7
|
+
|
|
8
|
+
This is a dataclass, not a pydantic model. ``BulkFailure.error`` carries
|
|
9
|
+
a live ``RtlsError`` instance — pydantic would refuse to validate that
|
|
10
|
+
without contortions, and the user-facing benefit of pydantic (alias-
|
|
11
|
+
tolerant deserialization, JSON dump) doesn't apply here: these objects
|
|
12
|
+
are constructed by the SDK, never parsed from wire format.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any, Generic, TypeVar
|
|
19
|
+
|
|
20
|
+
from ..errors import RtlsError
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class BulkFailure:
|
|
27
|
+
"""One row that failed inside a bulk operation.
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
error
|
|
32
|
+
The typed SDK exception that fired for this row.
|
|
33
|
+
input
|
|
34
|
+
Whatever the caller passed for this row (e.g. a uid string, a
|
|
35
|
+
dict of fields). Useful for retrying the failed subset.
|
|
36
|
+
uid
|
|
37
|
+
Convenience accessor when ``input`` is itself a uid.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
error: RtlsError
|
|
41
|
+
input: Any = None
|
|
42
|
+
uid: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class BulkResult(Generic[T]):
|
|
47
|
+
"""Result of a continue-on-error bulk operation.
|
|
48
|
+
|
|
49
|
+
The SDK accumulates successes and failures separately so the caller
|
|
50
|
+
can react to a partial outcome without parsing exception messages.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> result = client.tags.bulk_delete(["t-1", "t-missing", "t-3"])
|
|
55
|
+
>>> result.success_count, result.failure_count
|
|
56
|
+
(2, 1)
|
|
57
|
+
>>> result.failures[0].error
|
|
58
|
+
NotFound('trackable t-missing not found')
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
succeeded: list[T] = field(default_factory=list)
|
|
62
|
+
failures: list[BulkFailure] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def success_count(self) -> int:
|
|
66
|
+
return len(self.succeeded)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def failure_count(self) -> int:
|
|
70
|
+
return len(self.failures)
|
|
71
|
+
|
|
72
|
+
def __bool__(self) -> bool:
|
|
73
|
+
"""Truthy when nothing failed.
|
|
74
|
+
|
|
75
|
+
Lets callers write ``if result: ...`` for the happy path.
|
|
76
|
+
"""
|
|
77
|
+
return self.failure_count == 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["BulkFailure", "BulkResult"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Company model.
|
|
2
|
+
|
|
3
|
+
The server returns ``Company`` under several wire envelopes (login,
|
|
4
|
+
``/companies``, SSO callback) with different field subsets — see
|
|
5
|
+
RESEARCH §3. The SDK collapses them into one model with optional fields
|
|
6
|
+
that absorb the divergence.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ._base import _BaseModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Company(_BaseModel):
|
|
17
|
+
"""A company / tenant.
|
|
18
|
+
|
|
19
|
+
Many fields are optional because the wire payload varies by endpoint;
|
|
20
|
+
``uid`` and ``name`` are always present.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
uid: str
|
|
24
|
+
name: str
|
|
25
|
+
scope: str | None = None
|
|
26
|
+
role_uid: str | None = None
|
|
27
|
+
role: str | None = None
|
|
28
|
+
taggings_count: int | None = None
|
|
29
|
+
user_params: dict[str, Any] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = ["Company"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""``CsvBlob`` — the SDK's return type for CSV report downloads.
|
|
2
|
+
|
|
3
|
+
Dataclass with a ``filename`` hint (parsed from the server's
|
|
4
|
+
``response-filename`` custom header — RESEARCH §5 quirk #11) plus a
|
|
5
|
+
:meth:`write_to` convenience.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CsvBlob:
|
|
16
|
+
"""An in-memory CSV download.
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
content
|
|
21
|
+
The full CSV bytes. Already-deduped of duplicate header rows
|
|
22
|
+
when the report came back chunked.
|
|
23
|
+
filename
|
|
24
|
+
Server-suggested filename from the ``response-filename`` header,
|
|
25
|
+
or ``None`` if absent. Use it as the default when saving to disk.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
content: bytes
|
|
29
|
+
filename: str | None = None
|
|
30
|
+
|
|
31
|
+
def write_to(self, path: str | Path) -> None:
|
|
32
|
+
"""Save :attr:`content` to ``path``.
|
|
33
|
+
|
|
34
|
+
Convenience helper — equivalent to
|
|
35
|
+
``Path(path).write_bytes(self.content)``.
|
|
36
|
+
"""
|
|
37
|
+
Path(path).write_bytes(self.content)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["CsvBlob"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Floorplan model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._base import _BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Floorplan(_BaseModel):
|
|
11
|
+
"""A floorplan image attached to a sublocation.
|
|
12
|
+
|
|
13
|
+
The server returns ``display_name`` as the user-facing label and
|
|
14
|
+
``url`` as a relative path of the form
|
|
15
|
+
``/uploads/floorplan/image/<uid>/<original_filename>``. There is
|
|
16
|
+
no ``name`` field — the M9 read-only model carried one in error,
|
|
17
|
+
fixed in M10.
|
|
18
|
+
|
|
19
|
+
Pixel dimensions (``width``, ``height``) are server-computed from
|
|
20
|
+
the uploaded image; ``size`` is the byte size.
|
|
21
|
+
|
|
22
|
+
Fields kept permissive (``str | None``, ``Any``) because some
|
|
23
|
+
deployments null-out values the SDK doesn't know what to do with
|
|
24
|
+
yet (e.g. ``img_blob`` is ``None`` on the test server).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
uid: str
|
|
28
|
+
sublocation_uid: str | None = None
|
|
29
|
+
url: str | None = None
|
|
30
|
+
display_name: str | None = None
|
|
31
|
+
image_scale: float | None = None
|
|
32
|
+
image_rotation: float | None = None
|
|
33
|
+
image_offset_x: float | None = None
|
|
34
|
+
image_offset_y: float | None = None
|
|
35
|
+
width: int | None = None
|
|
36
|
+
height: int | None = None
|
|
37
|
+
size: int | None = None
|
|
38
|
+
selected: int | bool | None = None
|
|
39
|
+
img_blob: Any = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["Floorplan"]
|
rtls_sdk/models/group.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Group model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._base import _BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Group(_BaseModel):
|
|
9
|
+
"""A trackable group.
|
|
10
|
+
|
|
11
|
+
Groups are flat collections of trackables. Membership lives on the
|
|
12
|
+
trackable side (``Tag.groups``); the group itself only carries a name
|
|
13
|
+
and metadata.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
uid: str
|
|
17
|
+
name: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["Group"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Heatmap report models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from ._base import _BaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HeatmapInput(_BaseModel):
|
|
14
|
+
"""The request shape echoed back by the heatmap status endpoint."""
|
|
15
|
+
|
|
16
|
+
start: datetime | str | int | None = None
|
|
17
|
+
end: datetime | str | int | None = None
|
|
18
|
+
tag_uids: list[str] = Field(default_factory=list, alias="uids")
|
|
19
|
+
names: list[str] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Heatmap(_BaseModel):
|
|
23
|
+
"""A completed heatmap report.
|
|
24
|
+
|
|
25
|
+
``image_url`` is the URL of the rendered PNG/SVG (built from the
|
|
26
|
+
server's hostname + ``/api/v2/heatmap/{uid}`` + any query the
|
|
27
|
+
server returned).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
uid: str
|
|
31
|
+
image_url: str
|
|
32
|
+
name: str | None = None
|
|
33
|
+
input: HeatmapInput | None = None
|
|
34
|
+
output_metadata: dict[str, Any] = Field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["Heatmap", "HeatmapInput"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Return type for ``nodes.import_``.
|
|
2
|
+
|
|
3
|
+
Built by the SDK after a validate + per-row create loop. Dataclass
|
|
4
|
+
rather than pydantic because the ``failures`` list carries live
|
|
5
|
+
:class:`BulkFailure` instances (which wrap typed exceptions).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
from .bulk import BulkFailure
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ImportResult:
|
|
17
|
+
"""Result of a CSV-style import operation.
|
|
18
|
+
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
valid
|
|
22
|
+
MAC addresses the server's validation endpoint accepted as new.
|
|
23
|
+
invalid
|
|
24
|
+
MAC addresses rejected as malformed or otherwise invalid.
|
|
25
|
+
duplicate
|
|
26
|
+
MAC addresses already present in the server.
|
|
27
|
+
imported_count
|
|
28
|
+
Number of nodes actually created during the per-row loop. Equal
|
|
29
|
+
to ``len(valid) - len(failures)`` after a successful run.
|
|
30
|
+
failures
|
|
31
|
+
Per-row failures (only populated when validation succeeded but
|
|
32
|
+
subsequent ``nodes.create`` calls failed for individual rows).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
valid: list[str] = field(default_factory=list)
|
|
36
|
+
invalid: list[str] = field(default_factory=list)
|
|
37
|
+
duplicate: list[str] = field(default_factory=list)
|
|
38
|
+
imported_count: int = 0
|
|
39
|
+
failures: list[BulkFailure] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["ImportResult"]
|
rtls_sdk/models/node.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Node (hardware tag) model.
|
|
2
|
+
|
|
3
|
+
The same model is used for standalone-list responses and the embedded
|
|
4
|
+
``node`` field inside Trackable. The embedded form carries fewer fields;
|
|
5
|
+
they're declared optional so both parse cleanly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._base import _BaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Node(_BaseModel):
|
|
14
|
+
"""A hardware node (tag/beacon) registered to the system."""
|
|
15
|
+
|
|
16
|
+
uid: str | None = None
|
|
17
|
+
mac_address: str | None = None
|
|
18
|
+
# node_id is an int on the wire (a short numeric RF protocol ID, e.g. 5).
|
|
19
|
+
# M2 read tests assumed string; the live server returns int. Accept both
|
|
20
|
+
# so the model parses cleanly across deployments / mock fixtures.
|
|
21
|
+
node_id: int | str | None = None
|
|
22
|
+
responsive: bool | None = None
|
|
23
|
+
idle: bool | None = None
|
|
24
|
+
cbid: str | None = None
|
|
25
|
+
parent_sublocation_uid: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["Node"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Notification profile and notification-type models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import model_validator
|
|
8
|
+
|
|
9
|
+
from ._base import _BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotificationProfile(_BaseModel):
|
|
13
|
+
"""A notification profile: rules for emitting alerts on alarms."""
|
|
14
|
+
|
|
15
|
+
uid: str
|
|
16
|
+
name: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NotificationType(_BaseModel):
|
|
20
|
+
"""One entry from ``GET /notification_types``.
|
|
21
|
+
|
|
22
|
+
The live server returns this list as bare strings (e.g.
|
|
23
|
+
``"zone_violation"``); some deployments use objects. The model
|
|
24
|
+
accepts either form — a string is wrapped as ``NotificationType(name=...)``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name: str | None = None
|
|
28
|
+
type: str | None = None
|
|
29
|
+
|
|
30
|
+
@model_validator(mode="before")
|
|
31
|
+
@classmethod
|
|
32
|
+
def _normalize(cls, data: Any) -> Any:
|
|
33
|
+
if isinstance(data, str):
|
|
34
|
+
return {"name": data}
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["NotificationProfile", "NotificationType"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Position model with embedded-vs-standalone normalization.
|
|
2
|
+
|
|
3
|
+
The server emits position data under two wire formats (RESEARCH §3):
|
|
4
|
+
|
|
5
|
+
- **Embedded** inside Trackable: ``sl_uid``, ``sl_id``, ``dev_id``, ``mac``, ``ts``.
|
|
6
|
+
- **Standalone** under ``/positions``: ``area_uid``, ``trackable_uid``, ``mac_address``, ``ts``.
|
|
7
|
+
|
|
8
|
+
The SDK normalizes both into one canonical shape with the standalone
|
|
9
|
+
field names. Callers see ``area_uid``, never ``sl_uid``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pydantic import model_validator
|
|
18
|
+
|
|
19
|
+
from .._time import from_epoch_ms
|
|
20
|
+
from ._base import _BaseModel
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Position(_BaseModel):
|
|
24
|
+
"""A position fix for a trackable.
|
|
25
|
+
|
|
26
|
+
Field names are canonical (the standalone-endpoint form). Wire-format
|
|
27
|
+
aliases ``sl_uid`` / ``dev_id`` / ``mac`` are rewritten at parse time
|
|
28
|
+
so consumers don't need to know which endpoint produced the data.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
area_uid: str | None = None
|
|
32
|
+
trackable_uid: str | None = None
|
|
33
|
+
mac_address: str | None = None
|
|
34
|
+
project_uid: str | None = None
|
|
35
|
+
inactive: bool | None = None
|
|
36
|
+
x: float | None = None
|
|
37
|
+
y: float | None = None
|
|
38
|
+
z: float | None = None
|
|
39
|
+
ts: datetime | None = None
|
|
40
|
+
|
|
41
|
+
@model_validator(mode="before")
|
|
42
|
+
@classmethod
|
|
43
|
+
def _rewrite_aliases(cls, data: Any) -> Any:
|
|
44
|
+
if not isinstance(data, dict):
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
out = dict(data)
|
|
48
|
+
|
|
49
|
+
# Embedded-form → canonical-form renames.
|
|
50
|
+
if "area_uid" not in out and "sl_uid" in out:
|
|
51
|
+
out["area_uid"] = out["sl_uid"]
|
|
52
|
+
if "trackable_uid" not in out and "dev_id" in out:
|
|
53
|
+
out["trackable_uid"] = out["dev_id"]
|
|
54
|
+
if "mac_address" not in out and "mac" in out:
|
|
55
|
+
out["mac_address"] = out["mac"]
|
|
56
|
+
|
|
57
|
+
# Server sends ts as epoch ms; pydantic can parse ints to datetime
|
|
58
|
+
# if we hand it a float-seconds value, so convert eagerly.
|
|
59
|
+
ts_raw = out.get("ts")
|
|
60
|
+
if isinstance(ts_raw, (int, float)) and not isinstance(ts_raw, bool):
|
|
61
|
+
out["ts"] = from_epoch_ms(ts_raw)
|
|
62
|
+
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["Position"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Project model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from ._base import _BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Project(_BaseModel):
|
|
13
|
+
"""A project — a unit of access control + scoping.
|
|
14
|
+
|
|
15
|
+
Most operator-level resources (tags, zones, alarms, …) are scoped to
|
|
16
|
+
a project. The currently-active project is set on the SDK via the
|
|
17
|
+
``project_uid=`` constructor argument or ``RTLS_PROJECT_UID`` env var.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
uid: str
|
|
21
|
+
name: str | None = None
|
|
22
|
+
settings: dict[str, Any] | None = None
|
|
23
|
+
users: list[Any] = Field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["Project"]
|
rtls_sdk/models/pws.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""PWS (proximity warning system) event + report models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from ._base import _BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PwsEvent(_BaseModel):
|
|
11
|
+
"""One PWS event row from ``/external_events?event_type[]=pws``.
|
|
12
|
+
|
|
13
|
+
Fields beyond uid/ts are deployment-specific; rely on ``.raw`` for
|
|
14
|
+
anything the SDK doesn't expose explicitly.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
uid: str | None = None
|
|
18
|
+
trackable_uid: str | None = None
|
|
19
|
+
ts: int | float | None = None
|
|
20
|
+
zone_uid: str | None = None
|
|
21
|
+
type: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PwsReport(_BaseModel):
|
|
25
|
+
"""Aggregated PWS report — every page concatenated in memory.
|
|
26
|
+
|
|
27
|
+
For windows that span many pages, prefer
|
|
28
|
+
:meth:`EventsAPI.iter_pws` (lazy) over :meth:`ReportsAPI.pws`
|
|
29
|
+
(eager). The aggregate form is convenient for ad-hoc analysis;
|
|
30
|
+
the lazy form is friendlier to RAM and lets you stop early.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
events: list[PwsEvent] = Field(default_factory=list)
|
|
34
|
+
total_count: int | None = None
|
|
35
|
+
timezone: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["PwsEvent", "PwsReport"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Report and report-type models.
|
|
2
|
+
|
|
3
|
+
M2 ships just the read surface (``reports.list`` / ``reports.get``); the
|
|
4
|
+
heatmap / PWS / CSV report compounds land in M7.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from ._base import _BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ReportType(str, Enum):
|
|
15
|
+
"""Enumerated report types accepted by ``reports.list(..., type=)``."""
|
|
16
|
+
|
|
17
|
+
DISTANCE = "distance"
|
|
18
|
+
ALARMS = "alarms"
|
|
19
|
+
ATTENDANCE = "attendance"
|
|
20
|
+
ZONE_ACTIVITY = "zone_activity"
|
|
21
|
+
HEATMAP = "heatmap"
|
|
22
|
+
PWS = "pws"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Report(_BaseModel):
|
|
26
|
+
"""A generated report's metadata."""
|
|
27
|
+
|
|
28
|
+
uid: str
|
|
29
|
+
name: str | None = None
|
|
30
|
+
type: str | None = None
|
|
31
|
+
status: int | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["Report", "ReportType"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Return type for :meth:`ContextAPI.load` — the M6 mega-method.
|
|
2
|
+
|
|
3
|
+
One call hydrates ~20 entity types. Each is optional (a sub-call failure
|
|
4
|
+
records the exception on :attr:`errors` and leaves that field as its
|
|
5
|
+
default), so a partial-but-useful snapshot is preferable to a blanket
|
|
6
|
+
failure.
|
|
7
|
+
|
|
8
|
+
Dataclass rather than pydantic because :attr:`errors` holds live
|
|
9
|
+
:class:`RtlsError` instances. Construction is SDK-internal; the model
|
|
10
|
+
never round-trips through JSON.
|
|
11
|
+
|
|
12
|
+
Stable sub-resource names (used as ``errors`` dict keys; documented as
|
|
13
|
+
part of the public API contract): ``sites``, ``areas``, ``floorplans``,
|
|
14
|
+
``tags``, ``associations``, ``zones``, ``zone_prototypes``, ``groups``,
|
|
15
|
+
``alarms``, ``notifications``, ``notification_types``, ``subscribers``,
|
|
16
|
+
``system_subscribers``, ``anchors``, ``nodes``, ``users``, ``user_roles``,
|
|
17
|
+
``tag_templates``, ``helper_link``, ``monitor_link``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from ..errors import RtlsError
|
|
27
|
+
from .alarm import Alarm
|
|
28
|
+
from .anchor import Anchor
|
|
29
|
+
from .area import Area
|
|
30
|
+
from .association import Association
|
|
31
|
+
from .floorplan import Floorplan
|
|
32
|
+
from .group import Group
|
|
33
|
+
from .node import Node
|
|
34
|
+
from .notification import NotificationProfile, NotificationType
|
|
35
|
+
from .site import Site
|
|
36
|
+
from .subscriber import Subscriber, SystemSubscriber
|
|
37
|
+
from .tag import Tag
|
|
38
|
+
from .tag_template import TagTemplate
|
|
39
|
+
from .user import Role, User
|
|
40
|
+
from .zone import Zone
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SessionContext:
|
|
45
|
+
"""Hydrated snapshot of the deployment under the current scope."""
|
|
46
|
+
|
|
47
|
+
sites: list[Site] = field(default_factory=list)
|
|
48
|
+
areas: list[Area] = field(default_factory=list)
|
|
49
|
+
floorplans: list[Floorplan] = field(default_factory=list)
|
|
50
|
+
tags: list[Tag] = field(default_factory=list)
|
|
51
|
+
associations: list[Association] = field(default_factory=list)
|
|
52
|
+
zones: list[Zone] = field(default_factory=list)
|
|
53
|
+
zone_prototypes: list[dict[str, Any]] = field(default_factory=list)
|
|
54
|
+
groups: list[Group] = field(default_factory=list)
|
|
55
|
+
alarms: list[Alarm] = field(default_factory=list)
|
|
56
|
+
notifications: list[NotificationProfile] = field(default_factory=list)
|
|
57
|
+
notification_types: list[NotificationType] = field(default_factory=list)
|
|
58
|
+
subscribers: list[Subscriber] = field(default_factory=list)
|
|
59
|
+
system_subscribers: list[SystemSubscriber] = field(default_factory=list)
|
|
60
|
+
anchors: list[Anchor] = field(default_factory=list)
|
|
61
|
+
nodes: list[Node] = field(default_factory=list)
|
|
62
|
+
users: list[User] = field(default_factory=list)
|
|
63
|
+
user_roles: list[Role] = field(default_factory=list)
|
|
64
|
+
tag_templates: list[TagTemplate] = field(default_factory=list)
|
|
65
|
+
monitor_link: str | None = None
|
|
66
|
+
helper_link: str | None = None
|
|
67
|
+
partial: bool = False
|
|
68
|
+
errors: dict[str, RtlsError] = field(default_factory=dict)
|
|
69
|
+
loaded_at: datetime | None = None
|
|
70
|
+
|
|
71
|
+
def errors_summary(self) -> dict[str, str]:
|
|
72
|
+
"""Return a JSON-serializable view of :attr:`errors`.
|
|
73
|
+
|
|
74
|
+
``RtlsError`` instances aren't directly serializable; this
|
|
75
|
+
method converts each to a short ``"<type>: <message>"`` string,
|
|
76
|
+
suitable for logging or for caching the snapshot to disk.
|
|
77
|
+
"""
|
|
78
|
+
return {name: f"{type(err).__name__}: {err}" for name, err in self.errors.items()}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["SessionContext"]
|
rtls_sdk/models/site.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Site (location) model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._base import _BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Site(_BaseModel):
|
|
11
|
+
"""A top-level physical location.
|
|
12
|
+
|
|
13
|
+
Sites contain :class:`Area` rows (sublocations). Fields beyond
|
|
14
|
+
``uid`` / ``name`` are optional and surface what the server returns
|
|
15
|
+
on a successful create / read.
|
|
16
|
+
|
|
17
|
+
``shift`` is typed as ``Any`` because the wire format varies by
|
|
18
|
+
deployment (some servers emit ``{}`` / ``{...}``, others emit
|
|
19
|
+
``[]`` / ``[...]`` depending on whether shifts are configured).
|
|
20
|
+
The SDK doesn't take sides — read it as-is.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
uid: str
|
|
24
|
+
name: str | None = None
|
|
25
|
+
shift: Any = None
|
|
26
|
+
address: str | None = None
|
|
27
|
+
country: str | None = None
|
|
28
|
+
timezone: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["Site"]
|