offwork 0.2.0__tar.gz → 0.2.1__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 (46) hide show
  1. {offwork-0.2.0 → offwork-0.2.1}/PKG-INFO +1 -1
  2. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/backends/redis.py +43 -10
  3. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/backends/ws.py +8 -7
  4. {offwork-0.2.0 → offwork-0.2.1}/pyproject.toml +1 -1
  5. {offwork-0.2.0 → offwork-0.2.1}/LICENSE +0 -0
  6. {offwork-0.2.0 → offwork-0.2.1}/README.md +0 -0
  7. {offwork-0.2.0 → offwork-0.2.1}/offwork/__init__.py +0 -0
  8. {offwork-0.2.0 → offwork-0.2.1}/offwork/__main__.py +0 -0
  9. {offwork-0.2.0 → offwork-0.2.1}/offwork/_venv.py +0 -0
  10. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/__init__.py +0 -0
  11. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/_timeout.py +0 -0
  12. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/clients.py +0 -0
  13. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/ed25519.py +0 -0
  14. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/envelope.py +0 -0
  15. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/errors.py +0 -0
  16. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/identity.py +0 -0
  17. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/models.py +0 -0
  18. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/pairing.py +0 -0
  19. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/progress.py +0 -0
  20. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/signing.py +0 -0
  21. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/task.py +0 -0
  22. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/token.py +0 -0
  23. {offwork-0.2.0 → offwork-0.2.1}/offwork/core/version.py +0 -0
  24. {offwork-0.2.0 → offwork-0.2.1}/offwork/graph/__init__.py +0 -0
  25. {offwork-0.2.0 → offwork-0.2.1}/offwork/graph/analyzer.py +0 -0
  26. {offwork-0.2.0 → offwork-0.2.1}/offwork/graph/decorator.py +0 -0
  27. {offwork-0.2.0 → offwork-0.2.1}/offwork/graph/graph.py +0 -0
  28. {offwork-0.2.0 → offwork-0.2.1}/offwork/graph/store.py +0 -0
  29. {offwork-0.2.0 → offwork-0.2.1}/offwork/graph/tracing.py +0 -0
  30. {offwork-0.2.0 → offwork-0.2.1}/offwork/py.typed +0 -0
  31. {offwork-0.2.0 → offwork-0.2.1}/offwork/typing.py +0 -0
  32. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/__init__.py +0 -0
  33. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/backends/__init__.py +0 -0
  34. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/backends/base.py +0 -0
  35. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/backends/local.py +0 -0
  36. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/backends/rabbitmq.py +0 -0
  37. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/deps.py +0 -0
  38. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/remote.py +0 -0
  39. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/result.py +0 -0
  40. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/sandbox/Dockerfile +0 -0
  41. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/sandbox/__init__.py +0 -0
  42. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/sandbox/_protocol.py +0 -0
  43. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/sandbox/docker.py +0 -0
  44. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/sandbox/guest_agent.py +0 -0
  45. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/schedule.py +0 -0
  46. {offwork-0.2.0 → offwork-0.2.1}/offwork/worker/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: offwork
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Distributed Python task execution via automatic function serialization
5
5
  License: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -1,12 +1,15 @@
1
1
  """Redis-backed transport using ``RPUSH``/``BLPOP`` for tasks and results."""
2
2
 
3
+ import math
3
4
  import time
4
5
  import asyncio
5
6
  from typing import Any
7
+ from urllib.parse import parse_qs, urlparse
6
8
  from collections.abc import AsyncIterator
7
9
 
8
10
  try:
9
11
  import redis.asyncio as _redis
12
+ from redis.exceptions import TimeoutError as RedisTimeoutError
10
13
  except ImportError:
11
14
  raise ImportError(
12
15
  "redis package is required for RedisBackend. "
@@ -50,7 +53,11 @@ class RedisBackend(Backend):
50
53
  queue_key: str | None = None,
51
54
  result_ttl: int | None = None,
52
55
  ) -> None:
53
- self._redis: Any = _redis.Redis.from_url(url)
56
+ query = parse_qs(urlparse(url).query)
57
+ connect_kwargs: dict[str, Any] = {}
58
+ if "socket_timeout" not in query:
59
+ connect_kwargs["socket_timeout"] = None
60
+ self._redis: Any = _redis.Redis.from_url(url, **connect_kwargs)
54
61
  self._queue_key = queue_key or self.DEFAULT_QUEUE_KEY
55
62
  self._result_ttl = result_ttl or self.DEFAULT_RESULT_TTL
56
63
 
@@ -60,7 +67,13 @@ class RedisBackend(Backend):
60
67
  async def listen(self) -> AsyncIterator[str]:
61
68
  """Block on ``BLPOP`` and yield task JSON strings as they arrive."""
62
69
  while True:
63
- result = await self._redis.blpop(self._queue_key)
70
+ try:
71
+ result = await self._redis.blpop(self._queue_key)
72
+ except RedisTimeoutError:
73
+ task = asyncio.current_task()
74
+ if task is not None and task.cancelling():
75
+ raise asyncio.CancelledError() from None
76
+ continue
64
77
  if result is None:
65
78
  continue
66
79
  _, raw = result
@@ -73,14 +86,34 @@ class RedisBackend(Backend):
73
86
 
74
87
  async def get_result(self, task_id: str, timeout: float | None = None) -> str:
75
88
  key = f"{self.RESULT_PREFIX}{task_id}"
76
- t = int(timeout) if timeout else 0
77
- result = await self._redis.blpop(key, timeout=t)
78
- if result is None:
79
- raise TimeoutError(
80
- f"Timed out waiting for result of task {task_id}"
81
- )
82
- _, raw = result
83
- return raw.decode() if isinstance(raw, bytes) else raw
89
+ deadline = None if timeout is None else time.monotonic() + max(0.0, timeout)
90
+ while True:
91
+ if deadline is None:
92
+ block_seconds = 0
93
+ else:
94
+ remaining = deadline - time.monotonic()
95
+ if remaining <= 0:
96
+ raise TimeoutError(
97
+ f"Timed out waiting for result of task {task_id}"
98
+ )
99
+ block_seconds = max(1, math.ceil(remaining))
100
+ try:
101
+ result = await self._redis.blpop(key, timeout=block_seconds)
102
+ except RedisTimeoutError:
103
+ task = asyncio.current_task()
104
+ if task is not None and task.cancelling():
105
+ raise asyncio.CancelledError() from None
106
+ if deadline is not None and time.monotonic() >= deadline:
107
+ raise TimeoutError(
108
+ f"Timed out waiting for result of task {task_id}"
109
+ ) from None
110
+ continue
111
+ if result is None:
112
+ raise TimeoutError(
113
+ f"Timed out waiting for result of task {task_id}"
114
+ )
115
+ _, raw = result
116
+ return raw.decode() if isinstance(raw, bytes) else raw
84
117
 
85
118
  async def try_get_result(self, task_id: str) -> str | None:
86
119
  """Non-blocking ``LPOP``; returns ``None`` if not yet available."""
@@ -31,6 +31,14 @@ from collections.abc import AsyncIterator
31
31
  from typing import Any
32
32
  from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
33
33
 
34
+ try:
35
+ import websockets
36
+ except ImportError:
37
+ raise ImportError(
38
+ "websockets package is required for RabbitMQBackend. "
39
+ "Install it with: pip install websockets"
40
+ ) from None
41
+
34
42
  from offwork.core.version import _VERSION
35
43
  from offwork.worker.backends.base import Backend
36
44
 
@@ -86,13 +94,6 @@ class WebSocketBackend(Backend):
86
94
  # ------------------------------------------------------------------ #
87
95
 
88
96
  async def _connect(self) -> Any:
89
- try:
90
- import websockets
91
- except ImportError as exc:
92
- raise RuntimeError(
93
- "WebSocketBackend requires the 'websockets' package. "
94
- "Install with: pip install offwork[ws]"
95
- ) from exc
96
97
  ws = await websockets.connect(
97
98
  self._url,
98
99
  max_size=None, # broker payloads (graph_json) can be large
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "offwork"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Distributed Python task execution via automatic function serialization"
5
5
  readme = "README.md"
6
6
  authors = [
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
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
File without changes