spooled 1.2.0__tar.gz → 1.2.2__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 (67) hide show
  1. {spooled-1.2.0 → spooled-1.2.2}/PKG-INFO +1 -1
  2. {spooled-1.2.0 → spooled-1.2.2}/pyproject.toml +1 -1
  3. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/_version.py +1 -1
  4. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/realtime/sse.py +42 -20
  5. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/realtime/unified.py +22 -10
  6. {spooled-1.2.0 → spooled-1.2.2}/.gitignore +0 -0
  7. {spooled-1.2.0 → spooled-1.2.2}/LICENSE +0 -0
  8. {spooled-1.2.0 → spooled-1.2.2}/README.md +0 -0
  9. {spooled-1.2.0 → spooled-1.2.2}/proto/spooled.proto +0 -0
  10. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/__init__.py +0 -0
  11. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/async_client.py +0 -0
  12. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/client.py +0 -0
  13. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/config.py +0 -0
  14. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/errors.py +0 -0
  15. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/grpc/__init__.py +0 -0
  16. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/grpc/client.py +0 -0
  17. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/grpc/stubs/__init__.py +0 -0
  18. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/grpc/stubs/spooled_pb2.py +0 -0
  19. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/grpc/stubs/spooled_pb2.pyi +0 -0
  20. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/grpc/stubs/spooled_pb2_grpc.py +0 -0
  21. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/py.typed +0 -0
  22. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/realtime/__init__.py +0 -0
  23. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/realtime/events.py +0 -0
  24. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/realtime/websocket.py +0 -0
  25. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/__init__.py +0 -0
  26. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/admin.py +0 -0
  27. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/api_keys.py +0 -0
  28. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/auth.py +0 -0
  29. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/base.py +0 -0
  30. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/billing.py +0 -0
  31. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/dashboard.py +0 -0
  32. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/health.py +0 -0
  33. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/ingest.py +0 -0
  34. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/jobs.py +0 -0
  35. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/metrics.py +0 -0
  36. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/organizations.py +0 -0
  37. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/queues.py +0 -0
  38. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/schedules.py +0 -0
  39. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/webhooks.py +0 -0
  40. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/workers.py +0 -0
  41. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/resources/workflows.py +0 -0
  42. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/__init__.py +0 -0
  43. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/admin.py +0 -0
  44. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/api_keys.py +0 -0
  45. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/auth.py +0 -0
  46. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/billing.py +0 -0
  47. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/common.py +0 -0
  48. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/dashboard.py +0 -0
  49. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/health.py +0 -0
  50. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/jobs.py +0 -0
  51. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/organizations.py +0 -0
  52. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/queues.py +0 -0
  53. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/schedules.py +0 -0
  54. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/webhooks.py +0 -0
  55. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/workers.py +0 -0
  56. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/types/workflows.py +0 -0
  57. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/utils/__init__.py +0 -0
  58. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/utils/async_http.py +0 -0
  59. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/utils/casing.py +0 -0
  60. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/utils/circuit_breaker.py +0 -0
  61. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/utils/http.py +0 -0
  62. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/utils/jwt.py +0 -0
  63. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/utils/retry.py +0 -0
  64. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/worker/__init__.py +0 -0
  65. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/worker/async_worker.py +0 -0
  66. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/worker/types.py +0 -0
  67. {spooled-1.2.0 → spooled-1.2.2}/src/spooled/worker/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: spooled
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Official Python SDK for Spooled Cloud job queue service
5
5
  Project-URL: Homepage, https://spooled.cloud
6
6
  Project-URL: Documentation, https://spooled.cloud/docs/
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "spooled"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "Official Python SDK for Spooled Cloud job queue service"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -5,4 +5,4 @@ from importlib.metadata import PackageNotFoundError, version
5
5
  try:
6
6
  __version__ = version("spooled")
7
7
  except PackageNotFoundError: # pragma: no cover - source checkout without installed metadata
8
- __version__ = "1.2.0"
8
+ __version__ = "1.2.2"
@@ -12,6 +12,7 @@ import time
12
12
  from collections.abc import AsyncGenerator, Callable, Generator
13
13
  from enum import Enum
14
14
  from typing import Any
15
+ from urllib.parse import quote
15
16
 
16
17
  import httpx
17
18
 
@@ -285,8 +286,11 @@ class SSEClient:
285
286
  """Build SSE endpoint URL."""
286
287
  if self._job_id:
287
288
  return f"{self._base_url}/api/v1/events/jobs/{self._job_id}"
289
+ # A queue subscription uses /events?queue=: /events/queues/{name} carries
290
+ # only queue.stats, so job events never arrived for a queue filter.
291
+ # Requires API 0.1.114+.
288
292
  if self._queue:
289
- return f"{self._base_url}/api/v1/events/queues/{self._queue}"
293
+ return f"{self._base_url}/api/v1/events?queue={quote(self._queue, safe='')}"
290
294
  return f"{self._base_url}/api/v1/events"
291
295
 
292
296
  def connect(self) -> None:
@@ -300,16 +304,27 @@ class SSEClient:
300
304
 
301
305
  try:
302
306
  self._client = httpx.Client()
303
- self._response = self._client.stream(
304
- "GET",
305
- url,
306
- headers={
307
- "Authorization": f"Bearer {self._token}",
308
- "Accept": "text/event-stream",
309
- },
310
- ).__enter__()
311
-
312
- self._sse_client = sseclient.SSEClient(self._response.iter_lines()) # type: ignore[arg-type]
307
+ # send(stream=True) keeps the response open until close(). Entering
308
+ # client.stream(...) by hand dropped its context manager, which the
309
+ # garbage collector then exited — closing the stream before any
310
+ # event was read ("the stream has been closed").
311
+ self._response = self._client.send(
312
+ self._client.build_request(
313
+ "GET",
314
+ url,
315
+ headers={
316
+ "Authorization": f"Bearer {self._token}",
317
+ "Accept": "text/event-stream",
318
+ },
319
+ ),
320
+ stream=True,
321
+ )
322
+
323
+ # sseclient needs raw byte chunks: it finds event boundaries by the
324
+ # blank line between events. iter_lines() yields str lines with the
325
+ # newlines stripped, so the first line raised TypeError and the
326
+ # stream never produced an event.
327
+ self._sse_client = sseclient.SSEClient(self._response.iter_bytes()) # type: ignore[arg-type]
313
328
  self._set_state(SSEConnectionState.CONNECTED)
314
329
  self._reconnect_attempts = 0
315
330
  self._closed = False
@@ -629,8 +644,11 @@ class AsyncSSEClient:
629
644
  """Build SSE endpoint URL."""
630
645
  if self._job_id:
631
646
  return f"{self._base_url}/api/v1/events/jobs/{self._job_id}"
647
+ # A queue subscription uses /events?queue=: /events/queues/{name} carries
648
+ # only queue.stats, so job events never arrived for a queue filter.
649
+ # Requires API 0.1.114+.
632
650
  if self._queue:
633
- return f"{self._base_url}/api/v1/events/queues/{self._queue}"
651
+ return f"{self._base_url}/api/v1/events?queue={quote(self._queue, safe='')}"
634
652
  return f"{self._base_url}/api/v1/events"
635
653
 
636
654
  async def connect(self) -> None:
@@ -643,14 +661,18 @@ class AsyncSSEClient:
643
661
 
644
662
  try:
645
663
  self._client = httpx.AsyncClient()
646
- self._response = await self._client.stream(
647
- "GET",
648
- url,
649
- headers={
650
- "Authorization": f"Bearer {self._token}",
651
- "Accept": "text/event-stream",
652
- },
653
- ).__aenter__()
664
+ # See SSEClient.connect: keep the response open until close().
665
+ self._response = await self._client.send(
666
+ self._client.build_request(
667
+ "GET",
668
+ url,
669
+ headers={
670
+ "Authorization": f"Bearer {self._token}",
671
+ "Accept": "text/event-stream",
672
+ },
673
+ ),
674
+ stream=True,
675
+ )
654
676
  self._set_state(SSEConnectionState.CONNECTED)
655
677
  self._reconnect_attempts = 0
656
678
  self._closed = False
@@ -15,6 +15,7 @@ from collections.abc import Callable, Generator
15
15
  from dataclasses import dataclass
16
16
  from enum import Enum
17
17
  from typing import Any, Literal
18
+ from urllib.parse import quote
18
19
 
19
20
  from spooled.realtime.events import (
20
21
  RealtimeEvent,
@@ -509,17 +510,24 @@ class SpooledRealtime:
509
510
 
510
511
  try:
511
512
  self._http_client = httpx.Client()
512
- self._sse_response = self._http_client.stream(
513
- "GET",
514
- url,
515
- headers={
516
- "Authorization": f"Bearer {self._options.token}",
517
- "Accept": "text/event-stream",
518
- },
519
- ).__enter__()
513
+ # send(stream=True) keeps the response open until disconnect(); a
514
+ # hand-entered client.stream(...) was closed by the GC (see sse.py).
515
+ self._sse_response = self._http_client.send(
516
+ self._http_client.build_request(
517
+ "GET",
518
+ url,
519
+ headers={
520
+ "Authorization": f"Bearer {self._options.token}",
521
+ "Accept": "text/event-stream",
522
+ },
523
+ ),
524
+ stream=True,
525
+ )
520
526
 
521
527
  if HAS_SSE:
522
- self._sse_client = sseclient.SSEClient(self._sse_response.iter_lines())
528
+ # Raw byte chunks: sseclient splits events on the blank line,
529
+ # which iter_lines() strips (see realtime/sse.py).
530
+ self._sse_client = sseclient.SSEClient(self._sse_response.iter_bytes())
523
531
 
524
532
  self._set_state(ConnectionState.CONNECTED)
525
533
  self._reconnect_attempts = 0
@@ -578,8 +586,12 @@ class SpooledRealtime:
578
586
  if filter:
579
587
  if filter.job_id:
580
588
  return f"{self._options.base_url}/api/v1/events/jobs/{filter.job_id}"
589
+ # /events/queues/{name} carries only queue.stats; job events for a
590
+ # queue come from /events?queue= (API 0.1.114+).
581
591
  if filter.queue:
582
- return f"{self._options.base_url}/api/v1/events/queues/{filter.queue}"
592
+ return (
593
+ f"{self._options.base_url}/api/v1/events?queue={quote(filter.queue, safe='')}"
594
+ )
583
595
 
584
596
  return f"{self._options.base_url}/api/v1/events"
585
597
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes