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/__main__.py CHANGED
@@ -1,3 +1,3 @@
1
- from docket.cli import app
1
+ from .cli import app
2
2
 
3
3
  app()
docket/annotations.py CHANGED
@@ -34,7 +34,29 @@ class Annotation(abc.ABC):
34
34
 
35
35
 
36
36
  class Logged(Annotation):
37
- """Instructs docket to include arguments to this parameter in the log."""
37
+ """Instructs docket to include arguments to this parameter in the log.
38
+
39
+ If `length_only` is `True`, only the length of the argument will be included in
40
+ the log.
41
+
42
+ Example:
43
+
44
+ ```python
45
+ @task
46
+ def setup_new_customer(
47
+ customer_id: Annotated[int, Logged],
48
+ addresses: Annotated[list[Address], Logged(length_only=True)],
49
+ password: str,
50
+ ) -> None:
51
+ ...
52
+ ```
53
+
54
+ In the logs, you's see the task referenced as:
55
+
56
+ ```
57
+ setup_new_customer(customer_id=123, addresses[len 2], password=...)
58
+ ```
59
+ """
38
60
 
39
61
  length_only: bool = False
40
62
 
docket/cli.py CHANGED
@@ -259,11 +259,20 @@ def worker(
259
259
  help="Exit after the current docket is finished",
260
260
  ),
261
261
  ] = False,
262
+ healthcheck_port: Annotated[
263
+ int | None,
264
+ typer.Option(
265
+ "--healthcheck-port",
266
+ help="The port to serve a healthcheck on",
267
+ envvar="DOCKET_WORKER_HEALTHCHECK_PORT",
268
+ ),
269
+ ] = None,
262
270
  metrics_port: Annotated[
263
271
  int | None,
264
272
  typer.Option(
265
273
  "--metrics-port",
266
274
  help="The port to serve Prometheus metrics on",
275
+ envvar="DOCKET_WORKER_METRICS_PORT",
267
276
  ),
268
277
  ] = None,
269
278
  ) -> None:
@@ -279,6 +288,7 @@ def worker(
279
288
  scheduling_resolution=scheduling_resolution,
280
289
  schedule_automatic_tasks=schedule_automatic_tasks,
281
290
  until_finished=until_finished,
291
+ healthcheck_port=healthcheck_port,
282
292
  metrics_port=metrics_port,
283
293
  tasks=tasks,
284
294
  )
docket/dependencies.py CHANGED
@@ -49,6 +49,16 @@ class _CurrentWorker(Dependency):
49
49
 
50
50
 
51
51
  def CurrentWorker() -> "Worker":
52
+ """A dependency to access the current Worker.
53
+
54
+ Example:
55
+
56
+ ```python
57
+ @task
58
+ async def my_task(worker: Worker = CurrentWorker()) -> None:
59
+ assert isinstance(worker, Worker)
60
+ ```
61
+ """
52
62
  return cast("Worker", _CurrentWorker())
53
63
 
54
64
 
@@ -58,6 +68,16 @@ class _CurrentDocket(Dependency):
58
68
 
59
69
 
60
70
  def CurrentDocket() -> Docket:
71
+ """A dependency to access the current Docket.
72
+
73
+ Example:
74
+
75
+ ```python
76
+ @task
77
+ async def my_task(docket: Docket = CurrentDocket()) -> None:
78
+ assert isinstance(docket, Docket)
79
+ ```
80
+ """
61
81
  return cast(Docket, _CurrentDocket())
62
82
 
63
83
 
@@ -67,6 +87,16 @@ class _CurrentExecution(Dependency):
67
87
 
68
88
 
69
89
  def CurrentExecution() -> Execution:
90
+ """A dependency to access the current Execution.
91
+
92
+ Example:
93
+
94
+ ```python
95
+ @task
96
+ async def my_task(execution: Execution = CurrentExecution()) -> None:
97
+ assert isinstance(execution, Execution)
98
+ ```
99
+ """
70
100
  return cast(Execution, _CurrentExecution())
71
101
 
72
102
 
@@ -76,23 +106,56 @@ class _TaskKey(Dependency):
76
106
 
77
107
 
78
108
  def TaskKey() -> str:
109
+ """A dependency to access the key of the currently executing task.
110
+
111
+ Example:
112
+
113
+ ```python
114
+ @task
115
+ async def my_task(key: str = TaskKey()) -> None:
116
+ assert isinstance(key, str)
117
+ ```
118
+ """
79
119
  return cast(str, _TaskKey())
80
120
 
81
121
 
82
122
  class _TaskArgument(Dependency):
83
123
  parameter: str | None
124
+ optional: bool
84
125
 
85
- def __init__(self, parameter: str | None = None) -> None:
126
+ def __init__(self, parameter: str | None = None, optional: bool = False) -> None:
86
127
  self.parameter = parameter
128
+ self.optional = optional
87
129
 
88
130
  async def __aenter__(self) -> Any:
89
131
  assert self.parameter is not None
90
132
  execution = self.execution.get()
91
- return execution.get_argument(self.parameter)
133
+ try:
134
+ return execution.get_argument(self.parameter)
135
+ except KeyError:
136
+ if self.optional:
137
+ return None
138
+ raise
139
+
140
+
141
+ def TaskArgument(parameter: str | None = None, optional: bool = False) -> Any:
142
+ """A dependency to access a argument of the currently executing task. This is
143
+ often useful in dependency functions so they can access the arguments of the
144
+ task they are injected into.
145
+
146
+ Example:
92
147
 
148
+ ```python
149
+ async def customer_name(customer_id: int = TaskArgument()) -> str:
150
+ ...look up the customer's name by ID...
151
+ return "John Doe"
93
152
 
94
- def TaskArgument(parameter: str | None = None) -> Any:
95
- return cast(Any, _TaskArgument(parameter))
153
+ @task
154
+ async def greet_customer(customer_id: int, name: str = Depends(customer_name)) -> None:
155
+ print(f"Hello, {name}!")
156
+ ```
157
+ """
158
+ return cast(Any, _TaskArgument(parameter, optional))
96
159
 
97
160
 
98
161
  class _TaskLogger(Dependency):
@@ -110,15 +173,45 @@ class _TaskLogger(Dependency):
110
173
 
111
174
 
112
175
  def TaskLogger() -> logging.LoggerAdapter[logging.Logger]:
176
+ """A dependency to access a logger for the currently executing task. The logger
177
+ will automatically inject contextual information such as the worker and docket
178
+ name, the task key, and the current execution attempt number.
179
+
180
+ Example:
181
+
182
+ ```python
183
+ @task
184
+ async def my_task(logger: LoggerAdapter[Logger] = TaskLogger()) -> None:
185
+ logger.info("Hello, world!")
186
+ ```
187
+ """
113
188
  return cast(logging.LoggerAdapter[logging.Logger], _TaskLogger())
114
189
 
115
190
 
116
191
  class Retry(Dependency):
192
+ """Configures linear retries for a task. You can specify the total number of
193
+ attempts (or `None` to retry indefinitely), and the delay between attempts.
194
+
195
+ Example:
196
+
197
+ ```python
198
+ @task
199
+ async def my_task(retry: Retry = Retry(attempts=3)) -> None:
200
+ ...
201
+ ```
202
+ """
203
+
117
204
  single: bool = True
118
205
 
119
206
  def __init__(
120
207
  self, attempts: int | None = 1, delay: timedelta = timedelta(0)
121
208
  ) -> None:
209
+ """
210
+ Args:
211
+ attempts: The total number of attempts to make. If `None`, the task will
212
+ be retried indefinitely.
213
+ delay: The delay between attempts.
214
+ """
122
215
  self.attempts = attempts
123
216
  self.delay = delay
124
217
  self.attempt = 1
@@ -131,14 +224,32 @@ class Retry(Dependency):
131
224
 
132
225
 
133
226
  class ExponentialRetry(Retry):
134
- attempts: int
227
+ """Configures exponential retries for a task. You can specify the total number
228
+ of attempts (or `None` to retry indefinitely), and the minimum and maximum delays
229
+ between attempts.
230
+
231
+ Example:
232
+
233
+ ```python
234
+ @task
235
+ async def my_task(retry: ExponentialRetry = ExponentialRetry(attempts=3)) -> None:
236
+ ...
237
+ ```
238
+ """
135
239
 
136
240
  def __init__(
137
241
  self,
138
- attempts: int = 1,
242
+ attempts: int | None = 1,
139
243
  minimum_delay: timedelta = timedelta(seconds=1),
140
244
  maximum_delay: timedelta = timedelta(seconds=64),
141
245
  ) -> None:
246
+ """
247
+ Args:
248
+ attempts: The total number of attempts to make. If `None`, the task will
249
+ be retried indefinitely.
250
+ minimum_delay: The minimum delay between attempts.
251
+ maximum_delay: The maximum delay between attempts.
252
+ """
142
253
  super().__init__(attempts=attempts, delay=minimum_delay)
143
254
  self.minimum_delay = minimum_delay
144
255
  self.maximum_delay = maximum_delay
@@ -166,6 +277,19 @@ class ExponentialRetry(Retry):
166
277
 
167
278
 
168
279
  class Perpetual(Dependency):
280
+ """Declare a task that should be run perpetually. Perpetual tasks are automatically
281
+ rescheduled for the future after they finish (whether they succeed or fail). A
282
+ perpetual task can be scheduled at worker startup with the `automatic=True`.
283
+
284
+ Example:
285
+
286
+ ```python
287
+ @task
288
+ async def my_task(perpetual: Perpetual = Perpetual()) -> None:
289
+ ...
290
+ ```
291
+ """
292
+
169
293
  single = True
170
294
 
171
295
  every: timedelta
@@ -181,8 +305,7 @@ class Perpetual(Dependency):
181
305
  every: timedelta = timedelta(0),
182
306
  automatic: bool = False,
183
307
  ) -> None:
184
- """Declare a task that should be run perpetually.
185
-
308
+ """
186
309
  Args:
187
310
  every: The target interval between task executions.
188
311
  automatic: If set, this task will be automatically scheduled during worker
@@ -210,13 +333,29 @@ class Perpetual(Dependency):
210
333
 
211
334
 
212
335
  class Timeout(Dependency):
213
- single = True
336
+ """Configures a timeout for a task. You can specify the base timeout, and the
337
+ task will be cancelled if it exceeds this duration. The timeout may be extended
338
+ within the context of a single running task.
214
339
 
215
- base: timedelta
340
+ Example:
341
+
342
+ ```python
343
+ @task
344
+ async def my_task(timeout: Timeout = Timeout(timedelta(seconds=10))) -> None:
345
+ ...
346
+ ```
347
+ """
216
348
 
349
+ single: bool = True
350
+
351
+ base: timedelta
217
352
  _deadline: float
218
353
 
219
354
  def __init__(self, base: timedelta) -> None:
355
+ """
356
+ Args:
357
+ base: The base timeout duration.
358
+ """
220
359
  self.base = base
221
360
 
222
361
  async def __aenter__(self) -> "Timeout":
@@ -231,9 +370,16 @@ class Timeout(Dependency):
231
370
  return time.monotonic() >= self._deadline
232
371
 
233
372
  def remaining(self) -> timedelta:
373
+ """Get the remaining time until the timeout expires."""
234
374
  return timedelta(seconds=self._deadline - time.monotonic())
235
375
 
236
376
  def extend(self, by: timedelta | None = None) -> None:
377
+ """Extend the timeout by a given duration. If no duration is provided, the
378
+ base timeout will be used.
379
+
380
+ Args:
381
+ by: The duration to extend the timeout by.
382
+ """
237
383
  if by is None:
238
384
  by = self.base
239
385
  self._deadline += by.total_seconds()
@@ -321,6 +467,23 @@ class _Depends(Dependency, Generic[R]):
321
467
 
322
468
 
323
469
  def Depends(dependency: DependencyFunction[R]) -> R:
470
+ """Include a user-defined function as a dependency. Dependencies may either return
471
+ a value or an async context manager. If it returns a context manager, the
472
+ dependency will be entered and exited around the task, giving an opportunity to
473
+ control the lifetime of a resource, like a database connection.
474
+
475
+ Example:
476
+
477
+ ```python
478
+
479
+ async def my_dependency() -> str:
480
+ return "Hello, world!"
481
+
482
+ @task async def my_task(dependency: str = Depends(my_dependency)) -> None:
483
+ print(dependency)
484
+
485
+ ```
486
+ """
324
487
  return cast(R, _Depends(dependency))
325
488
 
326
489
 
docket/docket.py CHANGED
@@ -23,12 +23,12 @@ from typing import (
23
23
  cast,
24
24
  overload,
25
25
  )
26
- from uuid import uuid4
27
26
 
28
27
  import redis.exceptions
29
28
  from opentelemetry import propagate, trace
30
29
  from redis.asyncio import ConnectionPool, Redis
31
30
  from redis.asyncio.client import Pipeline
31
+ from uuid_extensions import uuid7
32
32
 
33
33
  from .execution import (
34
34
  Execution,
@@ -112,6 +112,20 @@ class DocketSnapshot:
112
112
 
113
113
 
114
114
  class Docket:
115
+ """A Docket represents a collection of tasks that may be scheduled for later
116
+ execution. With a Docket, you can add, replace, and cancel tasks.
117
+ Example:
118
+
119
+ ```python
120
+ @task
121
+ async def my_task(greeting: str, recipient: str) -> None:
122
+ print(f"{greeting}, {recipient}!")
123
+
124
+ async with Docket() as docket:
125
+ docket.add(my_task)("Hello", recipient="world")
126
+ ```
127
+ """
128
+
115
129
  tasks: dict[str, TaskFunction]
116
130
  strike_list: StrikeList
117
131
 
@@ -199,6 +213,11 @@ class Docket:
199
213
  await asyncio.shield(r.__aexit__(None, None, None))
200
214
 
201
215
  def register(self, function: TaskFunction) -> None:
216
+ """Register a task with the Docket.
217
+
218
+ Args:
219
+ function: The task to register.
220
+ """
202
221
  from .dependencies import validate_dependencies
203
222
 
204
223
  validate_dependencies(function)
@@ -229,7 +248,14 @@ class Docket:
229
248
  function: Callable[P, Awaitable[R]],
230
249
  when: datetime | None = None,
231
250
  key: str | None = None,
232
- ) -> Callable[P, Awaitable[Execution]]: ... # pragma: no cover
251
+ ) -> Callable[P, Awaitable[Execution]]:
252
+ """Add a task to the Docket.
253
+
254
+ Args:
255
+ function: The task function to add.
256
+ when: The time to schedule the task.
257
+ key: The key to schedule the task under.
258
+ """
233
259
 
234
260
  @overload
235
261
  def add(
@@ -237,7 +263,14 @@ class Docket:
237
263
  function: str,
238
264
  when: datetime | None = None,
239
265
  key: str | None = None,
240
- ) -> Callable[..., Awaitable[Execution]]: ... # pragma: no cover
266
+ ) -> Callable[..., Awaitable[Execution]]:
267
+ """Add a task to the Docket.
268
+
269
+ Args:
270
+ function: The name of a task to add.
271
+ when: The time to schedule the task.
272
+ key: The key to schedule the task under.
273
+ """
241
274
 
242
275
  def add(
243
276
  self,
@@ -245,6 +278,13 @@ class Docket:
245
278
  when: datetime | None = None,
246
279
  key: str | None = None,
247
280
  ) -> Callable[..., Awaitable[Execution]]:
281
+ """Add a task to the Docket.
282
+
283
+ Args:
284
+ function: The task to add.
285
+ when: The time to schedule the task.
286
+ key: The key to schedule the task under.
287
+ """
248
288
  if isinstance(function, str):
249
289
  function = self.tasks[function]
250
290
  else:
@@ -254,7 +294,7 @@ class Docket:
254
294
  when = datetime.now(timezone.utc)
255
295
 
256
296
  if key is None:
257
- key = f"{function.__name__}:{uuid4()}"
297
+ key = str(uuid7())
258
298
 
259
299
  async def scheduler(*args: P.args, **kwargs: P.kwargs) -> Execution:
260
300
  execution = Execution(function, args, kwargs, when, key, attempt=1)
@@ -277,7 +317,14 @@ class Docket:
277
317
  function: Callable[P, Awaitable[R]],
278
318
  when: datetime,
279
319
  key: str,
280
- ) -> Callable[P, Awaitable[Execution]]: ... # pragma: no cover
320
+ ) -> Callable[P, Awaitable[Execution]]:
321
+ """Replace a previously scheduled task on the Docket.
322
+
323
+ Args:
324
+ function: The task function to replace.
325
+ when: The time to schedule the task.
326
+ key: The key to schedule the task under.
327
+ """
281
328
 
282
329
  @overload
283
330
  def replace(
@@ -285,7 +332,14 @@ class Docket:
285
332
  function: str,
286
333
  when: datetime,
287
334
  key: str,
288
- ) -> Callable[..., Awaitable[Execution]]: ... # pragma: no cover
335
+ ) -> Callable[..., Awaitable[Execution]]:
336
+ """Replace a previously scheduled task on the Docket.
337
+
338
+ Args:
339
+ function: The name of a task to replace.
340
+ when: The time to schedule the task.
341
+ key: The key to schedule the task under.
342
+ """
289
343
 
290
344
  def replace(
291
345
  self,
@@ -293,6 +347,13 @@ class Docket:
293
347
  when: datetime,
294
348
  key: str,
295
349
  ) -> Callable[..., Awaitable[Execution]]:
350
+ """Replace a previously scheduled task on the Docket.
351
+
352
+ Args:
353
+ function: The task to replace.
354
+ when: The time to schedule the task.
355
+ key: The key to schedule the task under.
356
+ """
296
357
  if isinstance(function, str):
297
358
  function = self.tasks[function]
298
359
 
@@ -329,6 +390,11 @@ class Docket:
329
390
  TASKS_SCHEDULED.add(1, {**self.labels(), **execution.general_labels()})
330
391
 
331
392
  async def cancel(self, key: str) -> None:
393
+ """Cancel a previously scheduled task on the Docket.
394
+
395
+ Args:
396
+ key: The key of the task to cancel.
397
+ """
332
398
  with tracer.start_as_current_span(
333
399
  "docket.cancel",
334
400
  attributes={**self.labels(), "docket.key": key},
@@ -421,6 +487,14 @@ class Docket:
421
487
  operator: Operator | LiteralOperator = "==",
422
488
  value: Hashable | None = None,
423
489
  ) -> None:
490
+ """Strike a task from the Docket.
491
+
492
+ Args:
493
+ function: The task to strike.
494
+ parameter: The parameter to strike on.
495
+ operator: The operator to use.
496
+ value: The value to strike on.
497
+ """
424
498
  if not isinstance(function, (str, type(None))):
425
499
  function = function.__name__
426
500
 
@@ -436,6 +510,14 @@ class Docket:
436
510
  operator: Operator | LiteralOperator = "==",
437
511
  value: Hashable | None = None,
438
512
  ) -> None:
513
+ """Restore a previously stricken task to the Docket.
514
+
515
+ Args:
516
+ function: The task to restore.
517
+ parameter: The parameter to restore on.
518
+ operator: The operator to use.
519
+ value: The value to restore on.
520
+ """
439
521
  if not isinstance(function, (str, type(None))):
440
522
  function = function.__name__
441
523
 
@@ -501,6 +583,12 @@ class Docket:
501
583
  await asyncio.sleep(1)
502
584
 
503
585
  async def snapshot(self) -> DocketSnapshot:
586
+ """Get a snapshot of the Docket, including which tasks are scheduled or currently
587
+ running, as well as which workers are active.
588
+
589
+ Returns:
590
+ A snapshot of the Docket.
591
+ """
504
592
  running: list[RunningExecution] = []
505
593
  future: list[Execution] = []
506
594
 
@@ -585,6 +673,11 @@ class Docket:
585
673
  return f"{self.name}:task-workers:{task_name}"
586
674
 
587
675
  async def workers(self) -> Collection[WorkerInfo]:
676
+ """Get a list of all workers that have sent heartbeats to the Docket.
677
+
678
+ Returns:
679
+ A list of all workers that have sent heartbeats to the Docket.
680
+ """
588
681
  workers: list[WorkerInfo] = []
589
682
 
590
683
  oldest = datetime.now(timezone.utc).timestamp() - (
@@ -615,8 +708,15 @@ class Docket:
615
708
  return workers
616
709
 
617
710
  async def task_workers(self, task_name: str) -> Collection[WorkerInfo]:
618
- workers: list[WorkerInfo] = []
711
+ """Get a list of all workers that are able to execute a given task.
712
+
713
+ Args:
714
+ task_name: The name of the task.
619
715
 
716
+ Returns:
717
+ A list of all workers that are able to execute the given task.
718
+ """
719
+ workers: list[WorkerInfo] = []
620
720
  oldest = datetime.now(timezone.utc).timestamp() - (
621
721
  self.heartbeat_interval.total_seconds() * self.missed_heartbeats
622
722
  )
docket/execution.py CHANGED
@@ -3,15 +3,23 @@ import enum
3
3
  import inspect
4
4
  import logging
5
5
  from datetime import datetime
6
- from typing import Any, Awaitable, Callable, Hashable, Literal, Mapping, Self, cast
6
+ from typing import (
7
+ Any,
8
+ Awaitable,
9
+ Callable,
10
+ Hashable,
11
+ Literal,
12
+ Mapping,
13
+ Self,
14
+ cast,
15
+ )
7
16
 
8
17
  import cloudpickle # type: ignore[import]
9
-
10
- from opentelemetry import trace, propagate
11
18
  import opentelemetry.context
19
+ from opentelemetry import propagate, trace
12
20
 
13
21
  from .annotations import Logged
14
- from docket.instrumentation import message_getter
22
+ from .instrumentation import message_getter
15
23
 
16
24
  logger: logging.Logger = logging.getLogger(__name__)
17
25
 
@@ -117,6 +125,37 @@ class Execution:
117
125
  return [trace.Link(initiating_context)] if initiating_context.is_valid else []
118
126
 
119
127
 
128
+ def compact_signature(signature: inspect.Signature) -> str:
129
+ from .dependencies import Dependency
130
+
131
+ parameters: list[str] = []
132
+ dependencies: int = 0
133
+
134
+ for parameter in signature.parameters.values():
135
+ if isinstance(parameter.default, Dependency):
136
+ dependencies += 1
137
+ continue
138
+
139
+ parameter_definition = parameter.name
140
+ if parameter.annotation is not parameter.empty:
141
+ annotation = parameter.annotation
142
+ if hasattr(annotation, "__origin__"):
143
+ annotation = annotation.__args__[0]
144
+
145
+ type_name = getattr(annotation, "__name__", str(annotation))
146
+ parameter_definition = f"{parameter.name}: {type_name}"
147
+
148
+ if parameter.default is not parameter.empty:
149
+ parameter_definition = f"{parameter_definition} = {parameter.default!r}"
150
+
151
+ parameters.append(parameter_definition)
152
+
153
+ if dependencies > 0:
154
+ parameters.append("...")
155
+
156
+ return ", ".join(parameters)
157
+
158
+
120
159
  class Operator(enum.StrEnum):
121
160
  EQUAL = "=="
122
161
  NOT_EQUAL = "!="
docket/instrumentation.py CHANGED
@@ -1,5 +1,5 @@
1
- import threading
2
1
  from contextlib import contextmanager
2
+ from threading import Thread
3
3
  from typing import Generator, cast
4
4
 
5
5
  from opentelemetry import metrics
@@ -145,6 +145,34 @@ message_getter: MessageGetter = MessageGetter()
145
145
  message_setter: MessageSetter = MessageSetter()
146
146
 
147
147
 
148
+ @contextmanager
149
+ def healthcheck_server(
150
+ host: str = "0.0.0.0", port: int | None = None
151
+ ) -> Generator[None, None, None]:
152
+ if port is None:
153
+ yield
154
+ return
155
+
156
+ from http.server import BaseHTTPRequestHandler, HTTPServer
157
+
158
+ class HealthcheckHandler(BaseHTTPRequestHandler):
159
+ def do_GET(self):
160
+ self.send_response(200)
161
+ self.send_header("Content-type", "text/plain")
162
+ self.end_headers()
163
+ self.wfile.write(b"OK")
164
+
165
+ def log_message(self, format: str, *args: object) -> None:
166
+ # Suppress access logs from the webserver
167
+ pass
168
+
169
+ server = HTTPServer((host, port), HealthcheckHandler)
170
+ with server:
171
+ Thread(target=server.serve_forever, daemon=True).start()
172
+
173
+ yield
174
+
175
+
148
176
  @contextmanager
149
177
  def metrics_server(
150
178
  host: str = "0.0.0.0", port: int | None = None
@@ -173,8 +201,6 @@ def metrics_server(
173
201
  handler_class=_SilentHandler,
174
202
  )
175
203
  with server:
176
- t = threading.Thread(target=server.serve_forever)
177
- t.daemon = True
178
- t.start()
204
+ Thread(target=server.serve_forever, daemon=True).start()
179
205
 
180
206
  yield