pydocket 0.6.2__py3-none-any.whl → 0.6.4__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.

Potentially problematic release.


This version of pydocket might be problematic. Click here for more details.

docket/worker.py CHANGED
@@ -1,5 +1,7 @@
1
1
  import asyncio
2
2
  import logging
3
+ import os
4
+ import socket
3
5
  import sys
4
6
  import time
5
7
  from datetime import datetime, timedelta, timezone
@@ -11,15 +13,12 @@ from typing import (
11
13
  Self,
12
14
  cast,
13
15
  )
14
- from uuid import uuid4
15
16
 
16
17
  from opentelemetry import trace
17
18
  from opentelemetry.trace import Tracer
18
19
  from redis.asyncio import Redis
19
20
  from redis.exceptions import ConnectionError, LockError
20
21
 
21
- from docket.execution import get_signature
22
-
23
22
  from .dependencies import (
24
23
  Dependency,
25
24
  FailedDependency,
@@ -37,6 +36,7 @@ from .docket import (
37
36
  RedisMessageID,
38
37
  RedisReadGroupResponse,
39
38
  )
39
+ from .execution import compact_signature, get_signature
40
40
  from .instrumentation import (
41
41
  QUEUE_DEPTH,
42
42
  REDIS_DISRUPTIONS,
@@ -51,6 +51,7 @@ from .instrumentation import (
51
51
  TASKS_STARTED,
52
52
  TASKS_STRICKEN,
53
53
  TASKS_SUCCEEDED,
54
+ healthcheck_server,
54
55
  metrics_server,
55
56
  )
56
57
 
@@ -65,6 +66,18 @@ class _stream_due_tasks(Protocol):
65
66
 
66
67
 
67
68
  class Worker:
69
+ """A Worker executes tasks on a Docket. You may run as many workers as you like
70
+ to work a single Docket.
71
+
72
+ Example:
73
+
74
+ ```python
75
+ async with Docket() as docket:
76
+ async with Worker(docket) as worker:
77
+ await worker.run_forever()
78
+ ```
79
+ """
80
+
68
81
  docket: Docket
69
82
  name: str
70
83
  concurrency: int
@@ -86,7 +99,7 @@ class Worker:
86
99
  schedule_automatic_tasks: bool = True,
87
100
  ) -> None:
88
101
  self.docket = docket
89
- self.name = name or f"worker:{uuid4()}"
102
+ self.name = name or f"{socket.gethostname()}#{os.getpid()}"
90
103
  self.concurrency = concurrency
91
104
  self.redelivery_timeout = redelivery_timeout
92
105
  self.reconnection_delay = reconnection_delay
@@ -140,10 +153,14 @@ class Worker:
140
153
  scheduling_resolution: timedelta = timedelta(milliseconds=250),
141
154
  schedule_automatic_tasks: bool = True,
142
155
  until_finished: bool = False,
156
+ healthcheck_port: int | None = None,
143
157
  metrics_port: int | None = None,
144
158
  tasks: list[str] = ["docket.tasks:standard_tasks"],
145
159
  ) -> None:
146
- with metrics_server(port=metrics_port):
160
+ with (
161
+ healthcheck_server(port=healthcheck_port),
162
+ metrics_server(port=metrics_port),
163
+ ):
147
164
  async with Docket(name=docket_name, url=url) as docket:
148
165
  for task_path in tasks:
149
166
  docket.register_collection(task_path)
@@ -205,10 +222,7 @@ class Worker:
205
222
  self._execution_counts = {}
206
223
 
207
224
  async def _run(self, forever: bool = False) -> None:
208
- logger.info("Starting worker %r with the following tasks:", self.name)
209
- for task_name, task in self.docket.tasks.items():
210
- signature = get_signature(task)
211
- logger.info("* %s%s", task_name, signature)
225
+ self._startup_log()
212
226
 
213
227
  while True:
214
228
  try:
@@ -506,6 +520,8 @@ class Worker:
506
520
  arrow = "↬" if execution.attempt > 1 else "↪"
507
521
  logger.info("%s [%s] %s", arrow, ms(punctuality), call, extra=log_context)
508
522
 
523
+ dependencies: dict[str, Dependency] = {}
524
+
509
525
  with tracer.start_as_current_span(
510
526
  execution.function.__name__,
511
527
  kind=trace.SpanKind.CONSUMER,
@@ -516,17 +532,17 @@ class Worker:
516
532
  },
517
533
  links=execution.incoming_span_links(),
518
534
  ):
519
- async with resolved_dependencies(self, execution) as dependencies:
520
- # Preemptively reschedule the perpetual task for the future, or clear
521
- # the known task key for this task
522
- rescheduled = await self._perpetuate_if_requested(
523
- execution, dependencies
524
- )
525
- if not rescheduled:
526
- async with self.docket.redis() as redis:
527
- await self._delete_known_task(redis, execution)
535
+ try:
536
+ async with resolved_dependencies(self, execution) as dependencies:
537
+ # Preemptively reschedule the perpetual task for the future, or clear
538
+ # the known task key for this task
539
+ rescheduled = await self._perpetuate_if_requested(
540
+ execution, dependencies
541
+ )
542
+ if not rescheduled:
543
+ async with self.docket.redis() as redis:
544
+ await self._delete_known_task(redis, execution)
528
545
 
529
- try:
530
546
  dependency_failures = {
531
547
  k: v
532
548
  for k, v in dependencies.items()
@@ -568,24 +584,24 @@ class Worker:
568
584
  logger.info(
569
585
  "%s [%s] %s", arrow, ms(duration), call, extra=log_context
570
586
  )
571
- except Exception:
572
- duration = log_context["duration"] = time.time() - start
573
- TASKS_FAILED.add(1, counter_labels)
574
-
575
- retried = await self._retry_if_requested(execution, dependencies)
576
- if not retried:
577
- retried = await self._perpetuate_if_requested(
578
- execution, dependencies, timedelta(seconds=duration)
579
- )
587
+ except Exception:
588
+ duration = log_context["duration"] = time.time() - start
589
+ TASKS_FAILED.add(1, counter_labels)
580
590
 
581
- arrow = "↫" if retried else "↩"
582
- logger.exception(
583
- "%s [%s] %s", arrow, ms(duration), call, extra=log_context
591
+ retried = await self._retry_if_requested(execution, dependencies)
592
+ if not retried:
593
+ retried = await self._perpetuate_if_requested(
594
+ execution, dependencies, timedelta(seconds=duration)
584
595
  )
585
- finally:
586
- TASKS_RUNNING.add(-1, counter_labels)
587
- TASKS_COMPLETED.add(1, counter_labels)
588
- TASK_DURATION.record(duration, counter_labels)
596
+
597
+ arrow = "↫" if retried else "↩"
598
+ logger.exception(
599
+ "%s [%s] %s", arrow, ms(duration), call, extra=log_context
600
+ )
601
+ finally:
602
+ TASKS_RUNNING.add(-1, counter_labels)
603
+ TASKS_COMPLETED.add(1, counter_labels)
604
+ TASK_DURATION.record(duration, counter_labels)
589
605
 
590
606
  async def _run_function_with_timeout(
591
607
  self,
@@ -665,6 +681,11 @@ class Worker:
665
681
 
666
682
  return True
667
683
 
684
+ def _startup_log(self) -> None:
685
+ logger.info("Starting worker %r with the following tasks:", self.name)
686
+ for task_name, task in self.docket.tasks.items():
687
+ logger.info("* %s(%s)", task_name, compact_signature(get_signature(task)))
688
+
668
689
  @property
669
690
  def workers_set(self) -> str:
670
691
  return self.docket.workers_set
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydocket
3
+ Version: 0.6.4
4
+ Summary: A distributed background task system for Python functions
5
+ Project-URL: Homepage, https://github.com/chrisguidry/docket
6
+ Project-URL: Bug Tracker, https://github.com/chrisguidry/docket/issues
7
+ Author-email: Chris Guidry <guid@omg.lol>
8
+ License: # Released under MIT License
9
+
10
+ Copyright (c) 2025 Chris Guidry.
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ License-File: LICENSE
18
+ Classifier: Development Status :: 4 - Beta
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: cloudpickle>=3.1.1
27
+ Requires-Dist: opentelemetry-api>=1.30.0
28
+ Requires-Dist: opentelemetry-exporter-prometheus>=0.51b0
29
+ Requires-Dist: prometheus-client>=0.21.1
30
+ Requires-Dist: python-json-logger>=3.2.1
31
+ Requires-Dist: redis>=4.6
32
+ Requires-Dist: rich>=13.9.4
33
+ Requires-Dist: typer>=0.15.1
34
+ Requires-Dist: uuid7>=0.1.0
35
+ Description-Content-Type: text/markdown
36
+
37
+ Docket is a distributed background task system for Python functions with a focus
38
+ on the scheduling of future work as seamlessly and efficiently as immediate work.
39
+
40
+ [![PyPI - Version](https://img.shields.io/pypi/v/pydocket)](https://pypi.org/project/pydocket/)
41
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pydocket)](https://pypi.org/project/pydocket/)
42
+ [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions/workflows/ci.yml)
43
+ [![Codecov](https://img.shields.io/codecov/c/github/chrisguidry/docket)](https://app.codecov.io/gh/chrisguidry/docket)
44
+ [![PyPI - License](https://img.shields.io/pypi/l/pydocket)](https://github.com/chrisguidry/docket/blob/main/LICENSE)
45
+ [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://chrisguidry.github.io/docket/)
46
+
47
+ ## At a glance
48
+
49
+ ```python
50
+ from datetime import datetime, timedelta, timezone
51
+
52
+ from docket import Docket
53
+
54
+
55
+ async def greet(name: str, greeting="Hello") -> None:
56
+ print(f"{greeting}, {name} at {datetime.now()}!")
57
+
58
+
59
+ async with Docket() as docket:
60
+ await docket.add(greet)("Jane")
61
+
62
+ now = datetime.now(timezone.utc)
63
+ soon = now + timedelta(seconds=3)
64
+ await docket.add(greet, when=soon)("John", greeting="Howdy")
65
+ ```
66
+
67
+ ```python
68
+ from docket import Docket, Worker
69
+
70
+ async with Docket() as docket:
71
+ async with Worker(docket) as worker:
72
+ await worker.run_until_finished()
73
+ ```
74
+
75
+ ```
76
+ Hello, Jane at 2025-03-05 13:58:21.552644!
77
+ Howdy, John at 2025-03-05 13:58:24.550773!
78
+ ```
79
+
80
+ Check out our docs for more [details](http://chrisguidry.github.io/docket/),
81
+ [examples](https://chrisguidry.github.io/docket/getting-started/), and the [API
82
+ reference](https://chrisguidry.github.io/docket/api-reference/).
83
+
84
+ ## Why `docket`?
85
+
86
+ ⚡️ Snappy one-way background task processing without any bloat
87
+
88
+ 📅 Schedule immediate or future work seamlessly with the same interface
89
+
90
+ ⏭️ Skip problematic tasks or parameters without redeploying
91
+
92
+ 🌊 Purpose-built for Redis streams
93
+
94
+ 🧩 Fully type-complete and type-aware for your background task functions
95
+
96
+ ## Installing `docket`
97
+
98
+ Docket is [available on PyPI](https://pypi.org/project/pydocket/) under the package name
99
+ `pydocket`. It targets Python 3.12 or above.
100
+
101
+ With [`uv`](https://docs.astral.sh/uv/):
102
+
103
+ ```bash
104
+ uv pip install pydocket
105
+
106
+ or
107
+
108
+ uv add pydocket
109
+ ```
110
+
111
+ With `pip`:
112
+
113
+ ```bash
114
+ pip install pydocket
115
+ ```
116
+
117
+ Docket requires a [Redis](http://redis.io/) server with Streams support (which was
118
+ introduced in Redis 5.0.0). Docket is tested with Redis 6 and 7.
119
+
120
+ # Hacking on `docket`
121
+
122
+ We use [`uv`](https://docs.astral.sh/uv/) for project management, so getting set up
123
+ should be as simple as cloning the repo and running:
124
+
125
+ ```bash
126
+ uv sync
127
+ ```
128
+
129
+ The to run the test suite:
130
+
131
+ ```bash
132
+ pytest
133
+ ```
134
+
135
+ We aim to maintain 100% test coverage, which is required for all PRs to `docket`. We
136
+ believe that `docket` should stay small, simple, understandable, and reliable, and that
137
+ begins with testing all the dusty branches and corners. This will give us the
138
+ confidence to upgrade dependencies quickly and to adapt to new versions of Redis over
139
+ time.
@@ -0,0 +1,16 @@
1
+ docket/__init__.py,sha256=sY1T_NVsXQNOmOhOnfYmZ95dcE_52Ov6DSIVIMZp-1w,869
2
+ docket/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
3
+ docket/annotations.py,sha256=SFBrOMbpAh7P67u8fRTH-u3MVvJQxe0qYi92WAShAsw,2173
4
+ docket/cli.py,sha256=WPm_URZ54h8gHjrsHKP8SXpRzdeepmyH_FhQHai-Qus,20899
5
+ docket/dependencies.py,sha256=_31Fgn6A_4aWn5TJpXdbsPtimBVIPabNJkw49RRLJTc,16441
6
+ docket/docket.py,sha256=5e101CGLZ2tWNcADo4cdewapmXab47ieMCeQr6d92YQ,24478
7
+ docket/execution.py,sha256=6KozjnS96byvyCMTQ2-IkcIrPsqaPIVu2HZU0U4Be9E,14813
8
+ docket/instrumentation.py,sha256=f-GG5VS6EdS2It30qxjVpzWUBOZQcTnat-3KzPwwDgQ,5367
9
+ docket/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ docket/tasks.py,sha256=RIlSM2omh-YDwVnCz6M5MtmK8T_m_s1w2OlRRxDUs6A,1437
11
+ docket/worker.py,sha256=tJfk2rlHODzHaWBzpBXT8h-Lo7RDQ6gb6HU8b3T9gFA,27878
12
+ pydocket-0.6.4.dist-info/METADATA,sha256=R3ODtTRkrNkplBvC5-8pVsRjSLSfYKYHKXqZCT9Qr-w,5335
13
+ pydocket-0.6.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ pydocket-0.6.4.dist-info/entry_points.txt,sha256=4WOk1nUlBsUT5O3RyMci2ImuC5XFswuopElYcLHtD5k,47
15
+ pydocket-0.6.4.dist-info/licenses/LICENSE,sha256=YuVWU_ZXO0K_k2FG8xWKe5RGxV24AhJKTvQmKfqXuyk,1087
16
+ pydocket-0.6.4.dist-info/RECORD,,