pydocket 0.11.0__py3-none-any.whl → 0.11.1__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/__init__.py CHANGED
@@ -8,6 +8,7 @@ from importlib.metadata import version
8
8
 
9
9
  __version__ = version("pydocket")
10
10
 
11
+ from .agenda import Agenda
11
12
  from .annotations import Logged
12
13
  from .dependencies import (
13
14
  ConcurrencyLimit,
@@ -29,6 +30,7 @@ from .worker import Worker
29
30
 
30
31
  __all__ = [
31
32
  "__version__",
33
+ "Agenda",
32
34
  "ConcurrencyLimit",
33
35
  "CurrentDocket",
34
36
  "CurrentExecution",
docket/agenda.py ADDED
@@ -0,0 +1,201 @@
1
+ """
2
+ Agenda - A collection of tasks that can be scheduled together.
3
+
4
+ The Agenda class provides a way to collect multiple tasks and then scatter them
5
+ evenly over a time period to avoid overwhelming the system with immediate work.
6
+ """
7
+
8
+ import random
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import Any, Awaitable, Callable, Iterator, ParamSpec, TypeVar, overload
11
+
12
+ from uuid_extensions import uuid7
13
+
14
+ from .docket import Docket
15
+ from .execution import Execution, TaskFunction
16
+
17
+ P = ParamSpec("P")
18
+ R = TypeVar("R")
19
+
20
+
21
+ class Agenda:
22
+ """A collection of tasks to be scheduled together on a Docket.
23
+
24
+ The Agenda allows you to build up a collection of tasks with their arguments,
25
+ then schedule them all at once using various timing strategies like scattering.
26
+
27
+ Example:
28
+ >>> agenda = Agenda()
29
+ >>> agenda.add(process_item)(item1)
30
+ >>> agenda.add(process_item)(item2)
31
+ >>> agenda.add(send_email)(email)
32
+ >>> await agenda.scatter(docket, over=timedelta(minutes=50))
33
+ """
34
+
35
+ def __init__(self) -> None:
36
+ """Initialize an empty Agenda."""
37
+ self._tasks: list[
38
+ tuple[TaskFunction | str, tuple[Any, ...], dict[str, Any]]
39
+ ] = []
40
+
41
+ def __len__(self) -> int:
42
+ """Return the number of tasks in the agenda."""
43
+ return len(self._tasks)
44
+
45
+ def __iter__(
46
+ self,
47
+ ) -> Iterator[tuple[TaskFunction | str, tuple[Any, ...], dict[str, Any]]]:
48
+ """Iterate over tasks in the agenda."""
49
+ return iter(self._tasks)
50
+
51
+ @overload
52
+ def add(
53
+ self,
54
+ function: Callable[P, Awaitable[R]],
55
+ ) -> Callable[P, None]:
56
+ """Add a task function to the agenda.
57
+
58
+ Args:
59
+ function: The task function to add.
60
+
61
+ Returns:
62
+ A callable that accepts the task arguments.
63
+ """
64
+
65
+ @overload
66
+ def add(
67
+ self,
68
+ function: str,
69
+ ) -> Callable[..., None]:
70
+ """Add a task by name to the agenda.
71
+
72
+ Args:
73
+ function: The name of a registered task.
74
+
75
+ Returns:
76
+ A callable that accepts the task arguments.
77
+ """
78
+
79
+ def add(
80
+ self,
81
+ function: Callable[P, Awaitable[R]] | str,
82
+ ) -> Callable[..., None]:
83
+ """Add a task to the agenda.
84
+
85
+ Args:
86
+ function: The task function or name to add.
87
+
88
+ Returns:
89
+ A callable that accepts the task arguments and adds them to the agenda.
90
+ """
91
+
92
+ def scheduler(*args: Any, **kwargs: Any) -> None:
93
+ self._tasks.append((function, args, kwargs))
94
+
95
+ return scheduler
96
+
97
+ def clear(self) -> None:
98
+ """Clear all tasks from the agenda."""
99
+ self._tasks.clear()
100
+
101
+ async def scatter(
102
+ self,
103
+ docket: Docket,
104
+ over: timedelta,
105
+ start: datetime | None = None,
106
+ jitter: timedelta | None = None,
107
+ ) -> list[Execution]:
108
+ """Scatter the tasks in this agenda over a time period.
109
+
110
+ Tasks are distributed evenly across the specified time window,
111
+ optionally with random jitter to prevent thundering herd effects.
112
+
113
+ If an error occurs during scheduling, some tasks may have already been
114
+ scheduled successfully before the failure occurred.
115
+
116
+ Args:
117
+ docket: The Docket to schedule tasks on.
118
+ over: Time period to scatter tasks over (required).
119
+ start: When to start scattering from. Defaults to now.
120
+ jitter: Maximum random offset to add/subtract from each scheduled time.
121
+
122
+ Returns:
123
+ List of Execution objects for the scheduled tasks.
124
+
125
+ Raises:
126
+ KeyError: If any task name is not registered with the docket.
127
+ ValueError: If any task is stricken or 'over' is not positive.
128
+ """
129
+ if over.total_seconds() <= 0:
130
+ raise ValueError("'over' parameter must be a positive duration")
131
+
132
+ if not self._tasks:
133
+ return []
134
+
135
+ if start is None:
136
+ start = datetime.now(timezone.utc)
137
+
138
+ # Calculate even distribution over the time period
139
+ task_count = len(self._tasks)
140
+
141
+ if task_count == 1:
142
+ # Single task goes in the middle of the window
143
+ schedule_times = [start + over / 2]
144
+ else:
145
+ # Distribute tasks evenly across the window
146
+ # For n tasks, we want n points from start to start+over inclusive
147
+ interval = over / (task_count - 1)
148
+ schedule_times = [start + interval * i for i in range(task_count)]
149
+
150
+ # Apply jitter if specified
151
+ if jitter:
152
+ jittered_times: list[datetime] = []
153
+ for schedule_time in schedule_times:
154
+ # Random offset between -jitter and +jitter
155
+ offset = timedelta(
156
+ seconds=random.uniform(
157
+ -jitter.total_seconds(), jitter.total_seconds()
158
+ )
159
+ )
160
+ # Ensure the jittered time doesn't go before start
161
+ jittered_time = max(schedule_time + offset, start)
162
+ jittered_times.append(jittered_time)
163
+ schedule_times = jittered_times
164
+
165
+ # Build all Execution objects first, validating as we go
166
+ executions: list[Execution] = []
167
+ for (task_func, args, kwargs), schedule_time in zip(
168
+ self._tasks, schedule_times
169
+ ):
170
+ # Resolve task function if given by name
171
+ if isinstance(task_func, str):
172
+ if task_func not in docket.tasks:
173
+ raise KeyError(f"Task '{task_func}' is not registered")
174
+ resolved_func = docket.tasks[task_func]
175
+ else:
176
+ # Ensure task is registered
177
+ if task_func not in docket.tasks.values():
178
+ docket.register(task_func)
179
+ resolved_func = task_func
180
+
181
+ # Create execution with unique key
182
+ key = str(uuid7())
183
+ execution = Execution(
184
+ function=resolved_func,
185
+ args=args,
186
+ kwargs=kwargs,
187
+ when=schedule_time,
188
+ key=key,
189
+ attempt=1,
190
+ )
191
+ executions.append(execution)
192
+
193
+ # Schedule all tasks - if any fail, some tasks may have been scheduled
194
+ for execution in executions:
195
+ scheduler = docket.add(
196
+ execution.function, when=execution.when, key=execution.key
197
+ )
198
+ # Actually schedule the task - if this fails, earlier tasks remain scheduled
199
+ await scheduler(*execution.args, **execution.kwargs)
200
+
201
+ return executions
docket/docket.py CHANGED
@@ -455,7 +455,7 @@ class Docket:
455
455
  1,
456
456
  {
457
457
  **self.labels(),
458
- **execution.specific_labels(),
458
+ **execution.general_labels(),
459
459
  "docket.where": "docket",
460
460
  },
461
461
  )
docket/worker.py CHANGED
@@ -732,7 +732,7 @@ class Worker:
732
732
  execution.attempt += 1
733
733
  await self.docket.schedule(execution)
734
734
 
735
- TASKS_RETRIED.add(1, {**self.labels(), **execution.specific_labels()})
735
+ TASKS_RETRIED.add(1, {**self.labels(), **execution.general_labels()})
736
736
  return True
737
737
 
738
738
  async def _perpetuate_if_requested(
@@ -758,7 +758,7 @@ class Worker:
758
758
  )
759
759
 
760
760
  if duration is not None:
761
- TASKS_PERPETUATED.add(1, {**self.labels(), **execution.specific_labels()})
761
+ TASKS_PERPETUATED.add(1, {**self.labels(), **execution.general_labels()})
762
762
 
763
763
  return True
764
764
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydocket
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: A distributed background task system for Python functions
5
5
  Project-URL: Homepage, https://github.com/chrisguidry/docket
6
6
  Project-URL: Bug Tracker, https://github.com/chrisguidry/docket/issues
@@ -1,16 +1,17 @@
1
- docket/__init__.py,sha256=onwZzh73tESWoFBukbcW-7gjxoXb-yI7dutRD7tPN6g,915
1
+ docket/__init__.py,sha256=ChJS2JRyruj22Vi504eXrmQNPQ97L_Sj52OJCuhjoeM,956
2
2
  docket/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
3
+ docket/agenda.py,sha256=RqrVkCuWAvwn_q6graCU-lLRQltbJ0QQheJ34T-Gjck,6667
3
4
  docket/annotations.py,sha256=wttix9UOeMFMAWXAIJUfUw5GjESJZsACb4YXJCozP7Q,2348
4
5
  docket/cli.py,sha256=rTfri2--u4Q5PlXyh7Ub_F5uh3-TtZOWLUp9WY_TvAE,25750
5
6
  docket/dependencies.py,sha256=BC0bnt10cr9_S1p5JAP_bnC9RwZkTr9ulPBrxC7eZnA,20247
6
- docket/docket.py,sha256=jP5uI02in5chQvovRsnPaMhgLff3uiK42A-l3eBh2sE,31241
7
+ docket/docket.py,sha256=NWyulaZYfcNeaqZSJMG54bHqTC5gVggzYFHjpTTY90A,31240
7
8
  docket/execution.py,sha256=Lqzgj5EO3v5OD0w__5qBut7WnlEcHZfAYj-BYRdiJf8,15138
8
9
  docket/instrumentation.py,sha256=zLYgtuXbNOcotcSlD9pgLVdNp2rPddyxj9JwM3K19Go,5667
9
10
  docket/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  docket/tasks.py,sha256=RIlSM2omh-YDwVnCz6M5MtmK8T_m_s1w2OlRRxDUs6A,1437
11
- docket/worker.py,sha256=hv9ug5WIVAEFPUBCbJ5-lobInwNdMfi7-Ja6fLKdEQ8,35392
12
- pydocket-0.11.0.dist-info/METADATA,sha256=e34sVFSTXKuWhQ0O60UHfzkGojfAiEMNMkG9pjLGEqo,5419
13
- pydocket-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- pydocket-0.11.0.dist-info/entry_points.txt,sha256=4WOk1nUlBsUT5O3RyMci2ImuC5XFswuopElYcLHtD5k,47
15
- pydocket-0.11.0.dist-info/licenses/LICENSE,sha256=YuVWU_ZXO0K_k2FG8xWKe5RGxV24AhJKTvQmKfqXuyk,1087
16
- pydocket-0.11.0.dist-info/RECORD,,
12
+ docket/worker.py,sha256=P4j9uHXt5KcU5e9S4SmQ9v6OCRFMLjYwbMR9PeRvVXc,35390
13
+ pydocket-0.11.1.dist-info/METADATA,sha256=zl27q0Qf9js2bjyDHEjVWighIGWd4me36FLqY3yt5MI,5419
14
+ pydocket-0.11.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ pydocket-0.11.1.dist-info/entry_points.txt,sha256=4WOk1nUlBsUT5O3RyMci2ImuC5XFswuopElYcLHtD5k,47
16
+ pydocket-0.11.1.dist-info/licenses/LICENSE,sha256=YuVWU_ZXO0K_k2FG8xWKe5RGxV24AhJKTvQmKfqXuyk,1087
17
+ pydocket-0.11.1.dist-info/RECORD,,