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,68 @@
|
|
|
1
|
+
"""Subscriber and system-notification subscriber models.
|
|
2
|
+
|
|
3
|
+
These are distinct entities served by separate endpoints with different
|
|
4
|
+
envelope keys (RESEARCH §3 Subscriber normalization). The SDK keeps them
|
|
5
|
+
as two types rather than collapsing — they have different concerns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import Field, model_validator
|
|
13
|
+
|
|
14
|
+
from ._base import _BaseModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SubscriberAddresses(_BaseModel):
|
|
18
|
+
"""Contact addresses on a subscriber profile.
|
|
19
|
+
|
|
20
|
+
The server emits these fields as either a list of strings or a bare
|
|
21
|
+
string depending on the endpoint / version (RESEARCH §1 documented
|
|
22
|
+
arrays; the live server has been observed returning strings). The
|
|
23
|
+
model coerces both shapes into a list of strings so callers don't
|
|
24
|
+
have to branch.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
email: list[str] = Field(default_factory=list)
|
|
28
|
+
phone: list[str] = Field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
@model_validator(mode="before")
|
|
31
|
+
@classmethod
|
|
32
|
+
def _coerce_to_lists(cls, data: Any) -> Any:
|
|
33
|
+
if not isinstance(data, dict):
|
|
34
|
+
return data
|
|
35
|
+
out = dict(data)
|
|
36
|
+
for key in ("email", "phone"):
|
|
37
|
+
value = out.get(key)
|
|
38
|
+
if value is None:
|
|
39
|
+
out[key] = []
|
|
40
|
+
elif isinstance(value, str):
|
|
41
|
+
out[key] = [value] if value else []
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Subscriber(_BaseModel):
|
|
46
|
+
"""An alarm-notifications subscriber.
|
|
47
|
+
|
|
48
|
+
Returned under ``{subscribers: [...]}`` from ``GET /subscribers/``.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
uid: str | None = None
|
|
52
|
+
name: str | None = None
|
|
53
|
+
addresses: SubscriberAddresses | None = None
|
|
54
|
+
notifications: list[Any] = Field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SystemSubscriber(_BaseModel):
|
|
58
|
+
"""A system-event subscriber — distinct from alarm subscribers.
|
|
59
|
+
|
|
60
|
+
Returned under ``{systemNotificationSubscriber: [...]}`` from
|
|
61
|
+
``GET /system_notifications/``.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
user_uid: str | None = None
|
|
65
|
+
notifications: list[Any] = Field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["Subscriber", "SubscriberAddresses", "SystemSubscriber"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Host / uptime / connection / capabilities / version models."""
|
|
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 Host(_BaseModel):
|
|
13
|
+
"""Host info — hostname + serverStatus (disk, memory, …).
|
|
14
|
+
|
|
15
|
+
The ``server_status`` field's shape varies by deployment and Linux
|
|
16
|
+
distribution; surface it as a dict and let ``.raw`` carry the rest.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
hostname: str | None = None
|
|
20
|
+
server_status: dict[str, Any] | None = Field(default=None, alias="serverStatus")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Uptime(_BaseModel):
|
|
24
|
+
"""A row from ``GET /host/uptime`` — service name + uptime data."""
|
|
25
|
+
|
|
26
|
+
name: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Connection(_BaseModel):
|
|
30
|
+
"""A row from ``GET /host/connections`` — active server connection."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Capabilities(_BaseModel):
|
|
34
|
+
"""SDK-detected server capabilities — SSO availability + API version.
|
|
35
|
+
|
|
36
|
+
Built by :meth:`AuthAPI.capabilities`. The ``sso`` flag reflects whether
|
|
37
|
+
``GET /auth/sso/log_in`` resolves; ``api_version`` is ``"2.1"`` when SSO
|
|
38
|
+
is reachable, ``"2.0"`` otherwise (matches the JS reference client's
|
|
39
|
+
fallback heuristic, RESEARCH §4 "Probe SSO support").
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
sso: bool
|
|
43
|
+
api_version: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Version(_BaseModel):
|
|
47
|
+
"""Server version info from ``GET /version``.
|
|
48
|
+
|
|
49
|
+
The live server emits a nested shape: ``{api: {version}, rtls_frontend:
|
|
50
|
+
{version}, custom_product_attributes: {...}}``. Older deployments may
|
|
51
|
+
use the flat shape RESEARCH originally documented. All fields are
|
|
52
|
+
optional; the convenience properties below pull the version string
|
|
53
|
+
out of whichever form the server sent.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
api_version: str | None = None
|
|
57
|
+
mysql_version: str | None = None
|
|
58
|
+
node_version: str | None = None
|
|
59
|
+
modules: list[str] | dict[str, Any] | None = None
|
|
60
|
+
user_modules: list[str] | dict[str, Any] | None = None
|
|
61
|
+
api: dict[str, Any] | None = None
|
|
62
|
+
rtls_frontend: dict[str, Any] | None = None
|
|
63
|
+
custom_product_attributes: dict[str, Any] | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def api_version_str(self) -> str | None:
|
|
67
|
+
"""Return the API version as a string, regardless of wire shape."""
|
|
68
|
+
if self.api_version:
|
|
69
|
+
return self.api_version
|
|
70
|
+
if self.api and isinstance(self.api.get("version"), str):
|
|
71
|
+
value: str = self.api["version"]
|
|
72
|
+
return value
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def frontend_version_str(self) -> str | None:
|
|
77
|
+
"""Return the frontend version as a string, if available."""
|
|
78
|
+
if self.rtls_frontend and isinstance(self.rtls_frontend.get("version"), str):
|
|
79
|
+
value: str = self.rtls_frontend["version"]
|
|
80
|
+
return value
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class WsHost(_BaseModel):
|
|
85
|
+
"""The hostname for the WebSocket endpoint, returned by ``GET /wshost/``."""
|
|
86
|
+
|
|
87
|
+
ws_host: str | None = Field(default=None, alias="wsHost")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"Capabilities",
|
|
92
|
+
"Connection",
|
|
93
|
+
"Host",
|
|
94
|
+
"Uptime",
|
|
95
|
+
"Version",
|
|
96
|
+
"WsHost",
|
|
97
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Return type for ``system.health()``.
|
|
2
|
+
|
|
3
|
+
Aggregates three independent server-health calls into one struct with
|
|
4
|
+
per-call error isolation (DESIGN §4.17 / §5 R4). Dataclass because the
|
|
5
|
+
``errors`` dict carries live :class:`RtlsError` instances.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
from ..errors import RtlsError
|
|
13
|
+
from .system import Connection, Host, Uptime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SystemHealth:
|
|
18
|
+
"""Aggregated server-health snapshot.
|
|
19
|
+
|
|
20
|
+
Each sub-field is populated independently; a failure on one
|
|
21
|
+
sub-call sets that field to ``None`` and records the exception in
|
|
22
|
+
``errors``, leaving the rest of the snapshot intact (RESEARCH §4
|
|
23
|
+
"Refresh System-Health dashboard" — the JS client routes each
|
|
24
|
+
error independently).
|
|
25
|
+
|
|
26
|
+
``partial`` is ``True`` when any sub-call failed.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
host: Host | None = None
|
|
30
|
+
uptime: list[Uptime] | None = None
|
|
31
|
+
connections: list[Connection] | None = None
|
|
32
|
+
partial: bool = False
|
|
33
|
+
errors: dict[str, RtlsError] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["SystemHealth"]
|
rtls_sdk/models/tag.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tag (trackable_object) model.
|
|
2
|
+
|
|
3
|
+
The server returns trackables under several envelope shapes (single inside
|
|
4
|
+
``{trackableObjects: [t]}``, list inside the same, embedded inside
|
|
5
|
+
associations, etc.). The SDK normalizes to one ``Tag`` type regardless.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import Field, model_validator
|
|
13
|
+
|
|
14
|
+
from ._base import _BaseModel
|
|
15
|
+
from .node import Node
|
|
16
|
+
from .position import Position
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Tag(_BaseModel):
|
|
20
|
+
"""A tracked object (trackable / tag / asset).
|
|
21
|
+
|
|
22
|
+
Field aliases tolerate the embedded forms the server uses inside
|
|
23
|
+
other entities. Position and node sub-objects are eagerly parsed
|
|
24
|
+
into typed models when present.
|
|
25
|
+
|
|
26
|
+
The server has been observed to return ``groups: null`` on freshly-
|
|
27
|
+
created trackables (an unbound tag has no groups, but the field is
|
|
28
|
+
present). The SDK coerces ``None`` to ``[]`` so the typed list[str]
|
|
29
|
+
surface stays clean for consumers.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
uid: str
|
|
33
|
+
name: str
|
|
34
|
+
mac_address: str | None = None
|
|
35
|
+
self_zone_uid: str | None = None
|
|
36
|
+
groups: list[str] = Field(default_factory=list)
|
|
37
|
+
position: Position | None = None
|
|
38
|
+
node: Node | None = None
|
|
39
|
+
|
|
40
|
+
@model_validator(mode="before")
|
|
41
|
+
@classmethod
|
|
42
|
+
def _coerce_null_lists(cls, data: Any) -> Any:
|
|
43
|
+
if isinstance(data, dict) and data.get("groups") is None:
|
|
44
|
+
data = {**data, "groups": []}
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["Tag"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Tag-template model.
|
|
2
|
+
|
|
3
|
+
Returned by the client-only endpoint ``GET /trackable_objects/templates``
|
|
4
|
+
(RESEARCH §1). Used as a creation prefab.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from ._base import _BaseModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TagTemplate(_BaseModel):
|
|
17
|
+
"""A reusable shape for creating new trackables."""
|
|
18
|
+
|
|
19
|
+
uid: str | None = None
|
|
20
|
+
name: str | None = None
|
|
21
|
+
fields: dict[str, Any] = Field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["TagTemplate"]
|
rtls_sdk/models/user.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""User model with a derived ``role`` enum.
|
|
2
|
+
|
|
3
|
+
The server returns three boolean flags (``isSuperAdmin``,
|
|
4
|
+
``isSystemUser``, ``isCompanyAdmin``) plus a raw ``role`` string. The
|
|
5
|
+
SDK collapses this into a single :class:`Role` enum at parse time and
|
|
6
|
+
keeps the raw flags accessible via ``user.raw_flags`` for power users.
|
|
7
|
+
|
|
8
|
+
Enum values match the live server's accepted role strings (confirmed
|
|
9
|
+
via the user-create validation error in M5: ``company_admin``,
|
|
10
|
+
``project_system_admin``, ``project_operations_admin``,
|
|
11
|
+
``project_maintainer``, ``project_user``, ``system_admin``). DESIGN §9
|
|
12
|
+
open #4 — pending-backend-confirmation — is now resolved by direct
|
|
13
|
+
observation of the test server.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from pydantic import Field, computed_field, model_validator
|
|
22
|
+
|
|
23
|
+
from ._base import _BaseModel
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Role(str, Enum):
|
|
27
|
+
"""Resolved user role.
|
|
28
|
+
|
|
29
|
+
Values are the server's accepted role strings. Ordering in this
|
|
30
|
+
declaration is informational (most-privileged first) and does not
|
|
31
|
+
imply comparable enums — compare by string equality only.
|
|
32
|
+
|
|
33
|
+
The boolean wire flags (``isSuperAdmin`` etc.) and the raw role
|
|
34
|
+
string both map into one of these members. See
|
|
35
|
+
:attr:`User.role` for the resolution rules.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
SYSTEM_ADMIN = "system_admin"
|
|
39
|
+
COMPANY_ADMIN = "company_admin"
|
|
40
|
+
PROJECT_SYSTEM_ADMIN = "project_system_admin"
|
|
41
|
+
PROJECT_OPERATIONS_ADMIN = "project_operations_admin"
|
|
42
|
+
PROJECT_MAINTAINER = "project_maintainer"
|
|
43
|
+
PROJECT_USER = "project_user"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UserFlags(_BaseModel):
|
|
47
|
+
"""Raw boolean flags the server returns on every User object."""
|
|
48
|
+
|
|
49
|
+
is_super_admin: bool = Field(default=False, alias="isSuperAdmin")
|
|
50
|
+
is_system_user: bool = Field(default=False, alias="isSystemUser")
|
|
51
|
+
is_company_admin: bool = Field(default=False, alias="isCompanyAdmin")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class User(_BaseModel):
|
|
55
|
+
"""An RTLS user.
|
|
56
|
+
|
|
57
|
+
The wire-format fields ``isSuperAdmin`` / ``isSystemUser`` /
|
|
58
|
+
``isCompanyAdmin`` plus the raw ``role`` string are normalized into:
|
|
59
|
+
|
|
60
|
+
- ``role``: a single :class:`Role` enum value.
|
|
61
|
+
- ``raw_flags``: the four boolean flags untouched.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
email: str
|
|
65
|
+
uid: str | None = None
|
|
66
|
+
first_name: str | None = Field(default=None, alias="first_name")
|
|
67
|
+
last_name: str | None = Field(default=None, alias="last_name")
|
|
68
|
+
raw_flags: UserFlags = Field(default_factory=UserFlags)
|
|
69
|
+
|
|
70
|
+
@model_validator(mode="before")
|
|
71
|
+
@classmethod
|
|
72
|
+
def _gather_flags(cls, data: Any) -> Any:
|
|
73
|
+
if not isinstance(data, dict):
|
|
74
|
+
return data
|
|
75
|
+
# Lift the four wire flags into raw_flags. We keep them on the
|
|
76
|
+
# outer dict too in case downstream code (or the .raw property
|
|
77
|
+
# via extra="allow") wants them.
|
|
78
|
+
flags = {
|
|
79
|
+
k: data[k]
|
|
80
|
+
for k in (
|
|
81
|
+
"isSuperAdmin",
|
|
82
|
+
"isSystemUser",
|
|
83
|
+
"isCompanyAdmin",
|
|
84
|
+
)
|
|
85
|
+
if k in data
|
|
86
|
+
}
|
|
87
|
+
if flags:
|
|
88
|
+
data = {**data, "raw_flags": flags}
|
|
89
|
+
return data
|
|
90
|
+
|
|
91
|
+
@computed_field # type: ignore[prop-decorator]
|
|
92
|
+
@property
|
|
93
|
+
def role(self) -> Role:
|
|
94
|
+
"""Derived role for the user.
|
|
95
|
+
|
|
96
|
+
Resolution order (boolean flags win over raw string):
|
|
97
|
+
|
|
98
|
+
1. ``is_super_admin`` → :attr:`Role.SYSTEM_ADMIN` (server-wide root).
|
|
99
|
+
2. ``is_company_admin`` → :attr:`Role.COMPANY_ADMIN`.
|
|
100
|
+
3. Raw ``role`` string, if it parses cleanly.
|
|
101
|
+
4. Default → :attr:`Role.PROJECT_USER`.
|
|
102
|
+
|
|
103
|
+
The ``is_system_user`` flag is intentionally not mapped to a
|
|
104
|
+
Role member — the server's role vocabulary has no system-user
|
|
105
|
+
entry; the flag remains accessible via ``user.raw_flags.is_system_user``
|
|
106
|
+
for callers that need it.
|
|
107
|
+
"""
|
|
108
|
+
if self.raw_flags.is_super_admin:
|
|
109
|
+
return Role.SYSTEM_ADMIN
|
|
110
|
+
if self.raw_flags.is_company_admin:
|
|
111
|
+
return Role.COMPANY_ADMIN
|
|
112
|
+
raw_role = self.raw.get("role")
|
|
113
|
+
if isinstance(raw_role, str):
|
|
114
|
+
try:
|
|
115
|
+
return Role(raw_role.lower())
|
|
116
|
+
except ValueError:
|
|
117
|
+
pass
|
|
118
|
+
return Role.PROJECT_USER
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = ["Role", "User", "UserFlags"]
|
rtls_sdk/models/zone.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Zone model.
|
|
2
|
+
|
|
3
|
+
A "zone" in RTLS-speak covers both static zones (areas defined by
|
|
4
|
+
operators) and buffers (dynamic zones bound to a moving trackable). The
|
|
5
|
+
``dynamic`` flag discriminates: ``False`` = static zone, ``True`` = buffer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._base import _BaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Zone(_BaseModel):
|
|
14
|
+
"""A zone or buffer.
|
|
15
|
+
|
|
16
|
+
M1 ships the discriminator fields. M2 adds the geometric shape data
|
|
17
|
+
(polygon points, circle radius) and zone-type metadata.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
uid: str
|
|
21
|
+
name: str
|
|
22
|
+
dynamic: bool = False
|
|
23
|
+
shape: str | None = None
|
|
24
|
+
trackable_object_uid: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ["Zone"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Zone-event model.
|
|
2
|
+
|
|
3
|
+
Returned by ``GET /data/zone_events?format=json``. The CSV variant is
|
|
4
|
+
returned as raw bytes by the reports surface in M7.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._base import _BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ZoneEvent(_BaseModel):
|
|
13
|
+
"""A zone-traversal event (enter / exit / dwell)."""
|
|
14
|
+
|
|
15
|
+
uid: str | None = None
|
|
16
|
+
zone_uid: str | None = None
|
|
17
|
+
trackable_object_uid: str | None = None
|
|
18
|
+
event_type: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["ZoneEvent"]
|
rtls_sdk/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Resource sub-clients exposed on :class:`RtlsClient`."""
|
|
2
|
+
|
|
3
|
+
from .alarms import AlarmsAPI
|
|
4
|
+
from .anchors import AnchorsAPI
|
|
5
|
+
from .areas import AreasAPI
|
|
6
|
+
from .associations import AssociationsAPI
|
|
7
|
+
from .auth import AuthAPI
|
|
8
|
+
from .companies import CompaniesAPI
|
|
9
|
+
from .context import ContextAPI
|
|
10
|
+
from .events import EventsAPI
|
|
11
|
+
from .floorplans import FloorplansAPI
|
|
12
|
+
from .groups import GroupsAPI
|
|
13
|
+
from .logger import LoggerAPI
|
|
14
|
+
from .messaging import MessagingAPI
|
|
15
|
+
from .nodes import NodesAPI
|
|
16
|
+
from .notifications import NotificationsAPI
|
|
17
|
+
from .projects import ProjectsAPI
|
|
18
|
+
from .reports import ReportsAPI
|
|
19
|
+
from .sites import SitesAPI
|
|
20
|
+
from .subscribers import SubscribersAPI
|
|
21
|
+
from .system import SystemAPI
|
|
22
|
+
from .tags import TagsAPI
|
|
23
|
+
from .users import UsersAPI
|
|
24
|
+
from .zones import ZonesAPI
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"AlarmsAPI",
|
|
28
|
+
"AnchorsAPI",
|
|
29
|
+
"AreasAPI",
|
|
30
|
+
"AssociationsAPI",
|
|
31
|
+
"AuthAPI",
|
|
32
|
+
"CompaniesAPI",
|
|
33
|
+
"ContextAPI",
|
|
34
|
+
"EventsAPI",
|
|
35
|
+
"FloorplansAPI",
|
|
36
|
+
"GroupsAPI",
|
|
37
|
+
"LoggerAPI",
|
|
38
|
+
"MessagingAPI",
|
|
39
|
+
"NodesAPI",
|
|
40
|
+
"NotificationsAPI",
|
|
41
|
+
"ProjectsAPI",
|
|
42
|
+
"ReportsAPI",
|
|
43
|
+
"SitesAPI",
|
|
44
|
+
"SubscribersAPI",
|
|
45
|
+
"SystemAPI",
|
|
46
|
+
"TagsAPI",
|
|
47
|
+
"UsersAPI",
|
|
48
|
+
"ZonesAPI",
|
|
49
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Base for resource sub-clients.
|
|
2
|
+
|
|
3
|
+
Each ``RtlsClient`` attribute (``client.tags``, ``client.sites``, …) is an
|
|
4
|
+
instance of a subclass of :class:`ResourceAPI`. The base provides the
|
|
5
|
+
shared request helpers; subclasses translate REST paths into typed models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .._client import RtlsClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ResourceAPI:
|
|
19
|
+
"""Base for every resource sub-client."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, client: RtlsClient) -> None:
|
|
22
|
+
self._client = client
|
|
23
|
+
|
|
24
|
+
def _get(
|
|
25
|
+
self,
|
|
26
|
+
path: str,
|
|
27
|
+
*,
|
|
28
|
+
params: Any = None,
|
|
29
|
+
headers: dict[str, str] | None = None,
|
|
30
|
+
) -> httpx.Response:
|
|
31
|
+
return self._client._request("GET", path, params=params, headers=headers)
|
|
32
|
+
|
|
33
|
+
def _post(
|
|
34
|
+
self,
|
|
35
|
+
path: str,
|
|
36
|
+
*,
|
|
37
|
+
json: Any = None,
|
|
38
|
+
params: Any = None,
|
|
39
|
+
headers: dict[str, str] | None = None,
|
|
40
|
+
) -> httpx.Response:
|
|
41
|
+
return self._client._request("POST", path, json=json, params=params, headers=headers)
|
|
42
|
+
|
|
43
|
+
def _put(
|
|
44
|
+
self,
|
|
45
|
+
path: str,
|
|
46
|
+
*,
|
|
47
|
+
json: Any = None,
|
|
48
|
+
params: Any = None,
|
|
49
|
+
headers: dict[str, str] | None = None,
|
|
50
|
+
) -> httpx.Response:
|
|
51
|
+
return self._client._request("PUT", path, json=json, params=params, headers=headers)
|
|
52
|
+
|
|
53
|
+
def _delete(
|
|
54
|
+
self,
|
|
55
|
+
path: str,
|
|
56
|
+
*,
|
|
57
|
+
params: Any = None,
|
|
58
|
+
headers: dict[str, str] | None = None,
|
|
59
|
+
) -> httpx.Response:
|
|
60
|
+
return self._client._request("DELETE", path, params=params, headers=headers)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ["ResourceAPI"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Alarms 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 .._query import pack_array_param
|
|
10
|
+
from .._time import to_epoch_ms
|
|
11
|
+
from ..models import Alarm
|
|
12
|
+
from ._base import ResourceAPI
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AlarmsAPI(ResourceAPI):
|
|
16
|
+
"""Read alarm events and (clear) existing alarms.
|
|
17
|
+
|
|
18
|
+
Three time-bounded reads:
|
|
19
|
+
|
|
20
|
+
- :meth:`list_active` — currently-open alarms.
|
|
21
|
+
- :meth:`list_for_site` — alarms in a site over a window.
|
|
22
|
+
- :meth:`list_for_area` — alarms in a single sublocation over a window.
|
|
23
|
+
|
|
24
|
+
The CSV export variants of these endpoints are deferred to the
|
|
25
|
+
reports surface in M7 (``client.reports.alarms_csv``).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def list_active(self) -> list[Alarm]:
|
|
29
|
+
response = self._get("/api/v2/alarms/active/")
|
|
30
|
+
items = unwrap_resource(response.json(), "alarms")
|
|
31
|
+
return [Alarm.from_wire(item) for item in items]
|
|
32
|
+
|
|
33
|
+
def list_for_site(
|
|
34
|
+
self,
|
|
35
|
+
start: datetime,
|
|
36
|
+
end: datetime,
|
|
37
|
+
*,
|
|
38
|
+
site_uid: str,
|
|
39
|
+
alarm_types: list[str] | None = None,
|
|
40
|
+
) -> list[Alarm]:
|
|
41
|
+
"""Return alarms raised at ``site_uid`` in ``[start, end]``.
|
|
42
|
+
|
|
43
|
+
``alarm_types`` filters to specific alarm-type names (e.g.
|
|
44
|
+
``["zone_violation", "buffer_violation"]``); ``None`` lists all.
|
|
45
|
+
Timestamps are converted to epoch milliseconds per the server's
|
|
46
|
+
contract (RESEARCH §2). Naive ``datetime`` raises ``ValueError``.
|
|
47
|
+
"""
|
|
48
|
+
params: list[tuple[str, str]] = [
|
|
49
|
+
("from", str(to_epoch_ms(start, arg_name="start"))),
|
|
50
|
+
("to", str(to_epoch_ms(end, arg_name="end"))),
|
|
51
|
+
("location_uid", site_uid),
|
|
52
|
+
("format", "json"),
|
|
53
|
+
("report", "1"),
|
|
54
|
+
]
|
|
55
|
+
if alarm_types:
|
|
56
|
+
params.extend(pack_array_param("alarm_type_name", alarm_types))
|
|
57
|
+
response = self._get("/api/v2/alarms", params=params)
|
|
58
|
+
items = unwrap_resource(response.json(), "alarms")
|
|
59
|
+
return [Alarm.from_wire(item) for item in items]
|
|
60
|
+
|
|
61
|
+
def list_for_area(
|
|
62
|
+
self,
|
|
63
|
+
start: datetime,
|
|
64
|
+
end: datetime,
|
|
65
|
+
*,
|
|
66
|
+
area_uid: str,
|
|
67
|
+
alarm_types: list[str] | None = None,
|
|
68
|
+
) -> list[Alarm]:
|
|
69
|
+
"""Return alarms raised in a sublocation over a window.
|
|
70
|
+
|
|
71
|
+
Same conventions as :meth:`list_for_site`; the scoping parameter
|
|
72
|
+
is ``sublocation_uid`` instead of ``location_uid``.
|
|
73
|
+
"""
|
|
74
|
+
params: list[tuple[str, str]] = [
|
|
75
|
+
("from", str(to_epoch_ms(start, arg_name="start"))),
|
|
76
|
+
("to", str(to_epoch_ms(end, arg_name="end"))),
|
|
77
|
+
("sublocation_uid", area_uid),
|
|
78
|
+
("format", "json"),
|
|
79
|
+
("report", "1"),
|
|
80
|
+
]
|
|
81
|
+
if alarm_types:
|
|
82
|
+
params.extend(pack_array_param("alarm_type_name", alarm_types))
|
|
83
|
+
response = self._get("/api/v2/alarms", params=params)
|
|
84
|
+
items = unwrap_resource(response.json(), "alarms")
|
|
85
|
+
return [Alarm.from_wire(item) for item in items]
|
|
86
|
+
|
|
87
|
+
def update(self, uid: str, **fields: Any) -> Alarm:
|
|
88
|
+
"""Update / clear an alarm.
|
|
89
|
+
|
|
90
|
+
Uses **PUT** despite the public docs saying POST — the JS
|
|
91
|
+
reference client uses PUT and the server accepts only PUT
|
|
92
|
+
(RESEARCH Discrepancy #3).
|
|
93
|
+
"""
|
|
94
|
+
response = self._put(f"/api/v2/alarms/{uid}", json=fields)
|
|
95
|
+
payload = response.json()
|
|
96
|
+
try:
|
|
97
|
+
item = unwrap_resource_single(payload, "alarms")
|
|
98
|
+
except ValueError:
|
|
99
|
+
item = payload if isinstance(payload, dict) else {}
|
|
100
|
+
# The server may also return the alarm under the bare-array
|
|
101
|
+
# envelope ``{alarms: [...]}``; take the first row if so.
|
|
102
|
+
if isinstance(item, dict) and "alarms" in item and isinstance(item["alarms"], list):
|
|
103
|
+
first = item["alarms"][0] if item["alarms"] else {}
|
|
104
|
+
item = first if isinstance(first, dict) else {}
|
|
105
|
+
return Alarm.from_wire(item if isinstance(item, dict) else {})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = ["AlarmsAPI"]
|