spooled 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. spooled/__init__.py +93 -0
  2. spooled/async_client.py +294 -0
  3. spooled/client.py +303 -0
  4. spooled/config.py +146 -0
  5. spooled/errors.py +327 -0
  6. spooled/grpc/__init__.py +13 -0
  7. spooled/grpc/client.py +318 -0
  8. spooled/py.typed +0 -0
  9. spooled/realtime/__init__.py +17 -0
  10. spooled/realtime/events.py +104 -0
  11. spooled/realtime/sse.py +286 -0
  12. spooled/realtime/websocket.py +386 -0
  13. spooled/resources/__init__.py +56 -0
  14. spooled/resources/admin.py +215 -0
  15. spooled/resources/api_keys.py +92 -0
  16. spooled/resources/auth.py +114 -0
  17. spooled/resources/base.py +27 -0
  18. spooled/resources/billing.py +56 -0
  19. spooled/resources/dashboard.py +34 -0
  20. spooled/resources/health.py +66 -0
  21. spooled/resources/ingest.py +86 -0
  22. spooled/resources/jobs.py +319 -0
  23. spooled/resources/metrics.py +35 -0
  24. spooled/resources/organizations.py +169 -0
  25. spooled/resources/queues.py +111 -0
  26. spooled/resources/schedules.py +152 -0
  27. spooled/resources/webhooks.py +148 -0
  28. spooled/resources/workers.py +94 -0
  29. spooled/resources/workflows.py +166 -0
  30. spooled/types/__init__.py +273 -0
  31. spooled/types/admin.py +101 -0
  32. spooled/types/api_keys.py +72 -0
  33. spooled/types/auth.py +85 -0
  34. spooled/types/billing.py +36 -0
  35. spooled/types/common.py +26 -0
  36. spooled/types/dashboard.py +70 -0
  37. spooled/types/health.py +19 -0
  38. spooled/types/jobs.py +291 -0
  39. spooled/types/organizations.py +171 -0
  40. spooled/types/queues.py +79 -0
  41. spooled/types/schedules.py +111 -0
  42. spooled/types/webhooks.py +107 -0
  43. spooled/types/workers.py +80 -0
  44. spooled/types/workflows.py +114 -0
  45. spooled/utils/__init__.py +28 -0
  46. spooled/utils/async_http.py +393 -0
  47. spooled/utils/casing.py +71 -0
  48. spooled/utils/circuit_breaker.py +147 -0
  49. spooled/utils/http.py +368 -0
  50. spooled/utils/retry.py +152 -0
  51. spooled/worker/__init__.py +33 -0
  52. spooled/worker/async_worker.py +509 -0
  53. spooled/worker/types.py +176 -0
  54. spooled/worker/worker.py +521 -0
  55. spooled-1.0.0.dist-info/METADATA +306 -0
  56. spooled-1.0.0.dist-info/RECORD +58 -0
  57. spooled-1.0.0.dist-info/WHEEL +4 -0
  58. spooled-1.0.0.dist-info/licenses/LICENSE +201 -0
spooled/__init__.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ Spooled Cloud Python SDK
3
+
4
+ Official Python SDK for the Spooled Cloud job queue service.
5
+
6
+ Example:
7
+ >>> from spooled import SpooledClient
8
+ >>> client = SpooledClient(api_key="sk_live_...")
9
+ >>> result = client.jobs.create(
10
+ ... queue_name="emails",
11
+ ... payload={"to": "user@example.com"}
12
+ ... )
13
+ >>> print(f"Created job: {result.id}")
14
+ """
15
+
16
+ from spooled.config import (
17
+ CircuitBreakerConfig,
18
+ RetryConfig,
19
+ SpooledClientConfig,
20
+ )
21
+ from spooled.errors import (
22
+ AuthenticationError,
23
+ AuthorizationError,
24
+ CircuitBreakerOpenError,
25
+ ConflictError,
26
+ JobAbortedError,
27
+ NetworkError,
28
+ NotFoundError,
29
+ PayloadTooLargeError,
30
+ RateLimitError,
31
+ ServerError,
32
+ SpooledError,
33
+ TimeoutError,
34
+ ValidationError,
35
+ is_spooled_error,
36
+ )
37
+
38
+ # Version
39
+ __version__ = "1.0.0"
40
+
41
+ # These will be imported when the client modules are created
42
+ __all__ = [
43
+ # Version
44
+ "__version__",
45
+ # Configuration
46
+ "SpooledClientConfig",
47
+ "RetryConfig",
48
+ "CircuitBreakerConfig",
49
+ # Errors
50
+ "SpooledError",
51
+ "AuthenticationError",
52
+ "AuthorizationError",
53
+ "NotFoundError",
54
+ "ConflictError",
55
+ "ValidationError",
56
+ "PayloadTooLargeError",
57
+ "RateLimitError",
58
+ "ServerError",
59
+ "NetworkError",
60
+ "TimeoutError",
61
+ "CircuitBreakerOpenError",
62
+ "JobAbortedError",
63
+ "is_spooled_error",
64
+ # Clients (to be added)
65
+ "SpooledClient",
66
+ "AsyncSpooledClient",
67
+ # Worker (to be added)
68
+ "SpooledWorker",
69
+ "AsyncSpooledWorker",
70
+ ]
71
+
72
+
73
+ def __getattr__(name: str):
74
+ """Lazy import for clients and workers."""
75
+ if name == "SpooledClient":
76
+ from spooled.client import SpooledClient
77
+
78
+ return SpooledClient
79
+ if name == "AsyncSpooledClient":
80
+ from spooled.async_client import AsyncSpooledClient
81
+
82
+ return AsyncSpooledClient
83
+ if name == "SpooledWorker":
84
+ from spooled.worker import SpooledWorker
85
+
86
+ return SpooledWorker
87
+ if name == "AsyncSpooledWorker":
88
+ from spooled.worker import AsyncSpooledWorker
89
+
90
+ return AsyncSpooledWorker
91
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
92
+
93
+
@@ -0,0 +1,294 @@
1
+ """
2
+ Async Spooled Client
3
+
4
+ Main entry point for the async Spooled SDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from spooled.config import (
12
+ ResolvedConfig,
13
+ SpooledClientConfig,
14
+ resolve_config,
15
+ validate_config,
16
+ )
17
+ from spooled.errors import AuthenticationError
18
+ from spooled.resources.admin import AsyncAdminResource
19
+ from spooled.resources.api_keys import AsyncApiKeysResource
20
+ from spooled.resources.auth import AsyncAuthResource
21
+ from spooled.resources.billing import AsyncBillingResource
22
+ from spooled.resources.dashboard import AsyncDashboardResource
23
+ from spooled.resources.health import AsyncHealthResource
24
+ from spooled.resources.ingest import AsyncIngestResource
25
+ from spooled.resources.jobs import AsyncJobsResource
26
+ from spooled.resources.metrics import AsyncMetricsResource
27
+ from spooled.resources.organizations import AsyncOrganizationsResource
28
+ from spooled.resources.queues import AsyncQueuesResource
29
+ from spooled.resources.schedules import AsyncSchedulesResource
30
+ from spooled.resources.webhooks import AsyncWebhooksResource
31
+ from spooled.resources.workers import AsyncWorkersResource
32
+ from spooled.resources.workflows import AsyncWorkflowsResource
33
+ from spooled.utils.async_http import AsyncHttpClient, create_async_http_client
34
+ from spooled.utils.circuit_breaker import CircuitBreaker, create_circuit_breaker
35
+
36
+
37
+ class AsyncSpooledClient:
38
+ """
39
+ Spooled Cloud SDK Client (asynchronous).
40
+
41
+ Example:
42
+ >>> from spooled import AsyncSpooledClient
43
+ >>> async with AsyncSpooledClient(api_key="sk_live_...") as client:
44
+ ... result = await client.jobs.create({
45
+ ... "queue_name": "my-queue",
46
+ ... "payload": {"message": "Hello, World!"}
47
+ ... })
48
+ ... queues = await client.queues.list()
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ api_key: str | None = None,
54
+ *,
55
+ access_token: str | None = None,
56
+ refresh_token: str | None = None,
57
+ admin_key: str | None = None,
58
+ base_url: str | None = None,
59
+ timeout: float | None = None,
60
+ debug: bool = False,
61
+ config: SpooledClientConfig | None = None,
62
+ **kwargs: Any,
63
+ ) -> None:
64
+ """
65
+ Initialize the async Spooled client.
66
+
67
+ Args:
68
+ api_key: API key for authentication (sk_live_... or sk_test_...)
69
+ access_token: JWT access token (alternative to api_key)
70
+ refresh_token: JWT refresh token for auto-refresh
71
+ admin_key: Admin key for admin operations
72
+ base_url: API base URL (default: https://api.spooled.cloud)
73
+ timeout: Request timeout in seconds
74
+ debug: Enable debug logging
75
+ config: Full configuration object (overrides other params)
76
+ """
77
+ # Build config from params or use provided config
78
+ if config is not None:
79
+ self._config = resolve_config(config)
80
+ else:
81
+ config_params: dict[str, Any] = {
82
+ "api_key": api_key,
83
+ "access_token": access_token,
84
+ "refresh_token": refresh_token,
85
+ "admin_key": admin_key,
86
+ "debug": debug,
87
+ **kwargs,
88
+ }
89
+ if base_url is not None:
90
+ config_params["base_url"] = base_url
91
+ if timeout is not None:
92
+ config_params["timeout"] = timeout
93
+
94
+ # Remove None values
95
+ config_params = {k: v for k, v in config_params.items() if v is not None}
96
+ self._config = resolve_config(SpooledClientConfig(**config_params))
97
+
98
+ validate_config(self._config)
99
+
100
+ # Create circuit breaker
101
+ self._circuit_breaker = create_circuit_breaker(self._config.circuit_breaker)
102
+
103
+ # Create HTTP client
104
+ self._http = create_async_http_client(self._config, self._circuit_breaker)
105
+
106
+ # Token refresh state
107
+ self._token_expires_at: float | None = None
108
+
109
+ # Set up token refresh if using JWT
110
+ if (
111
+ self._config.access_token
112
+ and self._config.refresh_token
113
+ and self._config.auto_refresh_token
114
+ ):
115
+ self._http.set_refresh_token_fn(self._refresh_access_token)
116
+
117
+ # Create resource instances
118
+ self._auth = AsyncAuthResource(self._http)
119
+ self._jobs = AsyncJobsResource(self._http)
120
+ self._queues = AsyncQueuesResource(self._http)
121
+ self._workers = AsyncWorkersResource(self._http)
122
+ self._schedules = AsyncSchedulesResource(self._http)
123
+ self._workflows = AsyncWorkflowsResource(self._http)
124
+ self._webhooks = AsyncWebhooksResource(self._http)
125
+ self._api_keys = AsyncApiKeysResource(self._http)
126
+ self._organizations = AsyncOrganizationsResource(self._http)
127
+ self._billing = AsyncBillingResource(self._http)
128
+ self._dashboard = AsyncDashboardResource(self._http)
129
+ self._health = AsyncHealthResource(self._http)
130
+ self._metrics = AsyncMetricsResource(self._http)
131
+ self._admin = AsyncAdminResource(self._http, self._config.admin_key)
132
+ self._ingest = AsyncIngestResource(self._http)
133
+
134
+ if self._config.debug_fn:
135
+ self._config.debug_fn(
136
+ "AsyncSpooledClient initialized",
137
+ {
138
+ "base_url": self._config.base_url,
139
+ "has_api_key": bool(self._config.api_key),
140
+ "has_access_token": bool(self._config.access_token),
141
+ },
142
+ )
143
+
144
+ # Resource properties
145
+
146
+ @property
147
+ def auth(self) -> AsyncAuthResource:
148
+ """Authentication operations."""
149
+ return self._auth
150
+
151
+ @property
152
+ def jobs(self) -> AsyncJobsResource:
153
+ """Job operations."""
154
+ return self._jobs
155
+
156
+ @property
157
+ def queues(self) -> AsyncQueuesResource:
158
+ """Queue operations."""
159
+ return self._queues
160
+
161
+ @property
162
+ def workers(self) -> AsyncWorkersResource:
163
+ """Worker operations."""
164
+ return self._workers
165
+
166
+ @property
167
+ def schedules(self) -> AsyncSchedulesResource:
168
+ """Schedule operations."""
169
+ return self._schedules
170
+
171
+ @property
172
+ def workflows(self) -> AsyncWorkflowsResource:
173
+ """Workflow operations."""
174
+ return self._workflows
175
+
176
+ @property
177
+ def webhooks(self) -> AsyncWebhooksResource:
178
+ """Outgoing webhook operations."""
179
+ return self._webhooks
180
+
181
+ @property
182
+ def api_keys(self) -> AsyncApiKeysResource:
183
+ """API key operations."""
184
+ return self._api_keys
185
+
186
+ @property
187
+ def organizations(self) -> AsyncOrganizationsResource:
188
+ """Organization operations."""
189
+ return self._organizations
190
+
191
+ @property
192
+ def billing(self) -> AsyncBillingResource:
193
+ """Billing operations."""
194
+ return self._billing
195
+
196
+ @property
197
+ def dashboard(self) -> AsyncDashboardResource:
198
+ """Dashboard operations."""
199
+ return self._dashboard
200
+
201
+ @property
202
+ def health(self) -> AsyncHealthResource:
203
+ """Health endpoints (public)."""
204
+ return self._health
205
+
206
+ @property
207
+ def metrics(self) -> AsyncMetricsResource:
208
+ """Metrics endpoint (public)."""
209
+ return self._metrics
210
+
211
+ @property
212
+ def admin(self) -> AsyncAdminResource:
213
+ """Admin endpoints (requires admin_key)."""
214
+ return self._admin
215
+
216
+ @property
217
+ def ingest(self) -> AsyncIngestResource:
218
+ """Webhook ingestion endpoints."""
219
+ return self._ingest
220
+
221
+ # Token management
222
+
223
+ async def get_jwt_token(self) -> str:
224
+ """Get or acquire a JWT token for realtime connections."""
225
+ # If we have an access token, use it
226
+ if self._config.access_token:
227
+ return self._config.access_token
228
+
229
+ # If we only have an API key, exchange it for a JWT
230
+ if self._config.api_key:
231
+ response = await self.auth.login({"api_key": self._config.api_key})
232
+ self._http.set_auth_token(response.access_token)
233
+ # Update config with new tokens
234
+ self._config.access_token = response.access_token
235
+ self._config.refresh_token = response.refresh_token
236
+ return response.access_token
237
+
238
+ raise AuthenticationError("No authentication method available")
239
+
240
+ async def _refresh_access_token(self) -> str:
241
+ """Refresh the access token."""
242
+ if not self._config.refresh_token:
243
+ raise AuthenticationError("No refresh token available")
244
+
245
+ response = await self.auth.refresh({"refresh_token": self._config.refresh_token})
246
+ self._http.set_auth_token(response.access_token)
247
+ self._config.access_token = response.access_token
248
+
249
+ if self._config.debug_fn:
250
+ self._config.debug_fn("Token refreshed successfully", None)
251
+
252
+ return response.access_token
253
+
254
+ # Configuration access
255
+
256
+ def get_config(self) -> ResolvedConfig:
257
+ """Get current configuration (read-only)."""
258
+ return self._config
259
+
260
+ def get_circuit_breaker_stats(self) -> dict:
261
+ """Get circuit breaker statistics."""
262
+ return self._circuit_breaker.get_stats()
263
+
264
+ def reset_circuit_breaker(self) -> None:
265
+ """Reset the circuit breaker."""
266
+ self._circuit_breaker.reset()
267
+
268
+ # Context manager
269
+
270
+ async def close(self) -> None:
271
+ """Close the client and release resources."""
272
+ await self._http.close()
273
+
274
+ async def __aenter__(self) -> "AsyncSpooledClient":
275
+ return self
276
+
277
+ async def __aexit__(self, *args: Any) -> None:
278
+ await self.close()
279
+
280
+
281
+ async def create_async_client(
282
+ api_key: str | None = None,
283
+ **kwargs: Any,
284
+ ) -> AsyncSpooledClient:
285
+ """
286
+ Create a new AsyncSpooledClient instance.
287
+
288
+ Example:
289
+ >>> from spooled import create_async_client
290
+ >>> client = await create_async_client(api_key="sk_live_...")
291
+ """
292
+ return AsyncSpooledClient(api_key=api_key, **kwargs)
293
+
294
+
spooled/client.py ADDED
@@ -0,0 +1,303 @@
1
+ """
2
+ Spooled Client (synchronous)
3
+
4
+ Main entry point for the Spooled SDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from spooled.config import (
12
+ ResolvedConfig,
13
+ SpooledClientConfig,
14
+ resolve_config,
15
+ validate_config,
16
+ )
17
+ from spooled.errors import AuthenticationError
18
+ from spooled.resources.admin import AdminResource
19
+ from spooled.resources.api_keys import ApiKeysResource
20
+ from spooled.resources.auth import AuthResource
21
+ from spooled.resources.billing import BillingResource
22
+ from spooled.resources.dashboard import DashboardResource
23
+ from spooled.resources.health import HealthResource
24
+ from spooled.resources.ingest import IngestResource
25
+ from spooled.resources.jobs import JobsResource
26
+ from spooled.resources.metrics import MetricsResource
27
+ from spooled.resources.organizations import OrganizationsResource
28
+ from spooled.resources.queues import QueuesResource
29
+ from spooled.resources.schedules import SchedulesResource
30
+ from spooled.resources.webhooks import WebhooksResource
31
+ from spooled.resources.workers import WorkersResource
32
+ from spooled.resources.workflows import WorkflowsResource
33
+ from spooled.utils.circuit_breaker import CircuitBreaker, create_circuit_breaker
34
+ from spooled.utils.http import HttpClient, create_http_client
35
+
36
+
37
+ class SpooledClient:
38
+ """
39
+ Spooled Cloud SDK Client (synchronous).
40
+
41
+ Example:
42
+ >>> from spooled import SpooledClient
43
+ >>> client = SpooledClient(api_key="sk_live_...")
44
+ >>>
45
+ >>> # Create a job
46
+ >>> result = client.jobs.create({
47
+ ... "queue_name": "my-queue",
48
+ ... "payload": {"message": "Hello, World!"}
49
+ ... })
50
+ >>>
51
+ >>> # List queues
52
+ >>> queues = client.queues.list()
53
+
54
+ Using as context manager:
55
+ >>> with SpooledClient(api_key="sk_live_...") as client:
56
+ ... job = client.jobs.create({"queue_name": "test", "payload": {}})
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ api_key: str | None = None,
62
+ *,
63
+ access_token: str | None = None,
64
+ refresh_token: str | None = None,
65
+ admin_key: str | None = None,
66
+ base_url: str | None = None,
67
+ timeout: float | None = None,
68
+ debug: bool = False,
69
+ config: SpooledClientConfig | None = None,
70
+ **kwargs: Any,
71
+ ) -> None:
72
+ """
73
+ Initialize the Spooled client.
74
+
75
+ Args:
76
+ api_key: API key for authentication (sk_live_... or sk_test_...)
77
+ access_token: JWT access token (alternative to api_key)
78
+ refresh_token: JWT refresh token for auto-refresh
79
+ admin_key: Admin key for admin operations
80
+ base_url: API base URL (default: https://api.spooled.cloud)
81
+ timeout: Request timeout in seconds
82
+ debug: Enable debug logging
83
+ config: Full configuration object (overrides other params)
84
+ """
85
+ # Build config from params or use provided config
86
+ if config is not None:
87
+ self._config = resolve_config(config)
88
+ else:
89
+ config_params: dict[str, Any] = {
90
+ "api_key": api_key,
91
+ "access_token": access_token,
92
+ "refresh_token": refresh_token,
93
+ "admin_key": admin_key,
94
+ "debug": debug,
95
+ **kwargs,
96
+ }
97
+ if base_url is not None:
98
+ config_params["base_url"] = base_url
99
+ if timeout is not None:
100
+ config_params["timeout"] = timeout
101
+
102
+ # Remove None values
103
+ config_params = {k: v for k, v in config_params.items() if v is not None}
104
+ self._config = resolve_config(SpooledClientConfig(**config_params))
105
+
106
+ validate_config(self._config)
107
+
108
+ # Create circuit breaker
109
+ self._circuit_breaker = create_circuit_breaker(self._config.circuit_breaker)
110
+
111
+ # Create HTTP client
112
+ self._http = create_http_client(self._config, self._circuit_breaker)
113
+
114
+ # Token refresh state
115
+ self._refresh_promise: str | None = None
116
+ self._token_expires_at: float | None = None
117
+
118
+ # Set up token refresh if using JWT
119
+ if (
120
+ self._config.access_token
121
+ and self._config.refresh_token
122
+ and self._config.auto_refresh_token
123
+ ):
124
+ self._http.set_refresh_token_fn(self._refresh_access_token)
125
+
126
+ # Create resource instances
127
+ self._auth = AuthResource(self._http)
128
+ self._jobs = JobsResource(self._http)
129
+ self._queues = QueuesResource(self._http)
130
+ self._workers = WorkersResource(self._http)
131
+ self._schedules = SchedulesResource(self._http)
132
+ self._workflows = WorkflowsResource(self._http)
133
+ self._webhooks = WebhooksResource(self._http)
134
+ self._api_keys = ApiKeysResource(self._http)
135
+ self._organizations = OrganizationsResource(self._http)
136
+ self._billing = BillingResource(self._http)
137
+ self._dashboard = DashboardResource(self._http)
138
+ self._health = HealthResource(self._http)
139
+ self._metrics = MetricsResource(self._http)
140
+ self._admin = AdminResource(self._http, self._config.admin_key)
141
+ self._ingest = IngestResource(self._http)
142
+
143
+ if self._config.debug_fn:
144
+ self._config.debug_fn(
145
+ "SpooledClient initialized",
146
+ {
147
+ "base_url": self._config.base_url,
148
+ "has_api_key": bool(self._config.api_key),
149
+ "has_access_token": bool(self._config.access_token),
150
+ },
151
+ )
152
+
153
+ # Resource properties
154
+
155
+ @property
156
+ def auth(self) -> AuthResource:
157
+ """Authentication operations."""
158
+ return self._auth
159
+
160
+ @property
161
+ def jobs(self) -> JobsResource:
162
+ """Job operations."""
163
+ return self._jobs
164
+
165
+ @property
166
+ def queues(self) -> QueuesResource:
167
+ """Queue operations."""
168
+ return self._queues
169
+
170
+ @property
171
+ def workers(self) -> WorkersResource:
172
+ """Worker operations."""
173
+ return self._workers
174
+
175
+ @property
176
+ def schedules(self) -> SchedulesResource:
177
+ """Schedule operations."""
178
+ return self._schedules
179
+
180
+ @property
181
+ def workflows(self) -> WorkflowsResource:
182
+ """Workflow operations."""
183
+ return self._workflows
184
+
185
+ @property
186
+ def webhooks(self) -> WebhooksResource:
187
+ """Outgoing webhook operations."""
188
+ return self._webhooks
189
+
190
+ @property
191
+ def api_keys(self) -> ApiKeysResource:
192
+ """API key operations."""
193
+ return self._api_keys
194
+
195
+ @property
196
+ def organizations(self) -> OrganizationsResource:
197
+ """Organization operations."""
198
+ return self._organizations
199
+
200
+ @property
201
+ def billing(self) -> BillingResource:
202
+ """Billing operations."""
203
+ return self._billing
204
+
205
+ @property
206
+ def dashboard(self) -> DashboardResource:
207
+ """Dashboard operations."""
208
+ return self._dashboard
209
+
210
+ @property
211
+ def health(self) -> HealthResource:
212
+ """Health endpoints (public)."""
213
+ return self._health
214
+
215
+ @property
216
+ def metrics(self) -> MetricsResource:
217
+ """Metrics endpoint (public)."""
218
+ return self._metrics
219
+
220
+ @property
221
+ def admin(self) -> AdminResource:
222
+ """Admin endpoints (requires admin_key)."""
223
+ return self._admin
224
+
225
+ @property
226
+ def ingest(self) -> IngestResource:
227
+ """Webhook ingestion endpoints."""
228
+ return self._ingest
229
+
230
+ # Token management
231
+
232
+ def get_jwt_token(self) -> str:
233
+ """Get or acquire a JWT token for realtime connections."""
234
+ # If we have an access token, use it
235
+ if self._config.access_token:
236
+ return self._config.access_token
237
+
238
+ # If we only have an API key, exchange it for a JWT
239
+ if self._config.api_key:
240
+ response = self.auth.login({"api_key": self._config.api_key})
241
+ self._http.set_auth_token(response.access_token)
242
+ # Update config with new tokens
243
+ self._config.access_token = response.access_token
244
+ self._config.refresh_token = response.refresh_token
245
+ return response.access_token
246
+
247
+ raise AuthenticationError("No authentication method available")
248
+
249
+ def _refresh_access_token(self) -> str:
250
+ """Refresh the access token."""
251
+ if not self._config.refresh_token:
252
+ raise AuthenticationError("No refresh token available")
253
+
254
+ response = self.auth.refresh({"refresh_token": self._config.refresh_token})
255
+ self._http.set_auth_token(response.access_token)
256
+ self._config.access_token = response.access_token
257
+
258
+ if self._config.debug_fn:
259
+ self._config.debug_fn("Token refreshed successfully", None)
260
+
261
+ return response.access_token
262
+
263
+ # Configuration access
264
+
265
+ def get_config(self) -> ResolvedConfig:
266
+ """Get current configuration (read-only)."""
267
+ return self._config
268
+
269
+ def get_circuit_breaker_stats(self) -> dict:
270
+ """Get circuit breaker statistics."""
271
+ return self._circuit_breaker.get_stats()
272
+
273
+ def reset_circuit_breaker(self) -> None:
274
+ """Reset the circuit breaker."""
275
+ self._circuit_breaker.reset()
276
+
277
+ # Context manager
278
+
279
+ def close(self) -> None:
280
+ """Close the client and release resources."""
281
+ self._http.close()
282
+
283
+ def __enter__(self) -> "SpooledClient":
284
+ return self
285
+
286
+ def __exit__(self, *args: Any) -> None:
287
+ self.close()
288
+
289
+
290
+ def create_client(
291
+ api_key: str | None = None,
292
+ **kwargs: Any,
293
+ ) -> SpooledClient:
294
+ """
295
+ Create a new SpooledClient instance.
296
+
297
+ Example:
298
+ >>> from spooled import create_client
299
+ >>> client = create_client(api_key="sk_live_...")
300
+ """
301
+ return SpooledClient(api_key=api_key, **kwargs)
302
+
303
+