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.
- trinity_connect_client-0.2.0/PKG-INFO +264 -0
- trinity_connect_client-0.2.0/README.md +249 -0
- trinity_connect_client-0.2.0/pyproject.toml +43 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/__init__.py +13 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/decorators.py +19 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/exceptions/__init__.py +13 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/main.py +18 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/mixins/__init__.py +87 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/models/__init__.py +17 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/models/base.py +19 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/models/device.py +157 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/models/org.py +94 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/modules/__init__.py +0 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/modules/devices/__init__.py +209 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/modules/orgs/__init__.py +49 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/py.typed +0 -0
- trinity_connect_client-0.2.0/src/trinity_connect_client/validators.py +33 -0
|
@@ -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
|
+
[](https://www.python.org/downloads/)
|
|
19
|
+
[](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
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](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
|
+
)
|
|
File without changes
|
|
@@ -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)
|
|
File without changes
|
|
@@ -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")
|