libreclient 0.1.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.
Files changed (73) hide show
  1. libreclient-0.1.0/PKG-INFO +319 -0
  2. libreclient-0.1.0/README.md +301 -0
  3. libreclient-0.1.0/pyproject.toml +96 -0
  4. libreclient-0.1.0/src/libreclient/__init__.py +10 -0
  5. libreclient-0.1.0/src/libreclient/__init__.pyi +4 -0
  6. libreclient-0.1.0/src/libreclient/_base_client.py +70 -0
  7. libreclient-0.1.0/src/libreclient/_route_types.py +0 -0
  8. libreclient-0.1.0/src/libreclient/client.py +325 -0
  9. libreclient-0.1.0/src/libreclient/config.py +59 -0
  10. libreclient-0.1.0/src/libreclient/models/__init__.py +83 -0
  11. libreclient-0.1.0/src/libreclient/models/_base.py +35 -0
  12. libreclient-0.1.0/src/libreclient/models/alerts.py +31 -0
  13. libreclient-0.1.0/src/libreclient/models/arp.py +13 -0
  14. libreclient-0.1.0/src/libreclient/models/bills.py +21 -0
  15. libreclient-0.1.0/src/libreclient/models/device_groups.py +19 -0
  16. libreclient-0.1.0/src/libreclient/models/devices.py +41 -0
  17. libreclient-0.1.0/src/libreclient/models/index.py +15 -0
  18. libreclient-0.1.0/src/libreclient/models/inventory.py +15 -0
  19. libreclient-0.1.0/src/libreclient/models/locations.py +15 -0
  20. libreclient-0.1.0/src/libreclient/models/logs.py +13 -0
  21. libreclient-0.1.0/src/libreclient/models/poller_groups.py +15 -0
  22. libreclient-0.1.0/src/libreclient/models/pollers.py +13 -0
  23. libreclient-0.1.0/src/libreclient/models/port_groups.py +13 -0
  24. libreclient-0.1.0/src/libreclient/models/port_security.py +15 -0
  25. libreclient-0.1.0/src/libreclient/models/portgroups.py +9 -0
  26. libreclient-0.1.0/src/libreclient/models/ports.py +41 -0
  27. libreclient-0.1.0/src/libreclient/models/routing.py +13 -0
  28. libreclient-0.1.0/src/libreclient/models/services.py +18 -0
  29. libreclient-0.1.0/src/libreclient/models/switching.py +15 -0
  30. libreclient-0.1.0/src/libreclient/models/system.py +13 -0
  31. libreclient-0.1.0/src/libreclient/py.typed +0 -0
  32. libreclient-0.1.0/src/libreclient/routes/__init__.py +79 -0
  33. libreclient-0.1.0/src/libreclient/routes/__init__.pyi +79 -0
  34. libreclient-0.1.0/src/libreclient/routes/_synchronicity.py +7 -0
  35. libreclient-0.1.0/src/libreclient/routes/_types.py +65 -0
  36. libreclient-0.1.0/src/libreclient/routes/alerts.py +215 -0
  37. libreclient-0.1.0/src/libreclient/routes/alerts.pyi +220 -0
  38. libreclient-0.1.0/src/libreclient/routes/arp.py +35 -0
  39. libreclient-0.1.0/src/libreclient/routes/arp.pyi +30 -0
  40. libreclient-0.1.0/src/libreclient/routes/bills.py +167 -0
  41. libreclient-0.1.0/src/libreclient/routes/bills.pyi +166 -0
  42. libreclient-0.1.0/src/libreclient/routes/device_groups.py +204 -0
  43. libreclient-0.1.0/src/libreclient/routes/device_groups.pyi +168 -0
  44. libreclient-0.1.0/src/libreclient/routes/devices.py +720 -0
  45. libreclient-0.1.0/src/libreclient/routes/devices.pyi +692 -0
  46. libreclient-0.1.0/src/libreclient/routes/index.py +25 -0
  47. libreclient-0.1.0/src/libreclient/routes/index.pyi +24 -0
  48. libreclient-0.1.0/src/libreclient/routes/inventory.py +58 -0
  49. libreclient-0.1.0/src/libreclient/routes/inventory.pyi +46 -0
  50. libreclient-0.1.0/src/libreclient/routes/locations.py +119 -0
  51. libreclient-0.1.0/src/libreclient/routes/locations.pyi +113 -0
  52. libreclient-0.1.0/src/libreclient/routes/logs.py +165 -0
  53. libreclient-0.1.0/src/libreclient/routes/logs.pyi +137 -0
  54. libreclient-0.1.0/src/libreclient/routes/poller_groups.py +34 -0
  55. libreclient-0.1.0/src/libreclient/routes/poller_groups.pyi +28 -0
  56. libreclient-0.1.0/src/libreclient/routes/pollers.py +40 -0
  57. libreclient-0.1.0/src/libreclient/routes/pollers.pyi +38 -0
  58. libreclient-0.1.0/src/libreclient/routes/port_groups.py +87 -0
  59. libreclient-0.1.0/src/libreclient/routes/port_groups.pyi +87 -0
  60. libreclient-0.1.0/src/libreclient/routes/port_security.py +51 -0
  61. libreclient-0.1.0/src/libreclient/routes/port_security.pyi +52 -0
  62. libreclient-0.1.0/src/libreclient/routes/portgroups.py +64 -0
  63. libreclient-0.1.0/src/libreclient/routes/portgroups.pyi +57 -0
  64. libreclient-0.1.0/src/libreclient/routes/ports.py +138 -0
  65. libreclient-0.1.0/src/libreclient/routes/ports.pyi +132 -0
  66. libreclient-0.1.0/src/libreclient/routes/routing.py +247 -0
  67. libreclient-0.1.0/src/libreclient/routes/routing.pyi +242 -0
  68. libreclient-0.1.0/src/libreclient/routes/services.py +108 -0
  69. libreclient-0.1.0/src/libreclient/routes/services.pyi +102 -0
  70. libreclient-0.1.0/src/libreclient/routes/switching.py +98 -0
  71. libreclient-0.1.0/src/libreclient/routes/switching.pyi +112 -0
  72. libreclient-0.1.0/src/libreclient/routes/system.py +36 -0
  73. libreclient-0.1.0/src/libreclient/routes/system.pyi +35 -0
@@ -0,0 +1,319 @@
1
+ Metadata-Version: 2.4
2
+ Name: libreclient
3
+ Version: 0.1.0
4
+ Summary: Async and sync Python client for the LibreNMS API
5
+ Author: Justin Jeffery
6
+ Author-email: Justin Jeffery <34625666+jjeff07@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ Requires-Dist: niquests>=3.19.0
9
+ Requires-Dist: pydantic>=2.13.4
10
+ Requires-Dist: pydantic-settings>=2.14.1
11
+ Requires-Dist: synchronicity>=0.12.3
12
+ Requires-Python: >=3.12
13
+ Project-URL: Homepage, https://github.com/jjeff07/libreclient
14
+ Project-URL: Repository, https://github.com/jjeff07/libreclient.git
15
+ Project-URL: Issues, https://github.com/jjeff07/libreclient/issues
16
+ Project-URL: Changelog, https://github.com/jjeff07/libreclient/blob/main/CHANGELOG.md
17
+ Description-Content-Type: text/markdown
18
+
19
+ # libreclient
20
+
21
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
22
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
23
+
24
+ Async and sync Python client for the [LibreNMS](https://www.librenms.org/) API.
25
+
26
+ - **Dual interface** — use `LibreClientAsync` for async/await or `LibreClientSync` for traditional blocking calls.
27
+ - **Typed responses** — all endpoints return Pydantic models with full IDE autocomplete.
28
+ - **Environment-driven config** — configure via `LIBRENMS_URL` and `LIBRENMS_TOKEN` env vars or pass values directly.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install libreclient
34
+ ```
35
+
36
+ Or with [uv](https://docs.astral.sh/uv/):
37
+
38
+ ```bash
39
+ uv add libreclient
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ### Synchronous
45
+
46
+ ```python
47
+ from libreclient import LibreClientSync
48
+
49
+ client = LibreClientSync(url="https://librenms.example.com", token="your-api-token")
50
+
51
+ # List all devices
52
+ response = client.devices.list_devices()
53
+ for device in response.devices:
54
+ print(device["hostname"])
55
+
56
+ # Get a specific alert
57
+ alert = client.alerts.get_alert(42)
58
+ ```
59
+
60
+ ### Asynchronous
61
+
62
+ ```python
63
+ import asyncio
64
+ from libreclient import LibreClientAsync
65
+
66
+
67
+ async def main():
68
+ client = LibreClientAsync(url="https://librenms.example.com", token="your-api-token")
69
+
70
+ response = await client.devices.list_devices()
71
+ for device in response.devices:
72
+ print(device["hostname"])
73
+
74
+ await client.close()
75
+
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ ### Context Manager
81
+
82
+ ```python
83
+ # Sync
84
+ with LibreClientSync(url="https://librenms.example.com", token="your-api-token") as client:
85
+ print(client.system.ping())
86
+
87
+ # Async
88
+ async with LibreClientAsync(url="https://librenms.example.com", token="your-api-token") as client:
89
+ print(await client.system.ping())
90
+ ```
91
+
92
+ ## Configuration
93
+
94
+ Configuration is handled by [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). You can
95
+ pass values directly or set environment variables:
96
+
97
+ | Env Variable | Description | Default |
98
+ |------------------------|------------------------------------|--------------|
99
+ | `LIBRENMS_URL` | Base URL of your LibreNMS instance | *(required)* |
100
+ | `LIBRENMS_TOKEN` | API token (`X-Auth-Token`) | *(required)* |
101
+ | `LIBRENMS_VERIFY_SSL` | Verify TLS certificates | `true` |
102
+ | `LIBRENMS_API_VERSION` | API version path segment | `v0` |
103
+
104
+ A `.env` file in your working directory is also supported. Copy the included sample to get started:
105
+
106
+ ```bash
107
+ cp sample.env .env
108
+ # Edit .env with your LibreNMS URL and API token
109
+ ```
110
+
111
+ ## Available Route Namespaces
112
+
113
+ All route namespaces are accessible as properties on the client:
114
+
115
+ | Property | Description |
116
+ |------------------------|--------------------------------------------|
117
+ | `client.alerts` | Alert management and alert rules/templates |
118
+ | `client.arp` | ARP table lookups |
119
+ | `client.bills` | Billing data and graphs |
120
+ | `client.device_groups` | Device group management |
121
+ | `client.devices` | Device CRUD, discovery, components, graphs |
122
+ | `client.index` | List available API endpoints |
123
+ | `client.inventory` | Hardware inventory |
124
+ | `client.locations` | Location management |
125
+ | `client.logs` | Event, syslog, alert, and auth logs |
126
+ | `client.poller_groups` | Poller group info |
127
+ | `client.pollers` | Poller status |
128
+ | `client.port_groups` | Port group management |
129
+ | `client.port_security` | Port security (802.1X/MAB) |
130
+ | `client.ports` | Port info, search, and descriptions |
131
+ | `client.routing` | BGP, OSPF, VRF, MPLS, IPsec |
132
+ | `client.services` | Service monitoring |
133
+ | `client.switching` | VLANs, links, FDB, NAC |
134
+ | `client.system` | Ping and system info |
135
+
136
+ ---
137
+
138
+ ## Development
139
+
140
+ ### Prerequisites
141
+
142
+ - Python 3.12+
143
+ - [uv](https://docs.astral.sh/uv/) (package manager)
144
+
145
+ ### Setup
146
+
147
+ ```bash
148
+ git clone https://github.com/jjeff07/libreclient.git
149
+ cd libreclient
150
+ uv sync
151
+ ```
152
+
153
+ ### Running Tests
154
+
155
+ ```bash
156
+ # Unit tests
157
+ uv run pytest tests/unit
158
+
159
+ # Functional tests (requires .env with LIBRENMS_URL and LIBRENMS_TOKEN)
160
+ uv run pytest tests/functional
161
+ ```
162
+
163
+ ### Linting & Formatting
164
+
165
+ This project uses [Ruff](https://docs.astral.sh/ruff/) for both linting and formatting:
166
+
167
+ ```bash
168
+ # Check for lint issues
169
+ uv run ruff check
170
+
171
+ # Auto-fix lint issues
172
+ uv run ruff check --fix
173
+
174
+ # Format code
175
+ uv run ruff format
176
+
177
+ # Check formatting without changing files
178
+ uv run ruff format --check
179
+ ```
180
+
181
+ ### Complexity Checks
182
+
183
+ [complexipy](https://github.com/rohaquinlop/complexipy) is used to enforce a maximum cognitive complexity of 15 per
184
+ function:
185
+
186
+ ```bash
187
+ uv run complexipy .
188
+ ```
189
+
190
+ Results are output to `complexipy-results.json`. Any function exceeding the threshold will cause the check to fail.
191
+
192
+ ### Architecture
193
+
194
+ The project uses a single-implementation pattern: each route is written **once** as an async class.
195
+ The [synchronicity](https://github.com/modal-com/synchronicity) library then wraps each async class to produce a
196
+ synchronous counterpart at runtime.
197
+
198
+ ```
199
+ src/py_librenms/routes/alerts.py
200
+ ├── class Alerts ← async implementation (the only code you write)
201
+ └── AlertsSync = synchronizer.wrap(Alerts, ...) ← sync wrapper (auto-generated at import)
202
+ ```
203
+
204
+ This means:
205
+
206
+ - You only maintain one implementation per route.
207
+ - Both `LibreClientAsync` and `LibreClientSync` share the same logic.
208
+ - No code duplication between sync and async interfaces.
209
+
210
+ ### Type Stubs
211
+
212
+ Because `synchronicity` generates wrapper classes dynamically, IDEs can't infer their method signatures. To restore full
213
+ autocomplete and type checking, `.pyi` stub files are auto-generated.
214
+
215
+ **Regenerate stubs locally:**
216
+
217
+ ```bash
218
+ uv run python generate_stubs.py
219
+ ```
220
+
221
+ Stubs are generated automatically during the GitHub Actions release workflow, so you don't need to commit them — they're
222
+ in `.gitignore`.
223
+
224
+ ### Adding a New Route
225
+
226
+ 1. Create `src/py_librenms/routes/myroute.py` with an async class and `MyRouteSync = synchronizer.wrap(...)` at the
227
+ bottom.
228
+ 2. Create `src/py_librenms/models/myroute.py` with Pydantic response models.
229
+ 3. Add exports to `src/py_librenms/models/__init__.py`.
230
+ 4. Add exports to `src/py_librenms/routes/__init__.py`.
231
+ 5. Wire up the route in `src/py_librenms/client.py` (both sync and async clients).
232
+ 6. Run `uv run python generate_stubs.py`.
233
+ 7. Add tests in `tests/unit/routes/test_myroute.py` and `tests/unit/models/test_myroute.py`.
234
+
235
+ ### Commit Convention
236
+
237
+ This project enforces [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) via
238
+ [commitizen](https://commitizen-tools.github.io/commitizen/). A git hook validates every commit message automatically.
239
+
240
+ **Setup the hook (once per clone):**
241
+
242
+ ```bash
243
+ git config core.hooksPath .githooks
244
+ ```
245
+
246
+ **Format:**
247
+
248
+ ```
249
+ type(scope)?: description
250
+
251
+ [optional body]
252
+ [optional footer]
253
+ ```
254
+
255
+ **Allowed types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`, `bump`
256
+
257
+ **Examples:**
258
+
259
+ ```
260
+ feat(routing): add OSPFv3 port listing
261
+ fix: handle empty response from list_devices
262
+ docs: add upstream tracking section to README
263
+ test: add functional tests for switching routes
264
+ ```
265
+
266
+ ### Upstream API Tracking
267
+
268
+ This project tracks which LibreNMS release tag the route implementations are based on. The pinned version is stored in
269
+ `upstream_tracking.toml`.
270
+
271
+ ```bash
272
+ # Check if upstream has a newer release
273
+ python check_upstream.py
274
+
275
+ # See which API doc files changed
276
+ python check_upstream.py --diff
277
+
278
+ # See full unified diffs of changed docs
279
+ python check_upstream.py --full
280
+
281
+ # Compare against a specific tag instead of latest
282
+ python check_upstream.py --diff --tag 26.6.0
283
+
284
+ # Bump the pinned tag after reviewing changes
285
+ python check_upstream.py --bump
286
+ ```
287
+
288
+ ### Project Structure
289
+
290
+ ```
291
+ libreclient/
292
+ ├── src/py_librenms/
293
+ │ ├── __init__.py # Public API exports
294
+ │ ├── client.py # LibreClientSync & LibreClientAsync
295
+ │ ├── config.py # Pydantic-settings configuration
296
+ │ ├── _base_client.py # Shared HTTP transport logic
297
+ │ ├── models/ # Pydantic response models
298
+ │ └── routes/ # Route namespaces (async + sync wrappers)
299
+ │ ├── _types.py # ClientProtocol & utilities
300
+ │ ├── _synchronicity.py # Shared Synchronizer instance
301
+ │ ├── alerts.py # Example route implementation
302
+ │ └── ...
303
+ ├── tests/
304
+ │ ├── unit/
305
+ │ │ ├── models/ # Model validation tests
306
+ │ │ └── routes/ # Route logic tests (MockClient)
307
+ │ └── functional/ # Live API tests (requires .env)
308
+ ├── check_upstream.py # Detect upstream API doc changes
309
+ ├── upstream_tracking.toml # Pinned LibreNMS release tag
310
+ ├── generate_stubs.py # .pyi stub generator
311
+ ├── pyproject.toml
312
+ ├── CHANGELOG.md
313
+ └── LICENSE
314
+ ```
315
+
316
+ ## License
317
+
318
+ [MIT](LICENSE)
319
+
@@ -0,0 +1,301 @@
1
+ # libreclient
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
5
+
6
+ Async and sync Python client for the [LibreNMS](https://www.librenms.org/) API.
7
+
8
+ - **Dual interface** — use `LibreClientAsync` for async/await or `LibreClientSync` for traditional blocking calls.
9
+ - **Typed responses** — all endpoints return Pydantic models with full IDE autocomplete.
10
+ - **Environment-driven config** — configure via `LIBRENMS_URL` and `LIBRENMS_TOKEN` env vars or pass values directly.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install libreclient
16
+ ```
17
+
18
+ Or with [uv](https://docs.astral.sh/uv/):
19
+
20
+ ```bash
21
+ uv add libreclient
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### Synchronous
27
+
28
+ ```python
29
+ from libreclient import LibreClientSync
30
+
31
+ client = LibreClientSync(url="https://librenms.example.com", token="your-api-token")
32
+
33
+ # List all devices
34
+ response = client.devices.list_devices()
35
+ for device in response.devices:
36
+ print(device["hostname"])
37
+
38
+ # Get a specific alert
39
+ alert = client.alerts.get_alert(42)
40
+ ```
41
+
42
+ ### Asynchronous
43
+
44
+ ```python
45
+ import asyncio
46
+ from libreclient import LibreClientAsync
47
+
48
+
49
+ async def main():
50
+ client = LibreClientAsync(url="https://librenms.example.com", token="your-api-token")
51
+
52
+ response = await client.devices.list_devices()
53
+ for device in response.devices:
54
+ print(device["hostname"])
55
+
56
+ await client.close()
57
+
58
+
59
+ asyncio.run(main())
60
+ ```
61
+
62
+ ### Context Manager
63
+
64
+ ```python
65
+ # Sync
66
+ with LibreClientSync(url="https://librenms.example.com", token="your-api-token") as client:
67
+ print(client.system.ping())
68
+
69
+ # Async
70
+ async with LibreClientAsync(url="https://librenms.example.com", token="your-api-token") as client:
71
+ print(await client.system.ping())
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ Configuration is handled by [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). You can
77
+ pass values directly or set environment variables:
78
+
79
+ | Env Variable | Description | Default |
80
+ |------------------------|------------------------------------|--------------|
81
+ | `LIBRENMS_URL` | Base URL of your LibreNMS instance | *(required)* |
82
+ | `LIBRENMS_TOKEN` | API token (`X-Auth-Token`) | *(required)* |
83
+ | `LIBRENMS_VERIFY_SSL` | Verify TLS certificates | `true` |
84
+ | `LIBRENMS_API_VERSION` | API version path segment | `v0` |
85
+
86
+ A `.env` file in your working directory is also supported. Copy the included sample to get started:
87
+
88
+ ```bash
89
+ cp sample.env .env
90
+ # Edit .env with your LibreNMS URL and API token
91
+ ```
92
+
93
+ ## Available Route Namespaces
94
+
95
+ All route namespaces are accessible as properties on the client:
96
+
97
+ | Property | Description |
98
+ |------------------------|--------------------------------------------|
99
+ | `client.alerts` | Alert management and alert rules/templates |
100
+ | `client.arp` | ARP table lookups |
101
+ | `client.bills` | Billing data and graphs |
102
+ | `client.device_groups` | Device group management |
103
+ | `client.devices` | Device CRUD, discovery, components, graphs |
104
+ | `client.index` | List available API endpoints |
105
+ | `client.inventory` | Hardware inventory |
106
+ | `client.locations` | Location management |
107
+ | `client.logs` | Event, syslog, alert, and auth logs |
108
+ | `client.poller_groups` | Poller group info |
109
+ | `client.pollers` | Poller status |
110
+ | `client.port_groups` | Port group management |
111
+ | `client.port_security` | Port security (802.1X/MAB) |
112
+ | `client.ports` | Port info, search, and descriptions |
113
+ | `client.routing` | BGP, OSPF, VRF, MPLS, IPsec |
114
+ | `client.services` | Service monitoring |
115
+ | `client.switching` | VLANs, links, FDB, NAC |
116
+ | `client.system` | Ping and system info |
117
+
118
+ ---
119
+
120
+ ## Development
121
+
122
+ ### Prerequisites
123
+
124
+ - Python 3.12+
125
+ - [uv](https://docs.astral.sh/uv/) (package manager)
126
+
127
+ ### Setup
128
+
129
+ ```bash
130
+ git clone https://github.com/jjeff07/libreclient.git
131
+ cd libreclient
132
+ uv sync
133
+ ```
134
+
135
+ ### Running Tests
136
+
137
+ ```bash
138
+ # Unit tests
139
+ uv run pytest tests/unit
140
+
141
+ # Functional tests (requires .env with LIBRENMS_URL and LIBRENMS_TOKEN)
142
+ uv run pytest tests/functional
143
+ ```
144
+
145
+ ### Linting & Formatting
146
+
147
+ This project uses [Ruff](https://docs.astral.sh/ruff/) for both linting and formatting:
148
+
149
+ ```bash
150
+ # Check for lint issues
151
+ uv run ruff check
152
+
153
+ # Auto-fix lint issues
154
+ uv run ruff check --fix
155
+
156
+ # Format code
157
+ uv run ruff format
158
+
159
+ # Check formatting without changing files
160
+ uv run ruff format --check
161
+ ```
162
+
163
+ ### Complexity Checks
164
+
165
+ [complexipy](https://github.com/rohaquinlop/complexipy) is used to enforce a maximum cognitive complexity of 15 per
166
+ function:
167
+
168
+ ```bash
169
+ uv run complexipy .
170
+ ```
171
+
172
+ Results are output to `complexipy-results.json`. Any function exceeding the threshold will cause the check to fail.
173
+
174
+ ### Architecture
175
+
176
+ The project uses a single-implementation pattern: each route is written **once** as an async class.
177
+ The [synchronicity](https://github.com/modal-com/synchronicity) library then wraps each async class to produce a
178
+ synchronous counterpart at runtime.
179
+
180
+ ```
181
+ src/py_librenms/routes/alerts.py
182
+ ├── class Alerts ← async implementation (the only code you write)
183
+ └── AlertsSync = synchronizer.wrap(Alerts, ...) ← sync wrapper (auto-generated at import)
184
+ ```
185
+
186
+ This means:
187
+
188
+ - You only maintain one implementation per route.
189
+ - Both `LibreClientAsync` and `LibreClientSync` share the same logic.
190
+ - No code duplication between sync and async interfaces.
191
+
192
+ ### Type Stubs
193
+
194
+ Because `synchronicity` generates wrapper classes dynamically, IDEs can't infer their method signatures. To restore full
195
+ autocomplete and type checking, `.pyi` stub files are auto-generated.
196
+
197
+ **Regenerate stubs locally:**
198
+
199
+ ```bash
200
+ uv run python generate_stubs.py
201
+ ```
202
+
203
+ Stubs are generated automatically during the GitHub Actions release workflow, so you don't need to commit them — they're
204
+ in `.gitignore`.
205
+
206
+ ### Adding a New Route
207
+
208
+ 1. Create `src/py_librenms/routes/myroute.py` with an async class and `MyRouteSync = synchronizer.wrap(...)` at the
209
+ bottom.
210
+ 2. Create `src/py_librenms/models/myroute.py` with Pydantic response models.
211
+ 3. Add exports to `src/py_librenms/models/__init__.py`.
212
+ 4. Add exports to `src/py_librenms/routes/__init__.py`.
213
+ 5. Wire up the route in `src/py_librenms/client.py` (both sync and async clients).
214
+ 6. Run `uv run python generate_stubs.py`.
215
+ 7. Add tests in `tests/unit/routes/test_myroute.py` and `tests/unit/models/test_myroute.py`.
216
+
217
+ ### Commit Convention
218
+
219
+ This project enforces [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) via
220
+ [commitizen](https://commitizen-tools.github.io/commitizen/). A git hook validates every commit message automatically.
221
+
222
+ **Setup the hook (once per clone):**
223
+
224
+ ```bash
225
+ git config core.hooksPath .githooks
226
+ ```
227
+
228
+ **Format:**
229
+
230
+ ```
231
+ type(scope)?: description
232
+
233
+ [optional body]
234
+ [optional footer]
235
+ ```
236
+
237
+ **Allowed types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`, `bump`
238
+
239
+ **Examples:**
240
+
241
+ ```
242
+ feat(routing): add OSPFv3 port listing
243
+ fix: handle empty response from list_devices
244
+ docs: add upstream tracking section to README
245
+ test: add functional tests for switching routes
246
+ ```
247
+
248
+ ### Upstream API Tracking
249
+
250
+ This project tracks which LibreNMS release tag the route implementations are based on. The pinned version is stored in
251
+ `upstream_tracking.toml`.
252
+
253
+ ```bash
254
+ # Check if upstream has a newer release
255
+ python check_upstream.py
256
+
257
+ # See which API doc files changed
258
+ python check_upstream.py --diff
259
+
260
+ # See full unified diffs of changed docs
261
+ python check_upstream.py --full
262
+
263
+ # Compare against a specific tag instead of latest
264
+ python check_upstream.py --diff --tag 26.6.0
265
+
266
+ # Bump the pinned tag after reviewing changes
267
+ python check_upstream.py --bump
268
+ ```
269
+
270
+ ### Project Structure
271
+
272
+ ```
273
+ libreclient/
274
+ ├── src/py_librenms/
275
+ │ ├── __init__.py # Public API exports
276
+ │ ├── client.py # LibreClientSync & LibreClientAsync
277
+ │ ├── config.py # Pydantic-settings configuration
278
+ │ ├── _base_client.py # Shared HTTP transport logic
279
+ │ ├── models/ # Pydantic response models
280
+ │ └── routes/ # Route namespaces (async + sync wrappers)
281
+ │ ├── _types.py # ClientProtocol & utilities
282
+ │ ├── _synchronicity.py # Shared Synchronizer instance
283
+ │ ├── alerts.py # Example route implementation
284
+ │ └── ...
285
+ ├── tests/
286
+ │ ├── unit/
287
+ │ │ ├── models/ # Model validation tests
288
+ │ │ └── routes/ # Route logic tests (MockClient)
289
+ │ └── functional/ # Live API tests (requires .env)
290
+ ├── check_upstream.py # Detect upstream API doc changes
291
+ ├── upstream_tracking.toml # Pinned LibreNMS release tag
292
+ ├── generate_stubs.py # .pyi stub generator
293
+ ├── pyproject.toml
294
+ ├── CHANGELOG.md
295
+ └── LICENSE
296
+ ```
297
+
298
+ ## License
299
+
300
+ [MIT](LICENSE)
301
+