rtls-sdk 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. rtls_sdk/__init__.py +123 -0
  2. rtls_sdk/_auth.py +266 -0
  3. rtls_sdk/_client.py +419 -0
  4. rtls_sdk/_envelope.py +74 -0
  5. rtls_sdk/_http.py +145 -0
  6. rtls_sdk/_logging.py +143 -0
  7. rtls_sdk/_pagination.py +235 -0
  8. rtls_sdk/_query.py +114 -0
  9. rtls_sdk/_time.py +84 -0
  10. rtls_sdk/compounds/__init__.py +19 -0
  11. rtls_sdk/compounds/auth.py +100 -0
  12. rtls_sdk/compounds/context.py +159 -0
  13. rtls_sdk/compounds/groups.py +126 -0
  14. rtls_sdk/compounds/nodes.py +176 -0
  15. rtls_sdk/compounds/reports.py +339 -0
  16. rtls_sdk/compounds/system.py +43 -0
  17. rtls_sdk/compounds/tags.py +404 -0
  18. rtls_sdk/compounds/users.py +143 -0
  19. rtls_sdk/compounds/zones.py +203 -0
  20. rtls_sdk/errors.py +238 -0
  21. rtls_sdk/models/__init__.py +73 -0
  22. rtls_sdk/models/_base.py +46 -0
  23. rtls_sdk/models/alarm.py +23 -0
  24. rtls_sdk/models/anchor.py +25 -0
  25. rtls_sdk/models/area.py +28 -0
  26. rtls_sdk/models/association.py +48 -0
  27. rtls_sdk/models/bulk.py +80 -0
  28. rtls_sdk/models/company.py +32 -0
  29. rtls_sdk/models/csv_blob.py +40 -0
  30. rtls_sdk/models/floorplan.py +42 -0
  31. rtls_sdk/models/group.py +20 -0
  32. rtls_sdk/models/heatmap.py +37 -0
  33. rtls_sdk/models/import_result.py +42 -0
  34. rtls_sdk/models/node.py +28 -0
  35. rtls_sdk/models/notification.py +38 -0
  36. rtls_sdk/models/position.py +66 -0
  37. rtls_sdk/models/project.py +26 -0
  38. rtls_sdk/models/pws.py +38 -0
  39. rtls_sdk/models/report.py +34 -0
  40. rtls_sdk/models/session_context.py +81 -0
  41. rtls_sdk/models/site.py +31 -0
  42. rtls_sdk/models/subscriber.py +68 -0
  43. rtls_sdk/models/system.py +97 -0
  44. rtls_sdk/models/system_health.py +36 -0
  45. rtls_sdk/models/tag.py +48 -0
  46. rtls_sdk/models/tag_template.py +24 -0
  47. rtls_sdk/models/user.py +121 -0
  48. rtls_sdk/models/zone.py +27 -0
  49. rtls_sdk/models/zone_event.py +21 -0
  50. rtls_sdk/py.typed +0 -0
  51. rtls_sdk/resources/__init__.py +49 -0
  52. rtls_sdk/resources/_base.py +63 -0
  53. rtls_sdk/resources/alarms.py +108 -0
  54. rtls_sdk/resources/anchors.py +147 -0
  55. rtls_sdk/resources/areas.py +78 -0
  56. rtls_sdk/resources/associations.py +157 -0
  57. rtls_sdk/resources/auth.py +63 -0
  58. rtls_sdk/resources/companies.py +79 -0
  59. rtls_sdk/resources/context.py +50 -0
  60. rtls_sdk/resources/events.py +149 -0
  61. rtls_sdk/resources/floorplans.py +283 -0
  62. rtls_sdk/resources/groups.py +99 -0
  63. rtls_sdk/resources/logger.py +40 -0
  64. rtls_sdk/resources/messaging.py +51 -0
  65. rtls_sdk/resources/nodes.py +157 -0
  66. rtls_sdk/resources/notifications.py +55 -0
  67. rtls_sdk/resources/projects.py +67 -0
  68. rtls_sdk/resources/reports.py +180 -0
  69. rtls_sdk/resources/sites.py +115 -0
  70. rtls_sdk/resources/subscribers.py +110 -0
  71. rtls_sdk/resources/system.py +125 -0
  72. rtls_sdk/resources/tags.py +370 -0
  73. rtls_sdk/resources/users.py +275 -0
  74. rtls_sdk/resources/zones.py +199 -0
  75. rtls_sdk-0.2.0.dist-info/METADATA +141 -0
  76. rtls_sdk-0.2.0.dist-info/RECORD +78 -0
  77. rtls_sdk-0.2.0.dist-info/WHEEL +4 -0
  78. rtls_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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"]
@@ -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"]
@@ -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"]
@@ -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"]