span-panel-api 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 (77) hide show
  1. span_panel_api-0.1.0/LICENSE +21 -0
  2. span_panel_api-0.1.0/PKG-INFO +457 -0
  3. span_panel_api-0.1.0/README.md +436 -0
  4. span_panel_api-0.1.0/pyproject.toml +132 -0
  5. span_panel_api-0.1.0/src/span_panel_api/__init__.py +30 -0
  6. span_panel_api-0.1.0/src/span_panel_api/client.py +773 -0
  7. span_panel_api-0.1.0/src/span_panel_api/const.py +24 -0
  8. span_panel_api-0.1.0/src/span_panel_api/exceptions.py +45 -0
  9. span_panel_api-0.1.0/src/span_panel_api/generated_client/__init__.py +7 -0
  10. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/__init__.py +1 -0
  11. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/__init__.py +1 -0
  12. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/delete_client_api_v1_auth_clients_name_delete.py +154 -0
  13. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/deprecated_spaces_endpoint_stub_api_v1_spaces_get.py +161 -0
  14. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/deprecated_spaces_endpoint_stub_api_v1_spaces_spaces_id_get.py +153 -0
  15. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/deprecated_spaces_endpoint_stub_api_v1_spaces_spaces_id_post.py +153 -0
  16. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/generate_jwt_api_v1_auth_register_post.py +165 -0
  17. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_all_clients_api_v1_auth_clients_get.py +122 -0
  18. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_circuit_state_api_v_1_circuits_circuit_id_get.py +155 -0
  19. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_circuits_api_v1_circuits_get.py +122 -0
  20. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_client_api_v1_auth_clients_name_get.py +154 -0
  21. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_islanding_state_api_v1_islanding_state_get.py +126 -0
  22. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_main_relay_state_api_v1_panel_grid_get.py +122 -0
  23. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_panel_meter_api_v1_panel_meter_get.py +122 -0
  24. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_panel_power_api_v1_panel_power_get.py +122 -0
  25. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_panel_state_api_v1_panel_get.py +122 -0
  26. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_storage_nice_to_have_threshold_api_v1_storage_nice_to_have_thresh_get.py +126 -0
  27. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_storage_soe_api_v1_storage_soe_get.py +126 -0
  28. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_wifi_scan_api_v1_wifi_scan_get.py +122 -0
  29. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/run_panel_emergency_reconnect_api_v1_panel_emergency_reconnect_post.py +79 -0
  30. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/run_wifi_connect_api_v1_wifi_connect_post.py +165 -0
  31. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_circuit_state_api_v_1_circuits_circuit_id_post.py +180 -0
  32. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_main_relay_state_api_v1_panel_grid_post.py +163 -0
  33. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_storage_nice_to_have_threshold_api_v1_storage_nice_to_have_thresh_post.py +164 -0
  34. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_storage_soe_api_v1_storage_soe_post.py +164 -0
  35. span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/system_status_api_v1_status_get.py +122 -0
  36. span_panel_api-0.1.0/src/span_panel_api/generated_client/client.py +268 -0
  37. span_panel_api-0.1.0/src/span_panel_api/generated_client/errors.py +16 -0
  38. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/__init__.py +81 -0
  39. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/allowed_endpoint_groups.py +83 -0
  40. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/auth_in.py +88 -0
  41. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/auth_out.py +75 -0
  42. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/battery_storage.py +65 -0
  43. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/body_set_circuit_state_api_v1_circuits_circuit_id_post.py +158 -0
  44. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/boolean_in.py +59 -0
  45. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/branch.py +117 -0
  46. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuit.py +163 -0
  47. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuit_name_in.py +59 -0
  48. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuits_out.py +65 -0
  49. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuits_out_circuits.py +57 -0
  50. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/client.py +85 -0
  51. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/clients.py +65 -0
  52. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/clients_clients.py +57 -0
  53. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/door_state.py +10 -0
  54. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/feedthrough_energy.py +67 -0
  55. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/http_validation_error.py +75 -0
  56. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/islanding_state.py +59 -0
  57. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/main_meter_energy.py +67 -0
  58. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/network_status.py +75 -0
  59. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/nice_to_have_threshold.py +88 -0
  60. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/panel_meter.py +75 -0
  61. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/panel_power.py +67 -0
  62. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/panel_state.py +159 -0
  63. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/priority.py +11 -0
  64. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/priority_in.py +61 -0
  65. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/relay_state.py +10 -0
  66. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/relay_state_in.py +61 -0
  67. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/relay_state_out.py +59 -0
  68. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/software_status.py +75 -0
  69. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/state_of_energy.py +59 -0
  70. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/status_out.py +85 -0
  71. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/system_status.py +101 -0
  72. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/validation_error.py +75 -0
  73. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_access_point.py +110 -0
  74. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_connect_in.py +67 -0
  75. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_connect_out.py +99 -0
  76. span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_scan_out.py +73 -0
  77. span_panel_api-0.1.0/src/span_panel_api/generated_client/types.py +46 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 SpanPanel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,457 @@
1
+ Metadata-Version: 2.3
2
+ Name: span-panel-api
3
+ Version: 0.1.0
4
+ Summary: A client library for SPAN Panel API
5
+ Author: SpanPanel
6
+ Requires-Python: >=3.9,<4.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: attrs (>=22.2.0)
14
+ Requires-Dist: click (>=8.0.0,<9.0.0)
15
+ Requires-Dist: httpx (>=0.20.0,<0.29.0)
16
+ Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
17
+ Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
18
+ Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
19
+ Description-Content-Type: text/markdown
20
+
21
+ # SPAN Panel OpenAPI Client
22
+
23
+ A Python client library for accessing the SPAN Panel API.
24
+
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install span-panel-api
30
+ ```
31
+
32
+ ## Usage Patterns
33
+
34
+ The client supports two usage patterns depending on your use case:
35
+
36
+ ### Context Manager Pattern (Recommended for Scripts)
37
+
38
+ **Best for**: Scripts, one-off operations, short-lived applications
39
+
40
+ ```python
41
+ import asyncio
42
+ from span_panel_api import SpanPanelClient
43
+
44
+ async def main():
45
+ # Context manager automatically handles connection lifecycle
46
+ async with SpanPanelClient("192.168.1.100") as client:
47
+ # Authenticate
48
+ auth = await client.authenticate("my-script", "SPAN Control Script")
49
+
50
+ # Get panel status (no auth required)
51
+ status = await client.get_status()
52
+ print(f"Panel: {status.system.manufacturer}")
53
+
54
+ # Get circuits (requires auth)
55
+ circuits = await client.get_circuits()
56
+ for circuit_id, circuit in circuits.circuits.additional_properties.items():
57
+ print(f"{circuit.name}: {circuit.instant_power_w}W")
58
+
59
+ # Control a circuit
60
+ await client.set_circuit_relay("circuit-1", "OPEN")
61
+ await client.set_circuit_priority("circuit-1", "MUST_HAVE")
62
+
63
+ # Client is automatically closed when exiting context
64
+
65
+ asyncio.run(main())
66
+ ```
67
+
68
+ ### Long-Lived Pattern (Services and Integrations)
69
+
70
+ **Best for**: Long-running services, persistent connections, integration platforms
71
+
72
+ ```python
73
+ import asyncio
74
+ from span_panel_api import SpanPanelClient
75
+
76
+ class SpanPanelIntegration:
77
+ """Example long-running service integration pattern."""
78
+
79
+ def __init__(self, host: str):
80
+ # Create client but don't use context manager
81
+ self.client = SpanPanelClient(host)
82
+ self._authenticated = False
83
+
84
+ async def setup(self) -> None:
85
+ """Initialize the integration (called once)."""
86
+ try:
87
+ # Authenticate once during setup
88
+ await self.client.authenticate("my-service", "Panel Integration Service")
89
+ self._authenticated = True
90
+ except Exception as e:
91
+ await self.client.close() # Clean up on setup failure
92
+ raise
93
+
94
+ async def update_data(self) -> dict:
95
+ """Update all data (called periodically by coordinator)."""
96
+ if not self._authenticated:
97
+ await self.client.authenticate("my-service", "Panel Integration Service")
98
+ self._authenticated = True
99
+
100
+ try:
101
+ # Get all data in one update cycle
102
+ status = await self.client.get_status()
103
+ panel_state = await self.client.get_panel_state()
104
+ circuits = await self.client.get_circuits()
105
+ storage = await self.client.get_storage_soe()
106
+
107
+ return {
108
+ "status": status,
109
+ "panel": panel_state,
110
+ "circuits": circuits,
111
+ "storage": storage
112
+ }
113
+ except Exception:
114
+ self._authenticated = False # Reset auth on error
115
+ raise
116
+
117
+ async def set_circuit_priority(self, circuit_id: str, priority: str) -> None:
118
+ """Set circuit priority (called by service)."""
119
+ if not self._authenticated:
120
+ await self.client.authenticate("my-service", "Panel Integration Service")
121
+ self._authenticated = True
122
+
123
+ await self.client.set_circuit_priority(circuit_id, priority)
124
+
125
+ async def cleanup(self) -> None:
126
+ """Cleanup when integration is unloaded."""
127
+ await self.client.close()
128
+
129
+ # Usage in long-running service
130
+ async def main():
131
+ integration = SpanPanelIntegration("192.168.1.100")
132
+
133
+ try:
134
+ await integration.setup()
135
+
136
+ # Simulate coordinator updates
137
+ for i in range(10):
138
+ data = await integration.update_data()
139
+ print(f"Update {i}: {len(data['circuits'].circuits.additional_properties)} circuits")
140
+ await asyncio.sleep(30) # Service typically updates every 30 seconds
141
+
142
+ finally:
143
+ await integration.cleanup()
144
+
145
+ asyncio.run(main())
146
+ ```
147
+
148
+ ### Manual Pattern (Advanced Usage)
149
+
150
+ **Best for**: Custom connection management, special requirements
151
+
152
+ ```python
153
+ import asyncio
154
+ from span_panel_api import SpanPanelClient
155
+
156
+ async def manual_example():
157
+ """Manual client lifecycle management."""
158
+ client = SpanPanelClient("192.168.1.100")
159
+
160
+ try:
161
+ # Manually authenticate
162
+ await client.authenticate("manual-app", "Manual Application")
163
+
164
+ # Do work
165
+ status = await client.get_status()
166
+ circuits = await client.get_circuits()
167
+
168
+ print(f"Found {len(circuits.circuits.additional_properties)} circuits")
169
+
170
+ except Exception as e:
171
+ print(f"Error: {e}")
172
+ finally:
173
+ # IMPORTANT: Always close the client to free resources
174
+ await client.close()
175
+
176
+ asyncio.run(manual_example())
177
+ ```
178
+
179
+ ## When to Use Each Pattern
180
+
181
+ | Pattern | Use Case | Pros | Cons |
182
+ |---------|----------|------|------|
183
+ | **Context Manager** | Scripts, one-off tasks, testing | Automatic cleanup • Exception safe • Simple code | Creates/destroys connection each time • Not efficient for frequent calls |
184
+ | **Long-Lived** | Services, daemons, integration platforms | Efficient connection reuse • Better performance • Authentication persistence | Manual lifecycle management • Must handle cleanup |
185
+ | **Manual** | Custom requirements, debugging | Full control • Custom error handling | Must remember to call close() • More error-prone |
186
+
187
+ ## Error Handling
188
+
189
+ The client provides error categorization for different retry strategies:
190
+
191
+ ### Exception Types
192
+
193
+ ```python
194
+ from span_panel_api.exceptions import (
195
+ SpanPanelError, # Base exception
196
+ SpanPanelAPIError, # General API errors
197
+ SpanPanelAuthError, # 401/403 - need re-authentication
198
+ SpanPanelConnectionError, # Network connectivity issues
199
+ SpanPanelTimeoutError, # Request timeouts
200
+ SpanPanelRetriableError, # 502/503/504 - temporary issues, SHOULD retry
201
+ SpanPanelServerError, # 500 - application bugs, DO NOT retry
202
+ )
203
+ ```
204
+
205
+ ### HTTP Error Code Mapping
206
+
207
+ | Status Code | Exception | Retry? | Description | Action |
208
+ |-------------|-----------|--------|-------------|--------|
209
+ | **Authentication Errors** |
210
+ | 401, 403 | `SpanPanelAuthError` | Once (after re-auth) | Authentication required/failed | Re-authenticate and retry once |
211
+ | **Non-Retriable Server Errors** |
212
+ | 500 | `SpanPanelServerError` | **NO** | Internal server error (SPAN bug) | Show error, do not retry |
213
+ | **Retriable Server Errors** |
214
+ | 502 | `SpanPanelRetriableError` | Yes | Bad Gateway (proxy error) | Retry with exponential backoff |
215
+ | 503 | `SpanPanelRetriableError` | Yes | Service Unavailable | Retry with exponential backoff |
216
+ | 504 | `SpanPanelRetriableError` | Yes | Gateway Timeout | Retry with exponential backoff |
217
+ | **Other HTTP Errors** |
218
+ | 404, 400, etc | `SpanPanelAPIError` | Case by case | Client/request errors | Check request parameters |
219
+ | **Network Errors** |
220
+ | Connection failures | `SpanPanelConnectionError` | Yes | Network connectivity issues | Retry with backoff |
221
+ | Timeouts | `SpanPanelTimeoutError` | Yes | Request timed out | Retry with backoff |
222
+
223
+ ### Retry Strategy
224
+
225
+ ```python
226
+ async def example_request_with_retry():
227
+ """Example showing appropriate error handling."""
228
+ try:
229
+ return await client.get_circuits()
230
+ except SpanPanelAuthError:
231
+ # Re-authenticate and retry once
232
+ await client.authenticate("my-app", "My Application")
233
+ return await client.get_circuits()
234
+ except SpanPanelRetriableError as e:
235
+ # Temporary server issues - should retry with backoff
236
+ # 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
237
+ logger.warning(f"Retriable error {e.status_code}, will retry: {e}")
238
+ raise # Let retry logic handle the retry
239
+ except SpanPanelServerError as e:
240
+ # Application bugs on SPAN side - DO NOT retry
241
+ # 500 Internal Server Error (SPAN Panel bug, not your fault!)
242
+ logger.error(f"Server error {e.status_code}, not retrying: {e}")
243
+ raise # Show notification but don't waste resources retrying
244
+ except (SpanPanelConnectionError, SpanPanelTimeoutError):
245
+ # Network issues - should retry
246
+ raise
247
+ ```
248
+
249
+ ### Exception Handling
250
+
251
+ The client configures the underlying OpenAPI client with `raise_on_unexpected_status=True`, ensuring that HTTP errors (especially 500 responses) are converted to appropriate exceptions rather than being silently ignored.
252
+
253
+ ## API Reference
254
+
255
+ ### Client Initialization
256
+
257
+ ```python
258
+ client = SpanPanelClient(
259
+ host="192.168.1.100", # Required: SPAN Panel IP
260
+ port=80, # Optional: default 80
261
+ timeout=30.0, # Optional: request timeout
262
+ use_ssl=False # Optional: HTTPS (usually False for local)
263
+ )
264
+ ```
265
+
266
+ ### Authentication
267
+
268
+ ```python
269
+ # Register a new API client (one-time setup)
270
+ auth = await client.authenticate(
271
+ name="my-integration", # Required: client name
272
+ description="My Application" # Optional: description
273
+ )
274
+ # Token is stored and used for subsequent requests
275
+ ```
276
+
277
+ ### Panel Information
278
+
279
+ ```python
280
+ # System status (no authentication required)
281
+ status = await client.get_status()
282
+ print(f"System: {status.system}")
283
+ print(f"Network: {status.network}")
284
+
285
+ # Detailed panel state (requires authentication)
286
+ panel = await client.get_panel_state()
287
+ print(f"Grid power: {panel.instant_grid_power_w}W")
288
+ print(f"Main relay: {panel.main_relay_state}")
289
+
290
+ # Battery storage information
291
+ storage = await client.get_storage_soe()
292
+ print(f"Battery SOE: {storage.soe * 100:.1f}%")
293
+ print(f"Max capacity: {storage.max_energy_kwh}kWh")
294
+ ```
295
+
296
+ ### Circuit Control
297
+
298
+ ```python
299
+ # Get all circuits
300
+ circuits = await client.get_circuits()
301
+ for circuit_id, circuit in circuits.circuits.additional_properties.items():
302
+ print(f"Circuit {circuit_id}: {circuit.name}")
303
+ print(f" Power: {circuit.instant_power_w}W")
304
+ print(f" Relay: {circuit.relay_state}")
305
+ print(f" Priority: {circuit.priority}")
306
+
307
+ # Control circuit relay (OPEN/CLOSED)
308
+ await client.set_circuit_relay("circuit-1", "OPEN") # Turn off
309
+ await client.set_circuit_relay("circuit-1", "CLOSED") # Turn on
310
+
311
+ # Set circuit priority
312
+ await client.set_circuit_priority("circuit-1", "MUST_HAVE")
313
+ await client.set_circuit_priority("circuit-1", "NICE_TO_HAVE")
314
+ ```
315
+
316
+ ## Timeout and Retry Control
317
+
318
+ The SPAN Panel API client provides timeout and retry configuration:
319
+
320
+ - `timeout` (float, default: 30.0): The maximum time (in seconds) to wait for a response from the panel for each attempt.
321
+ - `retries` (int, default: 0): The number of times to retry a failed request due to network or retriable server errors. `retries=0` means no retries (1 total attempt), `retries=1` means 1 retry (2 total attempts), etc.
322
+ - `retry_timeout` (float, default: 0.5): The base wait time (in seconds) between retries, with exponential backoff.
323
+ - `retry_backoff_multiplier` (float, default: 2.0): The multiplier for exponential backoff between retries.
324
+
325
+ ### Example Usage
326
+
327
+ ```python
328
+ # No retries (default, fast feedback)
329
+ client = SpanPanelClient("192.168.1.100", timeout=10.0)
330
+
331
+ # Add retries for production
332
+ client = SpanPanelClient("192.168.1.100", timeout=10.0, retries=2, retry_timeout=1.0)
333
+
334
+ # Full retry configuration
335
+ client = SpanPanelClient(
336
+ "192.168.1.100",
337
+ timeout=10.0,
338
+ retries=3,
339
+ retry_timeout=0.5,
340
+ retry_backoff_multiplier=2.0
341
+ )
342
+
343
+ # Change retry settings at runtime
344
+ client.retries = 3
345
+ client.retry_timeout = 2.0
346
+ client.retry_backoff_multiplier = 1.5
347
+ ```
348
+
349
+ ### What does 'retries' mean?
350
+
351
+ | retries | Total Attempts | Description |
352
+ |---------|---------------|---------------------|
353
+ | 0 | 1 | No retries (default) |
354
+ | 1 | 2 | 1 retry |
355
+ | 2 | 3 | 2 retries |
356
+
357
+ Retry and timeout settings can be queried and changed at runtime.
358
+
359
+ ## Development Setup
360
+
361
+ ### Prerequisites
362
+ - Python 3.12+ (SPAN Panel requires Python 3.12+)
363
+ - [Poetry](https://python-poetry.org/) for dependency management
364
+
365
+ ### Installation
366
+
367
+ ```bash
368
+ # Clone and install
369
+ git clone <repository code URL>
370
+ cd span-panel-api
371
+ poetry install
372
+ poetry env activate
373
+
374
+ # Run tests
375
+ poetry run pytest
376
+
377
+ # Check coverage
378
+ python scripts/coverage.py
379
+ ```
380
+
381
+ ### Project Structure
382
+
383
+ ```
384
+ span_openapi/
385
+ ├── src/span_panel_api/ # Main client library
386
+ │ ├── client.py # SpanPanelClient (high-level wrapper)
387
+ │ ├── exceptions.py # Exception hierarchy
388
+ │ ├── const.py # HTTP status constants
389
+ │ └── generated_client/ # Auto-generated OpenAPI client
390
+ ├── tests/ # test suite
391
+ ├── scripts/coverage.py # Coverage checking utility
392
+ ├── openapi.json # SPAN Panel OpenAPI specification
393
+ └── pyproject.toml # Poetry configuration
394
+ ```
395
+
396
+
397
+
398
+ ## Advanced Usage
399
+
400
+ ### SSL Configuration
401
+
402
+ ```python
403
+ # For panels that support SSL locally
404
+ # Note: We do not currently observe panels supporting SSL for local access
405
+ client = SpanPanelClient(
406
+ host="span-panel.local",
407
+ use_ssl=True,
408
+ port=443
409
+ )
410
+ ```
411
+
412
+ ### Timeout Configuration
413
+
414
+ ```python
415
+ # Custom timeout for slow networks
416
+ client = SpanPanelClient(
417
+ host="192.168.1.100",
418
+ timeout=60.0 # 60 second timeout
419
+ )
420
+ ```
421
+
422
+ ## Testing and Coverage
423
+
424
+ ```bash
425
+ # Run full test suite
426
+ poetry run pytest
427
+
428
+ # Generate coverage report
429
+ python scripts/coverage.py --full
430
+
431
+ # Run just context manager tests
432
+ poetry run pytest tests/test_context_manager.py -v
433
+
434
+ # Check coverage meets threshold
435
+ python scripts/coverage.py --check --threshold 95
436
+
437
+ # Run with coverage
438
+ poetry run pytest --cov=span_panel_api tests/
439
+ ```
440
+
441
+ ## Contributing
442
+
443
+ 1. Get `openapi.json` SPAN Panel API specs
444
+
445
+ (for example via REST Client extension)
446
+
447
+ GET <https://span-panel-ip/api/v1/openapi.json>
448
+
449
+ 2. Regenerate client: `poetry run python generate_client.py`
450
+ 3. Update wrapper client in `src/span_panel_api/client.py` if needed
451
+ 4. Add tests for new functionality
452
+ 5. Update this README if adding new features
453
+
454
+ ## License
455
+
456
+ MIT License - see LICENSE file for details.
457
+