trinity-connect-client 0.2.0__tar.gz

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.
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.3
2
+ Name: trinity-connect-client
3
+ Version: 0.2.0
4
+ Summary: Official Python library for Trinity IoT Connect API interactions
5
+ Author: Jan Badenhorst, Andries Niemandt
6
+ Author-email: Jan Badenhorst <jan.badenhorst@trintel.co.za>, Andries Niemandt <andries.niemandt@trintel.co.za>
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Dist: requests>=2.32.4
11
+ Requires-Python: >=3.12
12
+ Project-URL: Homepage, https://github.com/trinity-telecomms/connect-py-client
13
+ Project-URL: Issues, https://github.com/trinity-telecomms/connect-py-client/issues
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Connect Client for Python
17
+
18
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
20
+
21
+ Official Python library for interacting with the Trinity IoT Connect API.
22
+ This library provides a clean interface for accessing the Connect API endpoints
23
+ with comprehensive error handling, type hints, and good test coverage.
24
+
25
+ ## Table of Contents
26
+
27
+ - [Installation](#installation)
28
+ - [Quick Start](#quick-start)
29
+ - [Configuration](#configuration)
30
+ - [Error Handling](#error-handling)
31
+ - [Development](#development)
32
+ - [Contributing](#contributing)
33
+ - [License](#license)
34
+
35
+ ## Installation
36
+
37
+ ### Using uv (recommended)
38
+
39
+ ```bash
40
+ uv add trinity-connect-client
41
+ ```
42
+
43
+ ### Using pip
44
+
45
+ ```bash
46
+ pip install trinity-connect-client
47
+ ```
48
+
49
+ ### From source
50
+
51
+ ```bash
52
+ uv add git+https://github.com/trinity-telecomms/connect-py-client@v0.2.0
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ```python
58
+ from trinity_connect_client import ConnectClient
59
+ from trinity_connect_client.exceptions import ResourceNotFoundError, UnauthorisedError
60
+
61
+ # Initialize the client
62
+ client = ConnectClient(
63
+ api_version="v4",
64
+ base_url="https://capi.trintel.co.za",
65
+ token="your-service-account-token"
66
+ )
67
+
68
+ # Get a device by ID (returns dict)
69
+ try:
70
+ device = client.devices.get(device_id=123)
71
+ print(f"Device name: {device['name']}")
72
+ except ResourceNotFoundError:
73
+ print("Device not found")
74
+ except UnauthorisedError:
75
+ print("Access denied")
76
+ ```
77
+
78
+ ## Using Response Models
79
+
80
+ The library provides type-safe dataclass models for API responses:
81
+
82
+ ```python
83
+ from trinity_connect_client import ConnectClient, Device, Company, Folder
84
+
85
+ client = ConnectClient(
86
+ api_version="v4",
87
+ base_url="https://capi.trintel.co.za",
88
+ token="your-service-account-token"
89
+ )
90
+
91
+ # Get device as dict, then convert to model for type safety
92
+ device_dict = client.devices.get(device_id=123)
93
+ device = Device.from_dict(device_dict)
94
+
95
+ # Now you have full type hints and IDE autocomplete
96
+ print(f"Device: {device.name}")
97
+ print(f"UID: {device.uid}")
98
+ print(f"Status: {device.status}")
99
+
100
+ # Works with all response types
101
+ company_dict = client.orgs.get(company_id=1)
102
+ company = Company.from_dict(company_dict)
103
+
104
+ folders_list = client.orgs.get_folders(company_id=1)
105
+ folders = [Folder.from_dict(f) for f in folders_list]
106
+ ```
107
+
108
+ **Available Models:**
109
+ - `Device` - Device information
110
+ - `Company` - Company/organization information
111
+ - `Folder` - Folder information
112
+ - `DeviceData` - Device telemetry data
113
+ - `DeviceEvent` - Device events
114
+ - `DeviceCommand` - Device commands
115
+
116
+ ## Configuration
117
+
118
+ ### Environment Variables
119
+
120
+ You can set your service account token via environment variables:
121
+
122
+ ```bash
123
+ export CONNECT_API_TOKEN="your-service-account-token"
124
+ export CONNECT_API_BASE_URL="https://capi.trintel.co.za"
125
+ ```
126
+
127
+ ```python
128
+ import os
129
+ from trinity_connect_client import ConnectClient
130
+
131
+ client = ConnectClient(
132
+ api_version="v4",
133
+ base_url=os.getenv("CONNECT_API_BASE_URL"),
134
+ token=os.getenv("CONNECT_API_TOKEN")
135
+ )
136
+ ```
137
+
138
+ ## Migration from v0.1.x to v0.2.0
139
+
140
+ Version 0.2.0 introduces breaking changes to authentication:
141
+
142
+ ### What Changed
143
+ - Credentials-based authentication (email/password) has been removed
144
+ - Service account token authentication is now required
145
+ - Token caching logic has been removed (tokens are long-lived)
146
+ - The `auth` module and login endpoint are no longer available
147
+
148
+ ### Upgrading
149
+
150
+ **Before (v0.1.x):**
151
+ ```python
152
+ client = ConnectClient(
153
+ base_url="https://capi.trintel.co.za",
154
+ credentials={
155
+ "email": "user@example.com",
156
+ "password": "password"
157
+ },
158
+ cache=cache_instance # Optional
159
+ )
160
+ ```
161
+
162
+ **After (v0.2.0):**
163
+ ```python
164
+ client = ConnectClient(
165
+ base_url="https://capi.trintel.co.za",
166
+ token="your-service-account-token"
167
+ )
168
+ ```
169
+
170
+ **How to get a service account token:**
171
+ Contact your Trinity IoT administrator to generate a service account token for your application.
172
+
173
+ ## Error Handling
174
+
175
+ The library raises specific exceptions for different error conditions:
176
+
177
+ ```python
178
+ from trinity_connect_client.exceptions import (
179
+ ResourceNotFoundError,
180
+ UnauthorisedError,
181
+ ConnectAPIError
182
+ )
183
+
184
+ try:
185
+ device = client.devices.get(device_id=123)
186
+ except ResourceNotFoundError:
187
+ print("Device not found (404)")
188
+ except UnauthorisedError:
189
+ print("Authentication failed (401)")
190
+ except PermissionError:
191
+ print("Access forbidden (403)")
192
+ except ConnectAPIError as e:
193
+ print(f"API error: {e}")
194
+ except ValueError as e:
195
+ print(f"Invalid input: {e}")
196
+ ```
197
+
198
+ ## Development
199
+
200
+ ### Setting up Development Environment
201
+
202
+ ```bash
203
+ # Clone the repository
204
+ git clone https://github.com/trinity-telecomms/connect-py-client.git
205
+ cd connect-py-client
206
+
207
+ # Install dependencies with uv
208
+ uv sync
209
+
210
+ # Run tests
211
+ uv run pytest
212
+
213
+ # Run linting
214
+ uv run ruff check .
215
+ ```
216
+
217
+ ### Running Tests
218
+
219
+ ```bash
220
+ # Run all tests
221
+ uv run pytest
222
+
223
+ # Run with coverage
224
+ uv run pytest --cov=connect_client
225
+
226
+ # Run specific test file
227
+ uv run pytest tests/modules/devices/test_devices_api.py
228
+ ```
229
+
230
+ ### Building the Package
231
+
232
+ This project uses the `uv_build` backend for building distributions:
233
+
234
+ ```bash
235
+ # Build both wheel and source distribution
236
+ uv build
237
+
238
+ # Build only wheel
239
+ uv build --wheel
240
+
241
+ # Build only source distribution
242
+ uv build --sdist
243
+
244
+ # Build to a specific directory
245
+ uv build --out-dir dist/
246
+ ```
247
+
248
+ The built distributions will be available in the `dist/` directory.
249
+
250
+ ## Contributing
251
+
252
+ 1. Fork the repository
253
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
254
+ 3. Make your changes
255
+ 4. Add tests for new functionality
256
+ 5. Run the test suite (`uv run pytest`)
257
+ 6. Commit your changes (`git commit -m 'Add amazing feature'`)
258
+ 7. Push to the branch (`git push origin feature/amazing-feature`)
259
+ 8. Open a Pull Request
260
+
261
+ ## License
262
+
263
+ This project is licensed under the MIT Licence -
264
+ see the [LICENCE](LICENSE) file for details.
@@ -0,0 +1,249 @@
1
+ # Connect Client for Python
2
+
3
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Official Python library for interacting with the Trinity IoT Connect API.
7
+ This library provides a clean interface for accessing the Connect API endpoints
8
+ with comprehensive error handling, type hints, and good test coverage.
9
+
10
+ ## Table of Contents
11
+
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Configuration](#configuration)
15
+ - [Error Handling](#error-handling)
16
+ - [Development](#development)
17
+ - [Contributing](#contributing)
18
+ - [License](#license)
19
+
20
+ ## Installation
21
+
22
+ ### Using uv (recommended)
23
+
24
+ ```bash
25
+ uv add trinity-connect-client
26
+ ```
27
+
28
+ ### Using pip
29
+
30
+ ```bash
31
+ pip install trinity-connect-client
32
+ ```
33
+
34
+ ### From source
35
+
36
+ ```bash
37
+ uv add git+https://github.com/trinity-telecomms/connect-py-client@v0.2.0
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from trinity_connect_client import ConnectClient
44
+ from trinity_connect_client.exceptions import ResourceNotFoundError, UnauthorisedError
45
+
46
+ # Initialize the client
47
+ client = ConnectClient(
48
+ api_version="v4",
49
+ base_url="https://capi.trintel.co.za",
50
+ token="your-service-account-token"
51
+ )
52
+
53
+ # Get a device by ID (returns dict)
54
+ try:
55
+ device = client.devices.get(device_id=123)
56
+ print(f"Device name: {device['name']}")
57
+ except ResourceNotFoundError:
58
+ print("Device not found")
59
+ except UnauthorisedError:
60
+ print("Access denied")
61
+ ```
62
+
63
+ ## Using Response Models
64
+
65
+ The library provides type-safe dataclass models for API responses:
66
+
67
+ ```python
68
+ from trinity_connect_client import ConnectClient, Device, Company, Folder
69
+
70
+ client = ConnectClient(
71
+ api_version="v4",
72
+ base_url="https://capi.trintel.co.za",
73
+ token="your-service-account-token"
74
+ )
75
+
76
+ # Get device as dict, then convert to model for type safety
77
+ device_dict = client.devices.get(device_id=123)
78
+ device = Device.from_dict(device_dict)
79
+
80
+ # Now you have full type hints and IDE autocomplete
81
+ print(f"Device: {device.name}")
82
+ print(f"UID: {device.uid}")
83
+ print(f"Status: {device.status}")
84
+
85
+ # Works with all response types
86
+ company_dict = client.orgs.get(company_id=1)
87
+ company = Company.from_dict(company_dict)
88
+
89
+ folders_list = client.orgs.get_folders(company_id=1)
90
+ folders = [Folder.from_dict(f) for f in folders_list]
91
+ ```
92
+
93
+ **Available Models:**
94
+ - `Device` - Device information
95
+ - `Company` - Company/organization information
96
+ - `Folder` - Folder information
97
+ - `DeviceData` - Device telemetry data
98
+ - `DeviceEvent` - Device events
99
+ - `DeviceCommand` - Device commands
100
+
101
+ ## Configuration
102
+
103
+ ### Environment Variables
104
+
105
+ You can set your service account token via environment variables:
106
+
107
+ ```bash
108
+ export CONNECT_API_TOKEN="your-service-account-token"
109
+ export CONNECT_API_BASE_URL="https://capi.trintel.co.za"
110
+ ```
111
+
112
+ ```python
113
+ import os
114
+ from trinity_connect_client import ConnectClient
115
+
116
+ client = ConnectClient(
117
+ api_version="v4",
118
+ base_url=os.getenv("CONNECT_API_BASE_URL"),
119
+ token=os.getenv("CONNECT_API_TOKEN")
120
+ )
121
+ ```
122
+
123
+ ## Migration from v0.1.x to v0.2.0
124
+
125
+ Version 0.2.0 introduces breaking changes to authentication:
126
+
127
+ ### What Changed
128
+ - Credentials-based authentication (email/password) has been removed
129
+ - Service account token authentication is now required
130
+ - Token caching logic has been removed (tokens are long-lived)
131
+ - The `auth` module and login endpoint are no longer available
132
+
133
+ ### Upgrading
134
+
135
+ **Before (v0.1.x):**
136
+ ```python
137
+ client = ConnectClient(
138
+ base_url="https://capi.trintel.co.za",
139
+ credentials={
140
+ "email": "user@example.com",
141
+ "password": "password"
142
+ },
143
+ cache=cache_instance # Optional
144
+ )
145
+ ```
146
+
147
+ **After (v0.2.0):**
148
+ ```python
149
+ client = ConnectClient(
150
+ base_url="https://capi.trintel.co.za",
151
+ token="your-service-account-token"
152
+ )
153
+ ```
154
+
155
+ **How to get a service account token:**
156
+ Contact your Trinity IoT administrator to generate a service account token for your application.
157
+
158
+ ## Error Handling
159
+
160
+ The library raises specific exceptions for different error conditions:
161
+
162
+ ```python
163
+ from trinity_connect_client.exceptions import (
164
+ ResourceNotFoundError,
165
+ UnauthorisedError,
166
+ ConnectAPIError
167
+ )
168
+
169
+ try:
170
+ device = client.devices.get(device_id=123)
171
+ except ResourceNotFoundError:
172
+ print("Device not found (404)")
173
+ except UnauthorisedError:
174
+ print("Authentication failed (401)")
175
+ except PermissionError:
176
+ print("Access forbidden (403)")
177
+ except ConnectAPIError as e:
178
+ print(f"API error: {e}")
179
+ except ValueError as e:
180
+ print(f"Invalid input: {e}")
181
+ ```
182
+
183
+ ## Development
184
+
185
+ ### Setting up Development Environment
186
+
187
+ ```bash
188
+ # Clone the repository
189
+ git clone https://github.com/trinity-telecomms/connect-py-client.git
190
+ cd connect-py-client
191
+
192
+ # Install dependencies with uv
193
+ uv sync
194
+
195
+ # Run tests
196
+ uv run pytest
197
+
198
+ # Run linting
199
+ uv run ruff check .
200
+ ```
201
+
202
+ ### Running Tests
203
+
204
+ ```bash
205
+ # Run all tests
206
+ uv run pytest
207
+
208
+ # Run with coverage
209
+ uv run pytest --cov=connect_client
210
+
211
+ # Run specific test file
212
+ uv run pytest tests/modules/devices/test_devices_api.py
213
+ ```
214
+
215
+ ### Building the Package
216
+
217
+ This project uses the `uv_build` backend for building distributions:
218
+
219
+ ```bash
220
+ # Build both wheel and source distribution
221
+ uv build
222
+
223
+ # Build only wheel
224
+ uv build --wheel
225
+
226
+ # Build only source distribution
227
+ uv build --sdist
228
+
229
+ # Build to a specific directory
230
+ uv build --out-dir dist/
231
+ ```
232
+
233
+ The built distributions will be available in the `dist/` directory.
234
+
235
+ ## Contributing
236
+
237
+ 1. Fork the repository
238
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
239
+ 3. Make your changes
240
+ 4. Add tests for new functionality
241
+ 5. Run the test suite (`uv run pytest`)
242
+ 6. Commit your changes (`git commit -m 'Add amazing feature'`)
243
+ 7. Push to the branch (`git push origin feature/amazing-feature`)
244
+ 8. Open a Pull Request
245
+
246
+ ## License
247
+
248
+ This project is licensed under the MIT Licence -
249
+ see the [LICENCE](LICENSE) file for details.
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "trinity-connect-client"
3
+ version = "0.2.0"
4
+ description = "Official Python library for Trinity IoT Connect API interactions"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Jan Badenhorst", email = "jan.badenhorst@trintel.co.za" },
8
+ { name = "Andries Niemandt", email = "andries.niemandt@trintel.co.za" }
9
+ ]
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+ requires-python = ">=3.12"
16
+ dependencies = [
17
+ "requests>=2.32.4",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/trinity-telecomms/connect-py-client"
22
+ Issues = "https://github.com/trinity-telecomms/connect-py-client/issues"
23
+
24
+ [build-system]
25
+ requires = ["uv_build >= 0.7.19, <0.9.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [dependency-groups]
29
+ lint = [
30
+ "ruff>=0.12.3",
31
+ ]
32
+ test = [
33
+ "pytest>=8.0.0",
34
+ "pytest-cov>=4.0.0",
35
+ "pytest-mock>=3.12.0",
36
+ "responses>=0.24.0",
37
+ ]
38
+
39
+ [tool.coverage.run]
40
+ data_file = ".coverage/.coverage"
41
+
42
+ [tool.coverage.html]
43
+ directory = ".coverage"
@@ -0,0 +1,13 @@
1
+ from .main import ConnectClient
2
+ from .models import Company, Device, DeviceCommand, DeviceData, DeviceEvent, Folder
3
+
4
+ __all__ = [
5
+ "ConnectClient",
6
+ "Company",
7
+ "Device",
8
+ "DeviceCommand",
9
+ "DeviceData",
10
+ "DeviceEvent",
11
+ "Folder",
12
+ ]
13
+ __version__ = "0.2.0"
@@ -0,0 +1,19 @@
1
+ from trinity_connect_client.exceptions import UnauthorisedError, ResourceNotFoundError
2
+
3
+
4
+ def handle_exceptions(func):
5
+ def wrapper(*args, **kwargs):
6
+ try:
7
+ return func(*args, **kwargs)
8
+ except ValueError:
9
+ raise
10
+ except UnauthorisedError:
11
+ raise
12
+ except PermissionError:
13
+ raise
14
+ except ResourceNotFoundError:
15
+ raise
16
+ except Exception:
17
+ raise
18
+
19
+ return wrapper
@@ -0,0 +1,13 @@
1
+ class ResourceNotFoundError(Exception):
2
+ def __init__(self, message):
3
+ super().__init__(message)
4
+
5
+
6
+ class ConnectAPIError(Exception):
7
+ def __init__(self, message):
8
+ super().__init__(message)
9
+
10
+
11
+ class UnauthorisedError(Exception):
12
+ def __init__(self, message):
13
+ super().__init__(message)
@@ -0,0 +1,18 @@
1
+ from .modules.devices import DevicesAPI
2
+ from .modules.orgs import OrgsAPI
3
+
4
+
5
+ class ConnectClient:
6
+ def __init__(self, **config):
7
+ self.api_version = config.get("api_version", "v4")
8
+ self.api_url = f"{config.get('base_url')}/api/{self.api_version}"
9
+
10
+ token = config.get("token")
11
+ if not token or not isinstance(token, str) or not token.strip():
12
+ raise ValueError("Token must be provided as a non-empty string")
13
+
14
+ self.token = token.strip()
15
+
16
+ # Resource Classes
17
+ self.devices = DevicesAPI(self)
18
+ self.orgs = OrgsAPI(self)
@@ -0,0 +1,87 @@
1
+ import requests
2
+
3
+ from trinity_connect_client.exceptions import (
4
+ ConnectAPIError,
5
+ ResourceNotFoundError,
6
+ UnauthorisedError,
7
+ )
8
+
9
+
10
+ class ResourceMixin:
11
+ def __init__(self, client):
12
+ self.client = client
13
+
14
+ def _url(self, path: str) -> str:
15
+ """
16
+ Build a full API URL from a path string.
17
+
18
+ :param path: The URL path (e.g., "devices/123" or "devices/uid/abc-123")
19
+ :return: Full URL including base API URL
20
+ """
21
+ # Remove leading slash if present for consistency
22
+ path = path.lstrip("/")
23
+ return f"{self.client.api_url}/{path}"
24
+
25
+ @staticmethod
26
+ def _get_default_headers():
27
+ """
28
+ Constructs the defaults headers required for Connect API requests. The
29
+ default headers do not include the Authorization header which is required
30
+ for most endpoints. Use _get_auth_headers() instead.
31
+ """
32
+ return {"Content-Type": "application/json", "Accept": "application/json"}
33
+
34
+ def _get_auth_headers(self):
35
+ default_headers = self._get_default_headers()
36
+ return {
37
+ **default_headers,
38
+ "Authorization": f"Bearer {self.client.token}",
39
+ }
40
+
41
+ def make_post_request(self, url, headers=None, json=None):
42
+ request_headers = self._get_auth_headers() if not headers else headers
43
+
44
+ try:
45
+ response = requests.post(url, headers=request_headers, json=json)
46
+ return response.status_code, response.json()
47
+ except Exception:
48
+ raise ConnectAPIError("Failed to make request to Connect API")
49
+
50
+ def make_patch_request(self, url, headers=None, json=None):
51
+ request_headers = self._get_auth_headers() if not headers else headers
52
+
53
+ try:
54
+ response = requests.patch(url, headers=request_headers, json=json)
55
+ return response.status_code, response.json()
56
+ except Exception:
57
+ raise ConnectAPIError("Failed to make request to Connect API")
58
+
59
+ def make_get_request(self, url, headers=None, params=None):
60
+ request_headers = self._get_auth_headers() if not headers else headers
61
+
62
+ try:
63
+ response = requests.get(url, headers=request_headers, params=params)
64
+ except Exception:
65
+ raise ConnectAPIError("Failed to make request to Connect API")
66
+
67
+ if response.status_code == 401:
68
+ raise UnauthorisedError("Authorisation failed")
69
+ if response.status_code == 403:
70
+ raise PermissionError("You are not authorised to access this resource")
71
+ if response.status_code == 404:
72
+ raise ResourceNotFoundError("Requested resource not found")
73
+ if response.status_code != 200:
74
+ raise ConnectAPIError("Connect API returned unexpected status code")
75
+
76
+ return response.json()
77
+
78
+ def get_linked_resource(self, url):
79
+ """
80
+ Helper method to get a linked resource from a previous interaction.
81
+ Some APIs return URLs in the response for related resources, previous or next
82
+ pages etc.
83
+
84
+ :param url:
85
+ :return:
86
+ """
87
+ return self.make_get_request(url)
@@ -0,0 +1,17 @@
1
+ """
2
+ Response models for Connect API endpoints.
3
+
4
+ These models provide type-safe representations of API responses.
5
+ """
6
+
7
+ from .device import Device, DeviceCommand, DeviceData, DeviceEvent
8
+ from .org import Company, Folder
9
+
10
+ __all__ = [
11
+ "Company",
12
+ "Device",
13
+ "DeviceCommand",
14
+ "DeviceData",
15
+ "DeviceEvent",
16
+ "Folder",
17
+ ]
@@ -0,0 +1,19 @@
1
+ """
2
+ Base classes for Connect API models.
3
+ """
4
+
5
+ from typing import Any, Dict
6
+
7
+
8
+ class BaseModel:
9
+ """Base class for all Connect API models."""
10
+
11
+ @classmethod
12
+ def from_dict(cls, data: Dict[str, Any]) -> "BaseModel":
13
+ """
14
+ Create a model instance from a dictionary.
15
+
16
+ :param data: Dictionary containing model data
17
+ :return: Model instance
18
+ """
19
+ raise NotImplementedError("Subclasses must implement from_dict")
@@ -0,0 +1,157 @@
1
+ """
2
+ Device-related models for Connect API.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional
7
+
8
+ from .base import BaseModel
9
+
10
+
11
+ @dataclass
12
+ class Device(BaseModel):
13
+ """Represents a device from the Connect API."""
14
+
15
+ id: int
16
+ url: str
17
+ name: str
18
+ description: str
19
+ state: int
20
+ t_type: int
21
+ tpp_id: Optional[int]
22
+ company: int
23
+ folder: int
24
+ state_display: str
25
+ t_type_display: str
26
+ company_name: str
27
+ folder_name: str
28
+ company_url: str
29
+ folder_url: str
30
+ aux_values_url: str
31
+ uid: str
32
+ imei: str
33
+ imei2: str
34
+ serial_number: str
35
+ comm_interval_contract: int
36
+ comm_state: int
37
+ comm_state_display: str
38
+ youngest_comm_timestamp: str
39
+ command_model: int
40
+ data_lens: int
41
+ event_lens: Optional[int]
42
+ profile: Optional[int]
43
+ commands_url: str
44
+ latest_data_url: str
45
+ events_url: str
46
+ meta_url: str
47
+ geo_url: str
48
+ category_url: str
49
+ tags_url: str
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: Dict[str, Any]) -> "Device":
53
+ """
54
+ Create a Device instance from a dictionary.
55
+
56
+ :param data: Dictionary containing device data
57
+ :return: Device instance
58
+ """
59
+ return cls(
60
+ id=data["id"],
61
+ url=data["url"],
62
+ name=data["name"],
63
+ description=data["description"],
64
+ state=data["state"],
65
+ t_type=data["t_type"],
66
+ tpp_id=data.get("tpp_id"),
67
+ company=data["company"],
68
+ folder=data["folder"],
69
+ state_display=data["state_display"],
70
+ t_type_display=data["t_type_display"],
71
+ company_name=data["company_name"],
72
+ folder_name=data["folder_name"],
73
+ company_url=data["company_url"],
74
+ folder_url=data["folder_url"],
75
+ aux_values_url=data["aux_values_url"],
76
+ uid=data["uid"],
77
+ imei=data["imei"],
78
+ imei2=data["imei2"],
79
+ serial_number=data["serial_number"],
80
+ comm_interval_contract=data["comm_interval_contract"],
81
+ comm_state=data["comm_state"],
82
+ comm_state_display=data["comm_state_display"],
83
+ youngest_comm_timestamp=data["youngest_comm_timestamp"],
84
+ command_model=data["command_model"],
85
+ data_lens=data["data_lens"],
86
+ event_lens=data.get("event_lens"),
87
+ profile=data.get("profile"),
88
+ commands_url=data["commands_url"],
89
+ latest_data_url=data["latest_data_url"],
90
+ events_url=data["events_url"],
91
+ meta_url=data["meta_url"],
92
+ geo_url=data["geo_url"],
93
+ category_url=data["category_url"],
94
+ tags_url=data["tags_url"],
95
+ )
96
+
97
+
98
+ @dataclass
99
+ class DeviceData(BaseModel):
100
+ """Represents device data/telemetry from the Connect API."""
101
+
102
+ # Generic structure for device data - can be extended based on actual API response
103
+ data: Dict[str, Any]
104
+
105
+ @classmethod
106
+ def from_dict(cls, data: Dict[str, Any]) -> "DeviceData":
107
+ """
108
+ Create a DeviceData instance from a dictionary.
109
+
110
+ :param data: Dictionary containing device data
111
+ :return: DeviceData instance
112
+ """
113
+ return cls(data=data)
114
+
115
+
116
+ @dataclass
117
+ class DeviceEvent(BaseModel):
118
+ """Represents a device event from the Connect API."""
119
+
120
+ # Generic structure for device events - can be extended based on actual API response
121
+ events: list[Dict[str, Any]]
122
+
123
+ @classmethod
124
+ def from_dict(cls, data: Dict[str, Any]) -> "DeviceEvent":
125
+ """
126
+ Create a DeviceEvent instance from a dictionary.
127
+
128
+ :param data: Dictionary containing event data
129
+ :return: DeviceEvent instance
130
+ """
131
+ # If data is already a list, wrap it
132
+ if isinstance(data, list):
133
+ return cls(events=data)
134
+ # If data has an 'events' key, use that
135
+ return cls(events=data.get("events", []))
136
+
137
+
138
+ @dataclass
139
+ class DeviceCommand(BaseModel):
140
+ """Represents a device command from the Connect API."""
141
+
142
+ # Generic structure for device commands - can be extended based on actual API response
143
+ commands: list[Dict[str, Any]]
144
+
145
+ @classmethod
146
+ def from_dict(cls, data: Dict[str, Any]) -> "DeviceCommand":
147
+ """
148
+ Create a DeviceCommand instance from a dictionary.
149
+
150
+ :param data: Dictionary containing command data
151
+ :return: DeviceCommand instance
152
+ """
153
+ # If data is already a list, wrap it
154
+ if isinstance(data, list):
155
+ return cls(commands=data)
156
+ # If data has a 'commands' key, use that
157
+ return cls(commands=data.get("commands", []))
@@ -0,0 +1,94 @@
1
+ """
2
+ Organisation-related models for Connect API.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict
7
+
8
+ from .base import BaseModel
9
+
10
+
11
+ @dataclass
12
+ class Company(BaseModel):
13
+ """Represents a company from the Connect API."""
14
+
15
+ id: int
16
+ url: str
17
+ name: str
18
+ state: int
19
+ state_display: str
20
+ is_ee: bool
21
+ url_profile: str
22
+ folder_url: str
23
+ url_folder_tree: str
24
+ url_sims: str
25
+ apn_urls: str
26
+ url_devices: str
27
+ url_budgets: str
28
+ url_access: str
29
+ url_tags: str
30
+ url_contracts: str
31
+ url_adaptations: str
32
+
33
+ @classmethod
34
+ def from_dict(cls, data: Dict[str, Any]) -> "Company":
35
+ """
36
+ Create a Company instance from a dictionary.
37
+
38
+ :param data: Dictionary containing company data
39
+ :return: Company instance
40
+ """
41
+ return cls(
42
+ id=data["id"],
43
+ url=data["url"],
44
+ name=data["name"],
45
+ state=data["state"],
46
+ state_display=data["state_display"],
47
+ is_ee=data["is_ee"],
48
+ url_profile=data["url_profile"],
49
+ folder_url=data["folder_url"],
50
+ url_folder_tree=data["url_folder_tree"],
51
+ url_sims=data["url_sims"],
52
+ apn_urls=data["apn_urls"],
53
+ url_devices=data["url_devices"],
54
+ url_budgets=data["url_budgets"],
55
+ url_access=data["url_access"],
56
+ url_tags=data["url_tags"],
57
+ url_contracts=data["url_contracts"],
58
+ url_adaptations=data["url_adaptations"],
59
+ )
60
+
61
+
62
+ @dataclass
63
+ class Folder(BaseModel):
64
+ """Represents a folder from the Connect API."""
65
+
66
+ id: int
67
+ url: str
68
+ name: str
69
+ path: str
70
+ human_path: str
71
+ parent: int
72
+ url_sims: str
73
+ url_devices: str
74
+ tree_id: int
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: Dict[str, Any]) -> "Folder":
78
+ """
79
+ Create a Folder instance from a dictionary.
80
+
81
+ :param data: Dictionary containing folder data
82
+ :return: Folder instance
83
+ """
84
+ return cls(
85
+ id=data["id"],
86
+ url=data["url"],
87
+ name=data["name"],
88
+ path=data["path"],
89
+ human_path=data["human_path"],
90
+ parent=data["parent"],
91
+ url_sims=data["url_sims"],
92
+ url_devices=data["url_devices"],
93
+ tree_id=data["tree_id"],
94
+ )
@@ -0,0 +1,209 @@
1
+ from typing import Dict, Any
2
+
3
+ from trinity_connect_client.decorators import handle_exceptions
4
+ from trinity_connect_client.mixins import ResourceMixin
5
+ from trinity_connect_client.validators import validate_id, validate_uid, validate_command
6
+
7
+
8
+ class DevicesAPI(ResourceMixin):
9
+ @handle_exceptions
10
+ def get(self, device_id: int) -> Dict[str, Any]:
11
+ """
12
+ GET a device by ID.
13
+
14
+ :param device_id: The ID of the device to retrieve
15
+ :return: A Device object as dictionary or error response
16
+ :raises ValueError: If device_id is not a positive integer
17
+ """
18
+ validate_id(device_id)
19
+ url = self._url(f"devices/{device_id}/")
20
+ return self.make_get_request(url)
21
+
22
+ @handle_exceptions
23
+ def get_by_uid(self, device_uid: str) -> Dict[str, Any]:
24
+ """
25
+ GET a device by UID.
26
+
27
+ :param device_uid: The UID of the device to retrieve
28
+ :return: A Device object as dictionary or error response
29
+ :raises ValueError: If device_uid is not a valid string
30
+ """
31
+ validate_uid(device_uid)
32
+ url = self._url(f"devices/uid/{device_uid}/")
33
+ return self.make_get_request(url)
34
+
35
+ @handle_exceptions
36
+ def get_latest_data_by_uid(self, device_uid: str, **filters: str) -> dict[str, Any]:
37
+ """
38
+ GET latest data for a device by UID.
39
+
40
+ :param device_uid: The UID of the device to retrieve
41
+ :return: A Device object as dictionary or error response
42
+ """
43
+ validate_uid(device_uid)
44
+ url = self._url(f"devices/uid/{device_uid}/data/latest/")
45
+ return self.make_get_request(url, params=filters)
46
+
47
+ @handle_exceptions
48
+ def get_events_by_uid(
49
+ self, device_uid: str, **filters: str
50
+ ) -> list[dict[str, Any]]:
51
+ """
52
+ GET events for a device by UID.
53
+
54
+ :param device_uid: The UID of the device to retrieve
55
+ :return:
56
+ """
57
+ validate_uid(device_uid)
58
+ url = self._url(f"devices/uid/{device_uid}/events/")
59
+ return self.make_get_request(url, params=filters)
60
+
61
+ @handle_exceptions
62
+ def get_commands_by_uid(
63
+ self, device_uid: str, **filters: str
64
+ ) -> list[dict[str, Any]]:
65
+ """
66
+ GET commands for a device by UID.
67
+
68
+ :param device_uid: The UID of the device to retrieve
69
+ :return:
70
+ """
71
+ validate_uid(device_uid)
72
+ url = self._url(f"devices/uid/{device_uid}/commands/")
73
+ return self.make_get_request(url, params=filters)
74
+
75
+ @handle_exceptions
76
+ def list_by_folder(self, folder_id: int, **filters: str) -> list[dict[str, Any]]:
77
+ """
78
+ GET list of devices by folder ID.
79
+
80
+ :param folder_id:
81
+ :param filters:
82
+ :return:
83
+ """
84
+ validate_id(folder_id)
85
+ url = self._url(f"devices/folder/{folder_id}/")
86
+ return self.make_get_request(url, params=filters)
87
+
88
+ @handle_exceptions
89
+ def list_by_folder_lite(
90
+ self, folder_id: int, **filters: str
91
+ ) -> list[dict[str, Any]]:
92
+ """
93
+ GET lightweight list of devices by folder ID.
94
+
95
+ :param folder_id:
96
+ :param filters:
97
+ :return:
98
+ """
99
+ validate_id(folder_id)
100
+ url = self._url(f"devices/folder/{folder_id}/lite/")
101
+ return self.make_get_request(url, params=filters)
102
+
103
+ @handle_exceptions
104
+ def move_to_folder(self, device_id: int, folder_id: int) -> dict[str, Any]:
105
+ """
106
+ Move a device identified by ID to a folder identified by ID.
107
+
108
+ :param device_id:
109
+ :param folder_id:
110
+ :return:
111
+ """
112
+ validate_id(device_id)
113
+ validate_id(folder_id)
114
+
115
+ url = self._url(f"devices/{device_id}/")
116
+ data = {
117
+ "folder": folder_id,
118
+ }
119
+ return self.make_patch_request(url, json=data)
120
+
121
+ @handle_exceptions
122
+ def move_to_folder_by_uid(self, device_uid: str, folder_id: int) -> dict[str, Any]:
123
+ """
124
+ Move a device identified by UID to a folder identified by ID.
125
+
126
+ :param device_uid:
127
+ :param folder_id:
128
+ :return:
129
+ """
130
+ validate_uid(device_uid)
131
+ validate_id(folder_id)
132
+
133
+ url = self._url(f"devices/uid/{device_uid}/")
134
+ data = {
135
+ "folder": folder_id,
136
+ }
137
+ return self.make_patch_request(url, json=data)
138
+
139
+ @handle_exceptions
140
+ def set_lifecycle(self, device_id: int, target_state: int) -> dict[str, Any]:
141
+ """
142
+ Change the lifecycle state of a device by ID.
143
+
144
+ :param device_id:
145
+ :param target_state:
146
+ :return:
147
+ """
148
+ validate_id(device_id)
149
+ validate_id(target_state)
150
+
151
+ url = self._url(f"devices/{device_id}/")
152
+ data = {
153
+ "state": target_state,
154
+ }
155
+ return self.make_patch_request(url, json=data)
156
+
157
+ @handle_exceptions
158
+ def set_lifecycle_by_uid(
159
+ self, device_uid: str, target_state: int
160
+ ) -> dict[str, Any]:
161
+ """
162
+ Change the lifecycle state of a device by UID.
163
+
164
+ :param device_uid:
165
+ :param target_state:
166
+ :return:
167
+ """
168
+ validate_uid(device_uid)
169
+ validate_id(target_state)
170
+
171
+ url = self._url(f"devices/uid/{device_uid}/")
172
+ data = {
173
+ "state": target_state,
174
+ }
175
+ return self.make_patch_request(url, json=data)
176
+
177
+ @handle_exceptions
178
+ def issue_command(self, device_id: int, command: dict) -> dict[str, Any]:
179
+ """
180
+ Issue an arbitrary command to a device by ID.
181
+
182
+ :param device_id:
183
+ :param command:
184
+ :return:
185
+ """
186
+ validate_id(device_id)
187
+ validate_command(command)
188
+ url = self._url(f"devices/{device_id}/command/send/")
189
+ data = {
190
+ **command,
191
+ }
192
+ return self.make_post_request(url, json=data)
193
+
194
+ @handle_exceptions
195
+ def issue_command_by_uid(self, device_uid: str, command: dict) -> dict[str, Any]:
196
+ """
197
+ Issue an arbitrary command to a device by UID.
198
+
199
+ :param device_uid:
200
+ :param command:
201
+ :return:
202
+ """
203
+ validate_uid(device_uid)
204
+ validate_command(command)
205
+ url = self._url(f"devices/uid/{device_uid}/command/send/")
206
+ data = {
207
+ **command,
208
+ }
209
+ return self.make_post_request(url, json=data)
@@ -0,0 +1,49 @@
1
+ from typing import Dict, List, Any, Union
2
+
3
+ from trinity_connect_client.decorators import handle_exceptions
4
+ from trinity_connect_client.mixins import ResourceMixin
5
+ from trinity_connect_client.validators import validate_id
6
+
7
+
8
+ class OrgsAPI(ResourceMixin):
9
+ @handle_exceptions
10
+ def get(self, company_id: int) -> Dict[str, Any]:
11
+ """
12
+ GET a company by ID.
13
+
14
+ :param company_id: The ID of the company to retrieve
15
+ :return: A Company object as dictionary or error response
16
+ """
17
+ validate_id(company_id)
18
+ url = self._url(f"orgs/company/{company_id}/")
19
+ return self.make_get_request(url)
20
+
21
+ @handle_exceptions
22
+ def get_folders(
23
+ self, company_id: int, **filters
24
+ ) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
25
+ """
26
+ GET company folders for a given company ID.
27
+
28
+ :param company_id: The ID of the company whose folders to retrieve
29
+ :param filters: Optional filters to apply to the request
30
+ :return: List of folder objects as dictionaries or error response
31
+ """
32
+ validate_id(company_id)
33
+ url = self._url(f"orgs/folders/company/{company_id}/")
34
+ return self.make_get_request(url, params=filters)
35
+
36
+ @handle_exceptions
37
+ def get_folder(
38
+ self, folder_id: int, **filters
39
+ ) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
40
+ """
41
+ GET folder for a given folder ID.
42
+
43
+ :param folder_id: The ID of the folder to retrieve
44
+ :param filters: Optional filters to apply to the request
45
+ :return: List of folder objects as dictionaries or error response
46
+ """
47
+ validate_id(folder_id)
48
+ url = self._url(f"orgs/folder/{folder_id}/")
49
+ return self.make_get_request(url, params=filters)
@@ -0,0 +1,33 @@
1
+ def validate_id(i):
2
+ if not isinstance(i, int) or i <= 0:
3
+ raise ValueError("ID must be a positive integer")
4
+
5
+
6
+ def validate_uid(u):
7
+ if not isinstance(u, str) or not u.strip():
8
+ raise ValueError("UID must be a non-empty string")
9
+
10
+
11
+ def validate_command(command):
12
+ if not isinstance(command, dict):
13
+ raise ValueError("Command must be a dictionary")
14
+
15
+ required_fields = {"rpc", "args", "pid", "ttl", "qos"}
16
+ if not required_fields.issubset(command.keys()):
17
+ missing = required_fields - command.keys()
18
+ raise ValueError(f"Command missing required fields: {missing}")
19
+
20
+ if not isinstance(command["rpc"], str):
21
+ raise ValueError("Command 'rpc' must be a string")
22
+
23
+ if not isinstance(command["args"], list):
24
+ raise ValueError("Command 'args' must be a list")
25
+
26
+ if not isinstance(command["pid"], (str, int)):
27
+ raise ValueError("Command 'pid' must be a string or integer")
28
+
29
+ if not isinstance(command["ttl"], (int, float)) or command["ttl"] < 0:
30
+ raise ValueError("Command 'ttl' must be a non-negative number")
31
+
32
+ if not isinstance(command["qos"], int) or command["qos"] < 0:
33
+ raise ValueError("Command 'qos' must be a non-negative integer")