From aeadedc658de60442c54966ac7788a7be06fde97 Mon Sep 17 00:00:00 2001 From: kasteph Date: Thu, 28 Mar 2024 18:45:31 +0100 Subject: [PATCH 001/375] docs: add structlog config --- docs/howto/production/logging.md | 45 +++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/howto/production/logging.md b/docs/howto/production/logging.md index 086da5523..8baa70d2a 100644 --- a/docs/howto/production/logging.md +++ b/docs/howto/production/logging.md @@ -27,7 +27,50 @@ to see them is to use a structured logging library such as [`structlog`]. If you want a minimal example of a logging setup that displays the extra attributes without using third party logging libraries, look at the -[Django demo] +[Django demo]. + +## structlog + +[`structlog`](https://www.structlog.org/en/stable/index.html) needs to be +configured in order to have `procrastinate`'s logs be formatted uniformly +with the rest of your application. + +The `structlog` docs has a [how to](https://www.structlog.org/en/stable/standard-library.html#rendering-using-structlog-based-formatters-within-logging). + +A minimal configuration would look like: + +```py +shared_processors = [ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.set_exc_info, + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, +] + +structlog.configure( + processors=shared_processors, + logger_factory=structlog.stdlib.LoggerFactory(), + cache_logger_on_first_use=True, +) + +formatter = structlog.stdlib.ProcessorFormatter( + foreign_pre_chain=shared_processors, + processors=[ + structlog.stdlib.ProcessorFormatter.remove_processors_meta, + structlog.dev.ConsoleRenderer(event_key="message"), + ], +) + +handler = logging.StreamHandler() +handler.setFormatter(formatter) + +root = logging.getLogger() +root.addHandler(handler) +root.setLevel(log_level) +``` + [extra]: https://timber.io/blog/the-pythonic-guide-to-logging/#adding-context [`structlog`]: https://www.structlog.org/en/stable/ From f00782e0bb668857ac4d0288b18707e750c6d8b9 Mon Sep 17 00:00:00 2001 From: kasteph Date: Mon, 1 Apr 2024 11:22:11 +0200 Subject: [PATCH 002/375] backticks Co-authored-by: Joachim Jablon --- docs/howto/production/logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/production/logging.md b/docs/howto/production/logging.md index 8baa70d2a..8ad0345d2 100644 --- a/docs/howto/production/logging.md +++ b/docs/howto/production/logging.md @@ -29,7 +29,7 @@ If you want a minimal example of a logging setup that displays the extra attributes without using third party logging libraries, look at the [Django demo]. -## structlog +## `structlog` [`structlog`](https://www.structlog.org/en/stable/index.html) needs to be configured in order to have `procrastinate`'s logs be formatted uniformly From d73cbfa29275281d59fb2ddfaf94a91fa11bcb9f Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 7 Apr 2024 13:07:55 +0200 Subject: [PATCH 003/375] Added a note --- docs/howto/production/logging.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/howto/production/logging.md b/docs/howto/production/logging.md index 8ad0345d2..5df46b07f 100644 --- a/docs/howto/production/logging.md +++ b/docs/howto/production/logging.md @@ -7,7 +7,7 @@ messages, they are added as [extra] elements to the logs themselves. This way, you can adapt the logs to whatever format suits your needs the most, using a log filter: -``` +```python import logging class ProcrastinateLogFilter(logging.Filter): @@ -29,6 +29,13 @@ If you want a minimal example of a logging setup that displays the extra attributes without using third party logging libraries, look at the [Django demo]. +:::{note} +When using the `procrastinate` CLI, procrastinate sets up the logs for you, +but the only customization available is `--log-format` and `--log-format-style`. +If you want to customize the log format further, you will need run your own +script that calls procrastinate's app methods. +::: + ## `structlog` [`structlog`](https://www.structlog.org/en/stable/index.html) needs to be @@ -39,7 +46,7 @@ The `structlog` docs has a [how to](https://www.structlog.org/en/stable/standard A minimal configuration would look like: -```py +```python shared_processors = [ structlog.contextvars.merge_contextvars, structlog.stdlib.add_logger_name, @@ -71,7 +78,6 @@ root.addHandler(handler) root.setLevel(log_level) ``` - [extra]: https://timber.io/blog/the-pythonic-guide-to-logging/#adding-context [`structlog`]: https://www.structlog.org/en/stable/ [Django demo]: https://github.com/procrastinate-org/procrastinate/blob/main/procrastinate_demos/demo_django/project/settings.py#L151 From 48a6d59138c7dfa79b812cbd03349d47a5988c0f Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Tue, 16 Jul 2024 18:09:39 +1000 Subject: [PATCH 004/375] refactor worker --- procrastinate/app.py | 38 +- procrastinate/cli.py | 6 +- procrastinate/exceptions.py | 13 - procrastinate/job_processor.py | 225 +++++++++ procrastinate/jobs.py | 10 + procrastinate/utils.py | 172 +------ procrastinate/worker.py | 422 +++++----------- tests/acceptance/test_async.py | 94 +++- tests/acceptance/test_nominal.py | 23 +- tests/integration/test_cli.py | 4 +- tests/integration/test_wait_stop.py | 34 +- tests/integration/test_worker.py | 58 ++- tests/unit/test_app.py | 6 +- tests/unit/test_cli.py | 8 +- tests/unit/test_job_processor.py | 469 ++++++++++++++++++ tests/unit/test_utils.py | 191 ++----- tests/unit/test_worker.py | 742 ++++------------------------ tests/unit/test_worker_sync.py | 27 +- 18 files changed, 1201 insertions(+), 1341 deletions(-) create mode 100644 procrastinate/job_processor.py create mode 100644 tests/unit/test_job_processor.py diff --git a/procrastinate/app.py b/procrastinate/app.py index 13955a957..419520ad8 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -4,7 +4,15 @@ import contextlib import functools import logging -from typing import TYPE_CHECKING, Any, Iterable, Iterator +from typing import ( + TYPE_CHECKING, + Any, + Iterable, + Iterator, + NotRequired, + TypedDict, + Unpack, +) from procrastinate import blueprints, exceptions, jobs, manager, schema, utils from procrastinate import connector as connector_module @@ -15,6 +23,18 @@ logger = logging.getLogger(__name__) +class WorkerOptions(TypedDict): + queues: NotRequired[Iterable[str]] + name: NotRequired[str] + concurrency: NotRequired[int] + wait: NotRequired[bool] + timeout: NotRequired[float] + listen_notify: NotRequired[bool] + delete_jobs: NotRequired[str | jobs.DeleteJobCondition] + additional_context: NotRequired[dict[str, Any]] + install_signal_handlers: NotRequired[bool] + + class App(blueprints.Blueprint): """ The App is the main entry point for procrastinate integration. @@ -52,7 +72,7 @@ def __init__( *, connector: connector_module.BaseConnector, import_paths: Iterable[str] | None = None, - worker_defaults: dict | None = None, + worker_defaults: WorkerOptions | None = None, periodic_defaults: dict | None = None, ): """ @@ -198,10 +218,10 @@ def configure_task( ) raise exceptions.TaskNotFound from exc - def _worker(self, **kwargs) -> worker.Worker: + def _worker(self, **kwargs: Unpack[WorkerOptions]) -> worker.Worker: from procrastinate import worker - final_kwargs = {**self.worker_defaults, **kwargs} + final_kwargs: WorkerOptions = {**self.worker_defaults, **kwargs} return worker.Worker(app=self, **final_kwargs) @@ -217,7 +237,7 @@ def perform_import_paths(self): extra={"action": "imported_tasks", "tasks": list(self.tasks)}, ) - async def run_worker_async(self, **kwargs) -> None: + async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: """ Run a worker. This worker will run in the foreground and execute the jobs in the provided queues. If wait is True, the function will not @@ -268,13 +288,7 @@ async def run_worker_async(self, **kwargs) -> None: """ self.perform_import_paths() worker = self._worker(**kwargs) - task = asyncio.create_task(worker.run()) - try: - await asyncio.shield(task) - except asyncio.CancelledError: - worker.stop() - await task - raise + await worker.run() def run_worker(self, **kwargs) -> None: """ diff --git a/procrastinate/cli.py b/procrastinate/cli.py index a4c33b875..5b664c104 100644 --- a/procrastinate/cli.py +++ b/procrastinate/cli.py @@ -539,7 +539,11 @@ async def worker_( print_stderr( f"Launching a worker on {'all queues' if not queues else ', '.join(queues)}" ) - await app.run_worker_async(**kwargs) + try: + await app.run_worker_async(**kwargs) + except asyncio.CancelledError: + # prevent the CLI from failing and raising an error when the worker is cancelled + pass async def defer( diff --git a/procrastinate/exceptions.py b/procrastinate/exceptions.py index 34b5e36d9..b17dbdd3c 100644 --- a/procrastinate/exceptions.py +++ b/procrastinate/exceptions.py @@ -50,19 +50,6 @@ def __init__(self, scheduled_at: datetime.datetime): super().__init__() -class JobError(ProcrastinateException): - """ - Job ended with an exception. - """ - - def __init__( - self, *args, retry_exception: JobRetry | None = None, critical: bool = False - ): - super().__init__(*args) - self.retry_exception = retry_exception - self.critical = critical - - class JobAborted(ProcrastinateException): """ Job was aborted. diff --git a/procrastinate/job_processor.py b/procrastinate/job_processor.py new file mode 100644 index 000000000..03d0e5139 --- /dev/null +++ b/procrastinate/job_processor.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import asyncio +import functools +import inspect +import logging +import time +from datetime import datetime +from typing import Awaitable, Callable + +from procrastinate import utils +from procrastinate.exceptions import JobAborted, TaskNotFound +from procrastinate.job_context import JobContext +from procrastinate.jobs import DeleteJobCondition, Job, Status +from procrastinate.manager import JobManager +from procrastinate.tasks import Task + + +def _find_task(task_registry: dict[str, Task], task_name: str) -> Task: + try: + return task_registry[task_name] + except KeyError as exc: + raise TaskNotFound from exc + + +class JobProcessor: + def __init__( + self, + *, + task_registry: dict[str, Task], + job_manager: JobManager, + job_queue: asyncio.Queue[Job], + job_semaphore: asyncio.Semaphore, + fetch_job_condition: asyncio.Condition, + worker_id: int, + base_context: JobContext, + logger: logging.Logger = logging.getLogger(__name__), + delete_jobs: str | DeleteJobCondition = DeleteJobCondition.NEVER.value, + ): + self.worker_id = worker_id + self._task_registry = task_registry + self._job_manager = job_manager + self._job_queue = job_queue + self._job_semaphore = job_semaphore + self._fetch_job_condition = fetch_job_condition + self._delete_jobs = ( + DeleteJobCondition(delete_jobs) + if isinstance(delete_jobs, str) + else delete_jobs + ) + self._base_context = base_context.evolve( + worker_id=self.worker_id, + additional_context=base_context.additional_context.copy(), + ) + + self.logger = logger + self.job_context: JobContext | None = None + self._retry_at: datetime | None = None + + def _create_job_context(self, job: Job) -> JobContext: + task = _find_task(self._task_registry, job.task_name) + return self._base_context.evolve(task=task, job=job) + + async def _persist_job_status(self, job: Job, status: Status): + if self._retry_at: + await self._job_manager.retry_job(job=job, retry_at=self._retry_at) + else: + delete_job = { + DeleteJobCondition.ALWAYS: True, + DeleteJobCondition.NEVER: False, + DeleteJobCondition.SUCCESSFUL: status == Status.SUCCEEDED, + }[self._delete_jobs] + await self._job_manager.finish_job( + job=job, status=status, delete_job=delete_job + ) + + self.job_context = None + self._job_queue.task_done() + self.logger.debug( + f"Acknowledged job completion {job.call_string}", + extra=self._base_context.log_extra(action="finish_task", status=status), + ) + + async def run(self): + while True: + job = await self._job_queue.get() + async with self._job_semaphore: + cancelledError: asyncio.CancelledError | None = None + status = Status.FAILED + try: + self.job_context = self._create_job_context(job) + self.logger.debug( + f"L poaded job info, about to start job {job.call_string}", + extra=self.job_context.log_extra(action="loaded_job_info"), + ) + process_job_task = asyncio.create_task(self._process_job()) + + try: + # the job is shielded from cancellation to enable graceful stop + await asyncio.shield(process_job_task) + except asyncio.CancelledError as e: + cancelledError = e + await process_job_task + + status = Status.SUCCEEDED + except TaskNotFound as exc: + self.logger.exception( + f"Task was not found: {exc}", + extra=self._base_context.log_extra( + action="task_not_found", exception=str(exc) + ), + ) + except JobAborted: + status = Status.ABORTED + except Exception: + # exception is already logged by _process_job, carry on + pass + finally: + persist_job_status_task = asyncio.create_task( + self._persist_job_status(job=job, status=status) + ) + try: + # prevent cancellation from stopping persistence of job status + await asyncio.shield(persist_job_status_task) + except asyncio.CancelledError: + await persist_job_status_task + raise + + async with self._fetch_job_condition: + self._fetch_job_condition.notify() + + # reraise the cancelled error we caught earlier + if cancelledError: + raise cancelledError + + async def _process_job(self): + assert self.job_context + assert self.job_context.task + assert self.job_context.job + assert self.job_context.job_result + + task = self.job_context.task + job = self.job_context.job + job_result = self.job_context.job_result + + start_time = time.time() + job_result.start_timestamp = start_time + self.logger.info( + f"Starting job {self.job_context.job.call_string}", + extra=self.job_context.log_extra(action="start_job"), + ) + + job_args = [] + + if task.pass_context: + job_args.append(self.job_context) + + task_result = None + log_title = "Error" + log_action = "job_error" + log_level = logging.ERROR + exc_info: bool | BaseException = False + + await_func: Callable[..., Awaitable] + if inspect.iscoroutinefunction(task.func): + await_func = task + else: + await_func = functools.partial(utils.sync_to_async, task) + + try: + task_result = await await_func(*job_args, **job.task_kwargs) + # In some cases, the task function might be a synchronous function + # that returns an awaitable without actually being a + # coroutinefunction. In that case, in the await above, we haven't + # actually called the task, but merely generated the awaitable that + # implements the task. In that case, we want to wait this awaitable. + # It's easy enough to be in that situation that the best course of + # action is probably to await the awaitable. + # It's not even sure it's worth emitting a warning + if inspect.isawaitable(task_result): + task_result = await task_result + except JobAborted as e: + task_result = None + log_title = "Aborted" + log_action = "job_aborted" + log_level = logging.INFO + exc_info = e + raise + except BaseException as e: + task_result = None + log_title = "Error" + log_action = "job_error" + log_level = logging.ERROR + exc_info = e + + job_retry = task.get_retry_exception(exception=e, job=job) + if job_retry: + self._retry_at = job_retry.scheduled_at + log_title = "Error, to retry" + log_action = "job_error_retry" + log_level = logging.INFO + else: + self._retry_at = None + raise + else: + log_title = "Success" + log_action = "job_success" + log_level = logging.INFO + exc_info = False + self._retry_at = None + finally: + end_time = time.time() + duration = end_time - start_time + job_result.end_timestamp = end_time + job_result.result = task_result + + extra = self.job_context.log_extra(action=log_action) + + text = ( + f"Job {job.call_string} ended with status: {log_title}, " + f"lasted {duration:.3f} s" + ) + if task_result: + text += f" - Result: {task_result}"[:250] + self.logger.log(log_level, text, extra=extra, exc_info=exc_info) diff --git a/procrastinate/jobs.py b/procrastinate/jobs.py index bcd55a6d5..536ee3e34 100644 --- a/procrastinate/jobs.py +++ b/procrastinate/jobs.py @@ -43,6 +43,16 @@ class Status(Enum): ABORTED = "aborted" #: The job was aborted +class DeleteJobCondition(Enum): + """ + An enumeration with all the possible conditions to delete a job + """ + + NEVER = "never" #: Keep jobs in database after completion + SUCCESSFUL = "successful" #: Delete only successful jobs + ALWAYS = "always" #: Always delete jobs at completion + + @attr.dataclass(frozen=True, kw_only=True) class Job: """ diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 4bd448130..a1dcefee3 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -15,13 +15,11 @@ AsyncIterator, Awaitable, Callable, - Coroutine, Generic, Iterable, TypeVar, ) -import attr import dateutil.parser from asgiref import sync @@ -205,162 +203,36 @@ async def _inner_coro() -> U: return _inner_coro().__await__() -class EndMain(Exception): - pass - - -@attr.dataclass() -class ExceptionRecord: - task: asyncio.Task - exc: Exception - - -async def run_tasks( - main_coros: Iterable[Coroutine], - side_coros: Iterable[Coroutine] | None = None, - graceful_stop_callback: Callable[[], Any] | None = None, -): +async def cancel_and_capture_errors(tasks: list[asyncio.Task]): """ - Run multiple coroutines in parallel: the main coroutines and the side - coroutines. Side coroutines are expected to run until they get cancelled. - Main corountines are expected to return at some point. By default, this - function will return None, but on certain circumstances, (see below) it can - raise a `RunTaskError`. A callback `graceful_stop_callback` will be called - if provided to ask the main coroutines to gracefully stop in case either - one of them or one of the side coroutines raise. - - - If all coroutines from main_coros return and there is no exception in the - coroutines from either `main_coros` or `side_coros`: - - coroutines from `side_coros` are cancelled and awaited - - the function return None - - - If any corountine from `main_coros` or `side_coros` raises an exception: - - `graceful_stop_callback` is called (the idea is that it should ask - coroutines from `main_coros` to exit gracefully) - - the function then wait for main_coros to finish, registering any - additional exception - - coroutines from `side_coros` are cancelled and awaited, registering any - additional exception - - all exceptions from coroutines in both `main_coros` and `side_coros` - are logged - - the function raises `RunTaskError` - - It's not expected that coroutines from `side_coros` return. If this - happens, the function will not react in a specific way. - - When a `RunTaskError` is raised because of one or more underlying - exceptions, one exception is the `__cause__` (the first main or side - coroutine that fails in the input iterables order, which will probably not - the chronologically the first one to be raised). All exceptions are logged. + Cancel all tasks and capture any error returned by any of those tasks (except the CancellationError itself) """ - # Ensure all passed coros are futures (in our case, Tasks). This means that - # all the coroutines start executing now. - # `name` argument to create_task only exist on python 3.8+ - main_tasks = [asyncio.create_task(coro, name=coro.__name__) for coro in main_coros] - side_tasks = [ - asyncio.create_task(coro, name=coro.__name__) for coro in side_coros or [] - ] - for task in main_tasks + side_tasks: - name = task.get_name() - logger.debug( - f"Started {name}", - extra={ - "action": f"{name}_start", - }, - ) - # Note that asyncio.gather() has 2 modes of execution: - # - asyncio.gather(*aws) - # Interrupts the gather at the first exception, and raises this - # exception. Otherwise, return a list containing return values for all - # coroutines - # - asyncio.gather(*aws, return_exceptions=True) - # Run every corouting until the end, return a list of either return - # values or raised exceptions (mixed). - - # The _main function will always raise: either an exception if one happens - # in the main tasks, or EndMain if every coroutine returned - async def _main(): - await asyncio.gather(*main_tasks) - raise EndMain - - exception_records: list[ExceptionRecord] = [] - try: - # side_tasks supposedly never finish, and _main always raises. - # Consequently, it's theoretically impossible to leave this try block - # without going through one of the except branches. - await asyncio.gather(_main(), *side_tasks) - except EndMain: - pass - except Exception as exc: - logger.error( - "Main coroutine error, initiating remaining coroutines stop. " - f"Cause: {exc!r}", - extra={ - "action": "run_tasks_stop_requested", - }, - ) - if graceful_stop_callback: - graceful_stop_callback() - - # Even if we asked the main tasks to stop, we still need to wait for - # them to actually stop. This may take some time. At this point, any - # additional exception will be registered but will not impact execution - # flow. - results = await asyncio.gather(*main_tasks, return_exceptions=True) - for task, result in zip(main_tasks, results): - if isinstance(result, Exception): - exception_records.append( - ExceptionRecord( - task=task, - exc=result, - ) - ) - else: - name = task.get_name() - logger.debug( - f"{name} finished execution", - extra={ - "action": f"{name}_stop", - }, - ) - - for task in side_tasks: - task.cancel() - try: - # task.cancel() says that the next time a task is executed, it will - # raise, but we need to give control back to the task for it to - # actually recieve the exception. - await task - except asyncio.CancelledError: - name = task.get_name() - logger.debug( - f"Stopped {name}", - extra={ - "action": f"{name}_stop", - }, - ) - except Exception as exc: - exception_records.append( - ExceptionRecord( - task=task, - exc=exc, - ) - ) - - for exception_record in exception_records: - name = exception_record.task.get_name() - message = f"{name} error: {exception_record.exc!r}" - action = f"{name}_error" + def log_task_exception(task: asyncio.Task, error: BaseException): logger.exception( - message, + f"{task.get_name()} error: {error!r}", + exc_info=error, extra={ - "action": action, + "action": f"{task.get_name()}_error", }, ) - if exception_records: - raise exceptions.RunTaskError from exception_records[0].exc + tasks_aggregate = asyncio.gather(*tasks, return_exceptions=True) + tasks_aggregate.cancel() + try: + results = await tasks_aggregate + for task, result in zip(tasks, results): + if not isinstance(result, BaseException): + continue + log_task_exception(task, error=result) + except asyncio.CancelledError: + # tasks have been cancelled. Log any exception from already completed tasks + for task in tasks: + if not task.done() or task.cancelled(): + continue + error = task.exception() + if error: + log_task_exception(task, error=error) def add_namespace(name: str, namespace: str) -> str: diff --git a/procrastinate/worker.py b/procrastinate/worker.py index f0374faaa..bf8af431e 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -2,51 +2,35 @@ import asyncio import contextlib -import functools -import inspect import logging import time -from enum import Enum -from typing import Any, Awaitable, Callable, Iterable +from typing import Any, Iterable -from procrastinate import ( - app, - exceptions, - job_context, - jobs, - periodic, - signals, - tasks, - utils, -) +from procrastinate import signals, utils +from procrastinate.app import App +from procrastinate.exceptions import TaskNotFound +from procrastinate.job_context import JobContext +from procrastinate.job_processor import JobProcessor +from procrastinate.jobs import DeleteJobCondition, Job +from procrastinate.periodic import PeriodicDeferrer +from procrastinate.tasks import Task logger = logging.getLogger(__name__) - WORKER_NAME = "worker" -WORKER_TIMEOUT = 5.0 # seconds -WORKER_CONCURRENCY = 1 # parallel task(s) - - -class DeleteJobCondition(Enum): - """ - An enumeration with all the possible conditions to delete a job - """ - - NEVER = "never" #: Keep jobs in database after completion - SUCCESSFUL = "successful" #: Delete only successful jobs - ALWAYS = "always" #: Always delete jobs at completion +WORKER_CONCURRENCY = 1 # maximum number of parallel jobs +POLLING_INTERVAL = 5.0 # seconds class Worker: def __init__( self, - app: app.App, + app: App, queues: Iterable[str] | None = None, - name: str | None = None, + name: str | None = WORKER_NAME, concurrency: int = WORKER_CONCURRENCY, wait: bool = True, - timeout: float = WORKER_TIMEOUT, + timeout: float = POLLING_INTERVAL, listen_notify: bool = True, delete_jobs: str | DeleteJobCondition = DeleteJobCondition.NEVER.value, additional_context: dict[str, Any] | None = None, @@ -54,76 +38,54 @@ def __init__( ): self.app = app self.queues = queues - self.worker_name: str = name or WORKER_NAME + self.worker_name = name self.concurrency = concurrency - - self.timeout = timeout self.wait = wait + self.polling_interval = timeout self.listen_notify = listen_notify - self.delete_jobs = ( - DeleteJobCondition(delete_jobs) - if isinstance(delete_jobs, str) - else delete_jobs - ) - - self.job_manager = self.app.job_manager + self.delete_jobs = delete_jobs + self.additional_context = additional_context self.install_signal_handlers = install_signal_handlers - if name: - self.logger = logger.getChild(name) + if self.worker_name: + self.logger = logger.getChild(self.worker_name) else: self.logger = logger - # Handling the info about the currently running task. - self.base_context: job_context.JobContext = job_context.JobContext( + self.base_context = JobContext( app=app, worker_name=self.worker_name, worker_queues=self.queues, additional_context=additional_context.copy() if additional_context else {}, ) - self.current_contexts: dict[int, job_context.JobContext] = {} - self.stop_requested = False - self.notify_event: asyncio.Event | None = None - - def context_for_worker( - self, worker_id: int, reset=False, **kwargs - ) -> job_context.JobContext: - """ - Retrieves the context for sub-sworker ``worker_id``. If not found, or ``reset`` - is True, context is recreated from ``self.base_context``. Additionnal parameters - are used to update the context. The resulting context is kept and will be - returned for later calls. - """ - if reset or worker_id not in self.current_contexts: - context = self.base_context - kwargs["worker_id"] = worker_id - kwargs["additional_context"] = self.base_context.additional_context.copy() - else: - context = self.current_contexts[worker_id] - - if kwargs: - context = context.evolve(**kwargs) - self.current_contexts[worker_id] = context - return context + self._run_task: asyncio.Task | None = None - async def listener(self): - assert self.notify_event - return await self.job_manager.listen_for_jobs( - event=self.notify_event, - queues=self.queues, + def stop(self): + self.logger.info( + "Stop requested", + extra=self.base_context.log_extra(action="stopping_worker"), ) + if self._run_task: + self._run_task.cancel() + async def periodic_deferrer(self): - deferrer = periodic.PeriodicDeferrer( + deferrer = PeriodicDeferrer( registry=self.app.periodic_registry, **self.app.periodic_defaults, ) return await deferrer.worker() - async def run(self) -> None: - self.notify_event = asyncio.Event() - self.stop_requested = False + def find_task(self, task_name: str) -> Task: + try: + return self.app.tasks[task_name] + except KeyError as exc: + raise TaskNotFound from exc + + async def run(self): + self._run_task = asyncio.current_task() + notify_event = asyncio.Event() self.logger.info( f"Starting worker on {self.base_context.queues_display}", @@ -131,226 +93,108 @@ async def run(self) -> None: action="start_worker", queues=self.queues ), ) - context = contextlib.nullcontext() - if self.install_signal_handlers: - context = signals.on_stop(self.stop) - - with context: - side_coros = [self.periodic_deferrer()] - if self.wait and self.listen_notify: - side_coros.append(self.listener()) - - await utils.run_tasks( - main_coros=( - self.single_worker(worker_id=worker_id) - for worker_id in range(self.concurrency) - ), - side_coros=side_coros, - graceful_stop_callback=self.stop, - ) - - self.logger.info( - f"Stopped worker on {self.base_context.queues_display}", - extra=self.base_context.log_extra(action="stop_worker", queues=self.queues), - ) - self.notify_event = None - - async def single_worker(self, worker_id: int): - current_timeout = self.timeout * (worker_id + 1) - while not self.stop_requested: - job = await self.job_manager.fetch_job(self.queues) - if job: - await self.process_job(job=job, worker_id=worker_id) - else: - if not self.wait or self.stop_requested: - break - await self.wait_for_job(timeout=current_timeout) - current_timeout = self.timeout * self.concurrency - - async def wait_for_job(self, timeout: float): - assert self.notify_event - self.logger.debug( - f"Waiting for new jobs on {self.base_context.queues_display}", - extra=self.base_context.log_extra( - action="waiting_for_jobs", queues=self.queues - ), - ) - self.notify_event.clear() - try: - await asyncio.wait_for(self.notify_event.wait(), timeout=timeout) - except asyncio.TimeoutError: - pass - else: - self.notify_event.clear() - - async def process_job(self, job: jobs.Job, worker_id: int = 0) -> None: - context = self.context_for_worker(worker_id=worker_id, job=job) - - self.logger.debug( - f"Loaded job info, about to start job {job.call_string}", - extra=context.log_extra(action="loaded_job_info"), - ) - - status, retry_at = None, None - try: - await self.run_job(job=job, worker_id=worker_id) - status = jobs.Status.SUCCEEDED - except exceptions.JobAborted: - status = jobs.Status.ABORTED - except exceptions.JobError as e: - status = jobs.Status.FAILED - if e.retry_exception: - retry_at = e.retry_exception.scheduled_at - if e.critical and e.__cause__: - raise e.__cause__ - - except exceptions.TaskNotFound as exc: - status = jobs.Status.FAILED - self.logger.exception( - f"Task was not found: {exc}", - extra=context.log_extra(action="task_not_found", exception=str(exc)), + job_queue = asyncio.Queue[Job](self.concurrency) + job_semaphore = asyncio.Semaphore(self.concurrency) + fetch_job_condition = asyncio.Condition() + job_processors = [ + JobProcessor( + task_registry=self.app.tasks, + base_context=self.base_context, + delete_jobs=self.delete_jobs, + job_manager=self.app.job_manager, + job_queue=job_queue, + job_semaphore=job_semaphore, + fetch_job_condition=fetch_job_condition, + worker_id=worker_id, + logger=self.logger, ) - finally: - if retry_at: - await self.job_manager.retry_job(job=job, retry_at=retry_at) - else: - assert status is not None - - delete_job = { - DeleteJobCondition.ALWAYS: True, - DeleteJobCondition.NEVER: False, - DeleteJobCondition.SUCCESSFUL: status == jobs.Status.SUCCEEDED, - }[self.delete_jobs] - - await self.job_manager.finish_job( - job=job, status=status, delete_job=delete_job - ) + for worker_id in range(self.concurrency) + ] - self.logger.debug( - f"Acknowledged job completion {job.call_string}", - extra=context.log_extra(action="finish_task", status=status), + job_processors_task = asyncio.gather(*(p.run() for p in job_processors)) + side_tasks = [asyncio.create_task(self.periodic_deferrer())] + if self.wait and self.listen_notify: + listener_coro = self.app.job_manager.listen_for_jobs( + event=notify_event, + queues=self.queues, ) - # Remove job information from the current context - self.context_for_worker(worker_id=worker_id, reset=True) + side_tasks.append(asyncio.create_task(listener_coro, name="listener")) - def find_task(self, task_name: str) -> tasks.Task: try: - return self.app.tasks[task_name] - except KeyError as exc: - raise exceptions.TaskNotFound from exc - - async def run_job(self, job: jobs.Job, worker_id: int) -> None: - task_name = job.task_name - - task = self.find_task(task_name=task_name) + context = contextlib.nullcontext() + if self.install_signal_handlers: + context = signals.on_stop(self.stop) + with context: + """Processes jobs until cancelled or until there is no more available job (wait=False)""" + while True: + out_of_job = None + while not out_of_job: + # only fetch job when the queue is not full and not all processors are busy + async with fetch_job_condition: + await fetch_job_condition.wait_for( + lambda: not job_queue.full() + and not job_semaphore.locked() + ) + job = await self.app.job_manager.fetch_job(queues=self.queues) + if job: + await job_queue.put(job) + else: + out_of_job = True + if out_of_job: + if not self.wait: + self.logger.info( + "No job found. Stopping worker because wait=False", + extra=self.base_context.log_extra( + action="stop_worker", queues=self.queues + ), + ) + # no more job to fetch and asked not to wait, exiting the loop + break + try: + # wait until notified a new job is available or until polling interval + notify_event.clear() + await asyncio.wait_for( + notify_event.wait(), timeout=self.polling_interval + ) + + except asyncio.TimeoutError: + # catch asyncio.TimeoutError and not TimeoutError as long as Python 3.10 and under are supported + + # polling interval has passed, resume loop and attempt to fetch a job + pass - context = self.context_for_worker(worker_id=worker_id, task=task) - - start_time = time.time() - context.job_result.start_timestamp = start_time - - self.logger.info( - f"Starting job {job.call_string}", - extra=context.log_extra(action="start_job"), - ) - job_args = [] - if task.pass_context: - job_args.append(context) - - # Initialise logging variables - task_result = None - log_title = "Error" - log_action = "job_error" - log_level = logging.ERROR - exc_info: bool | BaseException = False - - await_func: Callable[..., Awaitable] - if inspect.iscoroutinefunction(task.func): - await_func = task - else: - await_func = functools.partial(utils.sync_to_async, task) - - try: - task_result = await await_func(*job_args, **job.task_kwargs) - # In some cases, the task function might be a synchronous function - # that returns an awaitable without actually being a - # coroutinefunction. In that case, in the await above, we haven't - # actually called the task, but merely generated the awaitable that - # implements the task. In that case, we want to wait this awaitable. - # It's easy enough to be in that situation that the best course of - # action is probably to await the awaitable. - # It's not even sure it's worth emitting a warning - if inspect.isawaitable(task_result): - task_result = await task_result - - except exceptions.JobAborted as e: - task_result = None - log_title = "Aborted" - log_action = "job_aborted" - log_level = logging.INFO - exc_info = e - raise - - except BaseException as e: - task_result = None - log_title = "Error" - log_action = "job_error" - log_level = logging.ERROR - exc_info = e - critical = not isinstance(e, Exception) - - retry_exception = task.get_retry_exception(exception=e, job=job) - if retry_exception: - log_title = "Error, to retry" - log_action = "job_error_retry" - log_level = logging.INFO - raise exceptions.JobError( - retry_exception=retry_exception, critical=critical - ) from e - - else: - log_title = "Success" - log_action = "job_success" - log_level = logging.INFO - exc_info = False finally: - end_time = time.time() - duration = end_time - start_time - context.job_result.end_timestamp = end_time - context.job_result.result = task_result - - extra = context.log_extra(action=log_action) + await utils.cancel_and_capture_errors(side_tasks) + + pending_job_contexts = [ + processor.job_context + for processor in job_processors + if processor.job_context + ] + + now = time.time() + for context in pending_job_contexts: + self.logger.info( + "Waiting for job to finish: " + + context.job_description(current_timestamp=now), + extra=context.log_extra(action="ending_job"), + ) - text = ( - f"Job {job.call_string} ended with status: {log_title}, " - f"lasted {duration:.3f} s" + await job_queue.join() + job_processors_task.cancel() + job_processors_task.add_done_callback( + lambda fut: self.logger.info( + f"Stopped worker on {self.base_context.queues_display}", + extra=self.base_context.log_extra( + action="stop_worker", queues=self.queues + ), + ) ) - if task_result: - text += f" - Result: {task_result}"[:250] - self.logger.log(log_level, text, extra=extra, exc_info=exc_info) - - def stop(self): - # Ensure worker will stop after finishing their task - self.stop_requested = True - # Ensure workers currently waiting are awakened - if self.notify_event: - self.notify_event.set() - - # Logging - self.logger.info( - "Stop requested", - extra=self.base_context.log_extra(action="stopping_worker"), - ) - - contexts = [ - context for context in self.current_contexts.values() if context.job - ] - now = time.time() - for context in contexts: - self.logger.info( - "Waiting for job to finish: " - + context.job_description(current_timestamp=now), - extra=context.log_extra(action="ending_job"), - ) + try: + await job_processors_task + except asyncio.CancelledError: + # if we didn't initiate the cancellation ourselves, bubble up the cancelled error + if self._run_task and self._run_task.cancelled(): + raise diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index ba6618e10..8087c7e53 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -23,7 +23,7 @@ async def async_app(request, psycopg_connector, connection_params): yield app -async def test_defer(async_app): +async def test_defer(async_app: app_module.App): sum_results = [] product_results = [] @@ -46,7 +46,7 @@ async def product_task(a, b): assert product_results == [12] -async def test_cancel(async_app): +async def test_cancel(async_app: app_module.App): sum_results = [] @async_app.task(queue="default", name="sum_task") @@ -62,7 +62,7 @@ async def sum_task(a, b): status = await async_app.job_manager.get_job_status_async(job_id) assert status == Status.CANCELLED - jobs = await async_app.job_manager.list_jobs_async() + jobs = list(await async_app.job_manager.list_jobs_async()) assert len(jobs) == 2 await async_app.run_worker_async(queues=["default"], wait=False) @@ -70,7 +70,7 @@ async def sum_task(a, b): assert sum_results == [7] -async def test_cancel_with_delete(async_app): +async def test_cancel_with_delete(async_app: app_module.App): sum_results = [] @async_app.task(queue="default", name="sum_task") @@ -83,7 +83,7 @@ async def sum_task(a, b): result = await async_app.job_manager.cancel_job_by_id_async(job_id, delete_job=True) assert result is True - jobs = await async_app.job_manager.list_jobs_async() + jobs = list(await async_app.job_manager.list_jobs_async()) assert len(jobs) == 1 await async_app.run_worker_async(queues=["default"], wait=False) @@ -91,7 +91,7 @@ async def sum_task(a, b): assert sum_results == [7] -async def test_no_job_to_cancel_found(async_app): +async def test_no_job_to_cancel_found(async_app: app_module.App): @async_app.task(queue="default", name="example_task") def example_task(): pass @@ -104,22 +104,22 @@ def example_task(): status = await async_app.job_manager.get_job_status_async(job_id) assert status == Status.TODO - jobs = await async_app.job_manager.list_jobs_async() + jobs = list(await async_app.job_manager.list_jobs_async()) assert len(jobs) == 1 -async def test_abort(async_app): +async def test_abort(async_app: app_module.App): @async_app.task(queue="default", name="task1", pass_context=True) async def task1(context): while True: - await asyncio.sleep(0.1) + await asyncio.sleep(0.02) if await context.should_abort_async(): raise JobAborted @async_app.task(queue="default", name="task2", pass_context=True) def task2(context): while True: - time.sleep(0.1) + time.sleep(0.02) if context.should_abort(): raise JobAborted @@ -145,3 +145,77 @@ def task2(context): status = await async_app.job_manager.get_job_status_async(job2_id) assert status == Status.ABORTED + + +async def test_concurrency(async_app: app_module.App): + results = [] + + @async_app.task(queue="default", name="appender") + async def appender(a: int): + await asyncio.sleep(0.1) + results.append(a) + + deferred_tasks = [appender.defer_async(a=i) for i in range(1, 101)] + for task in deferred_tasks: + await task + + # with 20 concurrent workers, 100 tasks should take about 100/20 x 0.1 = 0.5s + # if there is no concurrency, it will take well over 2 seconds and fail + + start_time = time.time() + try: + await asyncio.wait_for( + async_app.run_worker_async(concurrency=20, wait=False), timeout=2 + ) + except asyncio.TimeoutError: + pytest.fail( + "Failed to process all jobs within 2 seconds. Is the concurrency respected?" + ) + duration = time.time() - start_time + + assert ( + duration >= 0.5 + ), "processing jobs faster than expected. Is the concurrency respected?" + + assert len(results) == 100, "Unexpected number of job executions" + + +async def test_polling(async_app: app_module.App): + @async_app.task(queue="default", name="sum") + async def sum(a: int, b: int): + return a + b + + # rely on polling to fetch new jobs + worker_task = asyncio.create_task( + async_app.run_worker_async( + concurrency=1, wait=True, listen_notify=False, timeout=0.3 + ) + ) + + # long enough for worker to wait until next polling + await asyncio.sleep(0.1) + + job_id = await sum.defer_async(a=5, b=4) + + await asyncio.sleep(0.1) + + job_status = await async_app.job_manager.get_job_status_async(job_id=job_id) + + assert job_status == Status.TODO, "Job fetched faster than expected." + + await asyncio.sleep(0.2) + + job_status = await async_app.job_manager.get_job_status_async(job_id=job_id) + + assert job_status == Status.SUCCEEDED, "Job should have been fetched and processed." + + try: + worker_task.cancel() + await asyncio.wait_for( + worker_task, + timeout=0.5, + ) + except asyncio.CancelledError: + pass + except asyncio.TimeoutError: + pytest.fail("Failed to stop worker") diff --git a/tests/acceptance/test_nominal.py b/tests/acceptance/test_nominal.py index 4766edb06..503d3b4ef 100644 --- a/tests/acceptance/test_nominal.py +++ b/tests/acceptance/test_nominal.py @@ -3,12 +3,25 @@ import signal import subprocess import time +from typing import Protocol, cast import pytest +class RunningWorker(Protocol): + def __call__( + self, *args: str, name: str = "worker", app: str = "app" + ) -> subprocess.Popen[str]: ... + + +class Worker(Protocol): + def __call__( + self, *args: str, sleep: int = 1, app: str = "app" + ) -> tuple[str, str]: ... + + @pytest.fixture -def worker(running_worker): +def worker(running_worker) -> Worker: def func(*queues, sleep=1, app="app"): process = running_worker(*queues, app=app) time.sleep(sleep) @@ -19,7 +32,7 @@ def func(*queues, sleep=1, app="app"): @pytest.fixture -def running_worker(process_env): +def running_worker(process_env) -> RunningWorker: def func(*queues, name="worker", app="app"): return subprocess.Popen( [ @@ -154,10 +167,10 @@ def test_lock(defer, running_worker): lines = dict( line.split()[1:] for line in stdout.splitlines() if line.startswith("->") ) - lines = sorted(lines, key=lines.get) + sorted_lines = sorted(lines, key=lambda x: lines[x]) # Check that it all happened in order - assert lines == ["before-1", "after-1", "before-2", "after-2"] + assert sorted_lines == ["before-1", "after-1", "before-2", "after-2"] # If locks didnt work, we would have # ["before-1", "before-2", "after-2", "after-1"] @@ -198,7 +211,7 @@ def test_periodic_deferrer(worker): # We're making a dict from the output results = dict( - (int(a) for a in e[5:].split()) + cast(tuple[int, int], (int(a) for a in e[5:].split())) for e in stdout.splitlines() if e.startswith("tick ") ) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index b54817382..58ae41dbe 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -8,7 +8,7 @@ import pytest -from procrastinate import __version__, cli, exceptions, worker +from procrastinate import __version__, cli, exceptions, jobs @dataclasses.dataclass @@ -82,7 +82,7 @@ async def test_worker(entrypoint, cli_app, mocker): timeout=8.3, wait=False, listen_notify=False, - delete_jobs=worker.DeleteJobCondition.ALWAYS, + delete_jobs=jobs.DeleteJobCondition.ALWAYS, ) diff --git a/tests/integration/test_wait_stop.py b/tests/integration/test_wait_stop.py index d1041a700..3e947966b 100644 --- a/tests/integration/test_wait_stop.py +++ b/tests/integration/test_wait_stop.py @@ -10,19 +10,18 @@ async def test_wait_for_activity(psycopg_connector): """ - Testing that a new event interrupts the wait + Testing that the work can be cancelled """ pg_app = app.App(connector=psycopg_connector) worker = worker_module.Worker(app=pg_app, timeout=2) - worker.notify_event = asyncio.Event() - task = asyncio.ensure_future(worker.single_worker(worker_id=0)) + task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting - worker.stop_requested = True - worker.notify_event.set() + task.cancel() try: - await asyncio.wait_for(task, timeout=0.2) + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, timeout=0.2) except asyncio.TimeoutError: pytest.fail("Failed to stop worker within .2s") @@ -33,17 +32,10 @@ async def test_wait_for_activity_timeout(psycopg_connector): """ pg_app = app.App(connector=psycopg_connector) worker = worker_module.Worker(app=pg_app, timeout=2) - worker.notify_event = asyncio.Event() - task = asyncio.ensure_future(worker.single_worker(worker_id=0)) - try: - await asyncio.sleep(0.2) # should be enough so that we're waiting - - worker.stop_requested = True - - with pytest.raises(asyncio.TimeoutError): - await asyncio.wait_for(task, timeout=0.2) - finally: - worker.notify_event.set() + task = asyncio.ensure_future(worker.run()) + await asyncio.sleep(0.2) # should be enough so that we're waiting + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(task, timeout=0.2) async def test_wait_for_activity_stop_from_signal(psycopg_connector, kill_own_pid): @@ -58,14 +50,15 @@ async def test_wait_for_activity_stop_from_signal(psycopg_connector, kill_own_pi kill_own_pid() try: - await asyncio.wait_for(task, timeout=0.2) + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, timeout=0.2) except asyncio.TimeoutError: pytest.fail("Failed to stop worker within .2s") async def test_wait_for_activity_stop(psycopg_connector): """ - Testing than calling job_manager.stop() interrupts the wait + Testing than calling worker.stop() interrupts the wait """ pg_app = app.App(connector=psycopg_connector) worker = worker_module.Worker(app=pg_app, timeout=2) @@ -75,6 +68,7 @@ async def test_wait_for_activity_stop(psycopg_connector): worker.stop() try: - await asyncio.wait_for(task, timeout=0.2) + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, timeout=0.2) except asyncio.TimeoutError: pytest.fail("Failed to stop worker within .2s") diff --git a/tests/integration/test_worker.py b/tests/integration/test_worker.py index a55e23f33..849f3860f 100644 --- a/tests/integration/test_worker.py +++ b/tests/integration/test_worker.py @@ -3,23 +3,40 @@ import asyncio import contextlib import signal +from typing import TYPE_CHECKING, cast import pytest from procrastinate import worker +from procrastinate.testing import InMemoryConnector + +if TYPE_CHECKING: + from procrastinate import App + + +# how long to wait before considering the test a fail +timeout = 0.05 + + +async def _wait_on_cancelled(task: asyncio.Task, timeout: float): + try: + await asyncio.wait_for(task, timeout=timeout) + except asyncio.CancelledError: + pass + except asyncio.TimeoutError: + pytest.fail("Failed to launch task within f{timeout}s") @contextlib.asynccontextmanager -async def running_worker(app): +async def running_worker(app: App): running_worker = worker.Worker(app=app, queues=["some_queue"]) task = asyncio.ensure_future(running_worker.run()) - running_worker.task = task - yield running_worker + yield running_worker, task running_worker.stop() - await asyncio.wait_for(task, timeout=0.5) + await _wait_on_cancelled(task, timeout=timeout) -async def test_run(app, caplog): +async def test_run(app: App, caplog): caplog.set_level("DEBUG") done = asyncio.Event() @@ -32,11 +49,12 @@ def t(): await t.defer_async() try: - await asyncio.wait_for(done.wait(), timeout=0.5) + await asyncio.wait_for(done.wait(), timeout=timeout) except asyncio.TimeoutError: - pytest.fail("Failed to launch task withing .5s") + pytest.fail(f"Failed to launch task withing {timeout}s") - assert [q[0] for q in app.connector.queries] == [ + connector = cast(InMemoryConnector, app.connector) + assert [q[0] for q in connector.queries] == [ "defer_job", "fetch_job", "finish_job", @@ -54,10 +72,10 @@ def t(): } <= logs -async def test_run_log_current_job_when_stopping(app, caplog): +async def test_run_log_current_job_when_stopping(app: App, caplog): caplog.set_level("DEBUG") - async with running_worker(app) as worker: + async with running_worker(app) as (worker, worker_task): @app.task(queue="some_queue") async def t(): @@ -65,10 +83,11 @@ async def t(): await t.defer_async() - try: - await asyncio.wait_for(worker.task, timeout=0.5) - except asyncio.TimeoutError: - pytest.fail("Failed to launch task within .5s") + with pytest.raises(asyncio.CancelledError): + try: + await asyncio.wait_for(worker_task, timeout=timeout) + except asyncio.TimeoutError: + pytest.fail("Failed to launch task within f{timeout}s") # We want to make sure that the log that names the current running task fired. logs = " ".join(r.message for r in caplog.records) @@ -79,18 +98,19 @@ async def t(): ) -async def test_run_no_listen_notify(app): +async def test_run_no_listen_notify(app: App): running_worker = worker.Worker(app=app, queues=["some_queue"], listen_notify=False) task = asyncio.ensure_future(running_worker.run()) try: await asyncio.sleep(0.01) - assert app.connector.notify_event is None + connector = cast(InMemoryConnector, app.connector) + assert connector.notify_event is None finally: running_worker.stop() - await asyncio.wait_for(task, timeout=0.5) + await _wait_on_cancelled(task, timeout=timeout) -async def test_run_no_signal_handlers(app, kill_own_pid): +async def test_run_no_signal_handlers(app: App, kill_own_pid): running_worker = worker.Worker( app=app, queues=["some_queue"], install_signal_handlers=False ) @@ -103,4 +123,4 @@ async def test_run_no_signal_handlers(app, kill_own_pid): kill_own_pid(signal=signal.SIGINT) finally: running_worker.stop() - await asyncio.wait_for(task, timeout=0.5) + await _wait_on_cancelled(task, timeout) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 8118abd02..419b2a623 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -28,7 +28,7 @@ def test_app_task_dont_read_function_attributes(app: app_module.App): def wrapped(): return "foo" - wrapped.pass_context = True + wrapped.pass_context = True # type: ignore task = app.task(wrapped) assert task.pass_context is False @@ -47,7 +47,7 @@ def test_app_register(app: app_module.App): assert app.tasks["bla"] == task -def test_app_worker(app, mocker): +def test_app_worker(app: app_module.App, mocker): Worker = mocker.patch("procrastinate.worker.Worker") app.worker_defaults["timeout"] = 12 @@ -253,7 +253,7 @@ def test_check_stack_is_called(mocker, connector): called = [] class MyApp(app_module.App): - def _check_stack(self): + def _check_stack(self): # pyright: ignore reportIncompatibleMethodOverride called.append(True) return "foo" diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 296df7f4a..038910f3b 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -8,7 +8,7 @@ import pytest -from procrastinate import app, cli, connector, exceptions, worker +from procrastinate import app, cli, connector, exceptions, jobs from procrastinate.connector import BaseConnector @@ -53,7 +53,7 @@ def test_main(mocker): ), ( ["worker", "--delete-jobs", "never"], - {"command": "worker", "delete_jobs": worker.DeleteJobCondition.NEVER}, + {"command": "worker", "delete_jobs": jobs.DeleteJobCondition.NEVER}, ), (["defer", "x"], {"command": "defer", "task": "x"}), (["defer", "x", "{}"], {"command": "defer", "task": "x", "json_args": "{}"}), @@ -281,7 +281,7 @@ def get_sync_connector(self) -> BaseConnector: cli.load_app("foobar") -async def test_shell_single_command(app, capsys): +async def test_shell_single_command(app: app.App, capsys): @app.task(name="foobar") def mytask(a): pass @@ -295,7 +295,7 @@ def mytask(a): assert out == "#1 foobar on default - [todo]\n" -async def test_shell_interactive_command(app, capsys, mocker): +async def test_shell_interactive_command(app: app.App, capsys, mocker): @app.task(name="foobar") def mytask(a): pass diff --git a/tests/unit/test_job_processor.py b/tests/unit/test_job_processor.py new file mode 100644 index 000000000..901cb4f0a --- /dev/null +++ b/tests/unit/test_job_processor.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +import asyncio +from typing import cast + +import pytest + +from procrastinate.app import App +from procrastinate.exceptions import JobAborted +from procrastinate.job_context import JobContext +from procrastinate.job_processor import JobProcessor +from procrastinate.jobs import DeleteJobCondition, Job, Status +from procrastinate.testing import InMemoryConnector + + +class CustomCriticalError(BaseException): + pass + + +@pytest.fixture +def worker_name() -> str: + return "worker" + + +@pytest.fixture +def job_queue(): + return asyncio.Queue[Job](2) + + +@pytest.fixture +def base_context(app, worker_name): + return JobContext( + app=app, worker_name=worker_name, additional_context={"foo": "bar"} + ) + + +@pytest.fixture +def job_semaphore(): + return asyncio.Semaphore(1) + + +@pytest.fixture +def job_processor(request, app, base_context, job_queue, job_semaphore): + param = getattr(request, "param", None) + + delete_jobs = cast(str, param["delete_jobs"]) if param else None + return JobProcessor( + task_registry=app.tasks, + job_manager=app.job_manager, + base_context=base_context, + job_queue=job_queue, + job_semaphore=job_semaphore, + worker_id=2, + delete_jobs=delete_jobs or DeleteJobCondition.NEVER, + fetch_job_condition=asyncio.Condition(), + ) + + +@pytest.fixture(autouse=True, scope="function") +async def running_job_processor_task(job_processor): + task = asyncio.create_task(job_processor.run()) + yield task + task.cancel() + try: + await asyncio.wait_for(task, 0.1) + except asyncio.CancelledError: + pass + except CustomCriticalError: + pass + + +async def test_run_wait_until_cancelled(job_processor): + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(job_processor.run(), 0.1) + + +async def test_run_job_async(app: App, job_queue): + result = [] + + @app.task(queue="yay", name="task_func") + async def task_func(a, b): + result.append(a + b) + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + assert result == [12] + + assert job.id + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.SUCCEEDED + + +async def test_run_job_status(app: App, job_queue): + result = [] + + @app.task(queue="yay", name="task_func") + async def task_func(a, b): + result.append(a + b) + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + assert job.id + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.SUCCEEDED + + +async def test_run_job_sync(app: App, job_queue): + result = [] + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + result.append(a + b) + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + assert result == [12] + + +async def test_run_job_semi_async(app: App, job_queue): + result = [] + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + async def inner(): + result.append(a + b) + + return inner() + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + assert result == [12] + + +async def test_run_job_log_result(caplog, app: App, job_queue): + caplog.set_level("INFO") + + @app.task(queue="yay", name="task_func") + async def task_func(a, b): + return a + b + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + records = [record for record in caplog.records if record.action == "job_success"] + assert len(records) == 1 + record = records[0] + assert record.result == 12 + assert "Result: 12" in record.message + + +async def test_run_job_aborted(caplog, app: App, job_queue): + caplog.set_level("INFO") + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + raise JobAborted() + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + records = [record for record in caplog.records if record.action == "job_aborted"] + assert len(records) == 1 + record = records[0] + assert record.levelname == "INFO" + assert "Aborted" in record.message + + +async def test_run_job_aborted_status(app: App, job_queue): + @app.task(queue="yay", name="task_func") + async def task_func(): + raise JobAborted() + + task_func.defer() + job = await app.job_manager.fetch_job(None) + assert job + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + assert job.id + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.ABORTED + + +async def test_run_job_error_log(caplog, app: App, job_queue): + caplog.set_level("INFO") + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + raise ValueError("Nope") + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + records = [record for record in caplog.records if record.action == "job_error"] + assert len(records) == 1 + record = records[0] + assert record.levelname == "ERROR" + assert "to retry" not in record.message + + +async def test_run_job_error_status(app: App, job_queue): + @app.task(queue="yay", name="task_func") + def task_func(): + raise ValueError("Nope") + + task_func.defer() + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + assert job.id + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.FAILED + + +@pytest.mark.parametrize( + "critical_error", + [ + (False), + (True), + ], +) +async def test_run_job_retry_failed_job(app: App, job_queue, critical_error): + @app.task(retry=1) + def task_func(): + raise CustomCriticalError("Nope") if critical_error else ValueError("Nope") + + task_func.defer() + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + connector = cast(InMemoryConnector, app.connector) + assert job.id + job_row = connector.jobs[job.id] + assert job_row["status"] == "todo" + assert job_row["scheduled_at"] is not None + assert job_row["attempts"] == 1 + + +async def test_run_job_critical_error( + caplog, app: App, job_queue, running_job_processor_task: asyncio.Task +): + caplog.set_level("INFO") + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + raise CustomCriticalError("Nope") + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + + with pytest.raises(BaseException, match="Nope"): + await asyncio.wait_for(job_queue.join(), 0.1) + await running_job_processor_task + + assert job.id + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.FAILED + + +async def test_run_task_not_found_log(caplog, app: App, job_queue): + caplog.set_level("INFO") + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + return a + b + + task_func.defer(a=9, b=3) + job = await app.job_manager.fetch_job(None) + assert job + job = job.evolve(task_name="random_task_name") + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + records = [record for record in caplog.records if record.action == "task_not_found"] + assert len(records) == 1 + record = records[0] + assert record.levelname == "ERROR" + + +async def test_run_task_not_found_status(app: App, job_queue): + @app.task(queue="yay", name="task_func") + def task_func(): + pass + + task_func.defer() + job = await app.job_manager.fetch_job(None) + assert job + job = job.evolve(task_name="random_task_name") + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + assert job.id + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.FAILED + + +async def test_worker_copy_additional_context(app: App, job_queue, base_context): + base_context.additional_context["foo"] = "baz" + + @app.task(pass_context=True) + async def task_func(jobContext: JobContext): + assert jobContext.additional_context["foo"] == "bar" + + await task_func.defer_async() + job = await app.job_manager.fetch_job(None) + assert job + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + +async def test_worker_pass_worker_id_to_context(app: App, job_queue, job_processor): + assert job_processor.worker_id == 2 + + @app.task(pass_context=True) + async def task_func(jobContext: JobContext): + assert jobContext.worker_id == 2 + + await task_func.defer_async() + job = await app.job_manager.fetch_job(None) + assert job + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + +@pytest.mark.parametrize( + "job_processor, fail_task", + [ + ({"delete_jobs": "successful"}, False), + ({"delete_jobs": "always"}, False), + ({"delete_jobs": "always"}, True), + ], + indirect=["job_processor"], +) +async def test_process_job_with_deletion(app: App, job_queue, fail_task): + @app.task() + async def task_func(): + if fail_task: + raise ValueError("Nope") + + await task_func.defer_async() + job = await app.job_manager.fetch_job(None) + assert job + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + connector = cast(InMemoryConnector, app.connector) + assert job.id not in connector.jobs + + +@pytest.mark.parametrize( + "job_processor, fail_task", + [ + ({"delete_jobs": "never"}, False), + ({"delete_jobs": "never"}, True), + ({"delete_jobs": "successful"}, True), + ], + indirect=["job_processor"], +) +async def test_process_job_without_deletion(app: App, job_queue, fail_task): + @app.task() + async def task_func(): + if fail_task: + raise ValueError("Nope") + + await task_func.defer_async() + job = await app.job_manager.fetch_job(None) + assert job + + job_queue.put_nowait(job) + await asyncio.wait_for(job_queue.join(), 0.1) + + connector = cast(InMemoryConnector, app.connector) + assert job.id in connector.jobs + + +@pytest.mark.parametrize( + "fail_task", + [ + (False), + (True), + ], +) +async def test_process_job_notifies_completion( + app: App, job_queue, fail_task, running_job_processor_task, job_semaphore +): + @app.task() + async def task_func(): + if fail_task: + raise ValueError("Nope") + + await task_func.defer_async() + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + + await asyncio.wait_for(job_semaphore.acquire(), 0.1) + job_semaphore.release() + + +async def test_cancelling_processor_waits_for_task( + app: App, job_queue, running_job_processor_task: asyncio.Task +): + complete_task_event = asyncio.Event() + + @app.task() + async def task_func(): + await complete_task_event.wait() + + await task_func.defer_async() + job = await app.job_manager.fetch_job(None) + assert job + job_queue.put_nowait(job) + + # this should timeout because task is waiting for complete_task_event + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(job_queue.join(), 0.05) + + running_job_processor_task.cancel() + + # this should still timeout when cancelled because it is waiting for task to complete + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(job_queue.join(), 0.05) + + # tell the task to complete + complete_task_event.set() + + # this should successfully complete the job and re-raise the CancelledError + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(running_job_processor_task, 0.1) + + assert job.id + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.SUCCEEDED diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8fe7702c3..fe1d0d353 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -3,6 +3,7 @@ import asyncio import datetime import functools +import logging import sys import time import types @@ -164,161 +165,6 @@ def __(): return _ -async def test_run_tasks(finished, coro, short, caplog): - caplog.set_level("ERROR") - # Two functions in main coros, both go through their ends - await utils.run_tasks(main_coros=[coro(1), coro(2, sleep=0.01)]) - assert finished == {1, 2} - - assert caplog.records == [] - - -async def test_run_tasks_graceful_stop_callback_not_called( - launched, coro, callback, short -): - # A graceful_stop_callback is provided but isn't used because the main - # coros return on their own. - await utils.run_tasks(main_coros=[coro(1)], graceful_stop_callback=callback(2)) - assert launched == {1} - - -async def test_run_tasks_graceful_stop_callback_called(launched, coro, callback, short): - # A main function is provided, but it crashes. This time, the graceful callback - # is called. - with pytest.raises(exceptions.RunTaskError): - await utils.run_tasks( - main_coros=[coro(1, exc=ZeroDivisionError)], - graceful_stop_callback=callback(2), - ) - assert launched == {1, 2} - - -async def test_run_tasks_graceful_stop_callback_called_side( - launched, finished, coro, callback, short -): - # Two main coros provided, one crashes and one succeeds. The - # graceful_stop_callback is called and the coro that succeeds is awaited - # until it returns - with pytest.raises(exceptions.RunTaskError): - await utils.run_tasks( - main_coros=[coro(1, sleep=0.01), coro(2, exc=ZeroDivisionError)], - graceful_stop_callback=callback(3), - ) - assert launched == {1, 2, 3} - assert finished == {1, 2} - - -async def test_run_tasks_side_coro(launched, finished, coro, short): - # When all the main coros have returned, the remaining side coros are - # cancelled - await utils.run_tasks(main_coros=[coro(1), coro(2)], side_coros=[coro(3, sleep=1)]) - assert launched == {1, 2, 3} - assert finished == {1, 2} - - -async def test_run_tasks_side_coro_crash(launched, finished, coro, short): - # There's a main and a side. The side crashes. Main is still awaited and - # the unction raises - with pytest.raises(exceptions.RunTaskError) as exc_info: - await utils.run_tasks( - main_coros=[coro(1, sleep=0.01)], - side_coros=[coro(2, exc=ZeroDivisionError)], - ) - assert launched == {1, 2} - assert finished == {1, 2} - assert isinstance(exc_info.value.__cause__, ZeroDivisionError) - - -async def test_run_tasks_main_coro_crash(launched, finished, coro, short): - # There's a main and a side. The main crashes. Side is cancelled, and the - # function raises - with pytest.raises(exceptions.RunTaskError) as exc_info: - await utils.run_tasks( - main_coros=[coro(1, exc=ZeroDivisionError)], - side_coros=[coro(2, sleep=1)], - ) - assert launched == {1, 2} - assert finished == {1} - assert isinstance(exc_info.value.__cause__, ZeroDivisionError) - - -async def test_run_tasks_main_coro_one_crashes(launched, finished, coro, short): - # 2 mains. One main crashes. The other finishes, and then the function fails. - with pytest.raises(exceptions.RunTaskError) as exc_info: - await utils.run_tasks( - main_coros=[coro(1, exc=ZeroDivisionError), coro(2, sleep=0.001)], - ) - assert launched == {1, 2} - assert finished == {1, 2} - assert isinstance(exc_info.value.__cause__, ZeroDivisionError) - - -async def test_run_tasks_main_coro_both_crash(launched, finished, coro, short): - # 2 mains. The 2 crash. The reported error is for the first one. - with pytest.raises(exceptions.RunTaskError) as exc_info: - await utils.run_tasks( - main_coros=[ - coro(1, sleep=0.001, exc=ValueError), - coro(2, exc=ZeroDivisionError), - ], - ) - assert launched == {1, 2} - assert finished == {1, 2} - assert isinstance(exc_info.value.__cause__, ValueError) - - -@pytest.fixture -def count_logs(caplog): - """Count how many logs match all the arguments""" - caplog.set_level("DEBUG") - - def _(**kwargs): - return sum( - all((getattr(record, key, None) == value) for key, value in kwargs.items()) - for record in caplog.records - ) - - return _ - - -async def test_run_tasks_logs(coro, short, count_logs): - # 2 mains. The 2 crash. The reported error is for the first one. - with pytest.raises(exceptions.RunTaskError): - await utils.run_tasks( - main_coros=[ - coro(1, exc=ZeroDivisionError("foo")), - coro(2), - ], - side_coros=[ - coro(3, exc=RuntimeError("bar")), - coro(4), - ], - ) - assert 4 == count_logs( - levelname="DEBUG", - message="Started func", - action="func_start", - ) - - assert 1 == count_logs( - levelname="DEBUG", - message="func finished execution", - action="func_stop", - ) - - assert 1 == count_logs( - levelname="ERROR", - message="func error: ZeroDivisionError('foo')", - action="func_error", - ) - - assert 1 == count_logs( - levelname="ERROR", - message="func error: RuntimeError('bar')", - action="func_error", - ) - - def test_utcnow(mocker): dt = mocker.patch("datetime.datetime") assert utils.utcnow() == dt.now.return_value @@ -380,7 +226,7 @@ async def close(): awaited.append("closed") context = utils.AwaitableContext(open_coro=open, close_coro=close, return_value=1) - context.awaited = awaited + context.awaited = awaited # type: ignore return context @@ -504,3 +350,36 @@ async def func2(): assert await func2() == 4 assert result == [1, 2, 3] + + +@pytest.mark.parametrize( + "task_1_error, task_2_error", + [ + (None, None), + (ValueError("Nope from task_1"), None), + (None, ValueError("Nope from task_2")), + (ValueError("Nope from task_1"), ValueError("Nope from task_2")), + ], +) +async def test_cancel_and_capture_errors(task_1_error, task_2_error, caplog): + caplog.set_level(logging.ERROR) + + async def task_1(): + if task_1_error: + raise task_1_error + else: + await asyncio.sleep(0.5) + + async def task_2(): + if task_2_error: + raise task_2_error + else: + await asyncio.sleep(0.5) + + tasks = [asyncio.create_task(task_1()), asyncio.create_task(task_2())] + await asyncio.sleep(0.01) + await asyncio.wait_for(utils.cancel_and_capture_errors(tasks), timeout=100) + + expected_error_count = sum(1 for error in (task_1_error, task_2_error) if error) + + assert len(caplog.records) == expected_error_count diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 257e2c871..7ddc7a6bb 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -1,699 +1,175 @@ from __future__ import annotations import asyncio +from typing import cast import pytest -from procrastinate import exceptions, job_context, jobs, tasks, worker +from procrastinate.app import App +from procrastinate.jobs import Status +from procrastinate.testing import InMemoryConnector +from procrastinate.worker import Worker -from .. import conftest +@pytest.mark.parametrize( + "available_jobs, concurrency", + [ + (0, 1), + (1, 1), + (2, 1), + (1, 2), + (2, 2), + (4, 2), + ], +) +async def test_worker_run_no_wait(app: App, available_jobs, concurrency): + worker = Worker(app, wait=False, concurrency=concurrency) -@pytest.fixture -def test_worker(app): - return worker.Worker(app, queues=None) - + @app.task + async def perform_job(): + pass -@pytest.fixture -def context(app): - def _(job): - return job_context.JobContext(app=app, worker_name="worker", job=job) + for i in range(available_jobs): + await perform_job.defer_async() - return _ + await asyncio.wait_for(worker.run(), 0.1) -def test_worker_additional_context(app): - worker_obj = worker.Worker(app=app, additional_context={"foo": "bar"}) - assert worker_obj.base_context.additional_context == {"foo": "bar"} +async def test_worker_run_wait_until_cancelled(app: App): + worker = Worker(app, wait=True) + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(worker.run(), 0.05) -async def test_run(test_worker, mocker, caplog): +async def test_worker_run_wait_stop(app: App, caplog): caplog.set_level("INFO") - - single_worker = mocker.Mock() - - async def mock(worker_id): - single_worker(worker_id=worker_id) - - test_worker.single_worker = mock - - await test_worker.run() - - single_worker.assert_called() + worker = Worker(app, wait=True) + run_task = asyncio.create_task(worker.run()) + # wait just enough to make sure the task is running + await asyncio.sleep(0.01) + worker.stop() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(run_task, 0.1) assert set(caplog.messages) == { "Starting worker on all queues", + "Stop requested", "Stopped worker on all queues", "No periodic task found, periodic deferrer will not run.", } -@pytest.mark.parametrize( - "side_effect, status", - [ - (None, "succeeded"), - (exceptions.JobAborted(), "aborted"), - (exceptions.JobError(), "failed"), - (exceptions.TaskNotFound(), "failed"), - ], -) -async def test_process_job( - mocker, test_worker, job_factory, connector, side_effect, status -): - async def coro(*args, **kwargs): - pass - - test_worker.run_job = mocker.Mock(side_effect=side_effect or coro) - job = job_factory(id=1) - await test_worker.job_manager.defer_job_async(job) - - await test_worker.process_job(job=job) +async def test_worker_run_once_log_messages(app: App, caplog): + caplog.set_level("INFO") + worker = Worker(app, wait=False) + await asyncio.wait_for(worker.run(), 0.1) - test_worker.run_job.assert_called_with(job=job, worker_id=0) - assert connector.jobs[1]["status"] == status + assert caplog.messages == [ + "Starting worker on all queues", + "No job found. Stopping worker because wait=False", + "Stopped worker on all queues", + ] -@pytest.mark.parametrize( - "side_effect, delete_jobs", - [ - (None, "successful"), - (None, "always"), - (exceptions.JobError(), "always"), - ], -) -async def test_process_job_with_deletion( - mocker, app, job_factory, connector, side_effect, delete_jobs -): - async def coro(*args, **kwargs): - pass +async def test_worker_run_wait_listen(app: App): + worker = Worker(app, wait=True, listen_notify=True, queues=["qq"]) + run_task = asyncio.create_task(worker.run()) + # wait just enough to make sure the task is running + await asyncio.sleep(0.01) - test_worker = worker.Worker(app, delete_jobs=delete_jobs) - test_worker.run_job = mocker.Mock(side_effect=side_effect or coro) - job = job_factory(id=1) - await test_worker.job_manager.defer_job_async(job) + connector = cast(InMemoryConnector, app.connector) - await test_worker.process_job(job=job) + assert connector.notify_event + assert connector.notify_channels == ["procrastinate_queue#qq"] - assert 1 not in connector.jobs + run_task.cancel() @pytest.mark.parametrize( - "side_effect, delete_jobs", + "available_jobs, concurrency", [ - (None, "never"), - (exceptions.JobError(), "never"), - (exceptions.JobError(), "successful"), + (2, 1), + (3, 2), ], ) -async def test_process_job_without_deletion( - mocker, app, job_factory, connector, side_effect, delete_jobs -): - async def coro(*args, **kwargs): - pass - - test_worker = worker.Worker(app, delete_jobs=delete_jobs) - test_worker.run_job = mocker.Mock(side_effect=side_effect or coro) - job = job_factory(id=1) - await test_worker.job_manager.defer_job_async(job) - - await test_worker.process_job(job=job) - - assert 1 in connector.jobs - - -async def test_process_job_retry_failed_job( - mocker, test_worker, job_factory, connector -): - async def coro(*args, **kwargs): - pass - - scheduled_at = conftest.aware_datetime(2000, 1, 1) - test_worker.run_job = mocker.Mock( - side_effect=exceptions.JobError( - retry_exception=exceptions.JobRetry(scheduled_at=scheduled_at) - ) - ) - job = job_factory(id=1) - await test_worker.job_manager.defer_job_async(job) - - await test_worker.process_job(job=job, worker_id=0) - - test_worker.run_job.assert_called_with(job=job, worker_id=0) - assert connector.jobs[1]["status"] == "todo" - assert connector.jobs[1]["scheduled_at"] == scheduled_at - assert connector.jobs[1]["attempts"] == 1 - - -async def test_process_job_retry_failed_job_critical( - mocker, test_worker, job_factory, connector -): - class TestException(BaseException): - pass - - job_exception = exceptions.JobError(critical=True) - job_exception.__cause__ = TestException() - - test_worker.run_job = mocker.Mock(side_effect=job_exception) - job = job_factory(id=1) - await test_worker.job_manager.defer_job_async(job) - - # Exceptions that extend BaseException should be re-raised after the failed job - # is scheduled for retry (if retry is applicable). - with pytest.raises(TestException): - await test_worker.process_job(job=job, worker_id=0) - - test_worker.run_job.assert_called_with(job=job, worker_id=0) - assert connector.jobs[1]["status"] == "failed" - assert connector.jobs[1]["scheduled_at"] is None - assert connector.jobs[1]["attempts"] == 1 - - -async def test_process_job_retry_failed_job_retry_critical( - mocker, test_worker, job_factory, connector -): - class TestException(BaseException): - pass - - scheduled_at = conftest.aware_datetime(2000, 1, 1) - job_exception = exceptions.JobError( - critical=True, retry_exception=exceptions.JobRetry(scheduled_at=scheduled_at) - ) - job_exception.__cause__ = TestException() - - test_worker.run_job = mocker.Mock(side_effect=job_exception) - job = job_factory(id=1) - await test_worker.job_manager.defer_job_async(job) - - # Exceptions that extend BaseException should be re-raised after the failed job - # is scheduled for retry (if retry is applicable). - with pytest.raises(TestException): - await test_worker.process_job(job=job, worker_id=0) - - test_worker.run_job.assert_called_with(job=job, worker_id=0) - assert connector.jobs[1]["status"] == "todo" - assert connector.jobs[1]["scheduled_at"] == scheduled_at - assert connector.jobs[1]["attempts"] == 1 - - -async def test_run_job(app): - result = [] - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - result.append(a + b) - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="task_func", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - await test_worker.run_job(job=job, worker_id=3) - - assert result == [12] - - -async def test_run_job_async(app): - result = [] - - @app.task(queue="yay", name="task_func") - async def task_func(a, b): - result.append(a + b) - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="task_func", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - await test_worker.run_job(job=job, worker_id=3) - - assert result == [12] - - -async def test_run_job_semi_async(app): - result = [] - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - async def inner(): - result.append(a + b) - - return inner() - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="task_func", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - await test_worker.run_job(job=job, worker_id=3) - - assert result == [12] - - -async def test_run_job_log_result(caplog, app): - caplog.set_level("INFO") - - result = [] - - def task_func(a, b): # pylint: disable=unused-argument - s = a + b - result.append(s) - return s - - task = tasks.Task(task_func, blueprint=app, queue="yay", name="job") - - app.tasks = {"task_func": task} - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="task_func", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - await test_worker.run_job(job=job, worker_id=3) - - assert result == [12] +async def test_worker_run_respects_concurrency(app: App, available_jobs, concurrency): + worker = Worker(app, wait=False, concurrency=concurrency) + run_task = asyncio.create_task(worker.run()) - records = [record for record in caplog.records if record.action == "job_success"] - assert len(records) == 1 - record = records[0] - assert record.result == 12 - assert "Result: 12" in record.message - - -@pytest.mark.parametrize( - "worker_name, logger_name, record_worker_name", - [(None, "worker", "worker"), ("w1", "worker.w1", "w1")], -) -async def test_run_job_log_name( - caplog, app, worker_name, logger_name, record_worker_name -): - caplog.set_level("INFO") - - test_worker = worker.Worker(app, name=worker_name, wait=False) + complete_tasks = asyncio.Event() @app.task - def task(): - pass - - await task.defer_async() - - await test_worker.run() - - # We're not interested in defer logs - records = [r for r in caplog.records if "worker" in r.name] - - assert len(records) - record_names = [record.name for record in records] - assert all([name.endswith(logger_name) for name in record_names]) - - worker_names = [getattr(record, "worker", {}).get("name") for record in records] - assert all([name == record_worker_name for name in worker_names]) - - -async def test_run_job_aborted(app, caplog): - caplog.set_level("INFO") - - def job_func(a, b): # pylint: disable=unused-argument - raise exceptions.JobAborted() - - task = tasks.Task(job_func, blueprint=app, queue="yay", name="job") - task.func = job_func - - app.tasks = {"job": task} - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="job", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - with pytest.raises(exceptions.JobAborted): - await test_worker.run_job(job=job, worker_id=3) - - assert ( - len( - [ - r - for r in caplog.records - if r.levelname == "INFO" and "Aborted" in r.message - ] - ) - == 1 - ) - - -async def test_run_job_error(app, caplog): - caplog.set_level("INFO") - - def job_func(a, b): # pylint: disable=unused-argument - raise ValueError("nope") - - task = tasks.Task(job_func, blueprint=app, queue="yay", name="job") - task.func = job_func - - app.tasks = {"job": task} - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="job", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - with pytest.raises(exceptions.JobError): - await test_worker.run_job(job=job, worker_id=3) - - assert ( - len( - [ - r - for r in caplog.records - if r.levelname == "ERROR" and "to retry" not in r.message - ] - ) - == 1 - ) - - -async def test_run_job_critical_error(app, caplog): - caplog.set_level("INFO") - - def job_func(a, b): # pylint: disable=unused-argument - raise BaseException("nope") - - task = tasks.Task(job_func, blueprint=app, queue="yay", name="job") - task.func = job_func - - app.tasks = {"job": task} - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="job", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - with pytest.raises(exceptions.JobError) as exc_info: - await test_worker.run_job(job=job, worker_id=3) - - assert exc_info.value.critical is True - - -async def test_run_job_retry(app, caplog): - caplog.set_level("INFO") - - def job_func(a, b): # pylint: disable=unused-argument - raise ValueError("nope") - - task = tasks.Task(job_func, blueprint=app, queue="yay", name="job", retry=True) - task.func = job_func - - app.tasks = {"job": task} - - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - task_name="job", - queueing_lock="houba", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - with pytest.raises(exceptions.JobError) as exc_info: - await test_worker.run_job(job=job, worker_id=3) - - assert isinstance(exc_info.value.retry_exception, exceptions.JobRetry) - - assert ( - len( - [ - r - for r in caplog.records - if r.levelname == "INFO" and "to retry" in r.message - ] - ) - == 1 - ) - assert len([r for r in caplog.records if r.levelname == "ERROR"]) == 0 - - -async def test_run_job_not_found(app): - job = jobs.Job( - id=16, - task_kwargs={"a": 9, "b": 3}, - lock="sherlock", - queueing_lock="houba", - task_name="job", - queue="yay", - ) - test_worker = worker.Worker(app, queues=["yay"]) - with pytest.raises(exceptions.TaskNotFound): - await test_worker.run_job(job=job, worker_id=3) - - -async def test_run_job_pass_context(app): - result = [] - - @app.task(queue="yay", name="job", pass_context=True) - def task_func(test_context, a): - result.extend([test_context, a]) - - job = jobs.Job( - id=16, - task_kwargs={"a": 1}, - lock="sherlock", - queueing_lock="houba", - task_name="job", - queue="yay", - ) - test_worker = worker.Worker( - app, queues=["yay"], name="my_worker", additional_context={"foo": "bar"} - ) - context = test_worker.context_for_worker(worker_id=3) - - await test_worker.run_job(job=job, worker_id=3) - - context = context.evolve(task=task_func) - - assert result == [ - context, - 1, - ] - - -async def test_wait_for_job_with_job(app, mocker): - test_worker = worker.Worker(app) - # notify_event is set to None initially, and we skip run() - test_worker.notify_event = mocker.Mock() - - wait_for = mocker.Mock() - - async def mock(coro, timeout): - wait_for(coro, timeout=timeout) - - mocker.patch("asyncio.wait_for", mock) - - await test_worker.wait_for_job(timeout=42) - - wait_for.assert_called_with(test_worker.notify_event.wait.return_value, timeout=42) - - assert test_worker.notify_event.mock_calls == [ - mocker.call.clear(), - mocker.call.wait(), - mocker.call.clear(), - ] + async def perform_job(): + await complete_tasks.wait() + for _ in range(available_jobs): + await perform_job.defer_async() -async def test_wait_for_job_without_job(app, mocker): - test_worker = worker.Worker(app) - # notify_event is set to None initially, and we skip run() - test_worker.notify_event = mocker.Mock() + # wait just enough to make sure the task is running + await asyncio.sleep(0.01) - wait_for = mocker.Mock(side_effect=asyncio.TimeoutError) + connector = cast(InMemoryConnector, app.connector) - async def mock(coro, timeout): - wait_for(coro, timeout=timeout) + doings_jobs = list(connector.list_jobs_all(status=Status.DOING.value)) + todo_jobs = list(connector.list_jobs_all(status=Status.TODO.value)) - mocker.patch("asyncio.wait_for", mock) + assert len(doings_jobs) == concurrency + assert len(todo_jobs) == available_jobs - concurrency - await test_worker.wait_for_job(timeout=42) + complete_tasks.set() + await asyncio.wait_for(run_task, 0.1) - wait_for.assert_called_with(test_worker.notify_event.wait.return_value, timeout=42) - assert test_worker.notify_event.mock_calls == [ - mocker.call.clear(), - mocker.call.wait(), - ] - - -async def test_single_worker_no_wait(app, mocker): - process_job = mocker.Mock() - wait_for_job = mocker.Mock() - - class TestWorker(worker.Worker): - async def process_job(self, job): - process_job(job=job) - - async def wait_for_job(self, timeout): - wait_for_job(timeout) - - await TestWorker(app=app, wait=False).single_worker(worker_id=0) - - assert process_job.called is False - assert wait_for_job.called is False - - -async def test_single_worker_stop_during_execution(app, mocker): - process_job = mocker.Mock() - wait_for_job = mocker.Mock() - - await app.configure_task("bla").defer_async() - - class TestWorker(worker.Worker): - async def process_job(self, job, worker_id): - process_job(job=job, worker_id=worker_id) - self.stop_requested = True - - async def wait_for_job(self, timeout): - wait_for_job(timeout=timeout) - - await TestWorker(app=app).single_worker(worker_id=0) - - assert wait_for_job.called is False - process_job.assert_called_once() - - -async def test_single_worker_stop_during_wait(app, mocker): - process_job = mocker.Mock() - wait_for_job = mocker.Mock() - - await app.configure_task("bla").defer_async() - - class TestWorker(worker.Worker): - async def process_job(self, job, worker_id): - process_job(job=job, worker_id=worker_id) - - async def wait_for_job(self, timeout): - wait_for_job() - self.stop_requested = True +async def test_worker_run_fetches_job_on_notification(app: App): + worker = Worker(app, wait=True, concurrency=1) - await TestWorker(app=app).single_worker(worker_id=0) + run_task = asyncio.create_task(worker.run()) - process_job.assert_called_once() - wait_for_job.assert_called_once() - - -async def test_single_worker_spread_wait(app, mocker): - process_job = mocker.Mock() - wait_for_job = mocker.Mock() - - await app.configure_task("bla").defer_async() - - class TestWorker(worker.Worker): - stop = False - - async def process_job(self, job, worker_id): - process_job(job=job, worker_id=worker_id) - - async def wait_for_job(self, timeout): - wait_for_job(timeout) - self.stop_requested = self.stop - self.stop = True - - await TestWorker(app=app, timeout=4, concurrency=7).single_worker(worker_id=3) - - process_job.assert_called_once() - assert wait_for_job.call_args_list == [mocker.call(4 * (3 + 1)), mocker.call(4 * 7)] - - -def test_context_for_worker(app): - test_worker = worker.Worker(app=app, name="foo") - expected = job_context.JobContext(app=app, worker_id=3, worker_name="foo") - - context = test_worker.context_for_worker(worker_id=3) - - assert context == expected - - -def test_context_for_worker_kwargs(app): - test_worker = worker.Worker(app=app, name="foo") - expected = job_context.JobContext(app=app, worker_id=3, worker_name="bar") - - context = test_worker.context_for_worker(worker_id=3, worker_name="bar") - - assert context == expected + complete_tasks = asyncio.Event() + @app.task + async def perform_job(): + await complete_tasks.wait() -def test_context_for_worker_value_kept(app): - test_worker = worker.Worker(app=app, name="foo") - expected = job_context.JobContext(app=app, worker_id=3, worker_name="bar") + await asyncio.sleep(0.01) - test_worker.context_for_worker(worker_id=3, worker_name="bar") - context = test_worker.context_for_worker(worker_id=3) + connector = cast(InMemoryConnector, app.connector) - assert context == expected + assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 1 + await asyncio.sleep(0.01) -def test_context_for_worker_reset(app): - test_worker = worker.Worker(app=app, name="foo") - expected = job_context.JobContext(app=app, worker_id=3, worker_name="foo") + assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 1 - test_worker.context_for_worker(worker_id=3, worker_name="bar") - context = test_worker.context_for_worker(worker_id=3, reset=True) + await perform_job.defer_async() + await asyncio.sleep(0.01) - assert context == expected + assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 2 + complete_tasks.set() + run_task.cancel() + try: + await asyncio.wait_for(run_task, timeout=0.2) + except asyncio.CancelledError: + pass -def test_worker_copy_additional_context(app): - additional_context = {"foo": "bar"} - test_worker = worker.Worker( - app=app, - name="worker", - additional_context=additional_context, - ) - # mutate the additional_context object and test that we have the original - # value in the worker - additional_context["foo"] = "baz" - assert test_worker.base_context.additional_context == {"foo": "bar"} +async def test_worker_run_respects_polling(app: App): + worker = Worker(app, wait=True, concurrency=1, timeout=0.05) + run_task = asyncio.create_task(worker.run()) -def test_context_for_worker_with_additional_context(app): - additional_context = {"foo": "bar"} - test_worker = worker.Worker( - app=app, - name="worker", - additional_context=additional_context, - ) + await asyncio.sleep(0.01) - context1 = test_worker.context_for_worker(worker_id=3) + connector = cast(InMemoryConnector, app.connector) + assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 1 - # mutate the additional_context object for one worker and test that it - # hasn't changed for other workers - context1.additional_context["foo"] = "baz" + await asyncio.sleep(0.05) - context2 = test_worker.context_for_worker(worker_id=4) + assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 2 - assert context2.additional_context == {"foo": "bar"} + run_task.cancel() + try: + await asyncio.wait_for(run_task, timeout=0.2) + except asyncio.CancelledError: + pass diff --git a/tests/unit/test_worker_sync.py b/tests/unit/test_worker_sync.py index 0ce9dadb1..f1badb92a 100644 --- a/tests/unit/test_worker_sync.py +++ b/tests/unit/test_worker_sync.py @@ -1,14 +1,13 @@ from __future__ import annotations -import asyncio - import pytest from procrastinate import exceptions, job_context, worker +from procrastinate.app import App @pytest.fixture -def test_worker(app): +def test_worker(app: App) -> worker.Worker: return worker.Worker(app=app, queues=["yay"]) @@ -22,7 +21,7 @@ def test_worker_find_task_missing(test_worker): test_worker.find_task("foobarbaz") -def test_worker_find_task(app): +def test_worker_find_task(app: App): test_worker = worker.Worker(app=app, queues=["yay"]) @app.task(name="foo") @@ -34,27 +33,7 @@ def task_func(): def test_stop(test_worker, caplog): caplog.set_level("INFO") - test_worker.notify_event = asyncio.Event() test_worker.stop() - assert test_worker.stop_requested is True - assert test_worker.notify_event.is_set() assert caplog.messages == ["Stop requested"] - - -def test_stop_log_job(test_worker, caplog, context, job_factory): - caplog.set_level("INFO") - test_worker.notify_event = asyncio.Event() - job = job_factory(id=42, task_name="bla") - ctx = context.evolve(job=job, worker_id=0) - test_worker.current_contexts[0] = ctx - - test_worker.stop() - - assert test_worker.stop_requested is True - assert test_worker.notify_event.is_set() - assert caplog.messages == [ - "Stop requested", - "Waiting for job to finish: worker 0: bla[42]()", - ] From 3e09c86310cddfee8e3480262ffbead8e5213364 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Tue, 16 Jul 2024 22:47:43 +1000 Subject: [PATCH 005/375] import NotRequired from typing_extensions --- procrastinate/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/procrastinate/app.py b/procrastinate/app.py index 419520ad8..108472f1c 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -9,11 +9,12 @@ Any, Iterable, Iterator, - NotRequired, TypedDict, Unpack, ) +from typing_extensions import NotRequired + from procrastinate import blueprints, exceptions, jobs, manager, schema, utils from procrastinate import connector as connector_module From 46e781f9e18b173647696da01e475d5c600ff359 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 13:20:56 +1000 Subject: [PATCH 006/375] fix django app typing --- procrastinate/contrib/django/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/procrastinate/contrib/django/settings.py b/procrastinate/contrib/django/settings.py index bd0a4e6ee..d773a9e5a 100644 --- a/procrastinate/contrib/django/settings.py +++ b/procrastinate/contrib/django/settings.py @@ -5,6 +5,8 @@ from django.conf import settings as django_settings from typing_extensions import dataclass_transform +from procrastinate.app import WorkerOptions + @dataclass_transform() class BaseSettings: @@ -20,7 +22,7 @@ class Settings(BaseSettings): AUTODISCOVER_MODULE_NAME: str = "tasks" IMPORT_PATHS: list[str] = [] DATABASE_ALIAS: str = "default" - WORKER_DEFAULTS: dict[str, str] | None = None + WORKER_DEFAULTS: WorkerOptions | None = None PERIODIC_DEFAULTS: dict[str, str] | None = None ON_APP_READY: str | None = None READONLY_MODELS: bool = True From 75a09e9974afb9ea5365aee8c2f601952353845b Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 13:21:18 +1000 Subject: [PATCH 007/375] fix test cleanup --- tests/unit/test_worker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 7ddc7a6bb..c106989dc 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -83,6 +83,10 @@ async def test_worker_run_wait_listen(app: App): assert connector.notify_channels == ["procrastinate_queue#qq"] run_task.cancel() + try: + await asyncio.wait_for(run_task, timeout=0.2) + except asyncio.CancelledError: + pass @pytest.mark.parametrize( @@ -167,7 +171,6 @@ async def test_worker_run_respects_polling(app: App): await asyncio.sleep(0.05) assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 2 - run_task.cancel() try: await asyncio.wait_for(run_task, timeout=0.2) From 460c24bd0adda3f6cca5738021f62a5530afe206 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 13:21:38 +1000 Subject: [PATCH 008/375] import Unpack from typing_extensions --- procrastinate/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/procrastinate/app.py b/procrastinate/app.py index 108472f1c..2bc1e2025 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -10,10 +10,9 @@ Iterable, Iterator, TypedDict, - Unpack, ) -from typing_extensions import NotRequired +from typing_extensions import NotRequired, Unpack from procrastinate import blueprints, exceptions, jobs, manager, schema, utils from procrastinate import connector as connector_module From be3c236dbf8e97bbc18a934533587865da069c52 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 13:22:34 +1000 Subject: [PATCH 009/375] fix typo --- procrastinate/job_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/procrastinate/job_processor.py b/procrastinate/job_processor.py index 03d0e5139..ffeb76ac6 100644 --- a/procrastinate/job_processor.py +++ b/procrastinate/job_processor.py @@ -90,7 +90,7 @@ async def run(self): try: self.job_context = self._create_job_context(job) self.logger.debug( - f"L poaded job info, about to start job {job.call_string}", + f"Loaded job info, about to start job {job.call_string}", extra=self.job_context.log_extra(action="loaded_job_info"), ) process_job_task = asyncio.create_task(self._process_job()) From 6f33245d783af7f65568940ff95a06e17fb8e7a1 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 15:43:40 +1000 Subject: [PATCH 010/375] fix tests on python 3.8 --- procrastinate/worker.py | 2 +- tests/acceptance/test_nominal.py | 6 +++--- tests/unit/test_job_processor.py | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index bf8af431e..43fd5c277 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -94,7 +94,7 @@ async def run(self): ), ) - job_queue = asyncio.Queue[Job](self.concurrency) + job_queue: asyncio.Queue[Job] = asyncio.Queue(self.concurrency) job_semaphore = asyncio.Semaphore(self.concurrency) fetch_job_condition = asyncio.Condition() job_processors = [ diff --git a/tests/acceptance/test_nominal.py b/tests/acceptance/test_nominal.py index 503d3b4ef..8f9e91cd6 100644 --- a/tests/acceptance/test_nominal.py +++ b/tests/acceptance/test_nominal.py @@ -3,7 +3,7 @@ import signal import subprocess import time -from typing import Protocol, cast +from typing import Protocol, Tuple, cast import pytest @@ -198,7 +198,7 @@ def test_queueing_lock(defer, running_worker): ) -def test_periodic_deferrer(worker): +def test_periodic_deferrer(worker: Worker): # We're launching a worker that executes a periodic task every second, and # letting it run for 2.5 s. It should execute the task 3 times, and print to stdout: # 0 @@ -211,7 +211,7 @@ def test_periodic_deferrer(worker): # We're making a dict from the output results = dict( - cast(tuple[int, int], (int(a) for a in e[5:].split())) + cast(Tuple[int, int], (int(a) for a in e[5:].split())) for e in stdout.splitlines() if e.startswith("tick ") ) diff --git a/tests/unit/test_job_processor.py b/tests/unit/test_job_processor.py index 901cb4f0a..f70034f2e 100644 --- a/tests/unit/test_job_processor.py +++ b/tests/unit/test_job_processor.py @@ -22,9 +22,11 @@ def worker_name() -> str: return "worker" +# it is important to make the fixture async because +# otherwise, the queue is created outside the main event loop on python 3.8 @pytest.fixture -def job_queue(): - return asyncio.Queue[Job](2) +async def job_queue() -> asyncio.Queue[Job]: + return asyncio.Queue(2) @pytest.fixture @@ -40,7 +42,7 @@ def job_semaphore(): @pytest.fixture -def job_processor(request, app, base_context, job_queue, job_semaphore): +async def job_processor(request, app, base_context, job_queue, job_semaphore): param = getattr(request, "param", None) delete_jobs = cast(str, param["delete_jobs"]) if param else None From 9a681cb09504b7a1d01e6b27e90956c14b856011 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 15:53:12 +1000 Subject: [PATCH 011/375] avoid reraising error in finally block --- procrastinate/job_processor.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/procrastinate/job_processor.py b/procrastinate/job_processor.py index ffeb76ac6..306375c83 100644 --- a/procrastinate/job_processor.py +++ b/procrastinate/job_processor.py @@ -85,7 +85,6 @@ async def run(self): while True: job = await self._job_queue.get() async with self._job_semaphore: - cancelledError: asyncio.CancelledError | None = None status = Status.FAILED try: self.job_context = self._create_job_context(job) @@ -98,9 +97,10 @@ async def run(self): try: # the job is shielded from cancellation to enable graceful stop await asyncio.shield(process_job_task) - except asyncio.CancelledError as e: - cancelledError = e + except asyncio.CancelledError: await process_job_task + status = Status.SUCCEEDED + raise status = Status.SUCCEEDED except TaskNotFound as exc: @@ -129,10 +129,6 @@ async def run(self): async with self._fetch_job_condition: self._fetch_job_condition.notify() - # reraise the cancelled error we caught earlier - if cancelledError: - raise cancelledError - async def _process_job(self): assert self.job_context assert self.job_context.task From 78d85e3d9dcfa47f4fd7d885b51ef297d7f983f7 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 16:10:54 +1000 Subject: [PATCH 012/375] use put_nowait --- procrastinate/worker.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 43fd5c277..b8dfb2968 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -130,7 +130,10 @@ async def run(self): while True: out_of_job = None while not out_of_job: - # only fetch job when the queue is not full and not all processors are busy + # we don't want to fetch any new job if all processors are busy + # or when the queue is already full + # it is preferable to let any other procrastinate worker process handle those + # jobs until we are ready to process more async with fetch_job_condition: await fetch_job_condition.wait_for( lambda: not job_queue.full() @@ -138,7 +141,14 @@ async def run(self): ) job = await self.app.job_manager.fetch_job(queues=self.queues) if job: - await job_queue.put(job) + # once a job has been fetched, we don't want to be cancelled until we put the job + # in the queue. For this reason, we prefer job_queue.put_nowait to job_queue.put + # + # The cleanup process ensures any job in the queue is awaited. + # + # We also made sure the queue is not full before fetching the job. + # Given only this worker adds to the queue, we don't need to worry about QueueFull being raised + job_queue.put_nowait(job) else: out_of_job = True if out_of_job: @@ -181,6 +191,7 @@ async def run(self): extra=context.log_extra(action="ending_job"), ) + # make sure any job in progress or still in the queue is given time to be processed await job_queue.join() job_processors_task.cancel() job_processors_task.add_done_callback( From badc87d53afc167e8650c12f2ab0a0e30943c42e Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 16:34:52 +1000 Subject: [PATCH 013/375] fix test cancellation --- tests/unit/test_job_processor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_job_processor.py b/tests/unit/test_job_processor.py index f70034f2e..5126cf0a5 100644 --- a/tests/unit/test_job_processor.py +++ b/tests/unit/test_job_processor.py @@ -62,7 +62,8 @@ async def job_processor(request, app, base_context, job_queue, job_semaphore): async def running_job_processor_task(job_processor): task = asyncio.create_task(job_processor.run()) yield task - task.cancel() + if not task.cancelled(): + task.cancel() try: await asyncio.wait_for(task, 0.1) except asyncio.CancelledError: From b49a755c72894fd09816424d1ff7825e9618b09f Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 17 Jul 2024 17:11:57 +1000 Subject: [PATCH 014/375] handle reraising cancellation on error --- procrastinate/job_processor.py | 8 ++++++-- tests/unit/test_job_processor.py | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/procrastinate/job_processor.py b/procrastinate/job_processor.py index 306375c83..5653891cf 100644 --- a/procrastinate/job_processor.py +++ b/procrastinate/job_processor.py @@ -84,6 +84,7 @@ async def _persist_job_status(self, job: Job, status: Status): async def run(self): while True: job = await self._job_queue.get() + cancelled_error: asyncio.CancelledError | None = None async with self._job_semaphore: status = Status.FAILED try: @@ -97,7 +98,8 @@ async def run(self): try: # the job is shielded from cancellation to enable graceful stop await asyncio.shield(process_job_task) - except asyncio.CancelledError: + except asyncio.CancelledError as e: + cancelled_error = e await process_job_task status = Status.SUCCEEDED raise @@ -114,7 +116,9 @@ async def run(self): status = Status.ABORTED except Exception: # exception is already logged by _process_job, carry on - pass + # if there is a pending cancellation, this needs to be reraised here + if cancelled_error: + raise cancelled_error finally: persist_job_status_task = asyncio.create_task( self._persist_job_status(job=job, status=status) diff --git a/tests/unit/test_job_processor.py b/tests/unit/test_job_processor.py index 5126cf0a5..96399a7f5 100644 --- a/tests/unit/test_job_processor.py +++ b/tests/unit/test_job_processor.py @@ -64,12 +64,12 @@ async def running_job_processor_task(job_processor): yield task if not task.cancelled(): task.cancel() - try: - await asyncio.wait_for(task, 0.1) - except asyncio.CancelledError: - pass - except CustomCriticalError: - pass + try: + await asyncio.wait_for(task, 0.1) + except asyncio.CancelledError: + pass + except CustomCriticalError: + pass async def test_run_wait_until_cancelled(job_processor): From 8764082df979d3061a5d9915efbf8a543bd083c3 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 21 Jul 2024 15:31:36 +1000 Subject: [PATCH 015/375] remove use of queue and JobProcessor --- procrastinate/job_context.py | 10 +- procrastinate/job_processor.py | 225 --------------- procrastinate/worker.py | 266 ++++++++++++----- tests/conftest.py | 4 +- tests/integration/test_worker.py | 126 --------- tests/unit/test_job_context.py | 27 +- tests/unit/test_job_processor.py | 472 ------------------------------- tests/unit/test_worker.py | 447 ++++++++++++++++++++++++++--- 8 files changed, 614 insertions(+), 963 deletions(-) delete mode 100644 procrastinate/job_processor.py delete mode 100644 tests/integration/test_worker.py delete mode 100644 tests/unit/test_job_processor.py diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index a35fe5582..1a003160b 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -34,7 +34,7 @@ def as_dict(self): return result -@attr.dataclass(frozen=True, kw_only=True) +@attr.dataclass(frozen=True, kw_only=True, eq=False) class JobContext: """ Execution context of a running job. @@ -49,8 +49,6 @@ class JobContext: Name of the worker (may be useful for logging) worker_queues : ``Optional[Iterable[str]]`` Queues listened by this worker - worker_id : ``int``` - In case there are multiple async sub-workers, this is the id of the sub-worker. job : `Job` Current `Job` instance task : `Task` @@ -60,18 +58,18 @@ class JobContext: app: app_module.App | None = None worker_name: str | None = None worker_queues: Iterable[str] | None = None - worker_id: int | None = None job: jobs.Job | None = None task: tasks.Task | None = None job_result: JobResult = attr.ib(factory=JobResult) additional_context: dict = attr.ib(factory=dict) + task_result: Any = None def log_extra(self, action: str, **kwargs: Any) -> types.JSONDict: extra: types.JSONDict = { "action": action, "worker": { "name": self.worker_name, - "id": self.worker_id, + "job_id": self.job.id if self.job else None, "queues": self.worker_queues, }, } @@ -91,7 +89,7 @@ def queues_display(self) -> str: return "all queues" def job_description(self, current_timestamp: float) -> str: - message = f"worker {self.worker_id}: " + message = "worker: " if self.job: message += self.job.call_string duration = self.job_result.duration(current_timestamp) diff --git a/procrastinate/job_processor.py b/procrastinate/job_processor.py deleted file mode 100644 index 5653891cf..000000000 --- a/procrastinate/job_processor.py +++ /dev/null @@ -1,225 +0,0 @@ -from __future__ import annotations - -import asyncio -import functools -import inspect -import logging -import time -from datetime import datetime -from typing import Awaitable, Callable - -from procrastinate import utils -from procrastinate.exceptions import JobAborted, TaskNotFound -from procrastinate.job_context import JobContext -from procrastinate.jobs import DeleteJobCondition, Job, Status -from procrastinate.manager import JobManager -from procrastinate.tasks import Task - - -def _find_task(task_registry: dict[str, Task], task_name: str) -> Task: - try: - return task_registry[task_name] - except KeyError as exc: - raise TaskNotFound from exc - - -class JobProcessor: - def __init__( - self, - *, - task_registry: dict[str, Task], - job_manager: JobManager, - job_queue: asyncio.Queue[Job], - job_semaphore: asyncio.Semaphore, - fetch_job_condition: asyncio.Condition, - worker_id: int, - base_context: JobContext, - logger: logging.Logger = logging.getLogger(__name__), - delete_jobs: str | DeleteJobCondition = DeleteJobCondition.NEVER.value, - ): - self.worker_id = worker_id - self._task_registry = task_registry - self._job_manager = job_manager - self._job_queue = job_queue - self._job_semaphore = job_semaphore - self._fetch_job_condition = fetch_job_condition - self._delete_jobs = ( - DeleteJobCondition(delete_jobs) - if isinstance(delete_jobs, str) - else delete_jobs - ) - self._base_context = base_context.evolve( - worker_id=self.worker_id, - additional_context=base_context.additional_context.copy(), - ) - - self.logger = logger - self.job_context: JobContext | None = None - self._retry_at: datetime | None = None - - def _create_job_context(self, job: Job) -> JobContext: - task = _find_task(self._task_registry, job.task_name) - return self._base_context.evolve(task=task, job=job) - - async def _persist_job_status(self, job: Job, status: Status): - if self._retry_at: - await self._job_manager.retry_job(job=job, retry_at=self._retry_at) - else: - delete_job = { - DeleteJobCondition.ALWAYS: True, - DeleteJobCondition.NEVER: False, - DeleteJobCondition.SUCCESSFUL: status == Status.SUCCEEDED, - }[self._delete_jobs] - await self._job_manager.finish_job( - job=job, status=status, delete_job=delete_job - ) - - self.job_context = None - self._job_queue.task_done() - self.logger.debug( - f"Acknowledged job completion {job.call_string}", - extra=self._base_context.log_extra(action="finish_task", status=status), - ) - - async def run(self): - while True: - job = await self._job_queue.get() - cancelled_error: asyncio.CancelledError | None = None - async with self._job_semaphore: - status = Status.FAILED - try: - self.job_context = self._create_job_context(job) - self.logger.debug( - f"Loaded job info, about to start job {job.call_string}", - extra=self.job_context.log_extra(action="loaded_job_info"), - ) - process_job_task = asyncio.create_task(self._process_job()) - - try: - # the job is shielded from cancellation to enable graceful stop - await asyncio.shield(process_job_task) - except asyncio.CancelledError as e: - cancelled_error = e - await process_job_task - status = Status.SUCCEEDED - raise - - status = Status.SUCCEEDED - except TaskNotFound as exc: - self.logger.exception( - f"Task was not found: {exc}", - extra=self._base_context.log_extra( - action="task_not_found", exception=str(exc) - ), - ) - except JobAborted: - status = Status.ABORTED - except Exception: - # exception is already logged by _process_job, carry on - # if there is a pending cancellation, this needs to be reraised here - if cancelled_error: - raise cancelled_error - finally: - persist_job_status_task = asyncio.create_task( - self._persist_job_status(job=job, status=status) - ) - try: - # prevent cancellation from stopping persistence of job status - await asyncio.shield(persist_job_status_task) - except asyncio.CancelledError: - await persist_job_status_task - raise - - async with self._fetch_job_condition: - self._fetch_job_condition.notify() - - async def _process_job(self): - assert self.job_context - assert self.job_context.task - assert self.job_context.job - assert self.job_context.job_result - - task = self.job_context.task - job = self.job_context.job - job_result = self.job_context.job_result - - start_time = time.time() - job_result.start_timestamp = start_time - self.logger.info( - f"Starting job {self.job_context.job.call_string}", - extra=self.job_context.log_extra(action="start_job"), - ) - - job_args = [] - - if task.pass_context: - job_args.append(self.job_context) - - task_result = None - log_title = "Error" - log_action = "job_error" - log_level = logging.ERROR - exc_info: bool | BaseException = False - - await_func: Callable[..., Awaitable] - if inspect.iscoroutinefunction(task.func): - await_func = task - else: - await_func = functools.partial(utils.sync_to_async, task) - - try: - task_result = await await_func(*job_args, **job.task_kwargs) - # In some cases, the task function might be a synchronous function - # that returns an awaitable without actually being a - # coroutinefunction. In that case, in the await above, we haven't - # actually called the task, but merely generated the awaitable that - # implements the task. In that case, we want to wait this awaitable. - # It's easy enough to be in that situation that the best course of - # action is probably to await the awaitable. - # It's not even sure it's worth emitting a warning - if inspect.isawaitable(task_result): - task_result = await task_result - except JobAborted as e: - task_result = None - log_title = "Aborted" - log_action = "job_aborted" - log_level = logging.INFO - exc_info = e - raise - except BaseException as e: - task_result = None - log_title = "Error" - log_action = "job_error" - log_level = logging.ERROR - exc_info = e - - job_retry = task.get_retry_exception(exception=e, job=job) - if job_retry: - self._retry_at = job_retry.scheduled_at - log_title = "Error, to retry" - log_action = "job_error_retry" - log_level = logging.INFO - else: - self._retry_at = None - raise - else: - log_title = "Success" - log_action = "job_success" - log_level = logging.INFO - exc_info = False - self._retry_at = None - finally: - end_time = time.time() - duration = end_time - start_time - job_result.end_timestamp = end_time - job_result.result = task_result - - extra = self.job_context.log_extra(action=log_action) - - text = ( - f"Job {job.call_string} ended with status: {log_title}, " - f"lasted {duration:.3f} s" - ) - if task_result: - text += f" - Result: {task_result}"[:250] - self.logger.log(log_level, text, extra=extra, exc_info=exc_info) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index b8dfb2968..9bf3474cd 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -2,16 +2,18 @@ import asyncio import contextlib +import functools +import inspect import logging import time -from typing import Any, Iterable +from datetime import datetime +from typing import Any, Awaitable, Callable, Iterable from procrastinate import signals, utils from procrastinate.app import App -from procrastinate.exceptions import TaskNotFound +from procrastinate.exceptions import JobAborted, JobRetry, TaskNotFound from procrastinate.job_context import JobContext -from procrastinate.job_processor import JobProcessor -from procrastinate.jobs import DeleteJobCondition, Job +from procrastinate.jobs import DeleteJobCondition, Job, Status from procrastinate.periodic import PeriodicDeferrer from procrastinate.tasks import Task @@ -43,7 +45,11 @@ def __init__( self.wait = wait self.polling_interval = timeout self.listen_notify = listen_notify - self.delete_jobs = delete_jobs + self.delete_jobs = ( + DeleteJobCondition(delete_jobs) + if isinstance(delete_jobs, str) + else delete_jobs + ) self.additional_context = additional_context self.install_signal_handlers = install_signal_handlers @@ -83,6 +89,147 @@ def find_task(self, task_name: str) -> Task: except KeyError as exc: raise TaskNotFound from exc + async def _persist_job_status( + self, job: Job, status: Status, retry_at: datetime | None + ): + if retry_at: + await self.app.job_manager.retry_job(job=job, retry_at=retry_at) + else: + delete_job = { + DeleteJobCondition.ALWAYS: True, + DeleteJobCondition.NEVER: False, + DeleteJobCondition.SUCCESSFUL: status == Status.SUCCEEDED, + }[self.delete_jobs] + await self.app.job_manager.finish_job( + job=job, status=status, delete_job=delete_job + ) + + @staticmethod + def _log_job_outcome( + status: Status, + job_context: JobContext, + job_retry: JobRetry | None, + exc_info: bool | BaseException = False, + ): + assert job_context.job + assert job_context.job_result + assert job_context.job_result.start_timestamp + assert job_context.job_result.end_timestamp + + if status == Status.SUCCEEDED: + log_action, log_title = "job_success", "Success" + elif status == Status.ABORTED: + log_action, log_title = "job_aborted", "Aborted" + elif job_retry: + log_action, log_title = "job_error_retry", "Error, to retry" + else: + log_action, log_title = "job_error", "Error" + + duration = ( + job_context.job_result.end_timestamp + - job_context.job_result.start_timestamp + ) + text = ( + f"Job {job_context.job.call_string} ended with status: {log_title}, " + f"lasted {duration:.3f} s" + ) + if job_context.job_result.result: + text += f" - Result: {job_context.job_result.result}"[:250] + + extra = job_context.log_extra(action=log_action) + log_level = logging.ERROR if status == Status.FAILED else logging.INFO + logger.log(log_level, text, extra=extra, exc_info=exc_info) + + async def _process_job(self, job_context: JobContext): + """ + Processes a given job and persists its status + """ + task = job_context.task + job_retry = None + exc_info = False + retry_at = None + job = job_context.job + assert job + + job_result = job_context.job_result + job_result.start_timestamp = time.time() + + try: + if not task: + raise TaskNotFound + + self.logger.debug( + f"Loaded job info, about to start job {job.call_string}", + extra=job_context.log_extra(action="loaded_job_info"), + ) + + self.logger.info( + f"Starting job {job.call_string}", + extra=job_context.log_extra(action="start_job"), + ) + + exc_info: bool | BaseException = False + + await_func: Callable[..., Awaitable] + if inspect.iscoroutinefunction(task.func): + await_func = task + else: + await_func = functools.partial(utils.sync_to_async, task) + + job_args = [job_context] if task.pass_context else [] + task_result = await await_func(*job_args, **job.task_kwargs) + # In some cases, the task function might be a synchronous function + # that returns an awaitable without actually being a + # coroutinefunction. In that case, in the await above, we haven't + # actually called the task, but merely generated the awaitable that + # implements the task. In that case, we want to wait this awaitable. + # It's easy enough to be in that situation that the best course of + # action is probably to await the awaitable. + # It's not even sure it's worth emitting a warning + if inspect.isawaitable(task_result): + task_result = await task_result + job_result.result = task_result + + except BaseException as e: + exc_info = e + if not isinstance(e, JobAborted): + job_retry = ( + task.get_retry_exception(exception=e, job=job) if task else None + ) + retry_at = job_retry.scheduled_at if job_retry else None + if isinstance(e, TaskNotFound): + self.logger.exception( + f"Task was not found: {e}", + extra=self.base_context.log_extra( + action="task_not_found", exception=str(e) + ), + ) + if not isinstance(e, Exception): + raise + + finally: + job_result.end_timestamp = time.time() + + if isinstance(exc_info, JobAborted): + status = Status.ABORTED + elif exc_info: + status = Status.FAILED + else: + status = Status.SUCCEEDED + + Worker._log_job_outcome( + status=status, + job_context=job_context, + job_retry=job_retry, + exc_info=exc_info, + ) + await self._persist_job_status(job=job, status=status, retry_at=retry_at) + + self.logger.debug( + f"Acknowledged job completion {job.call_string}", + extra=self.base_context.log_extra(action="finish_task", status=status), + ) + async def run(self): self._run_task = asyncio.current_task() notify_event = asyncio.Event() @@ -94,25 +241,9 @@ async def run(self): ), ) - job_queue: asyncio.Queue[Job] = asyncio.Queue(self.concurrency) job_semaphore = asyncio.Semaphore(self.concurrency) - fetch_job_condition = asyncio.Condition() - job_processors = [ - JobProcessor( - task_registry=self.app.tasks, - base_context=self.base_context, - delete_jobs=self.delete_jobs, - job_manager=self.app.job_manager, - job_queue=job_queue, - job_semaphore=job_semaphore, - fetch_job_condition=fetch_job_condition, - worker_id=worker_id, - logger=self.logger, - ) - for worker_id in range(self.concurrency) - ] - job_processors_task = asyncio.gather(*(p.run() for p in job_processors)) + running_jobs: dict[JobContext, asyncio.Task] = {} side_tasks = [asyncio.create_task(self.periodic_deferrer())] if self.wait and self.listen_notify: listener_coro = self.app.job_manager.listen_for_jobs( @@ -130,25 +261,37 @@ async def run(self): while True: out_of_job = None while not out_of_job: - # we don't want to fetch any new job if all processors are busy - # or when the queue is already full - # it is preferable to let any other procrastinate worker process handle those - # jobs until we are ready to process more - async with fetch_job_condition: - await fetch_job_condition.wait_for( - lambda: not job_queue.full() - and not job_semaphore.locked() + # acquire job_semaphore so that a new job is not fetched if maximum concurrency is reached + async with job_semaphore: + job = await self.app.job_manager.fetch_job( + queues=self.queues ) - job = await self.app.job_manager.fetch_job(queues=self.queues) if job: - # once a job has been fetched, we don't want to be cancelled until we put the job - # in the queue. For this reason, we prefer job_queue.put_nowait to job_queue.put - # - # The cleanup process ensures any job in the queue is awaited. - # - # We also made sure the queue is not full before fetching the job. - # Given only this worker adds to the queue, we don't need to worry about QueueFull being raised - job_queue.put_nowait(job) + # job_semaphore should be acquired straight because it is + # only acquired when not at full capacity at this time + # however, shield it from cancellation in the unlikely event the worker + # is cancelled at that precise time to not abandon the job + await asyncio.shield(job_semaphore.acquire()) + + job_context = self.base_context.evolve( + additional_context=self.base_context.additional_context.copy(), + job=job, + task=self.app.tasks.get(job.task_name), + ) + job_task = asyncio.create_task( + self._process_job(job_context) + ) + running_jobs[job_context] = job_task + + def on_job_complete( + job_context: JobContext, _: asyncio.Task + ): + del running_jobs[job_context] + job_semaphore.release() + + job_task.add_done_callback( + functools.partial(on_job_complete, job_context) + ) else: out_of_job = True if out_of_job: @@ -159,53 +302,38 @@ async def run(self): action="stop_worker", queues=self.queues ), ) - # no more job to fetch and asked not to wait, exiting the loop break try: - # wait until notified a new job is available or until polling interval + # awaken when a notification that a new job is available + # or after specified polling interval elapses notify_event.clear() await asyncio.wait_for( notify_event.wait(), timeout=self.polling_interval ) except asyncio.TimeoutError: - # catch asyncio.TimeoutError and not TimeoutError as long as Python 3.10 and under are supported - # polling interval has passed, resume loop and attempt to fetch a job pass finally: await utils.cancel_and_capture_errors(side_tasks) - pending_job_contexts = [ - processor.job_context - for processor in job_processors - if processor.job_context - ] - now = time.time() - for context in pending_job_contexts: + for job_context in running_jobs.keys(): self.logger.info( "Waiting for job to finish: " - + context.job_description(current_timestamp=now), - extra=context.log_extra(action="ending_job"), + + job_context.job_description(current_timestamp=now), + extra=job_context.log_extra(action="ending_job"), ) - # make sure any job in progress or still in the queue is given time to be processed - await job_queue.join() - job_processors_task.cancel() - job_processors_task.add_done_callback( - lambda fut: self.logger.info( - f"Stopped worker on {self.base_context.queues_display}", - extra=self.base_context.log_extra( - action="stop_worker", queues=self.queues - ), - ) + # wait for any in progress job to complete processing + # use return_exceptions to not cancel other job tasks if one was to fail + await asyncio.gather( + *(task for task in running_jobs.values()), return_exceptions=True + ) + self.logger.info( + f"Stopped worker on {self.base_context.queues_display}", + extra=self.base_context.log_extra( + action="stop_worker", queues=self.queues + ), ) - - try: - await job_processors_task - except asyncio.CancelledError: - # if we didn't initiate the cancellation ourselves, bubble up the cancelled error - if self._run_task and self._run_task.cancelled(): - raise diff --git a/tests/conftest.py b/tests/conftest.py index 6e4ab323d..a008e456c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,12 +164,12 @@ def reset_builtin_task_names(): @pytest.fixture -def not_opened_app(connector, reset_builtin_task_names): +def not_opened_app(connector, reset_builtin_task_names) -> app_module.App: return app_module.App(connector=connector) @pytest.fixture -def app(not_opened_app): +def app(not_opened_app: app_module.App): with not_opened_app.open() as app: yield app diff --git a/tests/integration/test_worker.py b/tests/integration/test_worker.py deleted file mode 100644 index 849f3860f..000000000 --- a/tests/integration/test_worker.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -import asyncio -import contextlib -import signal -from typing import TYPE_CHECKING, cast - -import pytest - -from procrastinate import worker -from procrastinate.testing import InMemoryConnector - -if TYPE_CHECKING: - from procrastinate import App - - -# how long to wait before considering the test a fail -timeout = 0.05 - - -async def _wait_on_cancelled(task: asyncio.Task, timeout: float): - try: - await asyncio.wait_for(task, timeout=timeout) - except asyncio.CancelledError: - pass - except asyncio.TimeoutError: - pytest.fail("Failed to launch task within f{timeout}s") - - -@contextlib.asynccontextmanager -async def running_worker(app: App): - running_worker = worker.Worker(app=app, queues=["some_queue"]) - task = asyncio.ensure_future(running_worker.run()) - yield running_worker, task - running_worker.stop() - await _wait_on_cancelled(task, timeout=timeout) - - -async def test_run(app: App, caplog): - caplog.set_level("DEBUG") - - done = asyncio.Event() - - @app.task(queue="some_queue") - def t(): - done.set() - - async with running_worker(app): - await t.defer_async() - - try: - await asyncio.wait_for(done.wait(), timeout=timeout) - except asyncio.TimeoutError: - pytest.fail(f"Failed to launch task withing {timeout}s") - - connector = cast(InMemoryConnector, app.connector) - assert [q[0] for q in connector.queries] == [ - "defer_job", - "fetch_job", - "finish_job", - ] - - logs = {(r.action, r.levelname) for r in caplog.records} - # remove the periodic_deferrer_no_task log record because that makes the test flaky - assert { - ("about_to_defer_job", "DEBUG"), - ("job_defer", "INFO"), - ("loaded_job_info", "DEBUG"), - ("start_job", "INFO"), - ("job_success", "INFO"), - ("finish_task", "DEBUG"), - } <= logs - - -async def test_run_log_current_job_when_stopping(app: App, caplog): - caplog.set_level("DEBUG") - - async with running_worker(app) as (worker, worker_task): - - @app.task(queue="some_queue") - async def t(): - worker.stop() - - await t.defer_async() - - with pytest.raises(asyncio.CancelledError): - try: - await asyncio.wait_for(worker_task, timeout=timeout) - except asyncio.TimeoutError: - pytest.fail("Failed to launch task within f{timeout}s") - - # We want to make sure that the log that names the current running task fired. - logs = " ".join(r.message for r in caplog.records) - assert "Stop requested" in logs - assert ( - "Waiting for job to finish: worker 0: tests.integration.test_worker.t[1]()" - in logs - ) - - -async def test_run_no_listen_notify(app: App): - running_worker = worker.Worker(app=app, queues=["some_queue"], listen_notify=False) - task = asyncio.ensure_future(running_worker.run()) - try: - await asyncio.sleep(0.01) - connector = cast(InMemoryConnector, app.connector) - assert connector.notify_event is None - finally: - running_worker.stop() - await _wait_on_cancelled(task, timeout=timeout) - - -async def test_run_no_signal_handlers(app: App, kill_own_pid): - running_worker = worker.Worker( - app=app, queues=["some_queue"], install_signal_handlers=False - ) - - task = asyncio.ensure_future(running_worker.run()) - try: - with pytest.raises(KeyboardInterrupt): - await asyncio.sleep(0.01) - # Test that handlers are NOT installed - kill_own_pid(signal=signal.SIGINT) - finally: - running_worker.stop() - await _wait_on_cancelled(task, timeout) diff --git a/tests/unit/test_job_context.py b/tests/unit/test_job_context.py index 0d3a5fe0d..ca349d3da 100644 --- a/tests/unit/test_job_context.py +++ b/tests/unit/test_job_context.py @@ -58,52 +58,47 @@ def test_evolve(): def test_log_extra(): - context = job_context.JobContext( - worker_name="a", worker_id=2, additional_context={"ha": "ho"} - ) + context = job_context.JobContext(worker_name="a", additional_context={"ha": "ho"}) assert context.log_extra(action="foo", bar="baz") == { "action": "foo", "bar": "baz", - "worker": {"name": "a", "id": 2, "queues": None}, + "worker": {"name": "a", "job_id": None, "queues": None}, } def test_log_extra_job(job_factory): job = job_factory() - context = job_context.JobContext(worker_name="a", worker_id=2, job=job) + context = job_context.JobContext(worker_name="a", job=job) assert context.log_extra(action="foo") == { "action": "foo", "job": job.log_context(), - "worker": {"name": "a", "id": 2, "queues": None}, + "worker": {"name": "a", "job_id": job.id, "queues": None}, } def test_job_description_no_job(job_factory): - descr = job_context.JobContext(worker_name="a", worker_id=2).job_description( - current_timestamp=0 - ) - assert descr == "worker 2: no current job" + descr = job_context.JobContext(worker_name="a").job_description(current_timestamp=0) + assert descr == "worker: no current job" def test_job_description_job_no_time(job_factory): job = job_factory(task_name="some_task", id=12, task_kwargs={"a": "b"}) - descr = job_context.JobContext( - worker_name="a", worker_id=2, job=job - ).job_description(current_timestamp=0) - assert descr == "worker 2: some_task[12](a='b')" + descr = job_context.JobContext(worker_name="a", job=job).job_description( + current_timestamp=0 + ) + assert descr == "worker: some_task[12](a='b')" def test_job_description_job_time(job_factory): job = job_factory(task_name="some_task", id=12, task_kwargs={"a": "b"}) descr = job_context.JobContext( worker_name="a", - worker_id=2, job=job, job_result=job_context.JobResult(start_timestamp=20.0), ).job_description(current_timestamp=30.0) - assert descr == "worker 2: some_task[12](a='b') (started 10.000 s ago)" + assert descr == "worker: some_task[12](a='b') (started 10.000 s ago)" async def test_should_abort(app, job_factory): diff --git a/tests/unit/test_job_processor.py b/tests/unit/test_job_processor.py deleted file mode 100644 index 96399a7f5..000000000 --- a/tests/unit/test_job_processor.py +++ /dev/null @@ -1,472 +0,0 @@ -from __future__ import annotations - -import asyncio -from typing import cast - -import pytest - -from procrastinate.app import App -from procrastinate.exceptions import JobAborted -from procrastinate.job_context import JobContext -from procrastinate.job_processor import JobProcessor -from procrastinate.jobs import DeleteJobCondition, Job, Status -from procrastinate.testing import InMemoryConnector - - -class CustomCriticalError(BaseException): - pass - - -@pytest.fixture -def worker_name() -> str: - return "worker" - - -# it is important to make the fixture async because -# otherwise, the queue is created outside the main event loop on python 3.8 -@pytest.fixture -async def job_queue() -> asyncio.Queue[Job]: - return asyncio.Queue(2) - - -@pytest.fixture -def base_context(app, worker_name): - return JobContext( - app=app, worker_name=worker_name, additional_context={"foo": "bar"} - ) - - -@pytest.fixture -def job_semaphore(): - return asyncio.Semaphore(1) - - -@pytest.fixture -async def job_processor(request, app, base_context, job_queue, job_semaphore): - param = getattr(request, "param", None) - - delete_jobs = cast(str, param["delete_jobs"]) if param else None - return JobProcessor( - task_registry=app.tasks, - job_manager=app.job_manager, - base_context=base_context, - job_queue=job_queue, - job_semaphore=job_semaphore, - worker_id=2, - delete_jobs=delete_jobs or DeleteJobCondition.NEVER, - fetch_job_condition=asyncio.Condition(), - ) - - -@pytest.fixture(autouse=True, scope="function") -async def running_job_processor_task(job_processor): - task = asyncio.create_task(job_processor.run()) - yield task - if not task.cancelled(): - task.cancel() - try: - await asyncio.wait_for(task, 0.1) - except asyncio.CancelledError: - pass - except CustomCriticalError: - pass - - -async def test_run_wait_until_cancelled(job_processor): - with pytest.raises(asyncio.TimeoutError): - await asyncio.wait_for(job_processor.run(), 0.1) - - -async def test_run_job_async(app: App, job_queue): - result = [] - - @app.task(queue="yay", name="task_func") - async def task_func(a, b): - result.append(a + b) - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - assert result == [12] - - assert job.id - status = await app.job_manager.get_job_status_async(job.id) - assert status == Status.SUCCEEDED - - -async def test_run_job_status(app: App, job_queue): - result = [] - - @app.task(queue="yay", name="task_func") - async def task_func(a, b): - result.append(a + b) - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - assert job.id - status = await app.job_manager.get_job_status_async(job.id) - assert status == Status.SUCCEEDED - - -async def test_run_job_sync(app: App, job_queue): - result = [] - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - result.append(a + b) - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - assert result == [12] - - -async def test_run_job_semi_async(app: App, job_queue): - result = [] - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - async def inner(): - result.append(a + b) - - return inner() - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - assert result == [12] - - -async def test_run_job_log_result(caplog, app: App, job_queue): - caplog.set_level("INFO") - - @app.task(queue="yay", name="task_func") - async def task_func(a, b): - return a + b - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - records = [record for record in caplog.records if record.action == "job_success"] - assert len(records) == 1 - record = records[0] - assert record.result == 12 - assert "Result: 12" in record.message - - -async def test_run_job_aborted(caplog, app: App, job_queue): - caplog.set_level("INFO") - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - raise JobAborted() - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - records = [record for record in caplog.records if record.action == "job_aborted"] - assert len(records) == 1 - record = records[0] - assert record.levelname == "INFO" - assert "Aborted" in record.message - - -async def test_run_job_aborted_status(app: App, job_queue): - @app.task(queue="yay", name="task_func") - async def task_func(): - raise JobAborted() - - task_func.defer() - job = await app.job_manager.fetch_job(None) - assert job - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - assert job.id - status = await app.job_manager.get_job_status_async(job.id) - assert status == Status.ABORTED - - -async def test_run_job_error_log(caplog, app: App, job_queue): - caplog.set_level("INFO") - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - raise ValueError("Nope") - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - records = [record for record in caplog.records if record.action == "job_error"] - assert len(records) == 1 - record = records[0] - assert record.levelname == "ERROR" - assert "to retry" not in record.message - - -async def test_run_job_error_status(app: App, job_queue): - @app.task(queue="yay", name="task_func") - def task_func(): - raise ValueError("Nope") - - task_func.defer() - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - assert job.id - status = await app.job_manager.get_job_status_async(job.id) - assert status == Status.FAILED - - -@pytest.mark.parametrize( - "critical_error", - [ - (False), - (True), - ], -) -async def test_run_job_retry_failed_job(app: App, job_queue, critical_error): - @app.task(retry=1) - def task_func(): - raise CustomCriticalError("Nope") if critical_error else ValueError("Nope") - - task_func.defer() - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - connector = cast(InMemoryConnector, app.connector) - assert job.id - job_row = connector.jobs[job.id] - assert job_row["status"] == "todo" - assert job_row["scheduled_at"] is not None - assert job_row["attempts"] == 1 - - -async def test_run_job_critical_error( - caplog, app: App, job_queue, running_job_processor_task: asyncio.Task -): - caplog.set_level("INFO") - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - raise CustomCriticalError("Nope") - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - - with pytest.raises(BaseException, match="Nope"): - await asyncio.wait_for(job_queue.join(), 0.1) - await running_job_processor_task - - assert job.id - status = await app.job_manager.get_job_status_async(job.id) - assert status == Status.FAILED - - -async def test_run_task_not_found_log(caplog, app: App, job_queue): - caplog.set_level("INFO") - - @app.task(queue="yay", name="task_func") - def task_func(a, b): - return a + b - - task_func.defer(a=9, b=3) - job = await app.job_manager.fetch_job(None) - assert job - job = job.evolve(task_name="random_task_name") - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - records = [record for record in caplog.records if record.action == "task_not_found"] - assert len(records) == 1 - record = records[0] - assert record.levelname == "ERROR" - - -async def test_run_task_not_found_status(app: App, job_queue): - @app.task(queue="yay", name="task_func") - def task_func(): - pass - - task_func.defer() - job = await app.job_manager.fetch_job(None) - assert job - job = job.evolve(task_name="random_task_name") - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - assert job.id - status = await app.job_manager.get_job_status_async(job.id) - assert status == Status.FAILED - - -async def test_worker_copy_additional_context(app: App, job_queue, base_context): - base_context.additional_context["foo"] = "baz" - - @app.task(pass_context=True) - async def task_func(jobContext: JobContext): - assert jobContext.additional_context["foo"] == "bar" - - await task_func.defer_async() - job = await app.job_manager.fetch_job(None) - assert job - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - -async def test_worker_pass_worker_id_to_context(app: App, job_queue, job_processor): - assert job_processor.worker_id == 2 - - @app.task(pass_context=True) - async def task_func(jobContext: JobContext): - assert jobContext.worker_id == 2 - - await task_func.defer_async() - job = await app.job_manager.fetch_job(None) - assert job - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - -@pytest.mark.parametrize( - "job_processor, fail_task", - [ - ({"delete_jobs": "successful"}, False), - ({"delete_jobs": "always"}, False), - ({"delete_jobs": "always"}, True), - ], - indirect=["job_processor"], -) -async def test_process_job_with_deletion(app: App, job_queue, fail_task): - @app.task() - async def task_func(): - if fail_task: - raise ValueError("Nope") - - await task_func.defer_async() - job = await app.job_manager.fetch_job(None) - assert job - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - connector = cast(InMemoryConnector, app.connector) - assert job.id not in connector.jobs - - -@pytest.mark.parametrize( - "job_processor, fail_task", - [ - ({"delete_jobs": "never"}, False), - ({"delete_jobs": "never"}, True), - ({"delete_jobs": "successful"}, True), - ], - indirect=["job_processor"], -) -async def test_process_job_without_deletion(app: App, job_queue, fail_task): - @app.task() - async def task_func(): - if fail_task: - raise ValueError("Nope") - - await task_func.defer_async() - job = await app.job_manager.fetch_job(None) - assert job - - job_queue.put_nowait(job) - await asyncio.wait_for(job_queue.join(), 0.1) - - connector = cast(InMemoryConnector, app.connector) - assert job.id in connector.jobs - - -@pytest.mark.parametrize( - "fail_task", - [ - (False), - (True), - ], -) -async def test_process_job_notifies_completion( - app: App, job_queue, fail_task, running_job_processor_task, job_semaphore -): - @app.task() - async def task_func(): - if fail_task: - raise ValueError("Nope") - - await task_func.defer_async() - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - - await asyncio.wait_for(job_semaphore.acquire(), 0.1) - job_semaphore.release() - - -async def test_cancelling_processor_waits_for_task( - app: App, job_queue, running_job_processor_task: asyncio.Task -): - complete_task_event = asyncio.Event() - - @app.task() - async def task_func(): - await complete_task_event.wait() - - await task_func.defer_async() - job = await app.job_manager.fetch_job(None) - assert job - job_queue.put_nowait(job) - - # this should timeout because task is waiting for complete_task_event - with pytest.raises(asyncio.TimeoutError): - await asyncio.wait_for(job_queue.join(), 0.05) - - running_job_processor_task.cancel() - - # this should still timeout when cancelled because it is waiting for task to complete - with pytest.raises(asyncio.TimeoutError): - await asyncio.wait_for(job_queue.join(), 0.05) - - # tell the task to complete - complete_task_event.set() - - # this should successfully complete the job and re-raise the CancelledError - with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(running_job_processor_task, 0.1) - - assert job.id - status = await app.job_manager.get_job_status_async(job.id) - assert status == Status.SUCCEEDED diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index c106989dc..d6388dda4 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -1,16 +1,38 @@ from __future__ import annotations import asyncio +import signal from typing import cast import pytest from procrastinate.app import App -from procrastinate.jobs import Status +from procrastinate.exceptions import JobAborted +from procrastinate.job_context import JobContext +from procrastinate.jobs import DEFAULT_QUEUE, Job, Status from procrastinate.testing import InMemoryConnector from procrastinate.worker import Worker +async def start_worker(worker: Worker): + task = asyncio.create_task(worker.run()) + await asyncio.sleep(0.01) + return task + + +@pytest.fixture +async def worker(app: App, request: pytest.FixtureRequest): + kwargs = request.param if hasattr(request, "param") else {} + worker = Worker(app, **kwargs) + yield worker + if worker._run_task and not worker._run_task.done(): + try: + worker._run_task.cancel() + await asyncio.wait_for(worker._run_task, timeout=0.2) + except asyncio.CancelledError: + pass + + @pytest.mark.parametrize( "available_jobs, concurrency", [ @@ -71,41 +93,33 @@ async def test_worker_run_once_log_messages(app: App, caplog): ] -async def test_worker_run_wait_listen(app: App): - worker = Worker(app, wait=True, listen_notify=True, queues=["qq"]) - run_task = asyncio.create_task(worker.run()) - # wait just enough to make sure the task is running - await asyncio.sleep(0.01) - - connector = cast(InMemoryConnector, app.connector) +async def test_worker_run_wait_listen(worker): + await start_worker(worker) + connector = cast(InMemoryConnector, worker.app.connector) assert connector.notify_event - assert connector.notify_channels == ["procrastinate_queue#qq"] - - run_task.cancel() - try: - await asyncio.wait_for(run_task, timeout=0.2) - except asyncio.CancelledError: - pass + assert connector.notify_channels == ["procrastinate_any_queue"] @pytest.mark.parametrize( - "available_jobs, concurrency", + "available_jobs, worker", [ - (2, 1), - (3, 2), + (2, {"concurrency": 1}), + (3, {"concurrency": 2}), ], + indirect=["worker"], ) -async def test_worker_run_respects_concurrency(app: App, available_jobs, concurrency): - worker = Worker(app, wait=False, concurrency=concurrency) - run_task = asyncio.create_task(worker.run()) - +async def test_worker_run_respects_concurrency( + worker: Worker, app: App, available_jobs +): complete_tasks = asyncio.Event() @app.task async def perform_job(): await complete_tasks.wait() + await start_worker(worker) + for _ in range(available_jobs): await perform_job.defer_async() @@ -117,25 +131,20 @@ async def perform_job(): doings_jobs = list(connector.list_jobs_all(status=Status.DOING.value)) todo_jobs = list(connector.list_jobs_all(status=Status.TODO.value)) - assert len(doings_jobs) == concurrency - assert len(todo_jobs) == available_jobs - concurrency + assert len(doings_jobs) == worker.concurrency + assert len(todo_jobs) == available_jobs - worker.concurrency complete_tasks.set() - await asyncio.wait_for(run_task, 0.1) -async def test_worker_run_fetches_job_on_notification(app: App): - worker = Worker(app, wait=True, concurrency=1) - - run_task = asyncio.create_task(worker.run()) - +async def test_worker_run_fetches_job_on_notification(worker, app: App): complete_tasks = asyncio.Event() @app.task async def perform_job(): await complete_tasks.wait() - await asyncio.sleep(0.01) + await start_worker(worker) connector = cast(InMemoryConnector, app.connector) @@ -151,19 +160,15 @@ async def perform_job(): assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 2 complete_tasks.set() - run_task.cancel() - try: - await asyncio.wait_for(run_task, timeout=0.2) - except asyncio.CancelledError: - pass - - -async def test_worker_run_respects_polling(app: App): - worker = Worker(app, wait=True, concurrency=1, timeout=0.05) - run_task = asyncio.create_task(worker.run()) - await asyncio.sleep(0.01) +@pytest.mark.parametrize( + "worker", + [({"timeout": 0.05})], + indirect=["worker"], +) +async def test_worker_run_respects_polling(worker, app): + await start_worker(worker) connector = cast(InMemoryConnector, app.connector) assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 1 @@ -171,8 +176,356 @@ async def test_worker_run_respects_polling(app: App): await asyncio.sleep(0.05) assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 2 - run_task.cancel() - try: - await asyncio.wait_for(run_task, timeout=0.2) - except asyncio.CancelledError: - pass + + +@pytest.mark.parametrize( + "worker, fail_task", + [ + ({"delete_jobs": "never"}, False), + ({"delete_jobs": "never"}, True), + ({"delete_jobs": "successful"}, True), + ], + indirect=["worker"], +) +async def test_process_job_without_deletion(app: App, worker, fail_task): + @app.task() + async def task_func(): + if fail_task: + raise ValueError("Nope") + + job_id = await task_func.defer_async() + + await start_worker(worker) + + connector = cast(InMemoryConnector, app.connector) + assert job_id in connector.jobs + + +@pytest.mark.parametrize( + "worker, fail_task", + [ + ({"delete_jobs": "successful"}, False), + ({"delete_jobs": "always"}, False), + ({"delete_jobs": "always"}, True), + ], + indirect=["worker"], +) +async def test_process_job_with_deletion(app: App, worker, fail_task): + @app.task() + async def task_func(): + if fail_task: + raise ValueError("Nope") + + job_id = await task_func.defer_async() + + await start_worker(worker) + + connector = cast(InMemoryConnector, app.connector) + assert job_id not in connector.jobs + + +async def test_stopping_worker_waits_for_task(app: App, worker): + complete_task_event = asyncio.Event() + + @app.task() + async def task_func(): + await complete_task_event.wait() + + run_task = await start_worker(worker) + + job_id = await task_func.defer_async() + + await asyncio.sleep(0.05) + + # this should still be running waiting for the task to complete + assert run_task.done() is False + + # tell the task to complete + complete_task_event.set() + + # this should successfully complete the job and re-raise the CancelledError + with pytest.raises(asyncio.CancelledError): + run_task.cancel() + await asyncio.wait_for(run_task, 0.1) + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + +@pytest.mark.parametrize( + "worker", + [({"additional_context": {"foo": "bar"}})], + indirect=["worker"], +) +async def test_worker_passes_additional_context(app: App, worker): + @app.task(pass_context=True) + async def task_func(jobContext: JobContext): + assert jobContext.additional_context["foo"] == "bar" + + job_id = await task_func.defer_async() + + await start_worker(worker) + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + +async def test_run_job_async(app: App, worker): + result = [] + + @app.task(queue="yay", name="task_func") + async def task_func(a, b): + result.append(a + b) + + job_id = task_func.defer(a=9, b=3) + + await start_worker(worker) + assert result == [12] + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + +async def test_run_job_sync(app: App, worker): + result = [] + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + result.append(a + b) + + job_id = task_func.defer(a=9, b=3) + + await start_worker(worker) + assert result == [12] + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + +async def test_run_job_semi_async(app: App, worker): + result = [] + + @app.task(queue="yay", name="task_func") + def task_func(a, b): + async def inner(): + result.append(a + b) + + return inner() + + job_id = task_func.defer(a=9, b=3) + + await start_worker(worker) + + assert result == [12] + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + +async def test_run_job_log_result(caplog, app: App, worker): + caplog.set_level("INFO") + + @app.task(queue="yay", name="task_func") + async def task_func(a, b): + return a + b + + task_func.defer(a=9, b=3) + + await start_worker(worker) + + records = [record for record in caplog.records if record.action == "job_success"] + assert len(records) == 1 + record = records[0] + assert record.result == 12 + assert "Result: 12" in record.message + + +async def test_run_task_not_found_status(app: App, worker, caplog): + job = await app.job_manager.defer_job_async( + Job( + task_name="random_task_name", + queue=DEFAULT_QUEUE, + lock=None, + queueing_lock=None, + ) + ) + assert job.id + + await start_worker(worker) + await asyncio.sleep(0.01) + status = await app.job_manager.get_job_status_async(job.id) + assert status == Status.FAILED + + records = [record for record in caplog.records if record.action == "task_not_found"] + assert len(records) == 1 + record = records[0] + assert record.levelname == "ERROR" + + +class CustomCriticalError(BaseException): + pass + + +@pytest.mark.parametrize( + "critical_error", + [ + (False), + (True), + ], +) +async def test_run_job_error(app: App, worker, critical_error, caplog): + @app.task(queue="yay", name="task_func") + def task_func(a, b): + raise CustomCriticalError("Nope") if critical_error else ValueError("Nope") + + job_id = task_func.defer(a=9, b=3) + + await start_worker(worker) + + await asyncio.sleep(0.01) + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.FAILED + + records = [record for record in caplog.records if record.action == "job_error"] + assert len(records) == 1 + record = records[0] + assert record.levelname == "ERROR" + assert "to retry" not in record.message + + +async def test_run_job_aborted(app: App, worker, caplog): + caplog.set_level("INFO") + + @app.task(queue="yay", name="task_func") + async def task_func(): + raise JobAborted() + + job_id = task_func.defer() + + await start_worker(worker) + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.ABORTED + + records = [record for record in caplog.records if record.action == "job_aborted"] + assert len(records) == 1 + record = records[0] + assert record.levelname == "INFO" + assert "Aborted" in record.message + + +@pytest.mark.parametrize( + "critical_error, recover_on_attempt_number, expected_status, expected_attempts", + [ + (False, 2, "succeeded", 2), + (True, 2, "succeeded", 2), + (False, 3, "failed", 2), + (True, 3, "failed", 2), + ], +) +async def test_run_job_retry_failed_job( + app: App, + worker, + critical_error, + recover_on_attempt_number, + expected_status, + expected_attempts, +): + worker.wait = False + + attempt = 0 + + @app.task(retry=1) + def task_func(): + nonlocal attempt + attempt += 1 + if attempt < recover_on_attempt_number: + raise CustomCriticalError("Nope") if critical_error else ValueError("Nope") + + job_id = task_func.defer() + + await start_worker(worker) + + await asyncio.sleep(0.01) + + connector = cast(InMemoryConnector, app.connector) + job_row = connector.jobs[job_id] + assert job_row["status"] == expected_status + assert job_row["attempts"] == expected_attempts + + +async def test_run_log_actions(app: App, caplog, worker): + caplog.set_level("DEBUG") + + done = asyncio.Event() + + @app.task(queue="some_queue") + def t(): + done.set() + + await t.defer_async() + + await start_worker(worker) + + await asyncio.wait_for(done.wait(), timeout=0.05) + + connector = cast(InMemoryConnector, app.connector) + assert [q[0] for q in connector.queries] == [ + "defer_job", + "fetch_job", + "finish_job", + "fetch_job", + ] + + logs = {(r.action, r.levelname) for r in caplog.records} + # remove the periodic_deferrer_no_task log record because that makes the test flaky + assert { + ("about_to_defer_job", "DEBUG"), + ("job_defer", "INFO"), + ("start_worker", "INFO"), + ("loaded_job_info", "DEBUG"), + ("start_job", "INFO"), + ("job_success", "INFO"), + ("finish_task", "DEBUG"), + } <= logs + + +async def test_run_log_current_job_when_stopping(app: App, worker, caplog): + caplog.set_level("DEBUG") + complete_job_event = asyncio.Event() + + @app.task(queue="some_queue") + async def t(): + await complete_job_event.wait() + + job_id = await t.defer_async() + run_task = await start_worker(worker) + worker.stop() + await asyncio.sleep(0.01) + complete_job_event.set() + + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(run_task, timeout=0.05) + # We want to make sure that the log that names the current running task fired. + logs = " ".join(r.message for r in caplog.records) + assert "Stop requested" in logs + assert ( + f"Waiting for job to finish: worker: tests.unit.test_worker.t[{job_id}]()" + in logs + ) + + +async def test_run_no_listen_notify(app: App, worker): + worker.listen_notify = False + await start_worker(worker) + connector = cast(InMemoryConnector, app.connector) + assert connector.notify_event is None + + +async def test_run_no_signal_handlers(worker, kill_own_pid): + worker.install_signal_handlers = False + await start_worker(worker) + with pytest.raises(KeyboardInterrupt): + await asyncio.sleep(0.01) + # Test that handlers are NOT installed + kill_own_pid(signal=signal.SIGINT) From 17084e35019d0c61dc76fce738e5af41fd108552 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 21 Jul 2024 20:22:52 +1000 Subject: [PATCH 016/375] make JobContext require app and job --- procrastinate/job_context.py | 43 ++++--------- procrastinate/utils.py | 7 ++ procrastinate/worker.py | 107 +++++++++++++++++++------------ tests/unit/test_blueprints.py | 1 - tests/unit/test_builtin_tasks.py | 16 ++++- tests/unit/test_job_context.py | 47 +++----------- tests/unit/test_utils.py | 7 ++ tests/unit/test_worker_sync.py | 4 +- 8 files changed, 113 insertions(+), 119 deletions(-) diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index 1a003160b..449281a2c 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -6,7 +6,7 @@ import attr from procrastinate import app as app_module -from procrastinate import jobs, tasks, types +from procrastinate import jobs, tasks, utils @attr.dataclass(kw_only=True) @@ -38,8 +38,7 @@ def as_dict(self): class JobContext: """ Execution context of a running job. - In theory, all attributes are optional. In practice, in a task, they will - always be set to their proper value. + Attributes ---------- @@ -52,51 +51,31 @@ class JobContext: job : `Job` Current `Job` instance task : `Task` - Current `Task` instance + Current `Task` instance. This can be None when the a task cannot be found for a given job. + Any task function being called with a job context can be guaranteed to have its own task instance set. """ - app: app_module.App | None = None + app: app_module.App worker_name: str | None = None worker_queues: Iterable[str] | None = None - job: jobs.Job | None = None + job: jobs.Job task: tasks.Task | None = None job_result: JobResult = attr.ib(factory=JobResult) additional_context: dict = attr.ib(factory=dict) task_result: Any = None - def log_extra(self, action: str, **kwargs: Any) -> types.JSONDict: - extra: types.JSONDict = { - "action": action, - "worker": { - "name": self.worker_name, - "job_id": self.job.id if self.job else None, - "queues": self.worker_queues, - }, - } - if self.job: - extra["job"] = self.job.log_context() - - return {**extra, **self.job_result.as_dict(), **kwargs} - def evolve(self, **update: Any) -> JobContext: return attr.evolve(self, **update) @property def queues_display(self) -> str: - if self.worker_queues: - return f"queues {', '.join(self.worker_queues)}" - else: - return "all queues" + return utils.queues_display(self.worker_queues) def job_description(self, current_timestamp: float) -> str: - message = "worker: " - if self.job: - message += self.job.call_string - duration = self.job_result.duration(current_timestamp) - if duration is not None: - message += f" (started {duration:.3f} s ago)" - else: - message += "no current job" + message = f"worker: {self.job.call_string}" + duration = self.job_result.duration(current_timestamp) + if duration is not None: + message += f" (started {duration:.3f} s ago)" return message diff --git a/procrastinate/utils.py b/procrastinate/utils.py index ad3492a6e..26065b38f 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -309,3 +309,10 @@ def async_context_decorator(func: Callable) -> Callable: def datetime_from_timedelta_params(params: TimeDeltaParams) -> datetime.datetime: return utcnow() + datetime.timedelta(**params) + + +def queues_display(queues: Iterable[str] | None) -> str: + if queues: + return f"queues {', '.join(queues)}" + else: + return "all queues" diff --git a/procrastinate/worker.py b/procrastinate/worker.py index aa563dfdb..2e507f048 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -8,10 +8,10 @@ import time from typing import Any, Awaitable, Callable, Iterable -from procrastinate import signals, utils +from procrastinate import signals, types, utils from procrastinate.app import App from procrastinate.exceptions import JobAborted, JobRetry, TaskNotFound -from procrastinate.job_context import JobContext +from procrastinate.job_context import JobContext, JobResult from procrastinate.jobs import DeleteJobCondition, Job, Status from procrastinate.periodic import PeriodicDeferrer from procrastinate.retry import RetryDecision @@ -58,19 +58,12 @@ def __init__( else: self.logger = logger - self.base_context = JobContext( - app=app, - worker_name=self.worker_name, - worker_queues=self.queues, - additional_context=additional_context.copy() if additional_context else {}, - ) - self._run_task: asyncio.Task | None = None def stop(self): self.logger.info( "Stop requested", - extra=self.base_context.log_extra(action="stopping_worker"), + extra=self._log_extra(job_context=None, action="stopping_worker"), ) if self._run_task: @@ -89,6 +82,26 @@ def find_task(self, task_name: str) -> Task: except KeyError as exc: raise TaskNotFound from exc + def _log_extra( + self, action: str, job_context: JobContext | None, **kwargs: Any + ) -> types.JSONDict: + extra: types.JSONDict = { + "action": action, + "worker": { + "name": self.worker_name, + "job_id": job_context.job.id if job_context else None, + "queues": self.queues, + }, + } + if job_context: + extra["job"] = job_context.job.log_context() + + return { + **extra, + **(job_context.job_result if job_context else JobResult()).as_dict(), + **kwargs, + } + async def _persist_job_status( self, job: Job, status: Status, retry_decision: RetryDecision | None ): @@ -110,18 +123,13 @@ async def _persist_job_status( job=job, status=status, delete_job=delete_job ) - @staticmethod def _log_job_outcome( + self, status: Status, job_context: JobContext, job_retry: JobRetry | None, exc_info: bool | BaseException = False, ): - assert job_context.job - assert job_context.job_result - assert job_context.job_result.start_timestamp - assert job_context.job_result.end_timestamp - if status == Status.SUCCEEDED: log_action, log_title = "job_success", "Success" elif status == Status.ABORTED: @@ -131,18 +139,20 @@ def _log_job_outcome( else: log_action, log_title = "job_error", "Error" - duration = ( - job_context.job_result.end_timestamp - - job_context.job_result.start_timestamp - ) - text = ( - f"Job {job_context.job.call_string} ended with status: {log_title}, " - f"lasted {duration:.3f} s" - ) + text = f"Job {job_context.job.call_string} ended with status: {log_title}, " + if ( + job_context.job_result.start_timestamp + and job_context.job_result.end_timestamp + ): + duration = ( + job_context.job_result.end_timestamp + - job_context.job_result.start_timestamp + ) + text += f"lasted {duration:.3f} s" if job_context.job_result.result: text += f" - Result: {job_context.job_result.result}"[:250] - extra = job_context.log_extra(action=log_action) + extra = self._log_extra(job_context=job_context, action=log_action) log_level = logging.ERROR if status == Status.FAILED else logging.INFO logger.log(log_level, text, extra=extra, exc_info=exc_info) @@ -166,12 +176,14 @@ async def _process_job(self, job_context: JobContext): self.logger.debug( f"Loaded job info, about to start job {job.call_string}", - extra=job_context.log_extra(action="loaded_job_info"), + extra=self._log_extra( + job_context=job_context, action="loaded_job_info" + ), ) self.logger.info( f"Starting job {job.call_string}", - extra=job_context.log_extra(action="start_job"), + extra=self._log_extra(job_context=job_context, action="start_job"), ) exc_info: bool | BaseException = False @@ -206,8 +218,10 @@ async def _process_job(self, job_context: JobContext): if isinstance(e, TaskNotFound): self.logger.exception( f"Task was not found: {e}", - extra=self.base_context.log_extra( - action="task_not_found", exception=str(e) + extra=self._log_extra( + job_context=job_context, + action="task_not_found", + exception=str(e), ), ) if not isinstance(e, Exception): @@ -223,7 +237,7 @@ async def _process_job(self, job_context: JobContext): else: status = Status.SUCCEEDED - Worker._log_job_outcome( + self._log_job_outcome( status=status, job_context=job_context, job_retry=job_retry, @@ -235,7 +249,9 @@ async def _process_job(self, job_context: JobContext): self.logger.debug( f"Acknowledged job completion {job.call_string}", - extra=self.base_context.log_extra(action="finish_task", status=status), + extra=self._log_extra( + action="finish_task", job_context=job_context, status=status + ), ) async def run(self): @@ -243,9 +259,9 @@ async def run(self): notify_event = asyncio.Event() self.logger.info( - f"Starting worker on {self.base_context.queues_display}", - extra=self.base_context.log_extra( - action="start_worker", queues=self.queues + f"Starting worker on {utils.queues_display(self.queues)}", + extra=self._log_extra( + action="start_worker", job_context=None, queues=self.queues ), ) @@ -281,8 +297,13 @@ async def run(self): # is cancelled at that precise time to not abandon the job await asyncio.shield(job_semaphore.acquire()) - job_context = self.base_context.evolve( - additional_context=self.base_context.additional_context.copy(), + job_context = JobContext( + app=self.app, + worker_name=self.worker_name, + worker_queues=self.queues, + additional_context=self.additional_context.copy() + if self.additional_context + else {}, job=job, task=self.app.tasks.get(job.task_name), ) @@ -306,8 +327,10 @@ def on_job_complete( if not self.wait: self.logger.info( "No job found. Stopping worker because wait=False", - extra=self.base_context.log_extra( - action="stop_worker", queues=self.queues + extra=self._log_extra( + job_context=None, + action="stop_worker", + queues=self.queues, ), ) break @@ -331,7 +354,7 @@ def on_job_complete( self.logger.info( "Waiting for job to finish: " + job_context.job_description(current_timestamp=now), - extra=job_context.log_extra(action="ending_job"), + extra=self._log_extra(job_context=None, action="ending_job"), ) # wait for any in progress job to complete processing @@ -340,8 +363,8 @@ def on_job_complete( *(task for task in running_jobs.values()), return_exceptions=True ) self.logger.info( - f"Stopped worker on {self.base_context.queues_display}", - extra=self.base_context.log_extra( - action="stop_worker", queues=self.queues + f"Stopped worker on {utils.queues_display(self.queues)}", + extra=self._log_extra( + action="stop_worker", queues=self.queues, job_context=None ), ) diff --git a/tests/unit/test_blueprints.py b/tests/unit/test_blueprints.py index 188cb6e4f..c677dbe25 100644 --- a/tests/unit/test_blueprints.py +++ b/tests/unit/test_blueprints.py @@ -219,7 +219,6 @@ def test_blueprint_task_explicit(blueprint: blueprints.Blueprint, mocker): def my_task(context: JobContext): return "foo" - assert my_task(JobContext()) == "foo" assert blueprint.tasks["foobar"].name == "foobar" assert blueprint.tasks["foobar"].queue == "bar" assert blueprint.tasks["foobar"].lock == "sher" diff --git a/tests/unit/test_builtin_tasks.py b/tests/unit/test_builtin_tasks.py index 7c577b478..2679dc980 100644 --- a/tests/unit/test_builtin_tasks.py +++ b/tests/unit/test_builtin_tasks.py @@ -1,13 +1,23 @@ from __future__ import annotations +from typing import cast + from procrastinate import builtin_tasks, job_context +from procrastinate.app import App +from procrastinate.testing import InMemoryConnector -async def test_remove_old_jobs(app): +async def test_remove_old_jobs(app: App, job_factory): + job = job_factory() await builtin_tasks.remove_old_jobs( - job_context.JobContext(app=app), max_hours=2, queue="queue_a", remove_error=True + job_context.JobContext(app=app, job=job), + max_hours=2, + queue="queue_a", + remove_error=True, ) - assert app.connector.queries == [ + + connector = cast(InMemoryConnector, app.connector) + assert connector.queries == [ ( "delete_old_jobs", {"nb_hours": 2, "queue": "queue_a", "statuses": ["succeeded", "failed"]}, diff --git a/tests/unit/test_job_context.py b/tests/unit/test_job_context.py index ca349d3da..fa2a2afae 100644 --- a/tests/unit/test_job_context.py +++ b/tests/unit/test_job_context.py @@ -3,6 +3,7 @@ import pytest from procrastinate import job_context +from procrastinate.app import App @pytest.mark.parametrize( @@ -44,58 +45,26 @@ def test_job_result_as_dict(job_result, expected, mocker): assert job_result.as_dict() == expected -@pytest.mark.parametrize( - "queues, result", [(None, "all queues"), (["foo", "bar"], "queues foo, bar")] -) -def test_queues_display(queues, result): - context = job_context.JobContext(worker_queues=queues) - assert context.queues_display == result - - -def test_evolve(): - context = job_context.JobContext(worker_name="a") - assert context.evolve(worker_name="b").worker_name == "b" - - -def test_log_extra(): - context = job_context.JobContext(worker_name="a", additional_context={"ha": "ho"}) - - assert context.log_extra(action="foo", bar="baz") == { - "action": "foo", - "bar": "baz", - "worker": {"name": "a", "job_id": None, "queues": None}, - } - - -def test_log_extra_job(job_factory): +def test_evolve(app: App, job_factory): job = job_factory() - context = job_context.JobContext(worker_name="a", job=job) - - assert context.log_extra(action="foo") == { - "action": "foo", - "job": job.log_context(), - "worker": {"name": "a", "job_id": job.id, "queues": None}, - } - - -def test_job_description_no_job(job_factory): - descr = job_context.JobContext(worker_name="a").job_description(current_timestamp=0) - assert descr == "worker: no current job" + context = job_context.JobContext(app=app, job=job, worker_name="a") + assert context.evolve(worker_name="b").worker_name == "b" -def test_job_description_job_no_time(job_factory): +def test_job_description_job_no_time(app: App, job_factory): job = job_factory(task_name="some_task", id=12, task_kwargs={"a": "b"}) - descr = job_context.JobContext(worker_name="a", job=job).job_description( + descr = job_context.JobContext(worker_name="a", job=job, app=app).job_description( current_timestamp=0 ) assert descr == "worker: some_task[12](a='b')" -def test_job_description_job_time(job_factory): +def test_job_description_job_time(app: App, job_factory): job = job_factory(task_name="some_task", id=12, task_kwargs={"a": "b"}) descr = job_context.JobContext( worker_name="a", job=job, + app=app, job_result=job_context.JobResult(start_timestamp=20.0), ).job_description(current_timestamp=30.0) assert descr == "worker: some_task[12](a='b') (started 10.000 s ago)" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index fe1d0d353..c28b044f5 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -383,3 +383,10 @@ async def task_2(): expected_error_count = sum(1 for error in (task_1_error, task_2_error) if error) assert len(caplog.records) == expected_error_count + + +@pytest.mark.parametrize( + "queues, result", [(None, "all queues"), (["foo", "bar"], "queues foo, bar")] +) +def test_queues_display(queues, result): + assert utils.queues_display(queues) == result diff --git a/tests/unit/test_worker_sync.py b/tests/unit/test_worker_sync.py index f1badb92a..7aaadce72 100644 --- a/tests/unit/test_worker_sync.py +++ b/tests/unit/test_worker_sync.py @@ -12,8 +12,8 @@ def test_worker(app: App) -> worker.Worker: @pytest.fixture -def context(): - return job_context.JobContext() +def context(app: App, job_factory): + return job_context.JobContext(app=app, job=job_factory()) def test_worker_find_task_missing(test_worker): From d61e0682094d106dd741eba08901c3ef1b7e192d Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 21 Jul 2024 20:38:00 +1000 Subject: [PATCH 017/375] simplify worker.run loop --- procrastinate/cli.py | 6 +- procrastinate/job_context.py | 2 +- procrastinate/worker.py | 268 +++++++++++++++++------------------ tests/unit/test_worker.py | 8 +- 4 files changed, 143 insertions(+), 141 deletions(-) diff --git a/procrastinate/cli.py b/procrastinate/cli.py index 5b664c104..c8884c212 100644 --- a/procrastinate/cli.py +++ b/procrastinate/cli.py @@ -11,7 +11,7 @@ from typing import Any, Awaitable, Callable, Literal, Union import procrastinate -from procrastinate import connector, exceptions, jobs, shell, types, utils, worker +from procrastinate import connector, exceptions, jobs, shell, types, utils logger = logging.getLogger(__name__) @@ -323,8 +323,8 @@ def configure_worker_parser(subparsers: argparse._SubParsersAction): add_argument( worker_parser, "--delete-jobs", - choices=worker.DeleteJobCondition, - type=worker.DeleteJobCondition, + choices=jobs.DeleteJobCondition, + type=jobs.DeleteJobCondition, help="If set, delete jobs on completion", envvar="WORKER_DELETE_JOBS", ) diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index 449281a2c..095604197 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -34,7 +34,7 @@ def as_dict(self): return result -@attr.dataclass(frozen=True, kw_only=True, eq=False) +@attr.dataclass(frozen=True, kw_only=True) class JobContext: """ Execution context of a running job. diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 2e507f048..9e38d1609 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -8,14 +8,18 @@ import time from typing import Any, Awaitable, Callable, Iterable -from procrastinate import signals, types, utils +from procrastinate import ( + exceptions, + job_context, + jobs, + periodic, + retry, + signals, + tasks, + types, + utils, +) from procrastinate.app import App -from procrastinate.exceptions import JobAborted, JobRetry, TaskNotFound -from procrastinate.job_context import JobContext, JobResult -from procrastinate.jobs import DeleteJobCondition, Job, Status -from procrastinate.periodic import PeriodicDeferrer -from procrastinate.retry import RetryDecision -from procrastinate.tasks import Task logger = logging.getLogger(__name__) @@ -34,7 +38,8 @@ def __init__( wait: bool = True, timeout: float = POLLING_INTERVAL, listen_notify: bool = True, - delete_jobs: str | DeleteJobCondition = DeleteJobCondition.NEVER.value, + delete_jobs: str + | jobs.DeleteJobCondition = jobs.DeleteJobCondition.NEVER.value, additional_context: dict[str, Any] | None = None, install_signal_handlers: bool = True, ): @@ -46,7 +51,7 @@ def __init__( self.polling_interval = timeout self.listen_notify = listen_notify self.delete_jobs = ( - DeleteJobCondition(delete_jobs) + jobs.DeleteJobCondition(delete_jobs) if isinstance(delete_jobs, str) else delete_jobs ) @@ -59,51 +64,56 @@ def __init__( self.logger = logger self._run_task: asyncio.Task | None = None + self.notify_event = asyncio.Event() + self.running_jobs: dict[asyncio.Task, job_context.JobContext] = {} def stop(self): self.logger.info( "Stop requested", - extra=self._log_extra(job_context=None, action="stopping_worker"), + extra=self._log_extra(context=None, action="stopping_worker"), ) if self._run_task: self._run_task.cancel() async def periodic_deferrer(self): - deferrer = PeriodicDeferrer( + deferrer = periodic.PeriodicDeferrer( registry=self.app.periodic_registry, **self.app.periodic_defaults, ) return await deferrer.worker() - def find_task(self, task_name: str) -> Task: + def find_task(self, task_name: str) -> tasks.Task: try: return self.app.tasks[task_name] except KeyError as exc: - raise TaskNotFound from exc + raise exceptions.TaskNotFound from exc def _log_extra( - self, action: str, job_context: JobContext | None, **kwargs: Any + self, action: str, context: job_context.JobContext | None, **kwargs: Any ) -> types.JSONDict: extra: types.JSONDict = { "action": action, "worker": { "name": self.worker_name, - "job_id": job_context.job.id if job_context else None, + "job_id": context.job.id if context else None, "queues": self.queues, }, } - if job_context: - extra["job"] = job_context.job.log_context() + if context: + extra["job"] = context.job.log_context() return { **extra, - **(job_context.job_result if job_context else JobResult()).as_dict(), + **(context.job_result if context else job_context.JobResult()).as_dict(), **kwargs, } async def _persist_job_status( - self, job: Job, status: Status, retry_decision: RetryDecision | None + self, + job: jobs.Job, + status: jobs.Status, + retry_decision: retry.RetryDecision | None, ): if retry_decision: await self.app.job_manager.retry_job( @@ -115,9 +125,9 @@ async def _persist_job_status( ) else: delete_job = { - DeleteJobCondition.ALWAYS: True, - DeleteJobCondition.NEVER: False, - DeleteJobCondition.SUCCESSFUL: status == Status.SUCCEEDED, + jobs.DeleteJobCondition.ALWAYS: True, + jobs.DeleteJobCondition.NEVER: False, + jobs.DeleteJobCondition.SUCCESSFUL: status == jobs.Status.SUCCEEDED, }[self.delete_jobs] await self.app.job_manager.finish_job( job=job, status=status, delete_job=delete_job @@ -125,65 +135,59 @@ async def _persist_job_status( def _log_job_outcome( self, - status: Status, - job_context: JobContext, - job_retry: JobRetry | None, + status: jobs.Status, + context: job_context.JobContext, + job_retry: exceptions.JobRetry | None, exc_info: bool | BaseException = False, ): - if status == Status.SUCCEEDED: + if status == jobs.Status.SUCCEEDED: log_action, log_title = "job_success", "Success" - elif status == Status.ABORTED: + elif status == jobs.Status.ABORTED: log_action, log_title = "job_aborted", "Aborted" elif job_retry: log_action, log_title = "job_error_retry", "Error, to retry" else: log_action, log_title = "job_error", "Error" - text = f"Job {job_context.job.call_string} ended with status: {log_title}, " - if ( - job_context.job_result.start_timestamp - and job_context.job_result.end_timestamp - ): + text = f"Job {context.job.call_string} ended with status: {log_title}, " + if context.job_result.start_timestamp and context.job_result.end_timestamp: duration = ( - job_context.job_result.end_timestamp - - job_context.job_result.start_timestamp + context.job_result.end_timestamp - context.job_result.start_timestamp ) text += f"lasted {duration:.3f} s" - if job_context.job_result.result: - text += f" - Result: {job_context.job_result.result}"[:250] + if context.job_result.result: + text += f" - Result: {context.job_result.result}"[:250] - extra = self._log_extra(job_context=job_context, action=log_action) - log_level = logging.ERROR if status == Status.FAILED else logging.INFO + extra = self._log_extra(context=context, action=log_action) + log_level = logging.ERROR if status == jobs.Status.FAILED else logging.INFO logger.log(log_level, text, extra=extra, exc_info=exc_info) - async def _process_job(self, job_context: JobContext): + async def _process_job(self, context: job_context.JobContext): """ Processes a given job and persists its status """ - task = job_context.task + task = context.task job_retry = None exc_info = False retry_decision = None - job = job_context.job + job = context.job assert job - job_result = job_context.job_result + job_result = context.job_result job_result.start_timestamp = time.time() try: if not task: - raise TaskNotFound + raise exceptions.TaskNotFound self.logger.debug( f"Loaded job info, about to start job {job.call_string}", - extra=self._log_extra( - job_context=job_context, action="loaded_job_info" - ), + extra=self._log_extra(context=context, action="loaded_job_info"), ) self.logger.info( f"Starting job {job.call_string}", - extra=self._log_extra(job_context=job_context, action="start_job"), + extra=self._log_extra(context=context, action="start_job"), ) exc_info: bool | BaseException = False @@ -194,7 +198,7 @@ async def _process_job(self, job_context: JobContext): else: await_func = functools.partial(utils.sync_to_async, task) - job_args = [job_context] if task.pass_context else [] + job_args = [context] if task.pass_context else [] task_result = await await_func(*job_args, **job.task_kwargs) # In some cases, the task function might be a synchronous function # that returns an awaitable without actually being a @@ -210,16 +214,16 @@ async def _process_job(self, job_context: JobContext): except BaseException as e: exc_info = e - if not isinstance(e, JobAborted): + if not isinstance(e, exceptions.JobAborted): job_retry = ( task.get_retry_exception(exception=e, job=job) if task else None ) retry_decision = job_retry.retry_decision if job_retry else None - if isinstance(e, TaskNotFound): + if isinstance(e, exceptions.TaskNotFound): self.logger.exception( f"Task was not found: {e}", extra=self._log_extra( - job_context=job_context, + context=context, action="task_not_found", exception=str(e), ), @@ -230,16 +234,16 @@ async def _process_job(self, job_context: JobContext): finally: job_result.end_timestamp = time.time() - if isinstance(exc_info, JobAborted): - status = Status.ABORTED + if isinstance(exc_info, exceptions.JobAborted): + status = jobs.Status.ABORTED elif exc_info: - status = Status.FAILED + status = jobs.Status.FAILED else: - status = Status.SUCCEEDED + status = jobs.Status.SUCCEEDED self._log_job_outcome( status=status, - job_context=job_context, + context=context, job_retry=job_retry, exc_info=exc_info, ) @@ -250,121 +254,115 @@ async def _process_job(self, job_context: JobContext): self.logger.debug( f"Acknowledged job completion {job.call_string}", extra=self._log_extra( - action="finish_task", job_context=job_context, status=status + action="finish_task", context=context, status=status ), ) + async def _fetch_and_process_jobs(self): + job_semaphore = asyncio.Semaphore(self.concurrency) + """Keeps in fetching and processing jobs until there are no job left to process""" + while True: + await job_semaphore.acquire() + try: + job = await self.app.job_manager.fetch_job(queues=self.queues) + except BaseException: + job_semaphore.release() + raise + + if not job: + break + + context = job_context.JobContext( + app=self.app, + worker_name=self.worker_name, + worker_queues=self.queues, + additional_context=self.additional_context.copy() + if self.additional_context + else {}, + job=job, + task=self.app.tasks.get(job.task_name), + ) + job_task = asyncio.create_task(self._process_job(context)) + self.running_jobs[job_task] = context + + def on_job_complete(task: asyncio.Task): + del self.running_jobs[task] + job_semaphore.release() + + job_task.add_done_callback(on_job_complete) + + async def _wait_for_job(self): + self.notify_event.clear() + try: + # awaken when a notification that a new job is available + # or after specified polling interval elapses + await asyncio.wait_for( + self.notify_event.wait(), timeout=self.polling_interval + ) + + except asyncio.TimeoutError: + # polling interval has passed, resume loop and attempt to fetch a job + pass + async def run(self): self._run_task = asyncio.current_task() - notify_event = asyncio.Event() self.logger.info( f"Starting worker on {utils.queues_display(self.queues)}", extra=self._log_extra( - action="start_worker", job_context=None, queues=self.queues + action="start_worker", context=None, queues=self.queues ), ) - job_semaphore = asyncio.Semaphore(self.concurrency) - - running_jobs: dict[JobContext, asyncio.Task] = {} + self.running_jobs = {} side_tasks = [asyncio.create_task(self.periodic_deferrer())] if self.wait and self.listen_notify: listener_coro = self.app.job_manager.listen_for_jobs( - event=notify_event, + event=self.notify_event, queues=self.queues, ) side_tasks.append(asyncio.create_task(listener_coro, name="listener")) + context = contextlib.nullcontext() + if self.install_signal_handlers: + context = signals.on_stop(self.stop) + try: - context = contextlib.nullcontext() - if self.install_signal_handlers: - context = signals.on_stop(self.stop) with context: - """Processes jobs until cancelled or until there is no more available job (wait=False)""" - while True: - out_of_job = None - while not out_of_job: - # acquire job_semaphore so that a new job is not fetched if maximum concurrency is reached - async with job_semaphore: - job = await self.app.job_manager.fetch_job( - queues=self.queues - ) - if job: - # job_semaphore should be acquired straight because it is - # only acquired when not at full capacity at this time - # however, shield it from cancellation in the unlikely event the worker - # is cancelled at that precise time to not abandon the job - await asyncio.shield(job_semaphore.acquire()) - - job_context = JobContext( - app=self.app, - worker_name=self.worker_name, - worker_queues=self.queues, - additional_context=self.additional_context.copy() - if self.additional_context - else {}, - job=job, - task=self.app.tasks.get(job.task_name), - ) - job_task = asyncio.create_task( - self._process_job(job_context) - ) - running_jobs[job_context] = job_task - - def on_job_complete( - job_context: JobContext, _: asyncio.Task - ): - del running_jobs[job_context] - job_semaphore.release() - - job_task.add_done_callback( - functools.partial(on_job_complete, job_context) - ) - else: - out_of_job = True - if out_of_job: - if not self.wait: - self.logger.info( - "No job found. Stopping worker because wait=False", - extra=self._log_extra( - job_context=None, - action="stop_worker", - queues=self.queues, - ), - ) - break - try: - # awaken when a notification that a new job is available - # or after specified polling interval elapses - notify_event.clear() - await asyncio.wait_for( - notify_event.wait(), timeout=self.polling_interval - ) - - except asyncio.TimeoutError: - # polling interval has passed, resume loop and attempt to fetch a job - pass + await self._fetch_and_process_jobs() + if not self.wait: + self.logger.info( + "No job found. Stopping worker because wait=False", + extra=self._log_extra( + context=None, + action="stop_worker", + queues=self.queues, + ), + ) + return + while True: + await self._wait_for_job() + await self._fetch_and_process_jobs() finally: await utils.cancel_and_capture_errors(side_tasks) now = time.time() - for job_context in running_jobs.keys(): + for context in self.running_jobs.values(): self.logger.info( "Waiting for job to finish: " - + job_context.job_description(current_timestamp=now), - extra=self._log_extra(job_context=None, action="ending_job"), + + context.job_description(current_timestamp=now), + extra=self._log_extra(context=None, action="ending_job"), ) # wait for any in progress job to complete processing # use return_exceptions to not cancel other job tasks if one was to fail await asyncio.gather( - *(task for task in running_jobs.values()), return_exceptions=True + *(task for task in self.running_jobs.keys()), return_exceptions=True ) self.logger.info( f"Stopped worker on {utils.queues_display(self.queues)}", extra=self._log_extra( - action="stop_worker", queues=self.queues, job_context=None + action="stop_worker", queues=self.queues, context=None ), ) diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index d6388dda4..b5e3c2fc6 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -382,11 +382,15 @@ def task_func(a, b): await start_worker(worker) - await asyncio.sleep(0.01) + await asyncio.sleep(0.05) status = await app.job_manager.get_job_status_async(job_id) assert status == Status.FAILED - records = [record for record in caplog.records if record.action == "job_error"] + records = [ + record + for record in caplog.records + if hasattr(record, "action") and record.action == "job_error" + ] assert len(records) == 1 record = records[0] assert record.levelname == "ERROR" From 60c6b2db25454f1850f75b9dc9a4b99fd4092022 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 21 Jul 2024 21:55:11 +1000 Subject: [PATCH 018/375] another import module update --- procrastinate/worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 9e38d1609..dff82899a 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -9,6 +9,7 @@ from typing import Any, Awaitable, Callable, Iterable from procrastinate import ( + app, exceptions, job_context, jobs, @@ -19,7 +20,6 @@ types, utils, ) -from procrastinate.app import App logger = logging.getLogger(__name__) @@ -31,7 +31,7 @@ class Worker: def __init__( self, - app: App, + app: app.App, queues: Iterable[str] | None = None, name: str | None = WORKER_NAME, concurrency: int = WORKER_CONCURRENCY, From c2a214fda772dda23a798728ea16372003f5430c Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 22 Jul 2024 07:38:03 +1000 Subject: [PATCH 019/375] fix semaphore logic from earlier refactoring --- procrastinate/worker.py | 10 +++++---- tests/unit/test_worker.py | 44 +++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index dff82899a..f4ad8b700 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -66,6 +66,7 @@ def __init__( self._run_task: asyncio.Task | None = None self.notify_event = asyncio.Event() self.running_jobs: dict[asyncio.Task, job_context.JobContext] = {} + self.job_semaphore = asyncio.Semaphore(self.concurrency) def stop(self): self.logger.info( @@ -259,17 +260,17 @@ async def _process_job(self, context: job_context.JobContext): ) async def _fetch_and_process_jobs(self): - job_semaphore = asyncio.Semaphore(self.concurrency) """Keeps in fetching and processing jobs until there are no job left to process""" while True: - await job_semaphore.acquire() + await self.job_semaphore.acquire() try: job = await self.app.job_manager.fetch_job(queues=self.queues) except BaseException: - job_semaphore.release() + self.job_semaphore.release() raise if not job: + self.job_semaphore.release() break context = job_context.JobContext( @@ -287,7 +288,7 @@ async def _fetch_and_process_jobs(self): def on_job_complete(task: asyncio.Task): del self.running_jobs[task] - job_semaphore.release() + self.job_semaphore.release() job_task.add_done_callback(on_job_complete) @@ -315,6 +316,7 @@ async def run(self): ) self.running_jobs = {} + self.job_semaphore = asyncio.Semaphore(self.concurrency) side_tasks = [asyncio.create_task(self.periodic_deferrer())] if self.wait and self.listen_notify: listener_coro = self.app.job_manager.listen_for_jobs( diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index b5e3c2fc6..935c3fc42 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -118,13 +118,10 @@ async def test_worker_run_respects_concurrency( async def perform_job(): await complete_tasks.wait() - await start_worker(worker) - for _ in range(available_jobs): await perform_job.defer_async() - # wait just enough to make sure the task is running - await asyncio.sleep(0.01) + await start_worker(worker) connector = cast(InMemoryConnector, app.connector) @@ -137,6 +134,45 @@ async def perform_job(): complete_tasks.set() +async def test_worker_run_respects_concurrency_variant(worker: Worker, app: App): + worker.concurrency = 2 + + max_parallelism = 0 + parallel_jobs = 0 + + @app.task + async def perform_job(sleep: float): + nonlocal max_parallelism + nonlocal parallel_jobs + parallel_jobs += 1 + + max_parallelism = max(max_parallelism, parallel_jobs) + await asyncio.sleep(sleep) + parallel_jobs -= 1 + + await perform_job.defer_async(sleep=0.05) + await perform_job.defer_async(sleep=0.1) + + await start_worker(worker) + + # wait enough to run out of job and to have one pending job + await asyncio.sleep(0.05) + + assert max_parallelism == 2 + assert parallel_jobs == 1 + + # defer more jobs than the worker can process in parallel + await perform_job.defer_async(sleep=0.05) + await perform_job.defer_async(sleep=0.05) + await perform_job.defer_async(sleep=0.05) + + worker.notify_event.set() + + await asyncio.sleep(0.2) + assert max_parallelism == 2 + assert parallel_jobs == 0 + + async def test_worker_run_fetches_job_on_notification(worker, app: App): complete_tasks = asyncio.Event() From a8c3f891ad97b3cb1ac41b71d2ec12ecc12ae47b Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 22 Jul 2024 22:17:37 +1000 Subject: [PATCH 020/375] use stop event for graceful shutdown --- procrastinate/utils.py | 43 +++++--- procrastinate/worker.py | 156 +++++++++++++++++----------- tests/integration/test_wait_stop.py | 8 +- tests/unit/test_app.py | 9 +- tests/unit/test_worker.py | 19 ++-- 5 files changed, 137 insertions(+), 98 deletions(-) diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 26065b38f..15e3d6167 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -15,6 +15,7 @@ AsyncIterator, Awaitable, Callable, + Coroutine, Generic, Iterable, TypeVar, @@ -218,22 +219,32 @@ def log_task_exception(task: asyncio.Task, error: BaseException): }, ) - tasks_aggregate = asyncio.gather(*tasks, return_exceptions=True) - tasks_aggregate.cancel() - try: - results = await tasks_aggregate - for task, result in zip(tasks, results): - if not isinstance(result, BaseException): - continue - log_task_exception(task, error=result) - except asyncio.CancelledError: - # tasks have been cancelled. Log any exception from already completed tasks - for task in tasks: - if not task.done() or task.cancelled(): - continue - error = task.exception() - if error: - log_task_exception(task, error=error) + pendng_tasks = (task for task in tasks if not task.done()) + + for task in pendng_tasks: + task.cancel() + + await asyncio.gather(*tasks, return_exceptions=True) + + for task in (task for task in tasks if task.done() and not task.cancelled()): + error = task.exception() + if error: + log_task_exception(task, error=error) + + +async def wait_any(*coros_or_futures: Coroutine | asyncio.Future): + """Starts and wait on the first coroutine to complete and return it + Other pending coroutines are cancelled""" + futures = set(asyncio.ensure_future(fut) for fut in coros_or_futures) + + done, pending = await asyncio.wait( + fs=futures, + return_when=asyncio.FIRST_COMPLETED, + ) + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) + return done def add_namespace(name: str, namespace: str) -> str: diff --git a/procrastinate/worker.py b/procrastinate/worker.py index f4ad8b700..04c6cfed1 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -63,19 +63,21 @@ def __init__( else: self.logger = logger - self._run_task: asyncio.Task | None = None - self.notify_event = asyncio.Event() - self.running_jobs: dict[asyncio.Task, job_context.JobContext] = {} - self.job_semaphore = asyncio.Semaphore(self.concurrency) + self._loop_task: asyncio.Future | None = None + self._notify_event = asyncio.Event() + self._running_jobs: dict[asyncio.Task, job_context.JobContext] = {} + self._job_semaphore = asyncio.Semaphore(self.concurrency) + self._stop_event = asyncio.Event() def stop(self): + if self._stop_event.is_set(): + return self.logger.info( "Stop requested", extra=self._log_extra(context=None, action="stopping_worker"), ) - if self._run_task: - self._run_task.cancel() + self._stop_event.set() async def periodic_deferrer(self): deferrer = periodic.PeriodicDeferrer( @@ -229,9 +231,6 @@ async def _process_job(self, context: job_context.JobContext): exception=str(e), ), ) - if not isinstance(e, Exception): - raise - finally: job_result.end_timestamp = time.time() @@ -260,17 +259,23 @@ async def _process_job(self, context: job_context.JobContext): ) async def _fetch_and_process_jobs(self): - """Keeps in fetching and processing jobs until there are no job left to process""" - while True: - await self.job_semaphore.acquire() + """Fetch and process jobs until there are no job left ready to be processed""" + while not self._stop_event.is_set(): + acquire_sem_task = asyncio.create_task(self._job_semaphore.acquire()) + await utils.wait_any(acquire_sem_task, self._stop_event.wait()) + if self._stop_event.is_set(): + if acquire_sem_task.done(): + self._job_semaphore.release() + break try: job = await self.app.job_manager.fetch_job(queues=self.queues) except BaseException: - self.job_semaphore.release() + self._job_semaphore.release() raise if not job: - self.job_semaphore.release() + self._notify_event.clear() + self._job_semaphore.release() break context = job_context.JobContext( @@ -284,50 +289,90 @@ async def _fetch_and_process_jobs(self): task=self.app.tasks.get(job.task_name), ) job_task = asyncio.create_task(self._process_job(context)) - self.running_jobs[job_task] = context + self._running_jobs[job_task] = context def on_job_complete(task: asyncio.Task): - del self.running_jobs[task] - self.job_semaphore.release() + del self._running_jobs[task] + self._job_semaphore.release() job_task.add_done_callback(on_job_complete) - async def _wait_for_job(self): - self.notify_event.clear() - try: - # awaken when a notification that a new job is available - # or after specified polling interval elapses - await asyncio.wait_for( - self.notify_event.wait(), timeout=self.polling_interval - ) + async def run(self): + """ + Run the worker + This will run forever until asked to stop/cancelled, or until no more job is available is configured not to wait + """ + self.run_task = asyncio.current_task() + loop_task = asyncio.create_task(self._run_loop()) - except asyncio.TimeoutError: - # polling interval has passed, resume loop and attempt to fetch a job - pass + try: + # shield the loop task from cancellation + # instead, a stop signal is set to enable graceful shutdown + await asyncio.shield(loop_task) + except asyncio.CancelledError: + self.stop() + await loop_task + raise + + async def _shutdown(self, side_tasks: list[asyncio.Task]): + """ + Gracefully shutdown the worker by cancelling side tasks + and waiting for all pending jobs. + """ + await utils.cancel_and_capture_errors(side_tasks) - async def run(self): - self._run_task = asyncio.current_task() + now = time.time() + for context in self._running_jobs.values(): + self.logger.info( + "Waiting for job to finish: " + + context.job_description(current_timestamp=now), + extra=self._log_extra(context=None, action="ending_job"), + ) + # wait for any in progress job to complete processing + # use return_exceptions to not cancel other job tasks if one was to fail + await asyncio.gather( + *(task for task in self._running_jobs.keys()), return_exceptions=True + ) self.logger.info( - f"Starting worker on {utils.queues_display(self.queues)}", + f"Stopped worker on {utils.queues_display(self.queues)}", extra=self._log_extra( - action="start_worker", context=None, queues=self.queues + action="stop_worker", queues=self.queues, context=None ), ) - self.running_jobs = {} - self.job_semaphore = asyncio.Semaphore(self.concurrency) + async def _start_side_tasks(self) -> list[asyncio.Task]: + """Start side tasks such as periodic deferrer and notification listener""" side_tasks = [asyncio.create_task(self.periodic_deferrer())] if self.wait and self.listen_notify: listener_coro = self.app.job_manager.listen_for_jobs( - event=self.notify_event, + event=self._notify_event, queues=self.queues, ) side_tasks.append(asyncio.create_task(listener_coro, name="listener")) + return side_tasks - context = contextlib.nullcontext() - if self.install_signal_handlers: - context = signals.on_stop(self.stop) + async def _run_loop(self): + """ + Run all side coroutines, then start fetching/processing jobs in a loop + """ + self.logger.info( + f"Starting worker on {utils.queues_display(self.queues)}", + extra=self._log_extra( + action="start_worker", context=None, queues=self.queues + ), + ) + self._notify_event.clear() + self._stop_event.clear() + self._running_jobs = {} + self._job_semaphore = asyncio.Semaphore(self.concurrency) + side_tasks = await self._start_side_tasks() + + context = ( + signals.on_stop(self.stop) + if self.install_signal_handlers + else contextlib.nullcontext() + ) try: with context: @@ -341,30 +386,15 @@ async def run(self): queues=self.queues, ), ) - return - - while True: - await self._wait_for_job() + self._stop_event.set() + + while not self._stop_event.is_set(): + # wait for a new job notification, a stop even or the next polling interval + await utils.wait_any( + self._notify_event.wait(), + asyncio.sleep(self.polling_interval), + self._stop_event.wait(), + ) await self._fetch_and_process_jobs() finally: - await utils.cancel_and_capture_errors(side_tasks) - - now = time.time() - for context in self.running_jobs.values(): - self.logger.info( - "Waiting for job to finish: " - + context.job_description(current_timestamp=now), - extra=self._log_extra(context=None, action="ending_job"), - ) - - # wait for any in progress job to complete processing - # use return_exceptions to not cancel other job tasks if one was to fail - await asyncio.gather( - *(task for task in self.running_jobs.keys()), return_exceptions=True - ) - self.logger.info( - f"Stopped worker on {utils.queues_display(self.queues)}", - extra=self._log_extra( - action="stop_worker", queues=self.queues, context=None - ), - ) + await self._shutdown(side_tasks=side_tasks) diff --git a/tests/integration/test_wait_stop.py b/tests/integration/test_wait_stop.py index 3e947966b..1c582b263 100644 --- a/tests/integration/test_wait_stop.py +++ b/tests/integration/test_wait_stop.py @@ -8,7 +8,7 @@ from procrastinate import worker as worker_module -async def test_wait_for_activity(psycopg_connector): +async def test_wait_for_activity_cancelled(psycopg_connector): """ Testing that the work can be cancelled """ @@ -50,8 +50,7 @@ async def test_wait_for_activity_stop_from_signal(psycopg_connector, kill_own_pi kill_own_pid() try: - with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(task, timeout=0.2) + await asyncio.wait_for(task, timeout=0.2) except asyncio.TimeoutError: pytest.fail("Failed to stop worker within .2s") @@ -68,7 +67,6 @@ async def test_wait_for_activity_stop(psycopg_connector): worker.stop() try: - with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(task, timeout=0.2) + await asyncio.wait_for(task, timeout=0.2) except asyncio.TimeoutError: pytest.fail("Failed to stop worker within .2s") diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 419b2a623..13bc6d4bd 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -2,6 +2,7 @@ import asyncio import collections +from typing import cast import pytest @@ -286,16 +287,16 @@ def bar(): assert app.periodic_registry is new_app.periodic_registry -def test_replace_connector(app): +def test_replace_connector(app: app_module.App): @app.task(name="foo") def foo(): pass foo.defer() - assert len(app.connector.jobs) == 1 + assert len(cast(testing.InMemoryConnector, app.connector).jobs) == 1 new_connector = testing.InMemoryConnector() with app.replace_connector(new_connector): - assert len(app.connector.jobs) == 0 + assert len(cast(testing.InMemoryConnector, app.connector).jobs) == 0 - assert len(app.connector.jobs) == 1 + assert len(cast(testing.InMemoryConnector, app.connector).jobs) == 1 diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 935c3fc42..9839b8873 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -25,10 +25,10 @@ async def worker(app: App, request: pytest.FixtureRequest): kwargs = request.param if hasattr(request, "param") else {} worker = Worker(app, **kwargs) yield worker - if worker._run_task and not worker._run_task.done(): + if worker.run_task and not worker.run_task.done(): + worker.stop() try: - worker._run_task.cancel() - await asyncio.wait_for(worker._run_task, timeout=0.2) + await asyncio.wait_for(worker.run_task, timeout=0.2) except asyncio.CancelledError: pass @@ -70,8 +70,7 @@ async def test_worker_run_wait_stop(app: App, caplog): # wait just enough to make sure the task is running await asyncio.sleep(0.01) worker.stop() - with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(run_task, 0.1) + await asyncio.wait_for(run_task, 0.1) assert set(caplog.messages) == { "Starting worker on all queues", @@ -86,11 +85,12 @@ async def test_worker_run_once_log_messages(app: App, caplog): worker = Worker(app, wait=False) await asyncio.wait_for(worker.run(), 0.1) - assert caplog.messages == [ + assert set(caplog.messages) == { "Starting worker on all queues", "No job found. Stopping worker because wait=False", "Stopped worker on all queues", - ] + "No periodic task found, periodic deferrer will not run.", + } async def test_worker_run_wait_listen(worker): @@ -166,7 +166,7 @@ async def perform_job(sleep: float): await perform_job.defer_async(sleep=0.05) await perform_job.defer_async(sleep=0.05) - worker.notify_event.set() + worker._notify_event.set() await asyncio.sleep(0.2) assert max_parallelism == 2 @@ -544,8 +544,7 @@ async def t(): await asyncio.sleep(0.01) complete_job_event.set() - with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(run_task, timeout=0.05) + await asyncio.wait_for(run_task, timeout=0.05) # We want to make sure that the log that names the current running task fired. logs = " ".join(r.message for r in caplog.records) assert "Stop requested" in logs From 6de6e3480491f285a955d0a048028aa06328b226 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 22 Jul 2024 22:20:57 +1000 Subject: [PATCH 021/375] update comment --- procrastinate/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 04c6cfed1..d0e8c5824 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -259,7 +259,7 @@ async def _process_job(self, context: job_context.JobContext): ) async def _fetch_and_process_jobs(self): - """Fetch and process jobs until there are no job left ready to be processed""" + """Fetch and process jobs until there is no job left or asked to stop""" while not self._stop_event.is_set(): acquire_sem_task = asyncio.create_task(self._job_semaphore.acquire()) await utils.wait_any(acquire_sem_task, self._stop_event.wait()) From 1139b81d998c9e7b37364e4c35ae30f9b9c383e3 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 25 Jul 2024 20:58:30 +1000 Subject: [PATCH 022/375] remove cli CancelledError catch --- procrastinate/cli.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/procrastinate/cli.py b/procrastinate/cli.py index c8884c212..5e6be3b2a 100644 --- a/procrastinate/cli.py +++ b/procrastinate/cli.py @@ -539,11 +539,7 @@ async def worker_( print_stderr( f"Launching a worker on {'all queues' if not queues else ', '.join(queues)}" ) - try: - await app.run_worker_async(**kwargs) - except asyncio.CancelledError: - # prevent the CLI from failing and raising an error when the worker is cancelled - pass + await app.run_worker_async(**kwargs) async def defer( From afc1118fbbf1b197863162a047d638c330cd917a Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 25 Jul 2024 21:22:11 +1000 Subject: [PATCH 023/375] no longer return done from wait_any --- procrastinate/utils.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 15e3d6167..8212c60b0 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -219,9 +219,7 @@ def log_task_exception(task: asyncio.Task, error: BaseException): }, ) - pendng_tasks = (task for task in tasks if not task.done()) - - for task in pendng_tasks: + for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) @@ -237,14 +235,13 @@ async def wait_any(*coros_or_futures: Coroutine | asyncio.Future): Other pending coroutines are cancelled""" futures = set(asyncio.ensure_future(fut) for fut in coros_or_futures) - done, pending = await asyncio.wait( - fs=futures, + _, pending = await asyncio.wait( + futures, return_when=asyncio.FIRST_COMPLETED, ) for task in pending: task.cancel() await asyncio.gather(*pending, return_exceptions=True) - return done def add_namespace(name: str, namespace: str) -> str: From a911e9c1345dc2003e69edd7081f8fe3df6f4c0f Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 25 Jul 2024 22:39:00 +1000 Subject: [PATCH 024/375] minor refactoring of worker --- procrastinate/worker.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index d0e8c5824..e5e9cc16a 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -38,8 +38,7 @@ def __init__( wait: bool = True, timeout: float = POLLING_INTERVAL, listen_notify: bool = True, - delete_jobs: str - | jobs.DeleteJobCondition = jobs.DeleteJobCondition.NEVER.value, + delete_jobs: str | jobs.DeleteJobCondition | None = None, additional_context: dict[str, Any] | None = None, install_signal_handlers: bool = True, ): @@ -131,7 +130,7 @@ async def _persist_job_status( jobs.DeleteJobCondition.ALWAYS: True, jobs.DeleteJobCondition.NEVER: False, jobs.DeleteJobCondition.SUCCESSFUL: status == jobs.Status.SUCCEEDED, - }[self.delete_jobs] + }[self.delete_jobs or jobs.DeleteJobCondition.NEVER] await self.app.job_manager.finish_job( job=job, status=status, delete_job=delete_job ) @@ -262,20 +261,18 @@ async def _fetch_and_process_jobs(self): """Fetch and process jobs until there is no job left or asked to stop""" while not self._stop_event.is_set(): acquire_sem_task = asyncio.create_task(self._job_semaphore.acquire()) - await utils.wait_any(acquire_sem_task, self._stop_event.wait()) - if self._stop_event.is_set(): - if acquire_sem_task.done(): - self._job_semaphore.release() - break + job = None try: + await utils.wait_any(acquire_sem_task, self._stop_event.wait()) + if self._stop_event.is_set(): + break job = await self.app.job_manager.fetch_job(queues=self.queues) - except BaseException: - self._job_semaphore.release() - raise + finally: + if (not job or self._stop_event.is_set()) and acquire_sem_task.done(): + self._job_semaphore.release() + self._notify_event.clear() if not job: - self._notify_event.clear() - self._job_semaphore.release() break context = job_context.JobContext( @@ -307,7 +304,7 @@ async def run(self): try: # shield the loop task from cancellation - # instead, a stop signal is set to enable graceful shutdown + # instead, a stop event is set to enable graceful shutdown await asyncio.shield(loop_task) except asyncio.CancelledError: self.stop() @@ -331,9 +328,7 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): # wait for any in progress job to complete processing # use return_exceptions to not cancel other job tasks if one was to fail - await asyncio.gather( - *(task for task in self._running_jobs.keys()), return_exceptions=True - ) + await asyncio.gather(*self._running_jobs, return_exceptions=True) self.logger.info( f"Stopped worker on {utils.queues_display(self.queues)}", extra=self._log_extra( @@ -341,7 +336,7 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): ), ) - async def _start_side_tasks(self) -> list[asyncio.Task]: + def _start_side_tasks(self) -> list[asyncio.Task]: """Start side tasks such as periodic deferrer and notification listener""" side_tasks = [asyncio.create_task(self.periodic_deferrer())] if self.wait and self.listen_notify: @@ -366,7 +361,7 @@ async def _run_loop(self): self._stop_event.clear() self._running_jobs = {} self._job_semaphore = asyncio.Semaphore(self.concurrency) - side_tasks = await self._start_side_tasks() + side_tasks = self._start_side_tasks() context = ( signals.on_stop(self.stop) From bc9b7e51ea8e89f0178f3456f279982d1c2d8559 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 25 Jul 2024 23:03:52 +1000 Subject: [PATCH 025/375] add comment and remove obsolete asserts --- procrastinate/builtin_tasks.py | 1 - procrastinate/job_context.py | 11 ++--------- procrastinate/worker.py | 3 ++- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/procrastinate/builtin_tasks.py b/procrastinate/builtin_tasks.py index f419558f9..f52c64338 100644 --- a/procrastinate/builtin_tasks.py +++ b/procrastinate/builtin_tasks.py @@ -28,7 +28,6 @@ async def remove_old_jobs( By default only successful jobs will be removed. When this parameter is True failed jobs will also be deleted. """ - assert context.app await context.app.job_manager.delete_old_jobs( nb_hours=max_hours, queue=queue, include_error=remove_error ) diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index 095604197..fda737344 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -80,19 +80,12 @@ def job_description(self, current_timestamp: float) -> str: return message def should_abort(self) -> bool: - assert self.app - assert self.job assert self.job.id - - job_id = self.job.id - status = self.app.job_manager.get_job_status(job_id) + status = self.app.job_manager.get_job_status(self.job.id) return status == jobs.Status.ABORTING async def should_abort_async(self) -> bool: - assert self.app - assert self.job assert self.job.id - job_id = self.job.id - status = await self.app.job_manager.get_job_status_async(job_id) + status = await self.app.job_manager.get_job_status_async(self.job.id) return status == jobs.Status.ABORTING diff --git a/procrastinate/worker.py b/procrastinate/worker.py index e5e9cc16a..e3399b0ba 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -152,6 +152,8 @@ def _log_job_outcome( log_action, log_title = "job_error", "Error" text = f"Job {context.job.call_string} ended with status: {log_title}, " + # in practice we should always have a start and end timestamp here + # but in theory the JobResult class allows it to be None if context.job_result.start_timestamp and context.job_result.end_timestamp: duration = ( context.job_result.end_timestamp - context.job_result.start_timestamp @@ -173,7 +175,6 @@ async def _process_job(self, context: job_context.JobContext): exc_info = False retry_decision = None job = context.job - assert job job_result = context.job_result job_result.start_timestamp = time.time() From 2d52537fbdb455822ca230b706e74ac776264e7f Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 25 Jul 2024 23:14:22 +1000 Subject: [PATCH 026/375] set _process_job task name --- procrastinate/worker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index e3399b0ba..9ee54b05b 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -286,7 +286,10 @@ async def _fetch_and_process_jobs(self): job=job, task=self.app.tasks.get(job.task_name), ) - job_task = asyncio.create_task(self._process_job(context)) + job_task = asyncio.create_task( + self._process_job(context), + name=f"process job {job.task_name}[{job.id}]", + ) self._running_jobs[job_task] = context def on_job_complete(task: asyncio.Task): From eefe5f2701f425fc5ee4bef7a5c6ed71ce1fff5e Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Fri, 26 Jul 2024 07:34:23 +1000 Subject: [PATCH 027/375] set jobs.DeleteJobCondition.NEVER at assignment --- procrastinate/worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 9ee54b05b..4dbc00ee8 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -53,7 +53,7 @@ def __init__( jobs.DeleteJobCondition(delete_jobs) if isinstance(delete_jobs, str) else delete_jobs - ) + ) or jobs.DeleteJobCondition.NEVER self.additional_context = additional_context self.install_signal_handlers = install_signal_handlers @@ -130,7 +130,7 @@ async def _persist_job_status( jobs.DeleteJobCondition.ALWAYS: True, jobs.DeleteJobCondition.NEVER: False, jobs.DeleteJobCondition.SUCCESSFUL: status == jobs.Status.SUCCEEDED, - }[self.delete_jobs or jobs.DeleteJobCondition.NEVER] + }[self.delete_jobs] await self.app.job_manager.finish_job( job=job, status=status, delete_job=delete_job ) From 28c44e14fc091e7754067ca6e7729211687a06b5 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Tue, 30 Jul 2024 21:59:29 +1000 Subject: [PATCH 028/375] add stop timeout (WIP) --- procrastinate/utils.py | 16 ++++++--- procrastinate/worker.py | 68 +++++++++++++++++++++++++++------------ tests/unit/test_worker.py | 29 +++++++++++++++++ 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 8212c60b0..4adda8e6c 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -228,20 +228,26 @@ def log_task_exception(task: asyncio.Task, error: BaseException): error = task.exception() if error: log_task_exception(task, error=error) + else: + logger.debug(f"Cancelled task ${task.get_name()}") -async def wait_any(*coros_or_futures: Coroutine | asyncio.Future): +async def wait_any( + *coros_or_futures: Coroutine | asyncio.Future, cancel_other_tasks: bool = True +): """Starts and wait on the first coroutine to complete and return it - Other pending coroutines are cancelled""" + Other pending coroutines are either cancelled or left running""" futures = set(asyncio.ensure_future(fut) for fut in coros_or_futures) _, pending = await asyncio.wait( futures, return_when=asyncio.FIRST_COMPLETED, ) - for task in pending: - task.cancel() - await asyncio.gather(*pending, return_exceptions=True) + + if cancel_other_tasks: + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) def add_namespace(name: str, namespace: str) -> str: diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 4dbc00ee8..be6d11068 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -67,10 +67,14 @@ def __init__( self._running_jobs: dict[asyncio.Task, job_context.JobContext] = {} self._job_semaphore = asyncio.Semaphore(self.concurrency) self._stop_event = asyncio.Event() + self._stop_timeout: float | None = None + self._stop_timed_out = False - def stop(self): + def stop(self, timeout: float | None = None): if self._stop_event.is_set(): return + self._stop_timed_out = False + self._stop_timeout = timeout self.logger.info( "Stop requested", extra=self._log_extra(context=None, action="stopping_worker"), @@ -195,25 +199,42 @@ async def _process_job(self, context: job_context.JobContext): exc_info: bool | BaseException = False - await_func: Callable[..., Awaitable] - if inspect.iscoroutinefunction(task.func): - await_func = task - else: - await_func = functools.partial(utils.sync_to_async, task) - - job_args = [context] if task.pass_context else [] - task_result = await await_func(*job_args, **job.task_kwargs) - # In some cases, the task function might be a synchronous function - # that returns an awaitable without actually being a - # coroutinefunction. In that case, in the await above, we haven't - # actually called the task, but merely generated the awaitable that - # implements the task. In that case, we want to wait this awaitable. - # It's easy enough to be in that situation that the best course of - # action is probably to await the awaitable. - # It's not even sure it's worth emitting a warning - if inspect.isawaitable(task_result): - task_result = await task_result - job_result.result = task_result + async def ensure_async() -> Callable[..., Awaitable]: + await_func: Callable[..., Awaitable] + if inspect.iscoroutinefunction(task.func): + await_func = task + else: + await_func = functools.partial(utils.sync_to_async, task) + + job_args = [context] if task.pass_context else [] + task_result = await await_func(*job_args, **job.task_kwargs) + # In some cases, the task function might be a synchronous function + # that returns an awaitable without actually being a + # coroutinefunction. In that case, in the await above, we haven't + # actually called the task, but merely generated the awaitable that + # implements the task. In that case, we want to wait this awaitable. + # It's easy enough to be in that situation that the best course of + # action is probably to await the awaitable. + # It's not even sure it's worth emitting a warning + if inspect.isawaitable(task_result): + task_result = await task_result + + return task_result + + async_task = asyncio.create_task(ensure_async()) + + await utils.wait_any( + async_task, self._stop_event.wait(), cancel_other_tasks=False + ) + + if self._stop_event.is_set() and not async_task.done(): + try: + await asyncio.wait_for(async_task, timeout=self._stop_timeout) + except asyncio.TimeoutError: + self._stop_timed_out = True + raise + + job_result.result = async_task.result() if async_task.done() else None except BaseException as e: exc_info = e @@ -234,7 +255,9 @@ async def _process_job(self, context: job_context.JobContext): finally: job_result.end_timestamp = time.time() - if isinstance(exc_info, exceptions.JobAborted): + if isinstance(exc_info, exceptions.JobAborted) or isinstance( + exc_info, asyncio.TimeoutError + ): status = jobs.Status.ABORTED elif exc_info: status = jobs.Status.FAILED @@ -397,3 +420,6 @@ async def _run_loop(self): await self._fetch_and_process_jobs() finally: await self._shutdown(side_tasks=side_tasks) + + if self._stop_timed_out: + raise asyncio.TimeoutError() diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 9839b8873..18aa27f67 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -288,6 +288,35 @@ async def task_func(): assert status == Status.SUCCEEDED +async def test_stopping_worker_cancels_task_after_timeout(app: App, worker): + complete_task_event = asyncio.Event() + + @app.task() + async def task_func(): + await asyncio.wait_for(complete_task_event.wait(), 0.2) + + run_task = await start_worker(worker) + + job_id = await task_func.defer_async() + + await asyncio.sleep(0.05) + + # this should still be running waiting for the task to complete + assert run_task.done() is False + + # we don't tell task to complete, it will be cancelled after timeout + + # this should mark the job as aborted and re-raise the CancelledError + with pytest.raises(asyncio.TimeoutError): + worker.stop(timeout=0.02) + await asyncio.sleep(0.1) + assert worker.run_task.done() + await worker.run_task + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.ABORTED + + @pytest.mark.parametrize( "worker", [({"additional_context": {"foo": "bar"}})], From 56ba20b5ae35330fa2fc2da281e50cf6decc65f2 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 1 Aug 2024 22:04:40 +1000 Subject: [PATCH 029/375] modify shutdown_timeout --- procrastinate/app.py | 6 ++++++ procrastinate/utils.py | 11 ++++------- procrastinate/worker.py | 34 +++++++++------------------------- tests/unit/test_worker.py | 14 ++++++++++---- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/procrastinate/app.py b/procrastinate/app.py index 2bc1e2025..c52a9e0db 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -29,6 +29,7 @@ class WorkerOptions(TypedDict): concurrency: NotRequired[int] wait: NotRequired[bool] timeout: NotRequired[float] + shutdown_timeout: NotRequired[float] listen_notify: NotRequired[bool] delete_jobs: NotRequired[str | jobs.DeleteJobCondition] additional_context: NotRequired[dict[str, Any]] @@ -267,6 +268,11 @@ async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: each database job poll. Raising this parameter can lower the rate at which the worker makes queries to the database for requesting jobs. (defaults to 5.0) + shutdown_timeout : ``float`` + Indicates the maximum duration (in seconds) the worker waits for jobs to + complete when requested stop. Jobs that have not been completed by that time + are aborted. A value of None corresponds to no timeout. + (defaults to None) listen_notify : ``bool`` If ``True``, the worker will dedicate a connection from the pool to listening to database events, notifying of newly available jobs. diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 4adda8e6c..250858d37 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -232,9 +232,7 @@ def log_task_exception(task: asyncio.Task, error: BaseException): logger.debug(f"Cancelled task ${task.get_name()}") -async def wait_any( - *coros_or_futures: Coroutine | asyncio.Future, cancel_other_tasks: bool = True -): +async def wait_any(*coros_or_futures: Coroutine | asyncio.Future): """Starts and wait on the first coroutine to complete and return it Other pending coroutines are either cancelled or left running""" futures = set(asyncio.ensure_future(fut) for fut in coros_or_futures) @@ -244,10 +242,9 @@ async def wait_any( return_when=asyncio.FIRST_COMPLETED, ) - if cancel_other_tasks: - for task in pending: - task.cancel() - await asyncio.gather(*pending, return_exceptions=True) + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) def add_namespace(name: str, namespace: str) -> str: diff --git a/procrastinate/worker.py b/procrastinate/worker.py index be6d11068..a4c44b7c3 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -37,6 +37,7 @@ def __init__( concurrency: int = WORKER_CONCURRENCY, wait: bool = True, timeout: float = POLLING_INTERVAL, + shutdown_timeout: float | None = None, listen_notify: bool = True, delete_jobs: str | jobs.DeleteJobCondition | None = None, additional_context: dict[str, Any] | None = None, @@ -67,14 +68,11 @@ def __init__( self._running_jobs: dict[asyncio.Task, job_context.JobContext] = {} self._job_semaphore = asyncio.Semaphore(self.concurrency) self._stop_event = asyncio.Event() - self._stop_timeout: float | None = None - self._stop_timed_out = False + self.shutdown_timeout = shutdown_timeout - def stop(self, timeout: float | None = None): + def stop(self): if self._stop_event.is_set(): return - self._stop_timed_out = False - self._stop_timeout = timeout self.logger.info( "Stop requested", extra=self._log_extra(context=None, action="stopping_worker"), @@ -221,20 +219,7 @@ async def ensure_async() -> Callable[..., Awaitable]: return task_result - async_task = asyncio.create_task(ensure_async()) - - await utils.wait_any( - async_task, self._stop_event.wait(), cancel_other_tasks=False - ) - - if self._stop_event.is_set() and not async_task.done(): - try: - await asyncio.wait_for(async_task, timeout=self._stop_timeout) - except asyncio.TimeoutError: - self._stop_timed_out = True - raise - - job_result.result = async_task.result() if async_task.done() else None + job_result.result = await ensure_async() except BaseException as e: exc_info = e @@ -256,7 +241,7 @@ async def ensure_async() -> Callable[..., Awaitable]: job_result.end_timestamp = time.time() if isinstance(exc_info, exceptions.JobAborted) or isinstance( - exc_info, asyncio.TimeoutError + exc_info, asyncio.CancelledError ): status = jobs.Status.ABORTED elif exc_info: @@ -332,10 +317,12 @@ async def run(self): try: # shield the loop task from cancellation # instead, a stop event is set to enable graceful shutdown - await asyncio.shield(loop_task) + await utils.wait_any(asyncio.shield(loop_task), self._stop_event.wait()) + if self._stop_event.is_set(): + await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) except asyncio.CancelledError: self.stop() - await loop_task + await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) raise async def _shutdown(self, side_tasks: list[asyncio.Task]): @@ -420,6 +407,3 @@ async def _run_loop(self): await self._fetch_and_process_jobs() finally: await self._shutdown(side_tasks=side_tasks) - - if self._stop_timed_out: - raise asyncio.TimeoutError() diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 18aa27f67..2839905e2 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -288,12 +288,14 @@ async def task_func(): assert status == Status.SUCCEEDED -async def test_stopping_worker_cancels_task_after_timeout(app: App, worker): +@pytest.mark.parametrize("mode", [("stop"), ("cancel")]) +async def test_stopping_worker_aborts_job_after_timeout(app: App, worker, mode): complete_task_event = asyncio.Event() + worker.shutdown_timeout = 0.02 @app.task() async def task_func(): - await asyncio.wait_for(complete_task_event.wait(), 0.2) + await complete_task_event.wait() run_task = await start_worker(worker) @@ -306,9 +308,13 @@ async def task_func(): # we don't tell task to complete, it will be cancelled after timeout - # this should mark the job as aborted and re-raise the CancelledError + # this should mark the job as aborted and raise a TimeoutError with pytest.raises(asyncio.TimeoutError): - worker.stop(timeout=0.02) + if mode == "stop": + worker.stop() + else: + run_task.cancel() + await asyncio.sleep(0.1) assert worker.run_task.done() await worker.run_task From b8d25fc4e0d9b33679b303d9d3f3cbf4ac0aed9e Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 1 Aug 2024 23:11:32 +1000 Subject: [PATCH 030/375] more shutdown timeout changes --- procrastinate/worker.py | 10 +++++-- tests/acceptance/test_async.py | 52 ++++++++++++++++++++++++++++++++++ tests/unit/test_worker.py | 19 +++++++------ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index f08bc7644..d18f4e6eb 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -325,10 +325,16 @@ async def run(self): # instead, a stop event is set to enable graceful shutdown await utils.wait_any(asyncio.shield(loop_task), self._stop_event.wait()) if self._stop_event.is_set(): - await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) + try: + await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) + except asyncio.TimeoutError: + pass except asyncio.CancelledError: self.stop() - await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) + try: + await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) + except asyncio.TimeoutError: + pass raise async def _shutdown(self, side_tasks: list[asyncio.Task]): diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index e13a8aa85..4212af2fc 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -238,3 +238,55 @@ async def example_task(context): status = await async_app.job_manager.get_job_status_async(job_id) assert status == Status.FAILED assert attempts == 1 + + +async def test_stop_worker(async_app: app_module.App): + results = [] + + @async_app.task(name="appender") + async def appender(a: int): + await asyncio.sleep(0.1) + results.append(a) + + job_ids: list[int] = [] + + job_ids.append(await appender.defer_async(a=1)) + job_ids.append(await appender.defer_async(a=2)) + + run_task = asyncio.create_task(async_app.run_worker_async(concurrency=2, wait=True)) + await asyncio.sleep(0.5) + + with pytest.raises(asyncio.CancelledError): + run_task.cancel() + await asyncio.wait_for(run_task, 1) + + for job_id in job_ids: + status = await async_app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + +async def test_stop_worker_aborts_jobs_past_shutdown_timeout(async_app: app_module.App): + @async_app.task(queue="default", name="fast_job") + async def fast_job(): + pass + + @async_app.task(queue="default", name="slow_job") + async def slow_job(): + await asyncio.sleep(2) + + fast_job_id = await fast_job.defer_async() + slow_job_id = await slow_job.defer_async() + + run_task = asyncio.create_task( + async_app.run_worker_async(wait=False, shutdown_timeout=0.3) + ) + await asyncio.sleep(0.05) + + with pytest.raises(asyncio.CancelledError): + run_task.cancel() + await run_task + + fast_job_status = await async_app.job_manager.get_job_status_async(fast_job_id) + slow_job_status = await async_app.job_manager.get_job_status_async(slow_job_id) + assert fast_job_status == Status.SUCCEEDED + assert slow_job_status == Status.ABORTED diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 2839905e2..9845b7c2d 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -308,16 +308,19 @@ async def task_func(): # we don't tell task to complete, it will be cancelled after timeout - # this should mark the job as aborted and raise a TimeoutError - with pytest.raises(asyncio.TimeoutError): - if mode == "stop": - worker.stop() - else: - run_task.cancel() + if mode == "stop": + worker.stop() await asyncio.sleep(0.1) - assert worker.run_task.done() - await worker.run_task + assert run_task.done() + await run_task + else: + with pytest.raises(asyncio.CancelledError): + run_task.cancel() + + await asyncio.sleep(0.1) + assert run_task.done() + await run_task status = await app.job_manager.get_job_status_async(job_id) assert status == Status.ABORTED From 1652416e760aa17def315e7d36e050669fda377b Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Wed, 31 Jul 2024 17:31:00 +0000 Subject: [PATCH 031/375] Use additional abort field on jobs table for abortion requests --- .../0030_add_abort_on_procrastinate_jobs.py | 35 +++ procrastinate/contrib/django/models.py | 2 +- procrastinate/job_context.py | 6 +- procrastinate/jobs.py | 1 - procrastinate/manager.py | 63 +++-- procrastinate/shell.py | 3 - ....02_01_add_abort_on_procrastinate_jobs.sql | 253 ++++++++++++++++++ procrastinate/sql/queries.sql | 6 +- procrastinate/sql/schema.sql | 52 ++-- procrastinate/testing.py | 9 +- procrastinate/worker.py | 4 +- tests/acceptance/test_async.py | 4 + tests/acceptance/test_shell.py | 14 +- .../integration/contrib/django/test_models.py | 2 + tests/integration/test_cli.py | 5 + tests/integration/test_manager.py | 4 +- tests/unit/test_jobs.py | 1 + tests/unit/test_manager.py | 10 +- tests/unit/test_shell.py | 18 +- tests/unit/test_tasks.py | 1 + tests/unit/test_testing.py | 1 + tests/unit/test_worker.py | 6 +- 22 files changed, 408 insertions(+), 92 deletions(-) create mode 100644 procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py create mode 100644 procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql diff --git a/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py b/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py new file mode 100644 index 000000000..717e1b8fa --- /dev/null +++ b/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from django.db import migrations, models + +from .. import migrations_utils + + +class Migration(migrations.Migration): + operations = [ + migrations_utils.RunProcrastinateSQL( + name="02.09.02_01_add_abort_on_procrastinate_jobs.sql" + ), + migrations.AlterField( + "procrastinatejob", + "status", + models.CharField( + choices=[ + ("todo", "todo"), + ("doing", "doing"), + ("succeeded", "succeeded"), + ("failed", "failed"), + ("cancelled", "cancelled"), + ("aborted", "aborted"), + ], + max_length=32, + ), + ), + migrations.AddField( + "procrastinatejob", + "abort", + models.BooleanField(), + ), + ] + name = "0030_add_abort_on_procrastinate_jobs" + dependencies = [("procrastinate", "0029_add_additional_params_to_retry_job")] diff --git a/procrastinate/contrib/django/models.py b/procrastinate/contrib/django/models.py index 3dfeb1494..1bd02a70e 100644 --- a/procrastinate/contrib/django/models.py +++ b/procrastinate/contrib/django/models.py @@ -65,7 +65,6 @@ class ProcrastinateJob(ProcrastinateReadOnlyModelMixin, models.Model): "succeeded", "failed", "cancelled", - "aborting", "aborted", ) id = models.BigAutoField(primary_key=True) @@ -78,6 +77,7 @@ class ProcrastinateJob(ProcrastinateReadOnlyModelMixin, models.Model): scheduled_at = models.DateTimeField(blank=True, null=True) attempts = models.IntegerField() queueing_lock = models.TextField(unique=True, blank=True, null=True) + abort = models.BooleanField() objects = ProcrastinateReadOnlyManager() diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index a35fe5582..f393c77be 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -108,8 +108,7 @@ def should_abort(self) -> bool: assert self.job.id job_id = self.job.id - status = self.app.job_manager.get_job_status(job_id) - return status == jobs.Status.ABORTING + return self.app.job_manager.get_job_abort(job_id) async def should_abort_async(self) -> bool: assert self.app @@ -117,5 +116,4 @@ async def should_abort_async(self) -> bool: assert self.job.id job_id = self.job.id - status = await self.app.job_manager.get_job_status_async(job_id) - return status == jobs.Status.ABORTING + return await self.app.job_manager.get_job_abort_async(job_id) diff --git a/procrastinate/jobs.py b/procrastinate/jobs.py index bcd55a6d5..98afebfd2 100644 --- a/procrastinate/jobs.py +++ b/procrastinate/jobs.py @@ -39,7 +39,6 @@ class Status(Enum): SUCCEEDED = "succeeded" #: The job ended successfully FAILED = "failed" #: The job ended with an error CANCELLED = "cancelled" #: The job was cancelled - ABORTING = "aborting" #: The job is requested to be aborted ABORTED = "aborted" #: The job was aborted diff --git a/procrastinate/manager.py b/procrastinate/manager.py index 9657e4528..1ae7492a7 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -252,9 +252,9 @@ def cancel_job_by_id( job_id : ``int`` The id of the job to cancel abort : ``bool`` - If True, a job in ``doing`` state will be marked as ``aborting``, but the task - itself has to respect the abortion request. If False, only jobs in ``todo`` - state will be set to ``cancelled`` and won't be processed by a worker anymore. + If True, a job will be marked for abortion, but the task itself has to + respect the abortion request. If False, only jobs in ``todo`` state will + be set to ``cancelled`` and won't be processed by a worker anymore. delete_job : ``bool`` If True, the job will be deleted from the database after being cancelled. Does not affect the jobs that should be aborted. @@ -290,9 +290,9 @@ async def cancel_job_by_id_async( job_id : ``int`` The id of the job to cancel abort : ``bool`` - If True, a job in ``doing`` state will be marked as ``aborting``, but the task - itself has to respect the abortion request. If False, only jobs in ``todo`` - state will be set to ``cancelled`` and won't be processed by a worker anymore. + If True, a job will be marked for abortion, but the task itself has to + respect the abortion request. If False, only jobs in ``todo`` state will + be set to ``cancelled`` and won't be processed by a worker anymore. delete_job : ``bool`` If True, the job will be deleted from the database after being cancelled. Does not affect the jobs that should be aborted. @@ -353,6 +353,42 @@ async def get_job_status_async(self, job_id: int) -> jobs.Status: ) return jobs.Status(result["status"]) + def get_job_abort(self, job_id: int) -> bool: + """ + Check if a job is marked for abortion by its id. + + Parameters + ---------- + job_id : ``int`` + The id of the job to get the status of + + Returns + ------- + ``bool`` + """ + result = self.connector.get_sync_connector().execute_query_one( + query=sql.queries["get_job_abort"], job_id=job_id + ) + return bool(result["abort"]) + + async def get_job_abort_async(self, job_id: int) -> bool: + """ + Check if a job is marked for abortion by its id. + + Parameters + ---------- + job_id : ``int`` + The id of the job to get the status of + + Returns + ------- + ``bool`` + """ + result = await self.connector.execute_query_one_async( + query=sql.queries["get_job_abort"], job_id=job_id + ) + return bool(result["abort"]) + async def retry_job( self, job: jobs.Job, @@ -584,8 +620,7 @@ async def list_queues_async( ------- ``List[Dict[str, Any]]`` A list of dictionaries representing queues stats (``name``, ``jobs_count``, - ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborting``, - ``aborted``). + ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborted``). """ return [ { @@ -596,7 +631,6 @@ async def list_queues_async( "succeeded": row["stats"].get("succeeded", 0), "failed": row["stats"].get("failed", 0), "cancelled": row["stats"].get("cancelled", 0), - "aborting": row["stats"].get("aborting", 0), "aborted": row["stats"].get("aborted", 0), } for row in await self.connector.execute_query_all_async( @@ -627,7 +661,6 @@ def list_queues( "succeeded": row["stats"].get("succeeded", 0), "failed": row["stats"].get("failed", 0), "cancelled": row["stats"].get("cancelled", 0), - "aborting": row["stats"].get("aborting", 0), "aborted": row["stats"].get("aborted", 0), } for row in self.connector.get_sync_connector().execute_query_all( @@ -664,8 +697,7 @@ async def list_tasks_async( ------- ``List[Dict[str, Any]]`` A list of dictionaries representing tasks stats (``name``, ``jobs_count``, - ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborting``, - ``aborted``). + ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborted``). """ return [ { @@ -676,7 +708,6 @@ async def list_tasks_async( "succeeded": row["stats"].get("succeeded", 0), "failed": row["stats"].get("failed", 0), "cancelled": row["stats"].get("cancelled", 0), - "aborting": row["stats"].get("aborting", 0), "aborted": row["stats"].get("aborted", 0), } for row in await self.connector.execute_query_all_async( @@ -707,7 +738,6 @@ def list_tasks( "succeeded": row["stats"].get("succeeded", 0), "failed": row["stats"].get("failed", 0), "cancelled": row["stats"].get("cancelled", 0), - "aborting": row["stats"].get("aborting", 0), "aborted": row["stats"].get("aborted", 0), } for row in self.connector.get_sync_connector().execute_query_all( @@ -744,8 +774,7 @@ async def list_locks_async( ------- ``List[Dict[str, Any]]`` A list of dictionaries representing locks stats (``name``, ``jobs_count``, - ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborting``, - ``aborted``). + ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborted``). """ result = [] for row in await self.connector.execute_query_all_async( @@ -764,7 +793,6 @@ async def list_locks_async( "succeeded": row["stats"].get("succeeded", 0), "failed": row["stats"].get("failed", 0), "cancelled": row["stats"].get("cancelled", 0), - "aborting": row["stats"].get("aborting", 0), "aborted": row["stats"].get("aborted", 0), } ) @@ -797,7 +825,6 @@ def list_locks( "succeeded": row["stats"].get("succeeded", 0), "failed": row["stats"].get("failed", 0), "cancelled": row["stats"].get("cancelled", 0), - "aborting": row["stats"].get("aborting", 0), "aborted": row["stats"].get("aborted", 0), } ) diff --git a/procrastinate/shell.py b/procrastinate/shell.py index c5940b7ed..206c78026 100644 --- a/procrastinate/shell.py +++ b/procrastinate/shell.py @@ -89,7 +89,6 @@ def do_list_queues(self, arg: str) -> None: f"succeeded: {queue['succeeded']}, " f"failed: {queue['failed']}, " f"cancelled: {queue['cancelled']}, " - f"aborting: {queue['aborting']}, " f"aborted: {queue['aborted']})" ) @@ -112,7 +111,6 @@ def do_list_tasks(self, arg: str) -> None: f"succeeded: {task['succeeded']}, " f"failed: {task['failed']}, " f"cancelled: {task['cancelled']}, " - f"aborting: {task['aborting']}, " f"aborted: {task['aborted']})" ) @@ -135,7 +133,6 @@ def do_list_locks(self, arg: str) -> None: f"succeeded: {lock['succeeded']}, " f"failed: {lock['failed']}, " f"cancelled: {lock['cancelled']}, " - f"aborting: {lock['aborting']}, " f"aborted: {lock['aborted']})" ) diff --git a/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql b/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql new file mode 100644 index 000000000..e169ac868 --- /dev/null +++ b/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql @@ -0,0 +1,253 @@ +-- Add an 'abort' column to the procrastinate_jobs table +ALTER TABLE procrastinate_jobs ADD COLUMN "abort" boolean DEFAULT false NOT NULL; + +-- Set abort flag on all jobs with 'aborting' status +UPDATE procrastinate_jobs SET abort = true WHERE status = 'aborting'; + +-- Delete the indexes that depends on the old status and enum type +DROP INDEX IF EXISTS procrastinate_jobs_queueing_lock_idx; +DROP INDEX IF EXISTS procrastinate_jobs_lock_idx; +DROP INDEX IF EXISTS procrastinate_jobs_id_lock_idx; + +-- Delete the triggers that depends on the old status type (to recreate them later) +DROP TRIGGER IF EXISTS procrastinate_jobs_notify_queue ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_trigger_status_events_update ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_trigger_status_events_insert ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_trigger_scheduled_events ON procrastinate_jobs; + +-- Delete the functions that depends on the old status type +DROP FUNCTION IF EXISTS procrastinate_fetch_job; +DROP FUNCTION IF EXISTS procrastinate_finish_job(bigint, procrastinate_job_status, boolean); +DROP FUNCTION IF EXISTS procrastinate_cancel_job; +DROP FUNCTION IF EXISTS procrastinate_trigger_status_events_procedure_update; +DROP FUNCTION IF EXISTS procrastinate_finish_job(integer, procrastinate_job_status, timestamp with time zone, boolean); + +-- Create a new enum type without 'aborting' +CREATE TYPE procrastinate_job_status_new AS ENUM ( + 'todo', + 'doing', + 'succeeded', + 'failed', + 'cancelled', + 'aborted' +); + +-- We need to drop the default temporarily as otherwise DatatypeMismatch would occur +ALTER TABLE procrastinate_jobs ALTER COLUMN status DROP DEFAULT; + +-- Alter the table to use the new type +ALTER TABLE procrastinate_jobs +ALTER COLUMN status TYPE procrastinate_job_status_new +USING ( + CASE status::text + WHEN 'aborting' THEN 'doing'::procrastinate_job_status_new + ELSE status::text::procrastinate_job_status_new + END +); + +-- Recreate the default +ALTER TABLE procrastinate_jobs ALTER COLUMN status SET DEFAULT 'todo'::procrastinate_job_status_new; + +-- Drop the old type +DROP TYPE procrastinate_job_status; + +-- Rename the new type to the original name +ALTER TYPE procrastinate_job_status_new RENAME TO procrastinate_job_status; + +-- Recreate the indexes +CREATE UNIQUE INDEX procrastinate_jobs_queueing_lock_idx ON procrastinate_jobs (queueing_lock) WHERE status = 'todo'; +CREATE UNIQUE INDEX procrastinate_jobs_lock_idx ON procrastinate_jobs (lock) WHERE status = 'doing'; +CREATE INDEX procrastinate_jobs_id_lock_idx ON procrastinate_jobs (id, lock) WHERE status = ANY (ARRAY['todo'::procrastinate_job_status, 'doing'::procrastinate_job_status]); + +-- Recreate and update the functions +CREATE OR REPLACE FUNCTION procrastinate_fetch_job( + target_queue_names character varying[] +) + RETURNS procrastinate_jobs + LANGUAGE plpgsql +AS $$ +DECLARE + found_jobs procrastinate_jobs; +BEGIN + WITH candidate AS ( + SELECT jobs.* + FROM procrastinate_jobs AS jobs + WHERE + -- reject the job if its lock has earlier jobs + NOT EXISTS ( + SELECT 1 + FROM procrastinate_jobs AS earlier_jobs + WHERE + jobs.lock IS NOT NULL + AND earlier_jobs.lock = jobs.lock + AND earlier_jobs.status IN ('todo', 'doing') + AND earlier_jobs.id < jobs.id) + AND jobs.status = 'todo' + AND (target_queue_names IS NULL OR jobs.queue_name = ANY( target_queue_names )) + AND (jobs.scheduled_at IS NULL OR jobs.scheduled_at <= now()) + ORDER BY jobs.priority DESC, jobs.id ASC LIMIT 1 + FOR UPDATE OF jobs SKIP LOCKED + ) + UPDATE procrastinate_jobs + SET status = 'doing' + FROM candidate + WHERE procrastinate_jobs.id = candidate.id + RETURNING procrastinate_jobs.* INTO found_jobs; + + RETURN found_jobs; +END; +$$; + +CREATE FUNCTION procrastinate_finish_job(job_id bigint, end_status procrastinate_job_status, delete_job boolean) + RETURNS void + LANGUAGE plpgsql +AS $$ +DECLARE + _job_id bigint; +BEGIN + IF end_status NOT IN ('succeeded', 'failed', 'aborted') THEN + RAISE 'End status should be either "succeeded", "failed" or "aborted" (job id: %)', job_id; + END IF; + IF delete_job THEN + DELETE FROM procrastinate_jobs + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + ELSE + UPDATE procrastinate_jobs + SET status = end_status, + abort = false, + attempts = CASE status + WHEN 'doing' THEN attempts + 1 ELSE attempts + END + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + END IF; + IF _job_id IS NULL THEN + RAISE 'Job was not found or not in "doing" or "todo" status (job id: %)', job_id; + END IF; +END; +$$; + +CREATE FUNCTION procrastinate_cancel_job(job_id bigint, abort boolean, delete_job boolean) + RETURNS bigint + LANGUAGE plpgsql +AS $$ +DECLARE + _job_id bigint; +BEGIN + IF delete_job THEN + DELETE FROM procrastinate_jobs + WHERE id = job_id AND status = 'todo' + RETURNING id INTO _job_id; + END IF; + IF _job_id IS NULL THEN + IF abort THEN + UPDATE procrastinate_jobs + SET abort = true, + status = CASE status + WHEN 'todo' THEN 'cancelled'::procrastinate_job_status ELSE status + END + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + ELSE + UPDATE procrastinate_jobs + SET status = 'cancelled'::procrastinate_job_status + WHERE id = job_id AND status = 'todo' + RETURNING id INTO _job_id; + END IF; + END IF; + RETURN _job_id; +END; +$$; + +CREATE FUNCTION procrastinate_trigger_status_events_procedure_update() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + WITH t AS ( + SELECT CASE + WHEN OLD.status = 'todo'::procrastinate_job_status + AND NEW.status = 'doing'::procrastinate_job_status + THEN 'started'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'todo'::procrastinate_job_status + THEN 'deferred_for_retry'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'failed'::procrastinate_job_status + THEN 'failed'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'succeeded'::procrastinate_job_status + THEN 'succeeded'::procrastinate_job_event_type + WHEN OLD.status = 'todo'::procrastinate_job_status + AND ( + NEW.status = 'cancelled'::procrastinate_job_status + OR NEW.status = 'failed'::procrastinate_job_status + OR NEW.status = 'succeeded'::procrastinate_job_status + ) + THEN 'cancelled'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'aborted'::procrastinate_job_status + THEN 'aborted'::procrastinate_job_event_type + ELSE NULL + END as event_type + ) + INSERT INTO procrastinate_events(job_id, type) + SELECT NEW.id, t.event_type + FROM t + WHERE t.event_type IS NOT NULL; + RETURN NEW; +END; +$$; + +CREATE FUNCTION procrastinate_finish_job(job_id integer, end_status procrastinate_job_status, next_scheduled_at timestamp with time zone, delete_job boolean) + RETURNS void + LANGUAGE plpgsql +AS $$ +DECLARE + _job_id bigint; +BEGIN + IF end_status NOT IN ('succeeded', 'failed') THEN + RAISE 'End status should be either "succeeded" or "failed" (job id: %)', job_id; + END IF; + IF delete_job THEN + DELETE FROM procrastinate_jobs + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + ELSE + UPDATE procrastinate_jobs + SET status = end_status, + attempts = + CASE + WHEN status = 'doing' THEN attempts + 1 + ELSE attempts + END + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + END IF; + IF _job_id IS NULL THEN + RAISE 'Job was not found or not in "doing" or "todo" status (job id: %)', job_id; + END IF; +END; +$$; + +-- Recreate the deleted triggers +CREATE TRIGGER procrastinate_jobs_notify_queue + AFTER INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue(); + +CREATE TRIGGER procrastinate_trigger_status_events_update + AFTER UPDATE OF status ON procrastinate_jobs + FOR EACH ROW + EXECUTE PROCEDURE procrastinate_trigger_status_events_procedure_update(); + +CREATE TRIGGER procrastinate_trigger_status_events_insert + AFTER INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_trigger_status_events_procedure_insert(); + +CREATE TRIGGER procrastinate_trigger_scheduled_events + AFTER UPDATE OR INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.scheduled_at IS NOT NULL AND new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_trigger_scheduled_events_procedure(); diff --git a/procrastinate/sql/queries.sql b/procrastinate/sql/queries.sql index 234217888..4a9f60ced 100644 --- a/procrastinate/sql/queries.sql +++ b/procrastinate/sql/queries.sql @@ -51,13 +51,17 @@ WHERE id IN ( SELECT procrastinate_finish_job(%(job_id)s, %(status)s, %(delete_job)s); -- cancel_job -- --- Cancel a job, changing it from "todo" to "cancelled" or from "doing" to "aborting" +-- Cancel a job, changing it from "todo" to "cancelled" or mark for abortion SELECT procrastinate_cancel_job(%(job_id)s, %(abort)s, %(delete_job)s) AS id; -- get_job_status -- -- Get the status of a job SELECT status FROM procrastinate_jobs WHERE id = %(job_id)s; +-- get_job_abort -- +-- Check if a job should be aborted +SELECT abort FROM procrastinate_jobs WHERE id = %(job_id)s; + -- retry_job -- -- Retry a job, changing it from "doing" to "todo" SELECT procrastinate_retry_job(%(job_id)s, %(retry_at)s, %(new_priority)s, %(new_queue_name)s, %(new_lock)s); diff --git a/procrastinate/sql/schema.sql b/procrastinate/sql/schema.sql index a2fe4d950..8958a8655 100644 --- a/procrastinate/sql/schema.sql +++ b/procrastinate/sql/schema.sql @@ -10,7 +10,6 @@ CREATE TYPE procrastinate_job_status AS ENUM ( 'succeeded', -- The job ended successfully 'failed', -- The job ended with an error 'cancelled', -- The job was cancelled - 'aborting', -- The job was requested to abort 'aborted' -- The job was aborted ); @@ -18,12 +17,12 @@ CREATE TYPE procrastinate_job_event_type AS ENUM ( 'deferred', -- Job created, in todo 'started', -- todo -> doing 'deferred_for_retry', -- doing -> todo - 'failed', -- doing or aborting -> failed - 'succeeded', -- doing or aborting -> succeeded + 'failed', -- doing -> failed + 'succeeded', -- doing -> succeeded 'cancelled', -- todo -> cancelled - 'abort_requested', -- doing -> aborting - 'aborted', -- doing or aborting -> aborted - 'scheduled' -- not an event transition, but recording when a task is scheduled for + 'abort_requested', -- not a state transition, but set in a separate field + 'aborted', -- doing -> aborted (only allowed when abort field is set) + 'scheduled' -- not a state transition, but recording when a task is scheduled for ); -- Tables @@ -38,7 +37,8 @@ CREATE TABLE procrastinate_jobs ( args jsonb DEFAULT '{}' NOT NULL, status procrastinate_job_status DEFAULT 'todo'::procrastinate_job_status NOT NULL, scheduled_at timestamp with time zone NULL, - attempts integer DEFAULT 0 NOT NULL + attempts integer DEFAULT 0 NOT NULL, + abort boolean DEFAULT false NOT NULL ); CREATE TABLE procrastinate_periodic_defers ( @@ -175,7 +175,7 @@ BEGIN WHERE jobs.lock IS NOT NULL AND earlier_jobs.lock = jobs.lock - AND earlier_jobs.status IN ('todo', 'doing', 'aborting') + AND earlier_jobs.status IN ('todo', 'doing') AND earlier_jobs.id < jobs.id) AND jobs.status = 'todo' AND (target_queue_names IS NULL OR jobs.queue_name = ANY( target_queue_names )) @@ -193,9 +193,6 @@ BEGIN END; $$; --- procrastinate_finish_job --- the next_scheduled_at argument is kept for compatibility reasons, it is to be --- removed after 1.0.0 is released CREATE FUNCTION procrastinate_finish_job(job_id bigint, end_status procrastinate_job_status, delete_job boolean) RETURNS void LANGUAGE plpgsql @@ -208,21 +205,20 @@ BEGIN END IF; IF delete_job THEN DELETE FROM procrastinate_jobs - WHERE id = job_id AND status IN ('todo', 'doing', 'aborting') + WHERE id = job_id AND status IN ('todo', 'doing') RETURNING id INTO _job_id; ELSE UPDATE procrastinate_jobs SET status = end_status, - attempts = - CASE - WHEN status = 'doing' THEN attempts + 1 - ELSE attempts - END - WHERE id = job_id AND status IN ('todo', 'doing', 'aborting') + abort = false, + attempts = CASE status + WHEN 'doing' THEN attempts + 1 ELSE attempts + END + WHERE id = job_id AND status IN ('todo', 'doing') RETURNING id INTO _job_id; END IF; IF _job_id IS NULL THEN - RAISE 'Job was not found or not in "doing", "todo" or "aborting" status (job id: %)', job_id; + RAISE 'Job was not found or not in "doing" or "todo" status (job id: %)', job_id; END IF; END; $$; @@ -242,10 +238,10 @@ BEGIN IF _job_id IS NULL THEN IF abort THEN UPDATE procrastinate_jobs - SET status = CASE status - WHEN 'todo' THEN 'cancelled'::procrastinate_job_status - WHEN 'doing' THEN 'aborting'::procrastinate_job_status - END + SET abort = true, + status = CASE status + WHEN 'todo' THEN 'cancelled'::procrastinate_job_status ELSE status + END WHERE id = job_id AND status IN ('todo', 'doing') RETURNING id INTO _job_id; ELSE @@ -336,12 +332,6 @@ BEGIN ) THEN 'cancelled'::procrastinate_job_event_type WHEN OLD.status = 'doing'::procrastinate_job_status - AND NEW.status = 'aborting'::procrastinate_job_status - THEN 'abort_requested'::procrastinate_job_event_type - WHEN ( - OLD.status = 'doing'::procrastinate_job_status - OR OLD.status = 'aborting'::procrastinate_job_status - ) AND NEW.status = 'aborted'::procrastinate_job_status THEN 'aborted'::procrastinate_job_event_type ELSE NULL @@ -406,8 +396,7 @@ CREATE TRIGGER procrastinate_trigger_delete_jobs FOR EACH ROW EXECUTE PROCEDURE procrastinate_unlink_periodic_defers(); --- Old versions of functions, for backwards compatibility (to be removed --- after 2.0.0) +-- Old versions of functions, for backwards compatibility (to be removed in a future release) -- procrastinate_defer_job -- the function without the priority argument is kept for compatibility reasons @@ -434,6 +423,7 @@ END; $$; -- procrastinate_finish_job +-- the next_scheduled_at argument is kept for compatibility reasons CREATE OR REPLACE FUNCTION procrastinate_finish_job(job_id integer, end_status procrastinate_job_status, next_scheduled_at timestamp with time zone, delete_job boolean) RETURNS void LANGUAGE plpgsql diff --git a/procrastinate/testing.py b/procrastinate/testing.py index 4af80bd2c..a17613bdc 100644 --- a/procrastinate/testing.py +++ b/procrastinate/testing.py @@ -138,6 +138,7 @@ def defer_job_one( "status": "todo", "scheduled_at": scheduled_at, "attempts": 0, + "abort": False, } self.events[id] = [] if scheduled_at: @@ -222,6 +223,7 @@ def finish_job_run(self, job_id: int, status: str, delete_job: bool) -> None: job_row = self.jobs[job_id] job_row["status"] = status job_row["attempts"] += 1 + job_row["abort"] = False self.events[job_id].append({"type": status, "at": utils.utcnow()}) def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: @@ -235,8 +237,8 @@ def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: job_row["status"] = "cancelled" return {"id": job_id} - if abort and job_row["status"] == "doing": - job_row["status"] = "aborting" + if abort: + job_row["abort"] = True return {"id": job_id} return {"id": None} @@ -244,6 +246,9 @@ def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: def get_job_status_one(self, job_id: int) -> dict: return {"status": self.jobs[job_id]["status"]} + def get_job_abort_one(self, job_id: int) -> dict: + return {"abort": self.jobs[job_id]["abort"]} + def retry_job_run( self, job_id: int, diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 1402d8260..49b9698ec 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -307,9 +307,9 @@ async def run_job(self, job: jobs.Job, worker_id: int) -> None: critical = not isinstance(e, Exception) assert job.id - status = await self.job_manager.get_job_status_async(job_id=job.id) + abort_requested = await self.job_manager.get_job_abort_async(job_id=job.id) - if status == jobs.Status.ABORTING: + if abort_requested: retry_exception = None else: retry_exception = task.get_retry_exception(exception=e, job=job) diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index 4056f4f07..6cc4499c6 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -142,9 +142,13 @@ def task2(context): status = await async_app.job_manager.get_job_status_async(job1_id) assert status == Status.ABORTED + abort = await async_app.job_manager.get_job_abort_async(job1_id) + assert abort is False status = await async_app.job_manager.get_job_status_async(job2_id) assert status == Status.ABORTED + abort = await async_app.job_manager.get_job_abort_async(job2_id) + assert abort is False async def test_retry_when_aborting(async_app): diff --git a/tests/acceptance/test_shell.py b/tests/acceptance/test_shell.py index 1194c3283..1613bc10c 100644 --- a/tests/acceptance/test_shell.py +++ b/tests/acceptance/test_shell.py @@ -96,19 +96,19 @@ async def test_shell(read, write, defer): await write("list_queues") assert await read() == [ - "default: 3 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 2, aborting: 0, aborted: 0)", - "other: 1 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 1, aborting: 0, aborted: 0)", + "default: 3 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 2, aborted: 0)", + "other: 1 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 1, aborted: 0)", ] await write("list_tasks") assert await read() == [ - "ns:tests.acceptance.app.sum_task: 3 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 2, aborting: 0, aborted: 0)", - "tests.acceptance.app.increment_task: 1 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 1, aborting: 0, aborted: 0)", + "ns:tests.acceptance.app.sum_task: 3 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 2, aborted: 0)", + "tests.acceptance.app.increment_task: 1 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 1, aborted: 0)", ] await write("list_locks") assert await read() == [ - "a: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", - "b: 1 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 1, aborting: 0, aborted: 0)", - "lock: 2 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 2, aborting: 0, aborted: 0)", + "a: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", + "b: 1 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 1, aborted: 0)", + "lock: 2 jobs (todo: 0, doing: 0, succeeded: 0, failed: 0, cancelled: 2, aborted: 0)", ] diff --git a/tests/integration/contrib/django/test_models.py b/tests/integration/contrib/django/test_models.py index f3fe8ba4a..29f2819a2 100644 --- a/tests/integration/contrib/django/test_models.py +++ b/tests/integration/contrib/django/test_models.py @@ -27,6 +27,7 @@ def test_procrastinate_job(db): "scheduled_at": None, "attempts": 0, "queueing_lock": None, + "abort": False, } @@ -47,6 +48,7 @@ def test_procrastinate_job__create__with_setting(db, settings): scheduled_at=datetime.datetime.now(datetime.timezone.utc), attempts=0, queueing_lock="baz", + abort=False, ) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index b54817382..4ae1aa399 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -167,6 +167,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 0, + "abort": False, } } @@ -192,6 +193,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 5, + "abort": False, } } @@ -222,6 +224,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 0, + "abort": False, } } @@ -251,6 +254,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 0, + "abort": False, } assert ( now + datetime.timedelta(seconds=9) @@ -315,6 +319,7 @@ async def test_defer_unknown(entrypoint, cli_app, connector): "status": "todo", "task_name": "hello", "priority": 0, + "abort": False, } } diff --git a/tests/integration/test_manager.py b/tests/integration/test_manager.py index 8c9173f53..48656bf32 100644 --- a/tests/integration/test_manager.py +++ b/tests/integration/test_manager.py @@ -271,7 +271,7 @@ async def test_finish_job_wrong_initial_status( await pg_job_manager.finish_job( job=job, status=jobs.Status.FAILED, delete_job=delete_job ) - assert 'Job was not found or not in "doing", "todo" or "aborting" status' in str( + assert 'Job was not found or not in "doing" or "todo" status' in str( excinfo.value.__cause__ ) @@ -484,7 +484,6 @@ async def test_list_queues_dict(fixture_jobs, pg_job_manager): "succeeded": 0, "failed": 1, "cancelled": 0, - "aborting": 0, "aborted": 0, } @@ -514,7 +513,6 @@ async def test_list_tasks_dict(fixture_jobs, pg_job_manager): "succeeded": 0, "failed": 1, "cancelled": 0, - "aborting": 0, "aborted": 0, } diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index 6c776772b..785f8c984 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -79,6 +79,7 @@ async def test_job_deferrer_defer_async(job_factory, job_manager, connector): "scheduled_at": None, "status": "todo", "task_name": "mytask", + "abort": False, } } diff --git a/tests/unit/test_manager.py b/tests/unit/test_manager.py index 3d14f2073..50da50e39 100644 --- a/tests/unit/test_manager.py +++ b/tests/unit/test_manager.py @@ -35,6 +35,7 @@ async def test_manager_defer_job(job_manager, job_factory, connector): "scheduled_at": None, "status": "todo", "task_name": "bla", + "abort": False, } } @@ -250,7 +251,8 @@ async def test_abort_doing_job(job_manager, job_factory, connector): "cancel_job", {"job_id": 1, "abort": True, "delete_job": False}, ) - assert connector.jobs[1]["status"] == "aborting" + assert connector.jobs[1]["status"] == "doing" + assert connector.jobs[1]["abort"] is True def test_get_job_status(job_manager, job_factory, connector): @@ -466,7 +468,6 @@ async def test_list_queues_async(job_manager, job_factory): "succeeded": 0, "failed": 0, "cancelled": 0, - "aborting": 0, "aborted": 0, } ] @@ -484,7 +485,6 @@ def test_list_queues_(job_manager, job_factory): "succeeded": 0, "failed": 0, "cancelled": 0, - "aborting": 0, "aborted": 0, } ] @@ -502,7 +502,6 @@ async def test_list_tasks_async(job_manager, job_factory): "succeeded": 0, "failed": 0, "cancelled": 0, - "aborting": 0, "aborted": 0, } ] @@ -520,7 +519,6 @@ def test_list_tasks(job_manager, job_factory): "succeeded": 0, "failed": 0, "cancelled": 0, - "aborting": 0, "aborted": 0, } ] @@ -538,7 +536,6 @@ async def test_list_locks_async(job_manager, job_factory): "succeeded": 0, "failed": 0, "cancelled": 0, - "aborting": 0, "aborted": 0, } ] @@ -556,7 +553,6 @@ def test_list_locks(job_manager, job_factory): "succeeded": 0, "failed": 0, "cancelled": 0, - "aborting": 0, "aborted": 0, } ] diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index 3e2db2cdb..c9d881d40 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -145,8 +145,8 @@ def test_list_queues(shell, connector, capsys): shell.do_list_queues("") captured = capsys.readouterr() assert captured.out.splitlines() == [ - "queue1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", - "queue2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", + "queue1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", + "queue2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", ] assert connector.queries == [ ( @@ -163,7 +163,7 @@ def test_list_queues_filters(shell, connector, capsys): shell.do_list_queues("queue=queue2 task=task2 lock=lock2 status=todo") captured = capsys.readouterr() assert captured.out.splitlines() == [ - "queue2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", + "queue2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", ] assert connector.queries == [ ( @@ -191,8 +191,8 @@ def test_list_tasks(shell, connector, capsys): shell.do_list_tasks("") captured = capsys.readouterr() assert captured.out.splitlines() == [ - "task1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", - "task2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", + "task1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", + "task2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", ] assert connector.queries == [ ( @@ -209,7 +209,7 @@ def test_list_tasks_filters(shell, connector, capsys): shell.do_list_tasks("queue=queue2 task=task2 lock=lock2 status=todo") captured = capsys.readouterr() assert captured.out.splitlines() == [ - "task2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", + "task2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", ] assert connector.queries == [ ( @@ -237,8 +237,8 @@ def test_list_locks(shell, connector, capsys): shell.do_list_locks("") captured = capsys.readouterr() assert captured.out.splitlines() == [ - "lock1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", - "lock2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", + "lock1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", + "lock2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", ] assert connector.queries == [ ( @@ -255,7 +255,7 @@ def test_list_locks_filters(shell, connector, capsys): shell.do_list_locks("queue=queue2 task=task2 lock=lock2 status=todo") captured = capsys.readouterr() assert captured.out.splitlines() == [ - "lock2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborting: 0, aborted: 0)", + "lock2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", ] assert connector.queries == [ ( diff --git a/tests/unit/test_tasks.py b/tests/unit/test_tasks.py index c6e757651..4269078af 100644 --- a/tests/unit/test_tasks.py +++ b/tests/unit/test_tasks.py @@ -38,6 +38,7 @@ async def test_task_defer_async(app: App, connector): "status": "todo", "scheduled_at": None, "attempts": 0, + "abort": False, } } diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py index d50915330..8ad6c3e1d 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/test_testing.py @@ -80,6 +80,7 @@ def test_defer_job_one(connector): "status": "todo", "scheduled_at": None, "attempts": 0, + "abort": False, } } assert connector.jobs[1] == job diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 790b295bb..f21bf1316 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -385,7 +385,7 @@ def job_func(a, b): # pylint: disable=unused-argument task_name="job", queue="yay", ) - app.job_manager.get_job_status_async = mocker.AsyncMock(return_value="doing") + app.job_manager.get_job_abort_async = mocker.AsyncMock(return_value=False) test_worker = worker.Worker(app, queues=["yay"]) with pytest.raises(exceptions.JobError): await test_worker.run_job(job=job, worker_id=3) @@ -421,7 +421,7 @@ def job_func(a, b): # pylint: disable=unused-argument task_name="job", queue="yay", ) - app.job_manager.get_job_status_async = mocker.AsyncMock(return_value="doing") + app.job_manager.get_job_abort_async = mocker.AsyncMock(return_value=False) test_worker = worker.Worker(app, queues=["yay"]) with pytest.raises(exceptions.JobError) as exc_info: await test_worker.run_job(job=job, worker_id=3) @@ -448,7 +448,7 @@ def job_func(a, b): # pylint: disable=unused-argument queueing_lock="houba", queue="yay", ) - app.job_manager.get_job_status_async = mocker.AsyncMock(return_value="doing") + app.job_manager.get_job_abort_async = mocker.AsyncMock(return_value=False) test_worker = worker.Worker(app, queues=["yay"]) with pytest.raises(exceptions.JobError) as exc_info: await test_worker.run_job(job=job, worker_id=3) From f5137be676144fd4b2a3ae1166ce84ec9b3320f4 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Wed, 31 Jul 2024 23:06:04 +0000 Subject: [PATCH 032/375] Rename abort field to abort_requested --- .../0030_add_abort_on_procrastinate_jobs.py | 2 +- procrastinate/contrib/django/models.py | 2 +- procrastinate/job_context.py | 4 ++-- procrastinate/manager.py | 20 +++++++++---------- ....02_01_add_abort_on_procrastinate_jobs.sql | 13 ++++++------ procrastinate/sql/queries.sql | 6 +++--- procrastinate/sql/schema.sql | 9 +++++---- procrastinate/testing.py | 10 +++++----- procrastinate/worker.py | 2 +- tests/acceptance/test_async.py | 8 ++++---- .../integration/contrib/django/test_models.py | 4 ++-- tests/integration/test_cli.py | 10 +++++----- tests/unit/test_jobs.py | 2 +- tests/unit/test_manager.py | 4 ++-- tests/unit/test_tasks.py | 2 +- tests/unit/test_testing.py | 2 +- tests/unit/test_worker.py | 6 +++--- 17 files changed, 54 insertions(+), 52 deletions(-) diff --git a/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py b/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py index 717e1b8fa..97a975d3c 100644 --- a/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py +++ b/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py @@ -27,7 +27,7 @@ class Migration(migrations.Migration): ), migrations.AddField( "procrastinatejob", - "abort", + "abort_requested", models.BooleanField(), ), ] diff --git a/procrastinate/contrib/django/models.py b/procrastinate/contrib/django/models.py index 1bd02a70e..8aaa118eb 100644 --- a/procrastinate/contrib/django/models.py +++ b/procrastinate/contrib/django/models.py @@ -77,7 +77,7 @@ class ProcrastinateJob(ProcrastinateReadOnlyModelMixin, models.Model): scheduled_at = models.DateTimeField(blank=True, null=True) attempts = models.IntegerField() queueing_lock = models.TextField(unique=True, blank=True, null=True) - abort = models.BooleanField() + abort_requested = models.BooleanField() objects = ProcrastinateReadOnlyManager() diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index f393c77be..a00cd1296 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -108,7 +108,7 @@ def should_abort(self) -> bool: assert self.job.id job_id = self.job.id - return self.app.job_manager.get_job_abort(job_id) + return self.app.job_manager.get_job_abort_requested(job_id) async def should_abort_async(self) -> bool: assert self.app @@ -116,4 +116,4 @@ async def should_abort_async(self) -> bool: assert self.job.id job_id = self.job.id - return await self.app.job_manager.get_job_abort_async(job_id) + return await self.app.job_manager.get_job_abort_requested_async(job_id) diff --git a/procrastinate/manager.py b/procrastinate/manager.py index 1ae7492a7..60073e954 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -353,41 +353,41 @@ async def get_job_status_async(self, job_id: int) -> jobs.Status: ) return jobs.Status(result["status"]) - def get_job_abort(self, job_id: int) -> bool: + def get_job_abort_requested(self, job_id: int) -> bool: """ - Check if a job is marked for abortion by its id. + Check if a job is requested for abortion. Parameters ---------- job_id : ``int`` - The id of the job to get the status of + The id of the job to get the abortion request of Returns ------- ``bool`` """ result = self.connector.get_sync_connector().execute_query_one( - query=sql.queries["get_job_abort"], job_id=job_id + query=sql.queries["get_job_abort_requested"], job_id=job_id ) - return bool(result["abort"]) + return bool(result["abort_requested"]) - async def get_job_abort_async(self, job_id: int) -> bool: + async def get_job_abort_requested_async(self, job_id: int) -> bool: """ - Check if a job is marked for abortion by its id. + Check if a job is requested for abortion. Parameters ---------- job_id : ``int`` - The id of the job to get the status of + The id of the job to get the abortion request of Returns ------- ``bool`` """ result = await self.connector.execute_query_one_async( - query=sql.queries["get_job_abort"], job_id=job_id + query=sql.queries["get_job_abort_requested"], job_id=job_id ) - return bool(result["abort"]) + return bool(result["abort_requested"]) async def retry_job( self, diff --git a/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql b/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql index e169ac868..5ac819206 100644 --- a/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql +++ b/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql @@ -1,8 +1,8 @@ --- Add an 'abort' column to the procrastinate_jobs table -ALTER TABLE procrastinate_jobs ADD COLUMN "abort" boolean DEFAULT false NOT NULL; +-- Add an 'abort_requested' column to the procrastinate_jobs table +ALTER TABLE procrastinate_jobs ADD COLUMN abort_requested boolean DEFAULT false NOT NULL; --- Set abort flag on all jobs with 'aborting' status -UPDATE procrastinate_jobs SET abort = true WHERE status = 'aborting'; +-- Set abort requested flag on all jobs with 'aborting' status +UPDATE procrastinate_jobs SET abort_requested = true WHERE status = 'aborting'; -- Delete the indexes that depends on the old status and enum type DROP INDEX IF EXISTS procrastinate_jobs_queueing_lock_idx; @@ -115,7 +115,7 @@ BEGIN ELSE UPDATE procrastinate_jobs SET status = end_status, - abort = false, + abort_requested = false, attempts = CASE status WHEN 'doing' THEN attempts + 1 ELSE attempts END @@ -143,7 +143,7 @@ BEGIN IF _job_id IS NULL THEN IF abort THEN UPDATE procrastinate_jobs - SET abort = true, + SET abort_requested = true, status = CASE status WHEN 'todo' THEN 'cancelled'::procrastinate_job_status ELSE status END @@ -217,6 +217,7 @@ BEGIN ELSE UPDATE procrastinate_jobs SET status = end_status, + abort_requested = false, attempts = CASE WHEN status = 'doing' THEN attempts + 1 diff --git a/procrastinate/sql/queries.sql b/procrastinate/sql/queries.sql index 4a9f60ced..ce4908f7d 100644 --- a/procrastinate/sql/queries.sql +++ b/procrastinate/sql/queries.sql @@ -58,9 +58,9 @@ SELECT procrastinate_cancel_job(%(job_id)s, %(abort)s, %(delete_job)s) AS id; -- Get the status of a job SELECT status FROM procrastinate_jobs WHERE id = %(job_id)s; --- get_job_abort -- --- Check if a job should be aborted -SELECT abort FROM procrastinate_jobs WHERE id = %(job_id)s; +-- get_job_abort_requested -- +-- Check if an abortion of a job was requested +SELECT abort_requested FROM procrastinate_jobs WHERE id = %(job_id)s; -- retry_job -- -- Retry a job, changing it from "doing" to "todo" diff --git a/procrastinate/sql/schema.sql b/procrastinate/sql/schema.sql index 8958a8655..a24a9951f 100644 --- a/procrastinate/sql/schema.sql +++ b/procrastinate/sql/schema.sql @@ -21,7 +21,7 @@ CREATE TYPE procrastinate_job_event_type AS ENUM ( 'succeeded', -- doing -> succeeded 'cancelled', -- todo -> cancelled 'abort_requested', -- not a state transition, but set in a separate field - 'aborted', -- doing -> aborted (only allowed when abort field is set) + 'aborted', -- doing -> aborted (only allowed when abort_requested field is set) 'scheduled' -- not a state transition, but recording when a task is scheduled for ); @@ -38,7 +38,7 @@ CREATE TABLE procrastinate_jobs ( status procrastinate_job_status DEFAULT 'todo'::procrastinate_job_status NOT NULL, scheduled_at timestamp with time zone NULL, attempts integer DEFAULT 0 NOT NULL, - abort boolean DEFAULT false NOT NULL + abort_requested boolean DEFAULT false NOT NULL ); CREATE TABLE procrastinate_periodic_defers ( @@ -210,7 +210,7 @@ BEGIN ELSE UPDATE procrastinate_jobs SET status = end_status, - abort = false, + abort_requested = false, attempts = CASE status WHEN 'doing' THEN attempts + 1 ELSE attempts END @@ -238,7 +238,7 @@ BEGIN IF _job_id IS NULL THEN IF abort THEN UPDATE procrastinate_jobs - SET abort = true, + SET abort_requested = true, status = CASE status WHEN 'todo' THEN 'cancelled'::procrastinate_job_status ELSE status END @@ -441,6 +441,7 @@ BEGIN ELSE UPDATE procrastinate_jobs SET status = end_status, + abort_requested = false, attempts = CASE WHEN status = 'doing' THEN attempts + 1 diff --git a/procrastinate/testing.py b/procrastinate/testing.py index a17613bdc..b5756c9ff 100644 --- a/procrastinate/testing.py +++ b/procrastinate/testing.py @@ -138,7 +138,7 @@ def defer_job_one( "status": "todo", "scheduled_at": scheduled_at, "attempts": 0, - "abort": False, + "abort_requested": False, } self.events[id] = [] if scheduled_at: @@ -223,7 +223,7 @@ def finish_job_run(self, job_id: int, status: str, delete_job: bool) -> None: job_row = self.jobs[job_id] job_row["status"] = status job_row["attempts"] += 1 - job_row["abort"] = False + job_row["abort_requested"] = False self.events[job_id].append({"type": status, "at": utils.utcnow()}) def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: @@ -238,7 +238,7 @@ def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: return {"id": job_id} if abort: - job_row["abort"] = True + job_row["abort_requested"] = True return {"id": job_id} return {"id": None} @@ -246,8 +246,8 @@ def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: def get_job_status_one(self, job_id: int) -> dict: return {"status": self.jobs[job_id]["status"]} - def get_job_abort_one(self, job_id: int) -> dict: - return {"abort": self.jobs[job_id]["abort"]} + def get_job_abort_requested_one(self, job_id: int) -> dict: + return {"abort_requested": self.jobs[job_id]["abort_requested"]} def retry_job_run( self, diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 49b9698ec..f1af47021 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -307,7 +307,7 @@ async def run_job(self, job: jobs.Job, worker_id: int) -> None: critical = not isinstance(e, Exception) assert job.id - abort_requested = await self.job_manager.get_job_abort_async(job_id=job.id) + abort_requested = await self.job_manager.get_job_abort_requested_async(job_id=job.id) if abort_requested: retry_exception = None diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index 6cc4499c6..ca0a6182a 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -142,13 +142,13 @@ def task2(context): status = await async_app.job_manager.get_job_status_async(job1_id) assert status == Status.ABORTED - abort = await async_app.job_manager.get_job_abort_async(job1_id) - assert abort is False + abort_requested = await async_app.job_manager.get_job_abort_requested_async(job1_id) + assert abort_requested is False status = await async_app.job_manager.get_job_status_async(job2_id) assert status == Status.ABORTED - abort = await async_app.job_manager.get_job_abort_async(job2_id) - assert abort is False + abort_requested = await async_app.job_manager.get_job_abort_requested_async(job2_id) + assert abort_requested is False async def test_retry_when_aborting(async_app): diff --git a/tests/integration/contrib/django/test_models.py b/tests/integration/contrib/django/test_models.py index 29f2819a2..52c65ace7 100644 --- a/tests/integration/contrib/django/test_models.py +++ b/tests/integration/contrib/django/test_models.py @@ -27,7 +27,7 @@ def test_procrastinate_job(db): "scheduled_at": None, "attempts": 0, "queueing_lock": None, - "abort": False, + "abort_requested": False, } @@ -48,7 +48,7 @@ def test_procrastinate_job__create__with_setting(db, settings): scheduled_at=datetime.datetime.now(datetime.timezone.utc), attempts=0, queueing_lock="baz", - abort=False, + abort_requested=False, ) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 4ae1aa399..21a06483a 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -167,7 +167,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 0, - "abort": False, + "abort_requested": False, } } @@ -193,7 +193,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 5, - "abort": False, + "abort_requested": False, } } @@ -224,7 +224,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 0, - "abort": False, + "abort_requested": False, } } @@ -254,7 +254,7 @@ def mytask(a): "status": "todo", "task_name": "hello", "priority": 0, - "abort": False, + "abort_requested": False, } assert ( now + datetime.timedelta(seconds=9) @@ -319,7 +319,7 @@ async def test_defer_unknown(entrypoint, cli_app, connector): "status": "todo", "task_name": "hello", "priority": 0, - "abort": False, + "abort_requested": False, } } diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index 785f8c984..b87ea49e1 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -79,7 +79,7 @@ async def test_job_deferrer_defer_async(job_factory, job_manager, connector): "scheduled_at": None, "status": "todo", "task_name": "mytask", - "abort": False, + "abort_requested": False, } } diff --git a/tests/unit/test_manager.py b/tests/unit/test_manager.py index 50da50e39..5318f38c9 100644 --- a/tests/unit/test_manager.py +++ b/tests/unit/test_manager.py @@ -35,7 +35,7 @@ async def test_manager_defer_job(job_manager, job_factory, connector): "scheduled_at": None, "status": "todo", "task_name": "bla", - "abort": False, + "abort_requested": False, } } @@ -252,7 +252,7 @@ async def test_abort_doing_job(job_manager, job_factory, connector): {"job_id": 1, "abort": True, "delete_job": False}, ) assert connector.jobs[1]["status"] == "doing" - assert connector.jobs[1]["abort"] is True + assert connector.jobs[1]["abort_requested"] is True def test_get_job_status(job_manager, job_factory, connector): diff --git a/tests/unit/test_tasks.py b/tests/unit/test_tasks.py index 4269078af..0005bd84b 100644 --- a/tests/unit/test_tasks.py +++ b/tests/unit/test_tasks.py @@ -38,7 +38,7 @@ async def test_task_defer_async(app: App, connector): "status": "todo", "scheduled_at": None, "attempts": 0, - "abort": False, + "abort_requested": False, } } diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py index 8ad6c3e1d..279d79730 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/test_testing.py @@ -80,7 +80,7 @@ def test_defer_job_one(connector): "status": "todo", "scheduled_at": None, "attempts": 0, - "abort": False, + "abort_requested": False, } } assert connector.jobs[1] == job diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index f21bf1316..0282f9d54 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -385,7 +385,7 @@ def job_func(a, b): # pylint: disable=unused-argument task_name="job", queue="yay", ) - app.job_manager.get_job_abort_async = mocker.AsyncMock(return_value=False) + app.job_manager.get_job_abort_requested_async = mocker.AsyncMock(return_value=False) test_worker = worker.Worker(app, queues=["yay"]) with pytest.raises(exceptions.JobError): await test_worker.run_job(job=job, worker_id=3) @@ -421,7 +421,7 @@ def job_func(a, b): # pylint: disable=unused-argument task_name="job", queue="yay", ) - app.job_manager.get_job_abort_async = mocker.AsyncMock(return_value=False) + app.job_manager.get_job_abort_requested_async = mocker.AsyncMock(return_value=False) test_worker = worker.Worker(app, queues=["yay"]) with pytest.raises(exceptions.JobError) as exc_info: await test_worker.run_job(job=job, worker_id=3) @@ -448,7 +448,7 @@ def job_func(a, b): # pylint: disable=unused-argument queueing_lock="houba", queue="yay", ) - app.job_manager.get_job_abort_async = mocker.AsyncMock(return_value=False) + app.job_manager.get_job_abort_requested_async = mocker.AsyncMock(return_value=False) test_worker = worker.Worker(app, queues=["yay"]) with pytest.raises(exceptions.JobError) as exc_info: await test_worker.run_job(job=job, worker_id=3) From 86aa2f33d336b861158a2132205908d383af6c8a Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Wed, 31 Jul 2024 23:16:02 +0000 Subject: [PATCH 033/375] Record event when abortion is requested --- ...09.02_01_add_abort_on_procrastinate_jobs.sql | 17 +++++++++++++++++ procrastinate/sql/schema.sql | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql b/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql index 5ac819206..1a0b7dc58 100644 --- a/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql +++ b/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql @@ -252,3 +252,20 @@ CREATE TRIGGER procrastinate_trigger_scheduled_events AFTER UPDATE OR INSERT ON procrastinate_jobs FOR EACH ROW WHEN ((new.scheduled_at IS NOT NULL AND new.status = 'todo'::procrastinate_job_status)) EXECUTE PROCEDURE procrastinate_trigger_scheduled_events_procedure(); + +-- Create additional function and trigger for abortion requests +CREATE FUNCTION procrastinate_trigger_abort_requested_events_procedure() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO procrastinate_events(job_id, type) + VALUES (NEW.id, 'abort_requested'::procrastinate_job_event_type); + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_trigger_abort_requested_events + AFTER UPDATE OF abort_requested ON procrastinate_jobs + FOR EACH ROW WHEN ((new.abort_requested = true)) + EXECUTE PROCEDURE procrastinate_trigger_abort_requested_events_procedure(); diff --git a/procrastinate/sql/schema.sql b/procrastinate/sql/schema.sql index a24a9951f..c75b67ba6 100644 --- a/procrastinate/sql/schema.sql +++ b/procrastinate/sql/schema.sql @@ -357,6 +357,17 @@ BEGIN END; $$; +CREATE FUNCTION procrastinate_trigger_abort_requested_events_procedure() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO procrastinate_events(job_id, type) + VALUES (NEW.id, 'abort_requested'::procrastinate_job_event_type); + RETURN NEW; +END; +$$; + CREATE FUNCTION procrastinate_unlink_periodic_defers() RETURNS trigger LANGUAGE plpgsql @@ -391,6 +402,11 @@ CREATE TRIGGER procrastinate_trigger_scheduled_events FOR EACH ROW WHEN ((new.scheduled_at IS NOT NULL AND new.status = 'todo'::procrastinate_job_status)) EXECUTE PROCEDURE procrastinate_trigger_scheduled_events_procedure(); +CREATE TRIGGER procrastinate_trigger_abort_requested_events + AFTER UPDATE OF abort_requested ON procrastinate_jobs + FOR EACH ROW WHEN ((new.abort_requested = true)) + EXECUTE PROCEDURE procrastinate_trigger_abort_requested_events_procedure(); + CREATE TRIGGER procrastinate_trigger_delete_jobs BEFORE DELETE ON procrastinate_jobs FOR EACH ROW EXECUTE PROCEDURE procrastinate_unlink_periodic_defers(); From 463511ce7a39f668b6aea03c2b62497e2b24711f Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Thu, 1 Aug 2024 17:48:05 +0000 Subject: [PATCH 034/375] Fix line formatting --- procrastinate/worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index f1af47021..2613a425e 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -307,7 +307,9 @@ async def run_job(self, job: jobs.Job, worker_id: int) -> None: critical = not isinstance(e, Exception) assert job.id - abort_requested = await self.job_manager.get_job_abort_requested_async(job_id=job.id) + abort_requested = await self.job_manager.get_job_abort_requested_async( + job_id=job.id + ) if abort_requested: retry_exception = None From 150b4d7c062ccc7af6030100d36bfb29eb317453 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 4 Aug 2024 09:15:23 +1000 Subject: [PATCH 035/375] more tests on stopping worker --- procrastinate/worker.py | 1 + tests/acceptance/test_async.py | 11 ++++++++++- tests/unit/test_app.py | 20 ++++++++++++++++++++ tests/unit/test_worker.py | 10 +++++++++- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index d18f4e6eb..95d666eda 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -330,6 +330,7 @@ async def run(self): except asyncio.TimeoutError: pass except asyncio.CancelledError: + # worker.run is cancelled, usually by cancelling app.run_worker_async self.stop() try: await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index 4212af2fc..6482fcaff 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -266,13 +266,20 @@ async def appender(a: int): async def test_stop_worker_aborts_jobs_past_shutdown_timeout(async_app: app_module.App): + slow_job_cancelled = False + @async_app.task(queue="default", name="fast_job") async def fast_job(): pass @async_app.task(queue="default", name="slow_job") async def slow_job(): - await asyncio.sleep(2) + nonlocal slow_job_cancelled + try: + await asyncio.sleep(2) + except asyncio.CancelledError: + slow_job_cancelled = True + raise fast_job_id = await fast_job.defer_async() slow_job_id = await slow_job.defer_async() @@ -290,3 +297,5 @@ async def slow_job(): slow_job_status = await async_app.job_manager.get_job_status_async(slow_job_id) assert fast_job_status == Status.SUCCEEDED assert slow_job_status == Status.ABORTED + + assert slow_job_cancelled diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 13bc6d4bd..682d96016 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -105,6 +105,26 @@ async def my_task(a): assert result == [1] +async def test_app_run_worker_async_abort(app: app_module.App): + result = [] + + @app.task + async def my_task(a): + await asyncio.sleep(3) + result.append(a) + + task = asyncio.create_task(app.run_worker_async(shutdown_timeout=0.1)) + await my_task.defer_async(a=1) + await asyncio.sleep(0.01) + task.cancel() + with pytest.raises(asyncio.CancelledError): + # this wait_for is just here to fail the test faster + await asyncio.wait_for(task, timeout=1) + pytest.fail("Expected the worker to be force stopped") + + assert result == [] + + def test_from_path(mocker): load = mocker.patch("procrastinate.utils.load_from_path") assert app_module.App.from_path("dotted.path") is load.return_value diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 9845b7c2d..32edc9215 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -293,9 +293,16 @@ async def test_stopping_worker_aborts_job_after_timeout(app: App, worker, mode): complete_task_event = asyncio.Event() worker.shutdown_timeout = 0.02 + task_cancelled = False + @app.task() async def task_func(): - await complete_task_event.wait() + nonlocal task_cancelled + try: + await complete_task_event.wait() + except asyncio.CancelledError: + task_cancelled = True + raise run_task = await start_worker(worker) @@ -324,6 +331,7 @@ async def task_func(): status = await app.job_manager.get_job_status_async(job_id) assert status == Status.ABORTED + assert task_cancelled @pytest.mark.parametrize( From 6af28a6691bafd44568d47d35267c71bf47e8280 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 4 Aug 2024 21:25:34 +1000 Subject: [PATCH 036/375] rename timeout to polling_interval --- procrastinate/app.py | 6 +++--- procrastinate/cli.py | 6 +++--- procrastinate/worker.py | 4 ++-- tests/acceptance/test_async.py | 2 +- tests/integration/test_cli.py | 4 ++-- tests/integration/test_wait_stop.py | 8 ++++---- tests/unit/test_app.py | 4 ++-- tests/unit/test_worker.py | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/procrastinate/app.py b/procrastinate/app.py index c52a9e0db..1dc468a1a 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -28,7 +28,7 @@ class WorkerOptions(TypedDict): name: NotRequired[str] concurrency: NotRequired[int] wait: NotRequired[bool] - timeout: NotRequired[float] + polling_interval: NotRequired[float] shutdown_timeout: NotRequired[float] listen_notify: NotRequired[bool] delete_jobs: NotRequired[str | jobs.DeleteJobCondition] @@ -263,7 +263,7 @@ async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: Name of the worker. Will be passed in the `JobContext` and used in the logs (defaults to ``None`` which will result in the worker named ``worker``). - timeout : ``float`` + polling_interval : ``float`` Indicates the maximum duration (in seconds) the worker waits between each database job poll. Raising this parameter can lower the rate at which the worker makes queries to the database for requesting jobs. @@ -277,7 +277,7 @@ async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: If ``True``, the worker will dedicate a connection from the pool to listening to database events, notifying of newly available jobs. If ``False``, the worker will just poll the database periodically - (see ``timeout``). (defaults to ``True``) + (see ``polling_interval``). (defaults to ``True``) delete_jobs : ``str`` If ``always``, the worker will automatically delete all jobs on completion. If ``successful`` the worker will only delete successful jobs. diff --git a/procrastinate/cli.py b/procrastinate/cli.py index 28c6189fb..fda2af678 100644 --- a/procrastinate/cli.py +++ b/procrastinate/cli.py @@ -291,11 +291,11 @@ def configure_worker_parser(subparsers: argparse._SubParsersAction): ) add_argument( worker_parser, - "-t", - "--timeout", + "-p", + "--polling-interval", type=float, help="How long to wait for database event push before polling", - envvar="WORKER_TIMEOUT", + envvar="WORKER_POLLING_INTERVAL", ) add_argument( worker_parser, diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 95d666eda..0cdbbb602 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -36,7 +36,7 @@ def __init__( name: str | None = WORKER_NAME, concurrency: int = WORKER_CONCURRENCY, wait: bool = True, - timeout: float = POLLING_INTERVAL, + polling_interval: float = POLLING_INTERVAL, shutdown_timeout: float | None = None, listen_notify: bool = True, delete_jobs: str | jobs.DeleteJobCondition | None = None, @@ -48,7 +48,7 @@ def __init__( self.worker_name = name self.concurrency = concurrency self.wait = wait - self.polling_interval = timeout + self.polling_interval = polling_interval self.listen_notify = listen_notify self.delete_jobs = ( jobs.DeleteJobCondition(delete_jobs) diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index 6482fcaff..de5591461 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -188,7 +188,7 @@ async def sum(a: int, b: int): # rely on polling to fetch new jobs worker_task = asyncio.create_task( async_app.run_worker_async( - concurrency=1, wait=True, listen_notify=False, timeout=0.3 + concurrency=1, wait=True, listen_notify=False, polling_interval=0.3 ) ) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 58ae41dbe..09586a903 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -69,7 +69,7 @@ async def test_worker(entrypoint, cli_app, mocker): cli_app.run_worker_async = mocker.AsyncMock() result = await entrypoint( "worker " - "--queues a,b --name=w1 --timeout=8.3 " + "--queues a,b --name=w1 --polling-interval=8.3 " "--one-shot --concurrency=10 --no-listen-notify --delete-jobs=always" ) @@ -79,7 +79,7 @@ async def test_worker(entrypoint, cli_app, mocker): concurrency=10, name="w1", queues=["a", "b"], - timeout=8.3, + polling_interval=8.3, wait=False, listen_notify=False, delete_jobs=jobs.DeleteJobCondition.ALWAYS, diff --git a/tests/integration/test_wait_stop.py b/tests/integration/test_wait_stop.py index 1c582b263..852aa23c6 100644 --- a/tests/integration/test_wait_stop.py +++ b/tests/integration/test_wait_stop.py @@ -13,7 +13,7 @@ async def test_wait_for_activity_cancelled(psycopg_connector): Testing that the work can be cancelled """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, timeout=2) + worker = worker_module.Worker(app=pg_app, polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting @@ -31,7 +31,7 @@ async def test_wait_for_activity_timeout(psycopg_connector): Testing that we timeout if nothing happens """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, timeout=2) + worker = worker_module.Worker(app=pg_app, polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting with pytest.raises(asyncio.TimeoutError): @@ -43,7 +43,7 @@ async def test_wait_for_activity_stop_from_signal(psycopg_connector, kill_own_pi Testing than ctrl+c interrupts the wait """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, timeout=2) + worker = worker_module.Worker(app=pg_app, polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting @@ -60,7 +60,7 @@ async def test_wait_for_activity_stop(psycopg_connector): Testing than calling worker.stop() interrupts the wait """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, timeout=2) + worker = worker_module.Worker(app=pg_app, polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 682d96016..1aa11db3f 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -51,11 +51,11 @@ def test_app_register(app: app_module.App): def test_app_worker(app: app_module.App, mocker): Worker = mocker.patch("procrastinate.worker.Worker") - app.worker_defaults["timeout"] = 12 + app.worker_defaults["polling_interval"] = 12 app._worker(queues=["yay"], name="w1", wait=False) Worker.assert_called_once_with( - queues=["yay"], app=app, name="w1", timeout=12, wait=False + queues=["yay"], app=app, name="w1", polling_interval=12, wait=False ) diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 32edc9215..14fe96fcc 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -200,7 +200,7 @@ async def perform_job(): @pytest.mark.parametrize( "worker", - [({"timeout": 0.05})], + [({"polling_interval": 0.05})], indirect=["worker"], ) async def test_worker_run_respects_polling(worker, app): From 9c4e19da671b6a87cc12595982904a5919577831 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 4 Aug 2024 21:45:57 +1000 Subject: [PATCH 037/375] update documentation --- docs/discussions.md | 20 ++------ docs/howto/advanced/shutdown.md | 88 +++++++++++++++++++++++++++++++++ docs/howto/basics/worker.md | 6 +-- 3 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 docs/howto/advanced/shutdown.md diff --git a/docs/discussions.md b/docs/discussions.md index 3e137b813..34b1c0bff 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -199,28 +199,16 @@ Having sub-workers wait for an available connection in the pool is suboptimal. Y resources will be better used with fewer sub-workers or a larger pool, but there are many factors to take into account when [sizing your pool](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections). -### Mind the `worker_timeout` +### Mind the `polling_interval` Even when the database doesn't notify workers regarding newly deferred jobs, idle workers still poll the database every now and then, just in case. There could be previously locked jobs that are now free, or scheduled jobs that have -reached the ETA. `worker_timeout` is the {py:meth}`App.run_worker` parameter (or the +reached the ETA. `polling_interval` is the {py:meth}`App.run_worker` parameter (or the equivalent CLI flag) that sizes this "every now and then". -On a non-concurrent idle worker, a database poll is run every `` -seconds. On a concurrent worker, sub-workers poll the database every -`*` seconds. This ensures that, on average, the time -between each database poll is still `` seconds. - -The initial timeout for the first loop of each sub-worker is modified so that the -workers are initially spread across all the total length of the timeout, but the -randomness in job duration could create a situation where there is a long gap between -polls. If you find this to happen in reality, please open an issue, and lower your -`worker_timeout`. - -Note that as long as jobs are regularly deferred, or there are enqueued jobs, -sub-workers will not wait and this will not be an issue. This is only about idle -workers taking time to notice that a previously unavailable job has become available. +A worker will keep fetching new jobs as long as they have capacity to process them. +The polling interval starts from the moment the last attempt to fetch a new job yields no result. ## Procrastinate's usage of PostgreSQL functions and procedures diff --git a/docs/howto/advanced/shutdown.md b/docs/howto/advanced/shutdown.md new file mode 100644 index 000000000..3c22b2155 --- /dev/null +++ b/docs/howto/advanced/shutdown.md @@ -0,0 +1,88 @@ +# Shutdown a worker + +A worker will keep running until: +- it has the option `wait=False` (default is `True`) and there is no job left +- it has the option `install_signal_handlers=True` (default is `True`) and receives a `SIGINT/SIGTERM` signal +- [task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) is called on the task created from `app.run_worker_async` + +When a worker is requested to stop, it will attempt to gracefully shut down by waiting for all running jobs to complete. +If a `shutdown_timeout` option is specified, the worker will attempt to abort all jobs that have not completed by that time. Cancelling the `run_worker_async` task a second time also results in the worker aborting running jobs. + +> The worker aborts its remaining jobs by calling [task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) on the underlying asyncio task that runs the job. +> +> It is possible for that task to handle `asyncio.CancelledError` and even suppress the cancellation. + +## Examples + +### Run a worker until no job is left + +```python +async with app.open_async(): + await app.run_worker_async(wait=False) + # at this point, the worker has gracefully shut down +``` + +### Run a worker until receiving a stop signal + +```python +async with app.open_async(): + # give jobs up to 10 seconds to complete when a stop signal is received + # all jobs still running after 10 seconds are aborted + # In the absence of shutdown_timeout, the task will complete when all jobs have completed. + await app.run_worker_async(shutdown_timeout=10) +``` + +### Run a worker until its Task is cancelled + +```python +async with app.open_async(): + worker = asyncio.create_task(app.run_worker_async()) + + # eventually + worker.cancel() + + try: + await worker + except asyncio.CancelledError: + # wait until all remaining jobs have completed, however long they take + await worker +``` + +### Run a worker until its Task is cancelled with a shutdown timeout + +```python +async with app.open_async(): + worker = asyncio.create_task(app.run_worker_async(shutdown_timeout=10)) + + # eventually + worker.cancel() + + try: + await worker + except asyncio.CancelledError: + # at this point, the worker is shut down. + # Any job that took longer than 10 seconds to complete have aborted + pass +``` + +### Cancel a worker Task and explicitly abort jobs after timeout + +```python +async with app.open_async(): + # Notice that shutdown_timeout is not specified + worker = asyncio.create_task(app.run_worker_async()) + + # eventually + worker.cancel() + + try: + # give the jobs 10 seconds to complete and abort remaining jobs + await asyncio.wait_for(worker, timeout=10) + except asyncio.CancelledError: + # all jobs have completed within 10 seconds + pass + except asyncio.TimeoutError: + # one or more jobs took longer than 10 seconds and have aborted. + pass + +``` diff --git a/docs/howto/basics/worker.md b/docs/howto/basics/worker.md index c5adb23a7..4de97245e 100644 --- a/docs/howto/basics/worker.md +++ b/docs/howto/basics/worker.md @@ -19,7 +19,7 @@ Naming the worker is optional. :::{note} {py:meth}`App.run_worker` will take care of launching an event loop, opening the app, -running the worker, and when it exists, closing the app and the event loop. +running the worker, and when it exits, closing the app and the event loop. On the other hand, {py:meth}`App.run_worker_async` needs to run while the app is open. The CLI takes care of opening the app. @@ -31,13 +31,13 @@ When running the worker inside a bigger application, you may want to use `install_signal_handlers=False` so that the worker doesn't interfere with your application's signal handlers. -:::{note} When you run the worker as a task, at any point, you can call `task.cancel()` to request the worker to gracefully stop at the next opportunity. You may then wait for it to actually stop using `await task` if you're ready to wait indefinitely, or `asyncio.wait_for(task, timeout)` if you want to set a timeout. -::: + + Here is an example FastAPI application that does this: From b047f6bdc76e756496a949e82ee5167972610d9f Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 4 Aug 2024 21:53:14 +1000 Subject: [PATCH 038/375] add suppress cancelation test --- tests/unit/test_worker.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 14fe96fcc..f7deb09df 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -334,6 +334,37 @@ async def task_func(): assert task_cancelled +async def test_stopping_worker_job_suppresses_cancellation(app: App, worker): + complete_task_event = asyncio.Event() + worker.shutdown_timeout = 0.02 + + @app.task() + async def task_func(): + try: + await complete_task_event.wait() + except asyncio.CancelledError: + # supress the cancellation + pass + + run_task = await start_worker(worker) + + job_id = await task_func.defer_async() + + await asyncio.sleep(0.05) + + # this should still be running waiting for the task to complete + assert run_task.done() is False + + worker.stop() + + await asyncio.sleep(0.1) + assert run_task.done() + await run_task + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + @pytest.mark.parametrize( "worker", [({"additional_context": {"foo": "bar"}})], From 2be167b2c1d25392b0372026ea8d52eeb6b45902 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 4 Aug 2024 22:00:59 +1000 Subject: [PATCH 039/375] add shutdown doc to toc --- docs/howto/advanced.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/howto/advanced.md b/docs/howto/advanced.md index aee8516a6..7719df9ad 100644 --- a/docs/howto/advanced.md +++ b/docs/howto/advanced.md @@ -17,4 +17,5 @@ advanced/events advanced/sync_defer advanced/custom_json_encoder_decoder advanced/blueprints +advanced/shutdown ::: From e0fc6b671ff4fe232731442cce1ba58de13fd16c Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 5 Aug 2024 21:25:32 +1000 Subject: [PATCH 040/375] give names to some tasks --- procrastinate/worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 0cdbbb602..41ac9a2cb 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -318,7 +318,7 @@ async def run(self): This will run forever until asked to stop/cancelled, or until no more job is available is configured not to wait """ self.run_task = asyncio.current_task() - loop_task = asyncio.create_task(self._run_loop()) + loop_task = asyncio.create_task(self._run_loop(), name="worker loop") try: # shield the loop task from cancellation @@ -365,7 +365,7 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): def _start_side_tasks(self) -> list[asyncio.Task]: """Start side tasks such as periodic deferrer and notification listener""" - side_tasks = [asyncio.create_task(self.periodic_deferrer())] + side_tasks = [asyncio.create_task(self.periodic_deferrer(), name="deferrer")] if self.wait and self.listen_notify: listener_coro = self.app.job_manager.listen_for_jobs( event=self._notify_event, From 7e756c3a7f9ff684fc0930839ff7b40fa0ad2ebd Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 5 Aug 2024 21:39:27 +1000 Subject: [PATCH 041/375] minor text change --- docs/howto/advanced/shutdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/advanced/shutdown.md b/docs/howto/advanced/shutdown.md index 3c22b2155..d3043ef63 100644 --- a/docs/howto/advanced/shutdown.md +++ b/docs/howto/advanced/shutdown.md @@ -60,7 +60,7 @@ async with app.open_async(): try: await worker except asyncio.CancelledError: - # at this point, the worker is shut down. + # at this point, the worker is shutdown. # Any job that took longer than 10 seconds to complete have aborted pass ``` From 9600f4a937055e56d7c293ae7718de331e8b265f Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 7 Aug 2024 21:38:45 +1000 Subject: [PATCH 042/375] doc updates --- docs/howto/advanced/shutdown.md | 77 ++++++++++++++++----------------- docs/howto/basics/worker.md | 8 +--- 2 files changed, 38 insertions(+), 47 deletions(-) diff --git a/docs/howto/advanced/shutdown.md b/docs/howto/advanced/shutdown.md index d3043ef63..8abc1cae2 100644 --- a/docs/howto/advanced/shutdown.md +++ b/docs/howto/advanced/shutdown.md @@ -8,10 +8,11 @@ A worker will keep running until: When a worker is requested to stop, it will attempt to gracefully shut down by waiting for all running jobs to complete. If a `shutdown_timeout` option is specified, the worker will attempt to abort all jobs that have not completed by that time. Cancelling the `run_worker_async` task a second time also results in the worker aborting running jobs. -> The worker aborts its remaining jobs by calling [task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) on the underlying asyncio task that runs the job. -> -> It is possible for that task to handle `asyncio.CancelledError` and even suppress the cancellation. +:::{note} +The worker aborts its remaining jobs by calling [task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) on the underlying asyncio task that runs the job. +It is possible for that task to handle `asyncio.CancelledError` and even suppress the cancellation. +::: ## Examples ### Run a worker until no job is left @@ -26,63 +27,59 @@ async with app.open_async(): ```python async with app.open_async(): - # give jobs up to 10 seconds to complete when a stop signal is received - # all jobs still running after 10 seconds are aborted - # In the absence of shutdown_timeout, the task will complete when all jobs have completed. - await app.run_worker_async(shutdown_timeout=10) + # give jobs up to 10 seconds to complete when a stop signal is received + # all jobs still running after 10 seconds are aborted + # In the absence of shutdown_timeout, the task will complete when all jobs have completed. + await app.run_worker_async(shutdown_timeout=10) ``` ### Run a worker until its Task is cancelled ```python async with app.open_async(): - worker = asyncio.create_task(app.run_worker_async()) - - # eventually - worker.cancel() - - try: - await worker - except asyncio.CancelledError: - # wait until all remaining jobs have completed, however long they take - await worker + worker = asyncio.create_task(app run_worker_async()) + # eventually + worker.cancel() + try: + await worker + except asyncio.CancelledError: + # wait until all remaining jobs have completed, however long they take + await worker ``` ### Run a worker until its Task is cancelled with a shutdown timeout ```python async with app.open_async(): - worker = asyncio.create_task(app.run_worker_async(shutdown_timeout=10)) - - # eventually - worker.cancel() - - try: - await worker - except asyncio.CancelledError: - # at this point, the worker is shutdown. - # Any job that took longer than 10 seconds to complete have aborted - pass + worker = asyncio.create_task(app.run_worker_async(shutdown_timeout=10)) + # eventually + worker.cancel() + try: + await worker + except asyncio.CancelledError: + # at this point, the worker is shutdown. + # Any job that took longer than 10 seconds to complete have aborted + pass ``` ### Cancel a worker Task and explicitly abort jobs after timeout ```python async with app.open_async(): - # Notice that shutdown_timeout is not specified - worker = asyncio.create_task(app.run_worker_async()) + # Notice that shutdown_timeout is not specified + worker = asyncio.create_task(app.run_worker_async()) - # eventually - worker.cancel() + # eventually + worker.cancel() - try: + try: # give the jobs 10 seconds to complete and abort remaining jobs - await asyncio.wait_for(worker, timeout=10) - except asyncio.CancelledError: - # all jobs have completed within 10 seconds - pass - except asyncio.TimeoutError: - # one or more jobs took longer than 10 seconds and have aborted. - pass + await asyncio.wait_for(worker, timeout=10) + except asyncio.CancelledError: + # all jobs have completed within 10 seconds + pass + except asyncio.TimeoutError: + # one or more jobs took longer than 10 seconds and have aborted. + pass ``` diff --git a/docs/howto/basics/worker.md b/docs/howto/basics/worker.md index 4de97245e..f26c6b484 100644 --- a/docs/howto/basics/worker.md +++ b/docs/howto/basics/worker.md @@ -31,13 +31,7 @@ When running the worker inside a bigger application, you may want to use `install_signal_handlers=False` so that the worker doesn't interfere with your application's signal handlers. -When you run the worker as a task, at any point, you can call `task.cancel()` -to request the worker to gracefully stop at the next opportunity. -You may then wait for it to actually stop using `await task` if you're -ready to wait indefinitely, or `asyncio.wait_for(task, timeout)` if you -want to set a timeout. - - +For more information about stopping the worker, see {doc}`../advanced/shutdown`. Here is an example FastAPI application that does this: From 359fc51a7e087fd662aa9ac757dd51e3aae83f4a Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 7 Aug 2024 21:50:30 +1000 Subject: [PATCH 043/375] Update docs/discussions.md Co-authored-by: Joachim Jablon --- docs/discussions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/discussions.md b/docs/discussions.md index 34b1c0bff..9c62cdfe5 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -199,7 +199,7 @@ Having sub-workers wait for an available connection in the pool is suboptimal. Y resources will be better used with fewer sub-workers or a larger pool, but there are many factors to take into account when [sizing your pool](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections). -### Mind the `polling_interval` +### How the `polling_interval` works Even when the database doesn't notify workers regarding newly deferred jobs, idle workers still poll the database every now and then, just in case. From 8e04c60c493fbd5966b08f285d539d460218947e Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Wed, 7 Aug 2024 21:56:49 +1000 Subject: [PATCH 044/375] more docs updates --- docs/discussions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/discussions.md b/docs/discussions.md index 9c62cdfe5..b0310c874 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -209,6 +209,9 @@ equivalent CLI flag) that sizes this "every now and then". A worker will keep fetching new jobs as long as they have capacity to process them. The polling interval starts from the moment the last attempt to fetch a new job yields no result. +:::{note} +The polling interval was previously called `timeout` in pre-v3 versions of Procrastinate. It was renamed to `polling_interval` for clarity. +::: ## Procrastinate's usage of PostgreSQL functions and procedures From f703a11e434ce80b5f27a6dfb4d401a81502b2ec Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 15 Aug 2024 00:00:44 +1000 Subject: [PATCH 045/375] async InMemoryConnector (testing) --- procrastinate/testing.py | 81 +++++++------- tests/conftest.py | 2 +- tests/integration/test_cli.py | 2 +- tests/unit/test_job_context.py | 2 - tests/unit/test_manager.py | 12 +- tests/unit/test_shell.py | 196 ++++++++++++++++++++------------- tests/unit/test_testing.py | 111 ++++++++++--------- tests/unit/test_worker.py | 18 +-- 8 files changed, 234 insertions(+), 190 deletions(-) diff --git a/procrastinate/testing.py b/procrastinate/testing.py index b5756c9ff..51ab384ee 100644 --- a/procrastinate/testing.py +++ b/procrastinate/testing.py @@ -50,7 +50,7 @@ def reset(self) -> None: def get_sync_connector(self) -> connector.BaseConnector: return self - def generic_execute(self, query, suffix, **arguments) -> Any: + async def generic_execute(self, query, suffix, **arguments) -> Any: """ Calling a query will call the _ method on this class. Suffix is "run" if no result is expected, @@ -58,32 +58,23 @@ def generic_execute(self, query, suffix, **arguments) -> Any: """ query_name = self.reverse_queries[query] self.queries.append((query_name, arguments)) - return getattr(self, f"{query_name}_{suffix}")(**arguments) + return await getattr(self, f"{query_name}_{suffix}")(**arguments) def make_dynamic_query(self, query, **identifiers: str) -> str: return query.format(**identifiers) - def execute_query(self, query: str, **arguments: Any) -> None: - self.generic_execute(query, "run", **arguments) - - def execute_query_one(self, query: str, **arguments: Any) -> dict[str, Any]: - return self.generic_execute(query, "one", **arguments) - - def execute_query_all(self, query: str, **arguments: Any) -> list[dict[str, Any]]: - return self.generic_execute(query, "all", **arguments) - async def execute_query_async(self, query: str, **arguments: Any) -> None: - self.generic_execute(query, "run", **arguments) + await self.generic_execute(query, "run", **arguments) async def execute_query_one_async( self, query: str, **arguments: Any ) -> dict[str, Any]: - return self.generic_execute(query, "one", **arguments) + return await self.generic_execute(query, "one", **arguments) async def execute_query_all_async( self, query: str, **arguments: Any ) -> list[dict[str, Any]]: - return self.generic_execute(query, "all", **arguments) + return await self.generic_execute(query, "all", **arguments) async def listen_notify( self, event: asyncio.Event, channels: Iterable[str] @@ -105,7 +96,7 @@ async def close_async(self) -> None: # End of BaseConnector methods - def defer_job_one( + async def defer_job_one( self, task_name: str, priority: int, @@ -151,7 +142,7 @@ def defer_job_one( self.notify_event.set() return job_row - def defer_periodic_job_one( + async def defer_periodic_job_one( self, queue: str, task_name: str, @@ -167,7 +158,7 @@ def defer_periodic_job_one( return {"id": None} self.periodic_defers[(task_name, periodic_id)] = defer_timestamp - return self.defer_job_one( + return await self.defer_job_one( task_name=task_name, queue=queue, priority=priority, @@ -191,7 +182,7 @@ def finished_jobs(self) -> list[JobRow]: if job["status"] in {"failed", "succeeded"} ] - def fetch_job_one(self, queues: Iterable[str] | None) -> dict: + async def fetch_job_one(self, queues: Iterable[str] | None) -> dict: # Creating a copy of the iterable so that we can modify it while we iterate filtered_jobs = [ @@ -215,7 +206,7 @@ def fetch_job_one(self, queues: Iterable[str] | None) -> dict: self.events[job["id"]].append({"type": "started", "at": utils.utcnow()}) return job - def finish_job_run(self, job_id: int, status: str, delete_job: bool) -> None: + async def finish_job_run(self, job_id: int, status: str, delete_job: bool) -> None: if delete_job: self.jobs.pop(job_id) return @@ -226,7 +217,7 @@ def finish_job_run(self, job_id: int, status: str, delete_job: bool) -> None: job_row["abort_requested"] = False self.events[job_id].append({"type": status, "at": utils.utcnow()}) - def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: + async def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: job_row = self.jobs[job_id] if job_row["status"] == "todo": @@ -243,13 +234,13 @@ def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> dict: return {"id": None} - def get_job_status_one(self, job_id: int) -> dict: + async def get_job_status_one(self, job_id: int) -> dict: return {"status": self.jobs[job_id]["status"]} - def get_job_abort_requested_one(self, job_id: int) -> dict: + async def get_job_abort_requested_one(self, job_id: int) -> dict: return {"abort_requested": self.jobs[job_id]["abort_requested"]} - def retry_job_run( + async def retry_job_run( self, job_id: int, retry_at: datetime.datetime, @@ -270,7 +261,7 @@ def retry_job_run( self.events[job_id].append({"type": "scheduled", "at": retry_at}) self.events[job_id].append({"type": "deferred_for_retry", "at": utils.utcnow()}) - def select_stalled_jobs_all(self, nb_seconds, queue, task_name): + async def select_stalled_jobs_all(self, nb_seconds, queue, task_name): return ( job for job in self.jobs.values() @@ -281,7 +272,7 @@ def select_stalled_jobs_all(self, nb_seconds, queue, task_name): and task_name in (job["task_name"], None) ) - def delete_old_jobs_run(self, nb_hours, queue, statuses): + async def delete_old_jobs_run(self, nb_hours, queue, statuses): for id, job in list(self.jobs.items()): if ( job["status"] in statuses @@ -293,47 +284,57 @@ def delete_old_jobs_run(self, nb_hours, queue, statuses): ): self.jobs.pop(id) - def listen_for_jobs_run(self) -> None: + async def listen_for_jobs_run(self) -> None: pass - def apply_schema_run(self) -> None: + async def apply_schema_run(self) -> None: pass - def list_jobs_all(self, **kwargs): + async def list_jobs_all(self, **kwargs): + jobs: list[JobRow] = [] for job in self.jobs.values(): if all( expected is None or str(job[key]) == str(expected) for key, expected in kwargs.items() ): - yield job + jobs.append(job) + return iter(jobs) - def list_queues_all(self, **kwargs): - jobs = list(self.list_jobs_all(**kwargs)) + async def list_queues_all(self, **kwargs): + result: list[dict] = [] + jobs = list(await self.list_jobs_all(**kwargs)) queues = sorted({job["queue_name"] for job in jobs}) for queue in queues: queue_jobs = [job for job in jobs if job["queue_name"] == queue] stats = Counter(job["status"] for job in queue_jobs) - yield {"name": queue, "jobs_count": len(queue_jobs), "stats": stats} + result.append( + {"name": queue, "jobs_count": len(queue_jobs), "stats": stats} + ) + return iter(result) - def list_tasks_all(self, **kwargs): - jobs = list(self.list_jobs_all(**kwargs)) + async def list_tasks_all(self, **kwargs): + result: list[dict] = [] + jobs = list(await self.list_jobs_all(**kwargs)) tasks = sorted({job["task_name"] for job in jobs}) for task in tasks: task_jobs = [job for job in jobs if job["task_name"] == task] stats = Counter(job["status"] for job in task_jobs) - yield {"name": task, "jobs_count": len(task_jobs), "stats": stats} + result.append({"name": task, "jobs_count": len(task_jobs), "stats": stats}) + return result - def list_locks_all(self, **kwargs): - jobs = list(self.list_jobs_all(**kwargs)) + async def list_locks_all(self, **kwargs): + result: list[dict] = [] + jobs = list(await self.list_jobs_all(**kwargs)) locks = sorted({job["lock"] for job in jobs}) for lock in locks: lock_jobs = [job for job in jobs if job["lock"] == lock] stats = Counter(job["status"] for job in lock_jobs) - yield {"name": lock, "jobs_count": len(lock_jobs), "stats": stats} + result.append({"name": lock, "jobs_count": len(lock_jobs), "stats": stats}) + return result - def set_job_status_run(self, id, status): + async def set_job_status_run(self, id, status): id = int(id) self.jobs[id]["status"] = status - def check_connection_one(self): + async def check_connection_one(self): return {"check": self.table_exists or None} diff --git a/tests/conftest.py b/tests/conftest.py index a008e456c..0da0291e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -180,7 +180,7 @@ def blueprint(): @pytest.fixture -def job_manager(app): +def job_manager(app: app_module.App): return app.job_manager diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 0da590712..5849faa11 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -285,7 +285,7 @@ async def test_defer_queueing_lock_ignore(entrypoint, cli_app, connector): def mytask(a): pass - cli_app.configure_task(name="hello", queueing_lock="houba").defer(a=1) + await cli_app.configure_task(name="hello", queueing_lock="houba").defer_async(a=1) result = await entrypoint( """defer --queueing-lock=houba --ignore-already-enqueued hello {"a":2}""" diff --git a/tests/unit/test_job_context.py b/tests/unit/test_job_context.py index fa2a2afae..6d514bf86 100644 --- a/tests/unit/test_job_context.py +++ b/tests/unit/test_job_context.py @@ -75,7 +75,6 @@ async def test_should_abort(app, job_factory): job = await app.job_manager.fetch_job(queues=None) await app.job_manager.cancel_job_by_id_async(job.id, abort=True) context = job_context.JobContext(app=app, job=job) - assert context.should_abort() is True assert await context.should_abort_async() is True @@ -84,5 +83,4 @@ async def test_should_not_abort(app, job_factory): job = await app.job_manager.fetch_job(queues=None) await app.job_manager.cancel_job_by_id_async(job.id) context = job_context.JobContext(app=app, job=job) - assert context.should_abort() is False assert await context.should_abort_async() is False diff --git a/tests/unit/test_manager.py b/tests/unit/test_manager.py index 5318f38c9..27d296330 100644 --- a/tests/unit/test_manager.py +++ b/tests/unit/test_manager.py @@ -231,7 +231,7 @@ async def test_cancel_doing_job(job_manager, job_factory, connector): await job_manager.defer_job_async(job=job) await job_manager.fetch_job(queues=None) - cancelled = job_manager.cancel_job_by_id(job_id=1) + cancelled = await job_manager.cancel_job_by_id_async(job_id=1) assert not cancelled assert connector.queries[-1] == ( "cancel_job", @@ -245,7 +245,7 @@ async def test_abort_doing_job(job_manager, job_factory, connector): await job_manager.defer_job_async(job=job) await job_manager.fetch_job(queues=None) - cancelled = job_manager.cancel_job_by_id(job_id=1, abort=True) + cancelled = await job_manager.cancel_job_by_id_async(job_id=1, abort=True) assert cancelled assert connector.queries[-1] == ( "cancel_job", @@ -445,7 +445,7 @@ def test_retry_job_by_id(job_manager, connector, job_factory, dt): async def test_list_jobs_async(job_manager, job_factory): - job = job_manager.defer_job(job=job_factory()) + job = await job_manager.defer_job_async(job=job_factory()) assert await job_manager.list_jobs_async() == [job] @@ -457,7 +457,7 @@ def test_list_jobs(job_manager, job_factory): async def test_list_queues_async(job_manager, job_factory): - job_manager.defer_job(job=job_factory(queue="foo")) + await job_manager.defer_job_async(job=job_factory(queue="foo")) assert await job_manager.list_queues_async() == [ { @@ -491,7 +491,7 @@ def test_list_queues_(job_manager, job_factory): async def test_list_tasks_async(job_manager, job_factory): - job_manager.defer_job(job=job_factory(task_name="foo")) + await job_manager.defer_job_async(job=job_factory(task_name="foo")) assert await job_manager.list_tasks_async() == [ { @@ -525,7 +525,7 @@ def test_list_tasks(job_manager, job_factory): async def test_list_locks_async(job_manager, job_factory): - job_manager.defer_job(job=job_factory(lock="foo")) + await job_manager.defer_job_async(job=job_factory(lock="foo")) assert await job_manager.list_locks_async() == [ { diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index c9d881d40..4ba1c4e34 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -1,5 +1,7 @@ from __future__ import annotations +import asyncio + import pytest from procrastinate import manager @@ -22,23 +24,27 @@ def test_EOF(shell): def test_list_jobs(shell, connector, capsys): - connector.defer_job_one( - "task1", - 0, - "lock1", - "queueing_lock1", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue1", + asyncio.run( + connector.defer_job_one( + "task1", + 0, + "lock1", + "queueing_lock1", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue1", + ) ) - connector.defer_job_one( - "task2", - 0, - "lock2", - "queueing_lock2", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue2", + asyncio.run( + connector.defer_job_one( + "task2", + 0, + "lock2", + "queueing_lock2", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue2", + ) ) shell.do_list_jobs("") @@ -63,23 +69,27 @@ def test_list_jobs(shell, connector, capsys): def test_list_jobs_filters(shell, connector, capsys): - connector.defer_job_one( - "task1", - 0, - "lock1", - "queueing_lock1", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue1", + asyncio.run( + connector.defer_job_one( + "task1", + 0, + "lock1", + "queueing_lock1", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue1", + ) ) - connector.defer_job_one( - "task2", - 0, - "lock2", - "queueing_lock2", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue2", + asyncio.run( + connector.defer_job_one( + "task2", + 0, + "lock2", + "queueing_lock2", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue2", + ) ) shell.do_list_jobs("id=2 queue=queue2 task=task2 lock=lock2 status=todo") @@ -103,23 +113,27 @@ def test_list_jobs_filters(shell, connector, capsys): def test_list_jobs_details(shell, connector, capsys): - connector.defer_job_one( - "task1", - 5, - "lock1", - "queueing_lock1", - {"x": 11}, - conftest.aware_datetime(1000, 1, 1), - "queue1", + asyncio.run( + connector.defer_job_one( + "task1", + 5, + "lock1", + "queueing_lock1", + {"x": 11}, + conftest.aware_datetime(1000, 1, 1), + "queue1", + ) ) - connector.defer_job_one( - "task2", - 7, - "lock2", - "queueing_lock2", - {"y": 22}, - conftest.aware_datetime(2000, 1, 1), - "queue2", + asyncio.run( + connector.defer_job_one( + "task2", + 7, + "lock2", + "queueing_lock2", + {"y": 22}, + conftest.aware_datetime(2000, 1, 1), + "queue2", + ) ) shell.do_list_jobs("details") @@ -139,8 +153,12 @@ def test_list_jobs_empty(shell, connector, capsys): def test_list_queues(shell, connector, capsys): - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + asyncio.run( + connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") + ) + asyncio.run( + connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + ) shell.do_list_queues("") captured = capsys.readouterr() @@ -157,8 +175,12 @@ def test_list_queues(shell, connector, capsys): def test_list_queues_filters(shell, connector, capsys): - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + asyncio.run( + connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") + ) + asyncio.run( + connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + ) shell.do_list_queues("queue=queue2 task=task2 lock=lock2 status=todo") captured = capsys.readouterr() @@ -185,8 +207,12 @@ def test_list_queues_empty(shell, connector, capsys): def test_list_tasks(shell, connector, capsys): - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + asyncio.run( + connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") + ) + asyncio.run( + connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + ) shell.do_list_tasks("") captured = capsys.readouterr() @@ -203,8 +229,12 @@ def test_list_tasks(shell, connector, capsys): def test_list_tasks_filters(shell, connector, capsys): - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + asyncio.run( + connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") + ) + asyncio.run( + connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + ) shell.do_list_tasks("queue=queue2 task=task2 lock=lock2 status=todo") captured = capsys.readouterr() @@ -231,8 +261,12 @@ def test_list_tasks_empty(shell, connector, capsys): def test_list_locks(shell, connector, capsys): - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + asyncio.run( + connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") + ) + asyncio.run( + connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + ) shell.do_list_locks("") captured = capsys.readouterr() @@ -249,8 +283,12 @@ def test_list_locks(shell, connector, capsys): def test_list_locks_filters(shell, connector, capsys): - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + asyncio.run( + connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") + ) + asyncio.run( + connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + ) shell.do_list_locks("queue=queue2 task=task2 lock=lock2 status=todo") captured = capsys.readouterr() @@ -277,16 +315,18 @@ def test_list_locks_empty(shell, connector, capsys): def test_retry(shell, connector, capsys): - connector.defer_job_one( - "task", - 0, - "lock", - "queueing_lock", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue", + asyncio.run( + connector.defer_job_one( + "task", + 0, + "lock", + "queueing_lock", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue", + ) ) - connector.set_job_status_run(1, "failed") + asyncio.run(connector.set_job_status_run(1, "failed")) shell.do_list_jobs("id=1") captured = capsys.readouterr() @@ -298,14 +338,16 @@ def test_retry(shell, connector, capsys): def test_cancel(shell, connector, capsys): - connector.defer_job_one( - "task", - 0, - "lock", - "queueing_lock", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue", + asyncio.run( + connector.defer_job_one( + "task", + 0, + "lock", + "queueing_lock", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue", + ) ) shell.do_list_jobs("id=1") diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py index 279d79730..537318437 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/test_testing.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from unittest.mock import AsyncMock import pytest @@ -15,28 +16,28 @@ def test_reset(connector): assert connector.jobs == {} -def test_generic_execute(connector): +async def test_generic_execute(connector): result = {} connector.reverse_queries = {"a": "b"} - def b(**kwargs): + async def b(**kwargs): result.update(kwargs) connector.b_youpi = b - connector.generic_execute("a", "youpi", i="j") + await connector.generic_execute("a", "youpi", i="j") assert result == {"i": "j"} -async def test_execute_query(connector, mocker): - connector.generic_execute = mocker.Mock() +async def test_execute_query(connector): + connector.generic_execute = AsyncMock() await connector.execute_query_async("a", b="c") connector.generic_execute.assert_called_with("a", "run", b="c") -async def test_execute_query_one(connector, mocker): - connector.generic_execute = mocker.Mock() +async def test_execute_query_one(connector): + connector.generic_execute = AsyncMock() assert ( await connector.execute_query_one_async("a", b="c") == connector.generic_execute.return_value @@ -44,8 +45,8 @@ async def test_execute_query_one(connector, mocker): connector.generic_execute.assert_called_with("a", "one", b="c") -async def test_execute_query_all_async(connector, mocker): - connector.generic_execute = mocker.Mock() +async def test_execute_query_all_async(connector): + connector.generic_execute = AsyncMock() assert ( await connector.execute_query_all_async("a", b="c") == connector.generic_execute.return_value @@ -57,8 +58,8 @@ def test_make_dynamic_query(connector): assert connector.make_dynamic_query("foo {bar}", bar="baz") == "foo baz" -def test_defer_job_one(connector): - job = connector.defer_job_one( +async def test_defer_job_one(connector): + job = await connector.defer_job_one( task_name="mytask", priority=5, lock="sher", @@ -86,8 +87,8 @@ def test_defer_job_one(connector): assert connector.jobs[1] == job -def test_defer_job_one_multiple_times(connector): - connector.defer_job_one( +async def test_defer_job_one_multiple_times(connector): + await connector.defer_job_one( task_name="mytask", priority=0, lock=None, @@ -96,7 +97,7 @@ def test_defer_job_one_multiple_times(connector): scheduled_at=None, queue="default", ) - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, lock=None, @@ -108,7 +109,7 @@ def test_defer_job_one_multiple_times(connector): assert len(connector.jobs) == 2 -def test_defer_same_job_with_queueing_lock_second_time_after_first_one_succeeded( +async def test_defer_same_job_with_queueing_lock_second_time_after_first_one_succeeded( connector, ): job_data = { @@ -122,21 +123,23 @@ def test_defer_same_job_with_queueing_lock_second_time_after_first_one_succeeded } # 1. Defer job with queueing-lock - job_row = connector.defer_job_one(**job_data) + job_row = await connector.defer_job_one(**job_data) assert len(connector.jobs) == 1 # 2. Defering a second time should fail, as first one # still in state `todo` with pytest.raises(exceptions.UniqueViolation): - connector.defer_job_one(**job_data) + await connector.defer_job_one(**job_data) assert len(connector.jobs) == 1 # 3. Finish first job - connector.finish_job_run(job_id=job_row["id"], status="finished", delete_job=False) + await connector.finish_job_run( + job_id=job_row["id"], status="finished", delete_job=False + ) # 4. Defering a second time should work now, # as first job in state `finished` - connector.defer_job_one(**job_data) + await connector.defer_job_one(**job_data) assert len(connector.jobs) == 2 @@ -158,7 +161,7 @@ def test_finished_jobs(connector): assert connector.finished_jobs == [{"status": "succeeded"}, {"status": "failed"}] -def test_select_stalled_jobs_all(connector): +async def test_select_stalled_jobs_all(connector): connector.jobs = { # We're not selecting this job because it's "succeeded" 1: { @@ -212,13 +215,13 @@ def test_select_stalled_jobs_all(connector): 6: [{"at": conftest.aware_datetime(2000, 1, 1)}], } - results = connector.select_stalled_jobs_all( + results = await connector.select_stalled_jobs_all( queue="marsupilami", task_name="mytask", nb_seconds=0 ) assert [job["id"] for job in results] == [5, 6] -def test_delete_old_jobs_run(connector): +async def test_delete_old_jobs_run(connector): connector.jobs = { # We're not deleting this job because it's "doing" 1: {"id": 1, "status": "doing", "queue_name": "marsupilami"}, @@ -236,15 +239,15 @@ def test_delete_old_jobs_run(connector): 4: [{"type": "succeeded", "at": conftest.aware_datetime(2000, 1, 1)}], } - connector.delete_old_jobs_run( + await connector.delete_old_jobs_run( queue="marsupilami", statuses=("succeeded"), nb_hours=0 ) assert 4 not in connector.jobs -def test_fetch_job_one(connector): +async def test_fetch_job_one(connector): # This one will be selected, then skipped the second time because it's processing - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -255,7 +258,7 @@ def test_fetch_job_one(connector): ) # This one because it's the wrong queue - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -265,7 +268,7 @@ def test_fetch_job_one(connector): queueing_lock="b", ) # This one because of the scheduled_at - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -275,7 +278,7 @@ def test_fetch_job_one(connector): queueing_lock="c", ) # This one because of the lock - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -285,7 +288,7 @@ def test_fetch_job_one(connector): queueing_lock="d", ) # We're taking this one. - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -295,13 +298,13 @@ def test_fetch_job_one(connector): queueing_lock="e", ) - assert connector.fetch_job_one(queues=["marsupilami"])["id"] == 1 - assert connector.fetch_job_one(queues=["marsupilami"])["id"] == 5 + assert (await connector.fetch_job_one(queues=["marsupilami"]))["id"] == 1 + assert (await connector.fetch_job_one(queues=["marsupilami"]))["id"] == 5 -def test_fetch_job_one_prioritized(connector): +async def test_fetch_job_one_prioritized(connector): # This one will be selected second as it has a lower priority - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=5, args={}, @@ -312,7 +315,7 @@ def test_fetch_job_one_prioritized(connector): ) # This one will be selected first as it has a higher priority - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=7, args={}, @@ -322,13 +325,13 @@ def test_fetch_job_one_prioritized(connector): queueing_lock=None, ) - assert connector.fetch_job_one(queues=None)["id"] == 2 - assert connector.fetch_job_one(queues=None)["id"] == 1 + assert (await connector.fetch_job_one(queues=None))["id"] == 2 + assert (await connector.fetch_job_one(queues=None))["id"] == 1 -def test_fetch_job_one_none_lock(connector): +async def test_fetch_job_one_none_lock(connector): """Testing that 2 jobs with locks "None" don't block one another""" - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -337,7 +340,7 @@ def test_fetch_job_one_none_lock(connector): lock=None, queueing_lock=None, ) - connector.defer_job_one( + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -347,12 +350,12 @@ def test_fetch_job_one_none_lock(connector): queueing_lock=None, ) - assert connector.fetch_job_one(queues=None)["id"] == 1 - assert connector.fetch_job_one(queues=None)["id"] == 2 + assert (await connector.fetch_job_one(queues=None))["id"] == 1 + assert (await connector.fetch_job_one(queues=None))["id"] == 2 -def test_finish_job_run(connector): - connector.defer_job_one( +async def test_finish_job_run(connector): + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -361,17 +364,17 @@ def test_finish_job_run(connector): lock="sher", queueing_lock="houba", ) - job_row = connector.fetch_job_one(queues=None) + job_row = await connector.fetch_job_one(queues=None) id = job_row["id"] - connector.finish_job_run(job_id=id, status="finished", delete_job=False) + await connector.finish_job_run(job_id=id, status="finished", delete_job=False) assert connector.jobs[id]["attempts"] == 1 assert connector.jobs[id]["status"] == "finished" -def test_retry_job_run(connector): - connector.defer_job_one( +async def test_retry_job_run(connector): + await connector.defer_job_one( task_name="mytask", priority=0, args={}, @@ -380,11 +383,11 @@ def test_retry_job_run(connector): lock="sher", queueing_lock="houba", ) - job_row = connector.fetch_job_one(queues=None) + job_row = await connector.fetch_job_one(queues=None) id = job_row["id"] retry_at = conftest.aware_datetime(2000, 1, 1) - connector.retry_job_run( + await connector.retry_job_run( job_id=id, retry_at=retry_at, new_priority=3, @@ -401,14 +404,14 @@ def test_retry_job_run(connector): assert len(connector.events[id]) == 4 -def test_apply_schema_run(connector): +async def test_apply_schema_run(connector): # If we don't crash, it's enough - connector.apply_schema_run() + await connector.apply_schema_run() -def test_listen_for_jobs_run(connector): +async def test_listen_for_jobs_run(connector): # If we don't crash, it's enough - connector.listen_for_jobs_run() + await connector.listen_for_jobs_run() async def test_defer_no_notify(connector): @@ -416,7 +419,7 @@ async def test_defer_no_notify(connector): # listened queue, the testing connector doesn't notify. event = asyncio.Event() await connector.listen_notify(event=event, channels="some_other_channel") - connector.defer_job_one( + await connector.defer_job_one( task_name="foo", priority=0, lock="bar", diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index f7deb09df..030e79a11 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -125,8 +125,8 @@ async def perform_job(): connector = cast(InMemoryConnector, app.connector) - doings_jobs = list(connector.list_jobs_all(status=Status.DOING.value)) - todo_jobs = list(connector.list_jobs_all(status=Status.TODO.value)) + doings_jobs = list(await connector.list_jobs_all(status=Status.DOING.value)) + todo_jobs = list(await connector.list_jobs_all(status=Status.TODO.value)) assert len(doings_jobs) == worker.concurrency assert len(todo_jobs) == available_jobs - worker.concurrency @@ -390,7 +390,7 @@ async def test_run_job_async(app: App, worker): async def task_func(a, b): result.append(a + b) - job_id = task_func.defer(a=9, b=3) + job_id = await task_func.defer_async(a=9, b=3) await start_worker(worker) assert result == [12] @@ -406,7 +406,7 @@ async def test_run_job_sync(app: App, worker): def task_func(a, b): result.append(a + b) - job_id = task_func.defer(a=9, b=3) + job_id = await task_func.defer_async(a=9, b=3) await start_worker(worker) assert result == [12] @@ -425,7 +425,7 @@ async def inner(): return inner() - job_id = task_func.defer(a=9, b=3) + job_id = await task_func.defer_async(a=9, b=3) await start_worker(worker) @@ -442,7 +442,7 @@ async def test_run_job_log_result(caplog, app: App, worker): async def task_func(a, b): return a + b - task_func.defer(a=9, b=3) + await task_func.defer_async(a=9, b=3) await start_worker(worker) @@ -491,7 +491,7 @@ async def test_run_job_error(app: App, worker, critical_error, caplog): def task_func(a, b): raise CustomCriticalError("Nope") if critical_error else ValueError("Nope") - job_id = task_func.defer(a=9, b=3) + job_id = await task_func.defer_async(a=9, b=3) await start_worker(worker) @@ -517,7 +517,7 @@ async def test_run_job_aborted(app: App, worker, caplog): async def task_func(): raise JobAborted() - job_id = task_func.defer() + job_id = await task_func.defer_async() await start_worker(worker) @@ -559,7 +559,7 @@ def task_func(): if attempt < recover_on_attempt_number: raise CustomCriticalError("Nope") if critical_error else ValueError("Nope") - job_id = task_func.defer() + job_id = await task_func.defer_async() await start_worker(worker) From 1a9dfd148a2ee5fa3057b5e69eb69d6d81f52e22 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Sun, 18 Aug 2024 21:12:37 +0000 Subject: [PATCH 046/375] Merge branch 'main' into v3 --- .github/workflows/ci.yml | 6 +- .pre-commit-config.yaml | 16 +- .readthedocs.yml | 2 +- LICENSE.md | 3 +- dev-env | 2 +- docs/conf.py | 14 +- docs/howto/advanced.md | 1 + docs/howto/advanced/sphinx.md | 30 + docs/howto/production/delete_finished_jobs.md | 2 + docs/reference.rst | 16 +- poetry.lock | 538 +++++++++--------- procrastinate/app.py | 29 +- procrastinate/blueprints.py | 2 + procrastinate/builtin_tasks.py | 16 +- .../contrib/aiopg/aiopg_connector.py | 16 +- procrastinate/contrib/django/admin.py | 96 +++- .../contrib/django/django_connector.py | 12 +- .../0030_alter_procrastinateevent_options.py | 17 + ...> 0031_add_abort_on_procrastinate_jobs.py} | 4 +- procrastinate/contrib/django/models.py | 24 + .../procrastinate/admin/summary.html | 48 ++ procrastinate/contrib/django/utils.py | 2 +- .../contrib/psycopg2/psycopg2_connector.py | 12 +- procrastinate/contrib/sphinx/__init__.py | 35 ++ procrastinate/jobs.py | 33 +- procrastinate/manager.py | 147 ++--- procrastinate/retry.py | 12 +- procrastinate/tasks.py | 60 +- procrastinate/testing.py | 8 +- procrastinate/utils.py | 2 +- procrastinate_demos/demo_async/tasks.py | 2 + pyproject.toml | 28 +- tests/acceptance/django_settings.py | 6 + tests/acceptance/test_sync.py | 51 ++ tests/conftest.py | 4 + .../contrib/django/test_django_connector.py | 21 +- .../integration/contrib/django/test_models.py | 28 + tests/integration/contrib/sphinx/__init__.py | 0 tests/integration/contrib/sphinx/conftest.py | 0 .../contrib/sphinx/test-root/conf.py | 8 + .../contrib/sphinx/test-root/index.rst | 5 + .../contrib/sphinx/test_autodoc.py | 26 + tests/unit/contrib/django/test_admin.py | 8 + .../contrib/django/test_django_connector.py | 20 + tests/unit/test_builtin_tasks.py | 8 +- 45 files changed, 924 insertions(+), 496 deletions(-) create mode 100644 docs/howto/advanced/sphinx.md create mode 100644 procrastinate/contrib/django/migrations/0030_alter_procrastinateevent_options.py rename procrastinate/contrib/django/migrations/{0030_add_abort_on_procrastinate_jobs.py => 0031_add_abort_on_procrastinate_jobs.py} (87%) create mode 100644 procrastinate/contrib/django/templates/procrastinate/admin/summary.html create mode 100644 procrastinate/contrib/sphinx/__init__.py create mode 100644 tests/integration/contrib/sphinx/__init__.py create mode 100644 tests/integration/contrib/sphinx/conftest.py create mode 100644 tests/integration/contrib/sphinx/test-root/conf.py create mode 100644 tests/integration/contrib/sphinx/test-root/index.rst create mode 100644 tests/integration/contrib/sphinx/test_autodoc.py create mode 100644 tests/unit/contrib/django/test_admin.py create mode 100644 tests/unit/contrib/django/test_django_connector.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 224d0f37b..9f22d8add 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,9 +46,7 @@ jobs: python-version: "${{ matrix.python-version }}" cache: "poetry" - - run: poetry env use "${{ matrix.python-version }}" - - - run: poetry install --extras "django sqlalchemy" + - run: poetry install --all-extras - name: Run tests run: scripts/tests @@ -75,7 +73,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.8" # Important for importlib_metadata + python-version: "3.8" cache: "poetry" - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9994aeee..f788fdc18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,27 +29,27 @@ repos: - id: trailing-whitespace - id: mixed-line-ending - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.373 + rev: v1.1.376 hooks: - id: pyright additional_dependencies: - aiopg==1.4.0 - anyio==4.4.0 - asgiref==3.8.1 - - attrs==23.2.0 + - attrs==24.2.0 - contextlib2==21.6.0 - croniter==3.0.3 - django-stubs==5.0.4 - - django==4.2.14 - - importlib-metadata==8.2.0 - - importlib-resources==6.4.0 + - django==4.2.15 + - importlib-resources==6.4.2 - psycopg2-binary==2.9.9 - psycopg[pool]==3.2.1 - python-dateutil==2.9.0.post0 - - sqlalchemy==2.0.31 + - sphinx==7.1.2 + - sqlalchemy==2.0.32 - typing-extensions==4.12.2 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.5 + rev: v0.6.1 hooks: - id: ruff args: [--fix, --unsafe-fixes] @@ -59,7 +59,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/ewjoachim/poetry-to-pre-commit - rev: 2.1.0 + rev: 2.2.0 hooks: - id: sync-repos args: [--map=pyright-python=pyright, --map=ruff-pre-commit=ruff] diff --git a/.readthedocs.yml b/.readthedocs.yml index 75c91a136..19de0a4c5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,7 +8,7 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "3.10" + python: "latest" jobs: post_create_environment: - python -m pip install poetry diff --git a/LICENSE.md b/LICENSE.md index 1634ad10c..355e6e8dc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,8 @@ MIT License Copyright (c) 2019-2021, PeopleDoc -Copyright (c) 2021-, Joachim Jablon, Eric Lemoine +Copyright (c) 2021-2023, Joachim Jablon, Eric Lemoine +Copyright (c) 2024-, Joachim Jablon, Kai Schlamp 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 diff --git a/dev-env b/dev-env index b374a2a74..4543c2ffd 100755 --- a/dev-env +++ b/dev-env @@ -61,6 +61,6 @@ echo "We've gone ahead and set up a few additional commands for you:" echo "- htmlcov: Opens the test coverage results in your browser" echo "- htmldoc: Opens the locally built sphinx documentation in your browser" echo "- lint: Run code formatters & linters" -echo "- docs: Build doc" +echo "- docs: Build doc (note: needs 'poetry install --with docs' which needs a recent Python)" echo "" echo 'Quit the poetry shell with the command `deactivate`' diff --git a/docs/conf.py b/docs/conf.py index 749c801f8..d85b9409e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,9 +24,10 @@ project = "Procrastinate" copyright = ( - f"""2019-{datetime.datetime.now().year}, Joachim Jablon, Eric Lemoine, PeopleDoc""" + f"2019-{datetime.datetime.now().year}, " + "Joachim Jablon, Eric Lemoine, Kai Schlamp, PeopleDoc" ) -author = "Joachim Jablon" +author = "Joachim Jablon, Eric Lemoine, Kai Schlamp" # -- General configuration --------------------------------------------------- @@ -38,10 +39,10 @@ "myst_parser", "sphinx.ext.napoleon", "sphinx.ext.autodoc", - "sphinx_autodoc_typehints", "sphinxcontrib.programoutput", "sphinx_github_changelog", "sphinx_copybutton", + "procrastinate.contrib.sphinx", ] myst_enable_extensions = [ @@ -97,6 +98,13 @@ html_favicon = "favicon.ico" +# -- Options for sphinx.ext.autodoc ------------------------------------------ + +autodoc_typehints = "both" +autodoc_type_aliases = { + "JSONDict": "procrastinate.types.JSONDict", +} + # -- Options for sphinx_github_changelog --------------------------------- sphinx_github_changelog_token = os.environ.get("CHANGELOG_GITHUB_TOKEN") diff --git a/docs/howto/advanced.md b/docs/howto/advanced.md index 7719df9ad..51884d9b5 100644 --- a/docs/howto/advanced.md +++ b/docs/howto/advanced.md @@ -18,4 +18,5 @@ advanced/sync_defer advanced/custom_json_encoder_decoder advanced/blueprints advanced/shutdown +advanced/sphinx ::: diff --git a/docs/howto/advanced/sphinx.md b/docs/howto/advanced/sphinx.md new file mode 100644 index 000000000..452bf8d37 --- /dev/null +++ b/docs/howto/advanced/sphinx.md @@ -0,0 +1,30 @@ +# Document my tasks with Sphinx & Autodoc + +If you use Sphinx's `autodoc` extension to document your project, you might +have noticed that your tasks are absent from the documentation. This is because +when you apply the `@app.task` decorator, you're actually replacing the +function with a Procrastinate Task object, which `autodoc` doesn't know how to +process. + +Procrastinate provides a small Sphinx extension to fix that. You may want to +ensure procrastinate is installed with the `sphinx` extra in the environment +where you build your doc. This is not mandatory, as it only adds sphinx itself +as a dependency, but if the extension ever needs other dependencies in the +future, they will be installed through the `sphinx` extra as well. + +```bash +$ pip install procrastinate[sphinx] +``` + +Then, add the following to your `conf.py`: + +```python +extensions = [ + # ... + "procrastinate.contrib.sphinx", + # ... +] +``` + +That's it. Your tasks will now be picked up by `autodoc` and included in your +documentation. diff --git a/docs/howto/production/delete_finished_jobs.md b/docs/howto/production/delete_finished_jobs.md index e47ecbfb8..14a7502a7 100644 --- a/docs/howto/production/delete_finished_jobs.md +++ b/docs/howto/production/delete_finished_jobs.md @@ -88,6 +88,8 @@ async def remove_old_jobs(context, timestamp): context, max_hours=72, remove_error=True, + remove_cancelled=True, + remove_aborted=True, ) ``` diff --git a/docs/reference.rst b/docs/reference.rst index dc6302e0f..a0e9f60f3 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -7,33 +7,26 @@ App .. autoclass:: procrastinate.App :members: open, open_async, task, run_worker, run_worker_async, configure_task, from_path, add_tasks_from, add_task_alias, with_connector, periodic, + tasks, job_manager Connectors ---------- .. autoclass:: procrastinate.PsycopgConnector - :members: - :exclude-members: open_async, close_async .. autoclass:: procrastinate.SyncPsycopgConnector - :members: - :exclude-members: open, close .. autoclass:: procrastinate.contrib.aiopg.AiopgConnector - :members: - :exclude-members: open_async, close_async .. autoclass:: procrastinate.contrib.psycopg2.Psycopg2Connector - :members: - :exclude-members: open, close .. autoclass:: procrastinate.testing.InMemoryConnector - :members: reset - + :members: reset, jobs Tasks ----- .. autoclass:: procrastinate.tasks.Task - :members: defer, defer_async, configure + :members: defer, defer_async, configure, name, aliases, retry_strategy, + pass_context, queue, lock, queueing_lock When tasks are created with argument ``pass_context``, they are provided a `JobContext` argument: @@ -62,6 +55,7 @@ Jobs ---- .. autoclass:: procrastinate.jobs.Job + :members: Retry strategies diff --git a/poetry.lock b/poetry.lock index e6de1fd50..bce5a7cd2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,32 +81,32 @@ files = [ [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "babel" -version = "2.15.0" +version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ - {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, - {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] [package.dependencies] @@ -298,63 +298,83 @@ files = [ [[package]] name = "coverage" -version = "7.6.0" +version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, - {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, - {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, - {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, - {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, - {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, - {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, - {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, - {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, - {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, - {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, - {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, - {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, - {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] [package.dependencies] @@ -380,13 +400,13 @@ pytz = ">2021.1" [[package]] name = "django" -version = "4.2.14" +version = "4.2.15" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, - {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, + {file = "Django-4.2.15-py3-none-any.whl", hash = "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30"}, + {file = "Django-4.2.15.tar.gz", hash = "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a"}, ] [package.dependencies] @@ -451,13 +471,13 @@ files = [ [[package]] name = "dunamai" -version = "1.21.2" +version = "1.22.0" description = "Dynamic version generation" optional = false python-versions = ">=3.5" files = [ - {file = "dunamai-1.21.2-py3-none-any.whl", hash = "sha256:87db76405bf9366f9b4925ff5bb1db191a9a1bd9f9693f81c4d3abb8298be6f0"}, - {file = "dunamai-1.21.2.tar.gz", hash = "sha256:05827fb5f032f5596bfc944b23f613c147e676de118681f3bb1559533d8a65c4"}, + {file = "dunamai-1.22.0-py3-none-any.whl", hash = "sha256:eab3894b31e145bd028a74b13491c57db01986a7510482c9b5fff3b4e53d77b7"}, + {file = "dunamai-1.22.0.tar.gz", hash = "sha256:375a0b21309336f0d8b6bbaea3e038c36f462318c68795166e31f9873fdad676"}, ] [package.dependencies] @@ -479,19 +499,19 @@ test = ["pytest (>=6)"] [[package]] name = "furo" -version = "2024.7.18" +version = "2024.8.6" description = "A clean customisable Sphinx documentation theme." optional = false python-versions = ">=3.8" files = [ - {file = "furo-2024.7.18-py3-none-any.whl", hash = "sha256:b192c7c1f59805494c8ed606d9375fdac6e6ba8178e747e72bc116745fb7e13f"}, - {file = "furo-2024.7.18.tar.gz", hash = "sha256:37b08c5fccc95d46d8712c8be97acd46043963895edde05b0f4f135d58325c83"}, + {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, + {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, ] [package.dependencies] beautifulsoup4 = "*" pygments = ">=2.7" -sphinx = ">=6.0,<8.0" +sphinx = ">=6.0,<9.0" sphinx-basic-ng = ">=1.0.0.beta2" [[package]] @@ -608,21 +628,21 @@ test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "p [[package]] name = "importlib-resources" -version = "6.4.0" +version = "6.4.2" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, + {file = "importlib_resources-6.4.2-py3-none-any.whl", hash = "sha256:8bba8c54a8a3afaa1419910845fa26ebd706dc716dd208d9b158b4b6966f5c5c"}, + {file = "importlib_resources-6.4.2.tar.gz", hash = "sha256:6cbfbefc449cc6e2095dd184691b7a12a04f40bc75dd4c55d31c34f174cdf57a"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] [[package]] name = "iniconfig" @@ -796,38 +816,38 @@ pg = ["psycopg2-binary"] [[package]] name = "mypy" -version = "1.11.0" +version = "1.11.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"}, - {file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"}, - {file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"}, - {file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"}, - {file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"}, - {file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"}, - {file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"}, - {file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"}, - {file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"}, - {file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"}, - {file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"}, - {file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"}, - {file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"}, - {file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"}, - {file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"}, - {file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"}, - {file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"}, - {file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"}, - {file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, + {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, + {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, + {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, + {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, + {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, + {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, + {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, + {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, + {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, + {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, + {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, + {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, + {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, + {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, + {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, + {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, + {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] @@ -1221,62 +1241,64 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -1302,29 +1324,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.5.5" +version = "0.6.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"}, - {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"}, - {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"}, - {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"}, - {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"}, - {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"}, - {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, + {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"}, + {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"}, + {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"}, + {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"}, + {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"}, + {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"}, + {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"}, ] [[package]] @@ -1343,18 +1365,18 @@ sqlalchemy = "*" [[package]] name = "setuptools" -version = "71.1.0" +version = "72.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"}, - {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"}, + {file = "setuptools-72.2.0-py3-none-any.whl", hash = "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4"}, + {file = "setuptools-72.2.0.tar.gz", hash = "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9"}, ] [package.extras] core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1392,13 +1414,13 @@ files = [ [[package]] name = "soupsieve" -version = "2.5" +version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, ] [[package]] @@ -1436,25 +1458,6 @@ docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] -[[package]] -name = "sphinx-autodoc-typehints" -version = "2.0.1" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149"}, - {file = "sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12"}, -] - -[package.dependencies] -sphinx = ">=7.1.2" - -[package.extras] -docs = ["furo (>=2024.1.29)"] -numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.4.2)", "diff-cover (>=8.0.3)", "pytest (>=8.0.1)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.9)"] - [[package]] name = "sphinx-basic-ng" version = "1.0.0b2" @@ -1492,13 +1495,13 @@ rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] [[package]] name = "sphinx-github-changelog" -version = "1.3.0" +version = "1.4.0" description = "Build a sphinx changelog from GitHub Releases" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "sphinx_github_changelog-1.3.0-py3-none-any.whl", hash = "sha256:eb5424d590ae7866e77b8db7eecf283678cba76b74d90b17bc4f3872976407eb"}, - {file = "sphinx_github_changelog-1.3.0.tar.gz", hash = "sha256:b898adc52131147305b9cb893c2a4cad0ba2912178ed8f88b62bf6f43a2baaa4"}, + {file = "sphinx_github_changelog-1.4.0-py3-none-any.whl", hash = "sha256:cdf2099ea3e4587ae8637be7ba609738bfdeca4bd80c5df6fc45046735ae5c2f"}, + {file = "sphinx_github_changelog-1.4.0.tar.gz", hash = "sha256:204745e93a1f280e4664977b5fee526b0a011c92ca19c304bd01fd641ddb6393"}, ] [package.dependencies] @@ -1611,60 +1614,60 @@ test = ["pytest"] [[package]] name = "sqlalchemy" -version = "2.0.31" +version = "2.0.32" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f68470edd70c3ac3b6cd5c2a22a8daf18415203ca1b036aaeb9b0fb6f54e8298"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e2c38c2a4c5c634fe6c3c58a789712719fa1bf9b9d6ff5ebfce9a9e5b89c1ca"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd15026f77420eb2b324dcb93551ad9c5f22fab2c150c286ef1dc1160f110203"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:352b2770097f41bff6029b280c0e03b217c2dcaddc40726f8f53ed58d8a85da4"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56d51ae825d20d604583f82c9527d285e9e6d14f9a5516463d9705dab20c3740"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-win32.whl", hash = "sha256:6e2622844551945db81c26a02f27d94145b561f9d4b0c39ce7bfd2fda5776dac"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:ccaf1b0c90435b6e430f5dd30a5aede4764942a695552eb3a4ab74ed63c5b8d3"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3b74570d99126992d4b0f91fb87c586a574a5872651185de8297c6f90055ae42"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f77c4f042ad493cb8595e2f503c7a4fe44cd7bd59c7582fd6d78d7e7b8ec52c"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1591329333daf94467e699e11015d9c944f44c94d2091f4ac493ced0119449"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74afabeeff415e35525bf7a4ecdab015f00e06456166a2eba7590e49f8db940e"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9c01990d9015df2c6f818aa8f4297d42ee71c9502026bb074e713d496e26b67"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66f63278db425838b3c2b1c596654b31939427016ba030e951b292e32b99553e"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-win32.whl", hash = "sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-win_amd64.whl", hash = "sha256:fa4b1af3e619b5b0b435e333f3967612db06351217c58bfb50cee5f003db2a5a"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f43e93057cf52a227eda401251c72b6fbe4756f35fa6bfebb5d73b86881e59b0"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d337bf94052856d1b330d5fcad44582a30c532a2463776e1651bd3294ee7e58b"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06fb43a51ccdff3b4006aafee9fcf15f63f23c580675f7734245ceb6b6a9e05"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b6e22630e89f0e8c12332b2b4c282cb01cf4da0d26795b7eae16702a608e7ca1"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:79a40771363c5e9f3a77f0e28b3302801db08040928146e6808b5b7a40749c88"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-win32.whl", hash = "sha256:501ff052229cb79dd4c49c402f6cb03b5a40ae4771efc8bb2bfac9f6c3d3508f"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:597fec37c382a5442ffd471f66ce12d07d91b281fd474289356b1a0041bdf31d"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc6d69f8829712a4fd799d2ac8d79bdeff651c2301b081fd5d3fe697bd5b4ab9"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23b9fbb2f5dd9e630db70fbe47d963c7779e9c81830869bd7d137c2dc1ad05fb"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21c97efcbb9f255d5c12a96ae14da873233597dfd00a3a0c4ce5b3e5e79704"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a6a9837589c42b16693cf7bf836f5d42218f44d198f9343dd71d3164ceeeac"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc251477eae03c20fae8db9c1c23ea2ebc47331bcd73927cdcaecd02af98d3c3"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2fd17e3bb8058359fa61248c52c7b09a97cf3c820e54207a50af529876451808"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-win32.whl", hash = "sha256:c76c81c52e1e08f12f4b6a07af2b96b9b15ea67ccdd40ae17019f1c373faa227"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:4b600e9a212ed59355813becbcf282cfda5c93678e15c25a0ef896b354423238"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"}, - {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"}, - {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c9045ecc2e4db59bfc97b20516dfdf8e41d910ac6fb667ebd3a79ea54084619"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1467940318e4a860afd546ef61fefb98a14d935cd6817ed07a228c7f7c62f389"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5954463675cb15db8d4b521f3566a017c8789222b8316b1e6934c811018ee08b"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167e7497035c303ae50651b351c28dc22a40bb98fbdb8468cdc971821b1ae533"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b27dfb676ac02529fb6e343b3a482303f16e6bc3a4d868b73935b8792edb52d0"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bf2360a5e0f7bd75fa80431bf8ebcfb920c9f885e7956c7efde89031695cafb8"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-win32.whl", hash = "sha256:306fe44e754a91cd9d600a6b070c1f2fadbb4a1a257b8781ccf33c7067fd3e4d"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-win_amd64.whl", hash = "sha256:99db65e6f3ab42e06c318f15c98f59a436f1c78179e6a6f40f529c8cc7100b22"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21b053be28a8a414f2ddd401f1be8361e41032d2ef5884b2f31d31cb723e559f"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b178e875a7a25b5938b53b006598ee7645172fccafe1c291a706e93f48499ff5"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723a40ee2cc7ea653645bd4cf024326dea2076673fc9d3d33f20f6c81db83e1d"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:295ff8689544f7ee7e819529633d058bd458c1fd7f7e3eebd0f9268ebc56c2a0"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49496b68cd190a147118af585173ee624114dfb2e0297558c460ad7495f9dfe2"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:acd9b73c5c15f0ec5ce18128b1fe9157ddd0044abc373e6ecd5ba376a7e5d961"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-win32.whl", hash = "sha256:9365a3da32dabd3e69e06b972b1ffb0c89668994c7e8e75ce21d3e5e69ddef28"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-win_amd64.whl", hash = "sha256:8bd63d051f4f313b102a2af1cbc8b80f061bf78f3d5bd0843ff70b5859e27924"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bab3db192a0c35e3c9d1560eb8332463e29e5507dbd822e29a0a3c48c0a8d92"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:19d98f4f58b13900d8dec4ed09dd09ef292208ee44cc9c2fe01c1f0a2fe440e9"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd33c61513cb1b7371fd40cf221256456d26a56284e7d19d1f0b9f1eb7dd7e8"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6ba0497c1d066dd004e0f02a92426ca2df20fac08728d03f67f6960271feec"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b6be53e4fde0065524f1a0a7929b10e9280987b320716c1509478b712a7688c"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:916a798f62f410c0b80b63683c8061f5ebe237b0f4ad778739304253353bc1cb"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-win32.whl", hash = "sha256:31983018b74908ebc6c996a16ad3690301a23befb643093fcfe85efd292e384d"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-win_amd64.whl", hash = "sha256:4363ed245a6231f2e2957cccdda3c776265a75851f4753c60f3004b90e69bfeb"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8afd5b26570bf41c35c0121801479958b4446751a3971fb9a480c1afd85558e"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c750987fc876813f27b60d619b987b057eb4896b81117f73bb8d9918c14f1cad"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada0102afff4890f651ed91120c1120065663506b760da4e7823913ebd3258be"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:78c03d0f8a5ab4f3034c0e8482cfcc415a3ec6193491cfa1c643ed707d476f16"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:3bd1cae7519283ff525e64645ebd7a3e0283f3c038f461ecc1c7b040a0c932a1"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-win32.whl", hash = "sha256:01438ebcdc566d58c93af0171c74ec28efe6a29184b773e378a385e6215389da"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-win_amd64.whl", hash = "sha256:4979dc80fbbc9d2ef569e71e0896990bc94df2b9fdbd878290bd129b65ab579c"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c742be912f57586ac43af38b3848f7688863a403dfb220193a882ea60e1ec3a"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62e23d0ac103bcf1c5555b6c88c114089587bc64d048fef5bbdb58dfd26f96da"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:251f0d1108aab8ea7b9aadbd07fb47fb8e3a5838dde34aa95a3349876b5a1f1d"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef18a84e5116340e38eca3e7f9eeaaef62738891422e7c2a0b80feab165905f"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3eb6a97a1d39976f360b10ff208c73afb6a4de86dd2a6212ddf65c4a6a2347d5"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0c1c9b673d21477cec17ab10bc4decb1322843ba35b481585facd88203754fc5"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-win32.whl", hash = "sha256:c41a2b9ca80ee555decc605bd3c4520cc6fef9abde8fd66b1cf65126a6922d65"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-win_amd64.whl", hash = "sha256:8a37e4d265033c897892279e8adf505c8b6b4075f2b40d77afb31f7185cd6ecd"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fec964fba2ef46476312a03ec8c425956b05c20220a1a03703537824b5e8e1"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:328429aecaba2aee3d71e11f2477c14eec5990fb6d0e884107935f7fb6001632"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85a01b5599e790e76ac3fe3aa2f26e1feba56270023d6afd5550ed63c68552b3"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf04784797dcdf4c0aa952c8d234fa01974c4729db55c45732520ce12dd95b4"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4488120becf9b71b3ac718f4138269a6be99a42fe023ec457896ba4f80749525"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14e09e083a5796d513918a66f3d6aedbc131e39e80875afe81d98a03312889e6"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-win32.whl", hash = "sha256:0d322cc9c9b2154ba7e82f7bf25ecc7c36fbe2d82e2933b3642fc095a52cfc78"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-win_amd64.whl", hash = "sha256:7dd8583df2f98dea28b5cd53a1beac963f4f9d087888d75f22fcc93a07cf8d84"}, + {file = "SQLAlchemy-2.0.32-py3-none-any.whl", hash = "sha256:e567a8793a692451f706b363ccf3c45e056b67d90ead58c3bc9471af5d212202"}, + {file = "SQLAlchemy-2.0.32.tar.gz", hash = "sha256:c1b88cc8b02b6a5f0efb0345a03672d4c897dc7d92585176f88c67346f565ea8"}, ] [package.dependencies] @@ -1746,13 +1749,13 @@ files = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20240724" +version = "6.0.12.20240808" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" files = [ - {file = "types-PyYAML-6.0.12.20240724.tar.gz", hash = "sha256:cf7b31ae67e0c5b2919c703d2affc415485099d3fe6666a6912f040fd05cb67f"}, - {file = "types_PyYAML-6.0.12.20240724-py3-none-any.whl", hash = "sha256:e5becec598f3aa3a2ddf671de4a75fa1c6856fbf73b2840286c9d50fae2d5d48"}, + {file = "types-PyYAML-6.0.12.20240808.tar.gz", hash = "sha256:b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af"}, + {file = "types_PyYAML-6.0.12.20240808-py3-none-any.whl", hash = "sha256:deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35"}, ] [[package]] @@ -1796,13 +1799,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "zipp" -version = "3.19.2" +version = "3.20.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, + {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, + {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, ] [package.extras] @@ -1813,9 +1816,10 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", aiopg = ["aiopg", "psycopg2-binary"] django = ["django"] psycopg2 = ["psycopg2-binary"] +sphinx = ["sphinx"] sqlalchemy = ["sqlalchemy"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "4c0b8b8ffde08064ffd59f6cf47bffff22f08479bfeba08fa9592672cf9f7659" +content-hash = "ac5a99fe7f7c8531669219ac47284b5b483282a953c12d3b997c92a373d241ae" diff --git a/procrastinate/app.py b/procrastinate/app.py index 1dc468a1a..ac5b429df 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -44,14 +44,6 @@ class App(blueprints.Blueprint): and use it to decorate your tasks with `App.task`. You can run a worker with `App.run_worker`. - - Attributes - ---------- - tasks : ``Dict[str, tasks.Task]`` - The mapping of all tasks known by the app. Only procrastinate is expected to - make changes to this mapping. - job_manager : `manager.JobManager` - The `JobManager` linked to the application """ @classmethod @@ -116,7 +108,10 @@ def __init__( self.worker_defaults = worker_defaults or {} self.periodic_defaults = periodic_defaults or {} - self.job_manager = manager.JobManager(connector=self.connector) + #: The :py:class:`~manager.JobManager` linked to the application + self.job_manager: manager.JobManager = manager.JobManager( + connector=self.connector + ) self._register_builtin_tasks() @@ -134,7 +129,7 @@ def with_connector(self, connector: connector_module.BaseConnector) -> App: Returns ------- - `App` + : A new compatible app. """ app = App( @@ -158,7 +153,7 @@ def replace_connector( connector : The new connector to use. - Returns + Yields ------- `App` A new compatible app. @@ -195,7 +190,7 @@ def configure_task( Parameters ---------- - name : str + name: Name of the task. If not explicitly defined, this will be the dotted path to the task (``my.module.my_task``) @@ -204,7 +199,7 @@ def configure_task( Returns ------- - ``jobs.JobDeferrer`` + : Launch ``.defer(**task_kwargs)`` on this object to defer your job. """ from procrastinate import tasks @@ -248,18 +243,18 @@ async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: Parameters ---------- - queues : ``Optional[Iterable[str]]`` + queues: ``Optional[Iterable[str]]`` List of queues to listen to, or None to listen to every queue (defaults to ``None``). - wait : ``bool`` + wait: ``bool`` If False, the worker will terminate as soon as it has caught up with the queues. If True, the worker will work until it is stopped by a signal (``ctrl+c``, ``SIGINT``, ``SIGTERM``) (defaults to ``True``). - concurrency : ``int`` + concurrency: ``int`` Indicates how many asynchronous jobs the worker can run in parallel. Do not use concurrency if you have synchronous blocking tasks. See `howto/production/concurrency` (defaults to ``1``). - name : ``Optional[str]`` + name: ``Optional[str]`` Name of the worker. Will be passed in the `JobContext` and used in the logs (defaults to ``None`` which will result in the worker named ``worker``). diff --git a/procrastinate/blueprints.py b/procrastinate/blueprints.py index 0304cb42e..5f737d3b4 100644 --- a/procrastinate/blueprints.py +++ b/procrastinate/blueprints.py @@ -69,6 +69,8 @@ def my_task(): """ def __init__(self) -> None: + #: The mapping of all tasks known by the app. Only procrastinate is + #: expected to make changes to this mapping. self.tasks: dict[str, Task] = {} self.periodic_registry = periodic.PeriodicRegistry() self._check_stack() diff --git a/procrastinate/builtin_tasks.py b/procrastinate/builtin_tasks.py index f52c64338..ca7fb3e29 100644 --- a/procrastinate/builtin_tasks.py +++ b/procrastinate/builtin_tasks.py @@ -12,6 +12,8 @@ async def remove_old_jobs( max_hours: int, queue: str | None = None, remove_error: bool | None = False, + remove_cancelled: bool | None = False, + remove_aborted: bool | None = False, ) -> None: """ This task cleans your database by removing old jobs. Note that jobs and linked @@ -24,10 +26,20 @@ async def remove_old_jobs( queue : The name of the queue in which jobs will be deleted. If not specified, the task will delete jobs from all queues. - remove_error : + remove_error: By default only successful jobs will be removed. When this parameter is True failed jobs will also be deleted. + remove_cancelled: + By default only successful jobs will be removed. When this parameter is True + cancelled jobs will also be deleted. + remove_aborted: + By default only successful jobs will be removed. When this parameter is True + aborted jobs will also be deleted. """ await context.app.job_manager.delete_old_jobs( - nb_hours=max_hours, queue=queue, include_error=remove_error + nb_hours=max_hours, + queue=queue, + include_error=remove_error, + include_cancelled=remove_cancelled, + include_aborted=remove_aborted, ) diff --git a/procrastinate/contrib/aiopg/aiopg_connector.py b/procrastinate/contrib/aiopg/aiopg_connector.py index 40d4b8a22..c3ef93976 100644 --- a/procrastinate/contrib/aiopg/aiopg_connector.py +++ b/procrastinate/contrib/aiopg/aiopg_connector.py @@ -98,35 +98,35 @@ def __init__( Parameters ---------- - json_dumps : + json_dumps: The JSON dumps function to use for serializing job arguments. Defaults to the function used by psycopg2. See the `psycopg2 doc`_. - json_loads : + json_loads: The JSON loads function to use for deserializing job arguments. Defaults to the function used by psycopg2. See the `psycopg2 doc`_. Unused if the pool is externally created and set into the connector through the ``App.open_async`` method. - dsn : ``Optional[str]`` + dsn: ``Optional[str]`` Passed to aiopg. Default is "" instead of None, which means if no argument is passed, it will connect to localhost:5432 instead of a Unix-domain local socket file. - enable_json : ``bool`` + enable_json: ``bool`` Passed to aiopg. Default is False instead of True to avoid messing with the global state. enable_hstore: ``bool`` Passed to aiopg. Default is False instead of True to avoid messing with the global state. - enable_uuid : ``bool`` + enable_uuid: ``bool`` Passed to aiopg. Default is False instead of True to avoid messing with the global state. - cursor_factory : ``psycopg2.extensions.cursor`` + cursor_factory: ``psycopg2.extensions.cursor`` Passed to aiopg. Default is ``psycopg2.extras.RealDictCursor`` instead of standard cursor. There is no identified use case for changing this. - maxsize : ``int`` + maxsize: ``int`` Passed to aiopg. If value is 1, then listen/notify feature will be deactivated. - minsize : ``int`` + minsize: ``int`` Passed to aiopg. Initial connections are not opened when the connector is created, but at first use of the pool. """ diff --git a/procrastinate/contrib/django/admin.py b/procrastinate/contrib/django/admin.py index 6d81bddc6..b606ffec7 100644 --- a/procrastinate/contrib/django/admin.py +++ b/procrastinate/contrib/django/admin.py @@ -1,11 +1,61 @@ from __future__ import annotations +import json + from django.contrib import admin +from django.template.loader import render_to_string +from django.utils import timezone +from django.utils.html import format_html +from django.utils.safestring import mark_safe from . import models +JOB_STATUS_EMOJI_MAPPING = { + "todo": "🗓️", + "doing": "🚂", + "failed": "❌", + "succeeded": "✅", + "cancelled": "🤚", + "aborted": "🔌", +} + + +class ProcrastinateEventInline(admin.StackedInline): + model = models.ProcrastinateEvent + + +@admin.register(models.ProcrastinateJob) +class ProcrastinateJobAdmin(admin.ModelAdmin): + fields = [ + "pk", + "short_task_name", + "pretty_args", + "pretty_status", + "queue_name", + "lock", + "queueing_lock", + "priority", + "scheduled_at", + "attempts", + ] + list_display = [ + "pk", + "short_task_name", + "pretty_args", + "pretty_status", + "summary", + ] + list_filter = [ + "status", + "queue_name", + "task_name", + "lock", + "queueing_lock", + "scheduled_at", + "priority", + ] + inlines = [ProcrastinateEventInline] -class ProcrastinateAdmin(admin.ModelAdmin): def get_readonly_fields( self, request, @@ -26,11 +76,41 @@ def has_add_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None): return False + @admin.display(description="Status") + def pretty_status(self, instance: models.ProcrastinateJob) -> str: + emoji = JOB_STATUS_EMOJI_MAPPING.get(instance.status, "") + return f"{emoji} {instance.status.title()}" + + @admin.display(description="Task Name") + def short_task_name(self, instance: models.ProcrastinateJob) -> str: + *modules, name = instance.task_name.split(".") + return format_html( + "{name}", + task_name=instance.task_name, + name=".".join(m[0] for m in modules) + f".{name}", + ) + + @admin.display(description="Args") + def pretty_args(self, instance: models.ProcrastinateJob) -> str: + indent = 2 if len(instance.args) > 1 or len(str(instance.args)) > 30 else None + pretty_json = json.dumps(instance.args, indent=indent) + if len(pretty_json) > 2000: + pretty_json = pretty_json[:2000] + "..." + return format_html( + '
{pretty_json}
', pretty_json=pretty_json + ) -admin.site.register( - [ - models.ProcrastinateJob, - models.ProcrastinateEvent, - ], - ProcrastinateAdmin, -) + @admin.display(description="Summary") + def summary(self, instance: models.ProcrastinateJob) -> str: + if last_event := instance.procrastinateevent_set.latest(): # type: ignore[attr-defined] + return mark_safe( + render_to_string( + "procrastinate/admin/summary.html", + { + "last_event": last_event, + "job": instance, + "now": timezone.now(), + }, + ).strip() + ) + return "" diff --git a/procrastinate/contrib/django/django_connector.py b/procrastinate/contrib/django/django_connector.py index cf54515c0..909c7ac8b 100644 --- a/procrastinate/contrib/django/django_connector.py +++ b/procrastinate/contrib/django/django_connector.py @@ -10,6 +10,7 @@ ) import asgiref.sync +from django import db as django_db from django.core import exceptions as django_exceptions from django.db import connections from django.db.backends.base.base import BaseDatabaseWrapper @@ -41,7 +42,14 @@ def wrap_exceptions() -> Generator[None, None, None]: ) with wrap(): - yield + try: + yield + except django_db.DatabaseError as exc: + # __cause__ is always defined but might be None, it's set by Django + # (using `raise x from y) to the original db driver exception + if exc.__cause__: + raise exc.__cause__ + raise exc class DjangoConnector(connector.BaseAsyncConnector): @@ -148,7 +156,7 @@ def get_worker_connector(self) -> connector.BaseAsyncConnector: Returns ------- - ``procrastinate.contrib.aiopg.AiopgConnector`` or ``procrastinate.contrib.psycopg3.PsycopgConnector`` + : A connector that can be used in a worker """ alias = settings.settings.DATABASE_ALIAS diff --git a/procrastinate/contrib/django/migrations/0030_alter_procrastinateevent_options.py b/procrastinate/contrib/django/migrations/0030_alter_procrastinateevent_options.py new file mode 100644 index 000000000..62434e079 --- /dev/null +++ b/procrastinate/contrib/django/migrations/0030_alter_procrastinateevent_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.8 on 2024-08-08 14:27 +from __future__ import annotations + +from django.db import migrations + + +class Migration(migrations.Migration): + operations = [ + migrations.AlterModelOptions( + name="procrastinateevent", + options={"get_latest_by": "at", "managed": False}, + ), + ] + name = "0030_alter_procrastinateevent_options" + dependencies = [ + ("procrastinate", "0029_add_additional_params_to_retry_job"), + ] diff --git a/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py b/procrastinate/contrib/django/migrations/0031_add_abort_on_procrastinate_jobs.py similarity index 87% rename from procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py rename to procrastinate/contrib/django/migrations/0031_add_abort_on_procrastinate_jobs.py index 97a975d3c..f968078d5 100644 --- a/procrastinate/contrib/django/migrations/0030_add_abort_on_procrastinate_jobs.py +++ b/procrastinate/contrib/django/migrations/0031_add_abort_on_procrastinate_jobs.py @@ -31,5 +31,5 @@ class Migration(migrations.Migration): models.BooleanField(), ), ] - name = "0030_add_abort_on_procrastinate_jobs" - dependencies = [("procrastinate", "0029_add_additional_params_to_retry_job")] + name = "0031_add_abort_on_procrastinate_jobs" + dependencies = [("procrastinate", "0030_alter_procrastinateevent_options")] diff --git a/procrastinate/contrib/django/models.py b/procrastinate/contrib/django/models.py index 8aaa118eb..9ae4bd10c 100644 --- a/procrastinate/contrib/django/models.py +++ b/procrastinate/contrib/django/models.py @@ -4,6 +4,8 @@ from django.db import models +from procrastinate import jobs + from . import exceptions, settings @@ -85,6 +87,24 @@ class Meta: # type: ignore managed = False db_table = "procrastinate_jobs" + @property + def procrastinate_job(self) -> jobs.Job: + return jobs.Job( + id=self.id, + queue=self.queue_name, + task_name=self.task_name, + task_kwargs=self.args, + priority=self.priority, + lock=self.lock, + status=self.status, + scheduled_at=self.scheduled_at, + attempts=self.attempts, + queueing_lock=self.queueing_lock, + ) + + def __str__(self) -> str: + return self.procrastinate_job.call_string + class ProcrastinateEvent(ProcrastinateReadOnlyModelMixin, models.Model): TYPES = ( @@ -108,6 +128,10 @@ class ProcrastinateEvent(ProcrastinateReadOnlyModelMixin, models.Model): class Meta: # type: ignore managed = False db_table = "procrastinate_events" + get_latest_by = "at" + + def __str__(self) -> str: + return f"Event {self.id} - Job {self.job_id}: {self.type} at {self.at}" # type: ignore class ProcrastinatePeriodicDefer(ProcrastinateReadOnlyModelMixin, models.Model): diff --git a/procrastinate/contrib/django/templates/procrastinate/admin/summary.html b/procrastinate/contrib/django/templates/procrastinate/admin/summary.html new file mode 100644 index 000000000..733c66b97 --- /dev/null +++ b/procrastinate/contrib/django/templates/procrastinate/admin/summary.html @@ -0,0 +1,48 @@ +
    +
  • + {{ last_event.type | title }} + {{ last_event.at|timesince }} ago +
  • + {% if job.queue_name %} +
  • + Queue: + {{ job.queue_name }} +
  • + {% endif %} + {% if job.lock %} +
  • + Lock: + {{ job.lock }} +
  • + {% endif %} + {% if job.queueing_lock %} +
  • + Queueing lock: + {{ job.queueing_lock }} +
  • + {% endif %} + {% if job.priority != 0 %} +
  • + Priority: + {{ job.priority }} +
  • + {% endif %} + {% if job.scheduled_at %} +
  • + Scheduled: + + {% if job.scheduled_at > now %} + in {{ job.scheduled_at|timeuntil }} + {% else %} + {{ job.scheduled_at|timesince }} ago + {% endif %} + +
  • + {% endif %} + {% if job.attempts > 1 %} +
  • + Attempts: + {{ job.attempts }} +
  • + {% endif %} +
diff --git a/procrastinate/contrib/django/utils.py b/procrastinate/contrib/django/utils.py index c7b279f0a..9def57551 100644 --- a/procrastinate/contrib/django/utils.py +++ b/procrastinate/contrib/django/utils.py @@ -20,7 +20,7 @@ def connector_params(alias: str = "default") -> dict[str, Any]: Returns ------- - ``Dict[str, Any]`` + : Provide these keyword arguments when instantiating your connector """ wrapper = connections[alias] diff --git a/procrastinate/contrib/psycopg2/psycopg2_connector.py b/procrastinate/contrib/psycopg2/psycopg2_connector.py index 5c6e6b64f..bb847e95d 100644 --- a/procrastinate/contrib/psycopg2/psycopg2_connector.py +++ b/procrastinate/contrib/psycopg2/psycopg2_connector.py @@ -83,23 +83,23 @@ def __init__( Parameters ---------- - json_dumps : + json_dumps: The JSON dumps function to use for serializing job arguments. Defaults to the function used by psycopg2. See the `psycopg2 doc`_. - json_loads : + json_loads: The JSON loads function to use for deserializing job arguments. Defaults to the function used by psycopg2. See the `psycopg2 doc`_. Unused if the pool is externally created and set into the connector through the ``App.open`` method. - minconn : int + minconn: int Passed to psycopg2, default set to 1 (same as aiopg). - maxconn : int + maxconn: int Passed to psycopg2, default set to 10 (same as aiopg). - dsn : ``Optional[str]`` + dsn: ``Optional[str]`` Passed to psycopg2. Default is "" instead of None, which means if no argument is passed, it will connect to localhost:5432 instead of a Unix-domain local socket file. - cursor_factory : ``psycopg2.extensions.cursor`` + cursor_factory: ``psycopg2.extensions.cursor`` Passed to psycopg2. Default is ``psycopg2.extras.RealDictCursor`` instead of standard cursor. There is no identified use case for changing this. diff --git a/procrastinate/contrib/sphinx/__init__.py b/procrastinate/contrib/sphinx/__init__.py new file mode 100644 index 000000000..e3b84398e --- /dev/null +++ b/procrastinate/contrib/sphinx/__init__.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import Any + +from sphinx.application import Sphinx +from sphinx.ext.autodoc import FunctionDocumenter + +from procrastinate import tasks + + +class ProcrastinateTaskDocumenter(FunctionDocumenter): + objtype = "procrastinate_task" + directivetype = "function" + member_order = 40 + + @classmethod + def can_document_member( + cls, + member: Any, + membername: str, + isattr: bool, + parent: Any, + ) -> bool: + return isinstance(member, tasks.Task) + + +def setup(app: Sphinx): + app.setup_extension("sphinx.ext.autodoc") # Require autodoc extension + + app.add_autodocumenter(ProcrastinateTaskDocumenter) + + return { + "version": "1", + "parallel_read_safe": True, + } diff --git a/procrastinate/jobs.py b/procrastinate/jobs.py index b2ea808fe..1127c370f 100644 --- a/procrastinate/jobs.py +++ b/procrastinate/jobs.py @@ -57,42 +57,29 @@ class Job: """ A job is the launching of a specific task with specific values for the keyword arguments. - - Attributes - ---------- - id : - Internal id uniquely identifying the job. - status : - Status of the job. - priority : - Priority of the job. - queue : - Queue name the job will be run in. - lock : - No two jobs with the same lock string can run simultaneously - queueing_lock : - No two jobs with the same queueing lock can be waiting in the queue. - task_name : - Name of the associated task. - task_kwargs : - Arguments used to call the task. - scheduled_at : - Date and time after which the job is expected to run. - attempts : - Number of times the job has been tried. """ + #: Internal id uniquely identifying the job. id: int | None = None + #: Status of the job. status: str | None = None + #: Queue name the job will be run in. queue: str + #: Priority of the job. priority: int = DEFAULT_PRIORITY + #: No two jobs with the same lock string can run simultaneously lock: str | None + #: No two jobs with the same queueing lock can be waiting in the queue. queueing_lock: str | None + #: Name of the associated task. task_name: str + #: Arguments used to call the task. task_kwargs: types.JSONDict = attr.ib(factory=dict) + #: Date and time after which the job is expected to run. scheduled_at: datetime.datetime | None = attr.ib( default=None, validator=check_aware ) + #: Number of times the job has been tried. attempts: int = 0 @classmethod diff --git a/procrastinate/manager.py b/procrastinate/manager.py index 60073e954..d2f4db67d 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -29,11 +29,12 @@ async def defer_job_async(self, job: jobs.Job) -> jobs.Job: Parameters ---------- - job : `jobs.Job` + job: + The job to defer Returns ------- - `jobs.Job` + : A copy of the job instance with the id set. """ # Make sure this code stays synchronized with .defer_job() @@ -125,12 +126,12 @@ async def fetch_job(self, queues: Iterable[str] | None) -> jobs.Job | None: Parameters ---------- - queues : ``Optional[Iterable[str]]`` + queues: Filter by job queue names Returns ------- - ``Optional[jobs.Job]`` + : None if no suitable job was found. The job otherwise. """ @@ -156,17 +157,13 @@ async def get_stalled_jobs( Parameters ---------- - nb_seconds : ``int`` + nb_seconds: Only jobs that have been in ``doing`` state for longer than this will be returned - queue : ``Optional[str]`` + queue: Filter by job queue name - task_name : ``Optional[str]`` + task_name: Filter by job task name - - Returns - ------- - ``Iterable[jobs.Job]`` """ rows = await self.connector.execute_query_all_async( query=sql.queries["select_stalled_jobs"], @@ -181,25 +178,35 @@ async def delete_old_jobs( nb_hours: int, queue: str | None = None, include_error: bool | None = False, + include_cancelled: bool | None = False, + include_aborted: bool | None = False, ) -> None: """ - Delete jobs that have reached a final state (``succeeded`` or ``failed``). + Delete jobs that have reached a final state (``succeeded``, ``failed``, + ``cancelled``, or ``aborted``). By default, only considers jobs that have + succeeded. Parameters ---------- - nb_hours : ``int`` + nb_hours: Consider jobs that been in a final state for more than ``nb_hours`` - queue : ``Optional[str]`` + queue: Filter by job queue name - include_error : ``Optional[bool]`` - If ``True``, only succeeded jobs will be considered. If ``False``, both - succeeded and failed jobs will be considered, ``False`` by default + include_error: + If ``True``, also consider errored jobs. ``False`` by default + include_cancelled: + If ``True``, also consider cancelled jobs. ``False`` by default. + include_aborted: + If ``True``, also consider aborted jobs. ``False`` by default. """ # We only consider finished jobs by default - if not include_error: - statuses = [jobs.Status.SUCCEEDED.value] - else: - statuses = [jobs.Status.SUCCEEDED.value, jobs.Status.FAILED.value] + statuses = [jobs.Status.SUCCEEDED.value] + if include_error: + statuses.append(jobs.Status.FAILED.value) + if include_cancelled: + statuses.append(jobs.Status.CANCELLED.value) + if include_aborted: + statuses.append(jobs.Status.ABORTED.value) await self.connector.execute_query_async( query=sql.queries["delete_old_jobs"], @@ -219,8 +226,8 @@ async def finish_job( Parameters ---------- - job : `jobs.Job` - status : `jobs.Status` + job: + status: ``succeeded``, ``failed`` or ``aborted`` """ assert job.id # TODO remove this @@ -249,19 +256,19 @@ def cancel_job_by_id( Parameters ---------- - job_id : ``int`` + job_id: The id of the job to cancel - abort : ``bool`` + abort: If True, a job will be marked for abortion, but the task itself has to respect the abortion request. If False, only jobs in ``todo`` state will be set to ``cancelled`` and won't be processed by a worker anymore. - delete_job : ``bool`` + delete_job: If True, the job will be deleted from the database after being cancelled. Does not affect the jobs that should be aborted. Returns ------- - ``bool`` + : If True, the job was cancelled (or its abortion was requested). If False, nothing was done: either there is no job with this id or it's not in a state where it may be cancelled (i.e. `todo` or `doing`) @@ -287,19 +294,19 @@ async def cancel_job_by_id_async( Parameters ---------- - job_id : ``int`` + job_id: The id of the job to cancel - abort : ``bool`` + abort: If True, a job will be marked for abortion, but the task itself has to respect the abortion request. If False, only jobs in ``todo`` state will be set to ``cancelled`` and won't be processed by a worker anymore. - delete_job : ``bool`` + delete_job: If True, the job will be deleted from the database after being cancelled. Does not affect the jobs that should be aborted. Returns ------- - ``bool`` + : If True, the job was cancelled (or its abortion was requested). If False, nothing was done: either there is no job with this id or it's not in a state where it may be cancelled (i.e. `todo` or `doing`) @@ -323,12 +330,12 @@ def get_job_status(self, job_id: int) -> jobs.Status: Parameters ---------- - job_id : ``int`` + job_id: The id of the job to get the status of Returns ------- - `jobs.Status` + : """ result = self.connector.get_sync_connector().execute_query_one( query=sql.queries["get_job_status"], job_id=job_id @@ -341,12 +348,12 @@ async def get_job_status_async(self, job_id: int) -> jobs.Status: Parameters ---------- - job_id : ``int`` + job_id: The id of the job to get the status of Returns ------- - `jobs.Status` + : """ result = await self.connector.execute_query_one_async( query=sql.queries["get_job_status"], job_id=job_id @@ -402,18 +409,18 @@ async def retry_job( Parameters ---------- - job : `jobs.Job` - retry_at : ``Optional[datetime.datetime]`` + job: + retry_at: If set at present time or in the past, the job may be retried immediately. Otherwise, the job will be retried no sooner than this date & time. Should be timezone-aware (even if UTC). Defaults to present time. - priority : ``Optional[int]`` + priority: If set, the job will be retried with this priority. If not set, the priority remains unchanged. - queue : ``Optional[int]`` + queue: If set, the job will be retried on this queue. If not set, the queue remains unchanged. - lock : ``Optional[int]`` + lock: If set, the job will be retried with this lock. If not set, the lock remains unchanged. """ @@ -439,18 +446,18 @@ async def retry_job_by_id_async( Parameters ---------- - job_id : ``int`` - retry_at : ``datetime.datetime`` + job_id: + retry_at: If set at present time or in the past, the job may be retried immediately. Otherwise, the job will be retried no sooner than this date & time. Should be timezone-aware (even if UTC). - priority : ``Optional[int]`` + priority: If set, the job will be retried with this priority. If not set, the priority remains unchanged. - queue : ``Optional[int]`` + queue: If set, the job will be retried on this queue. If not set, the queue remains unchanged. - lock : ``Optional[int]`` + lock: If set, the job will be retried with this lock. If not set, the lock remains unchanged. """ @@ -495,9 +502,9 @@ async def listen_for_jobs( Parameters ---------- - event : ``asyncio.Event`` + event: This event will be set each time a defer operation occurs - queues : ``Optional[Iterable[str]]`` + queues: If ``None``, all defer operations will be considered. If an iterable of queue names is passed, only defer operations on those queues will be considered. Defaults to ``None`` @@ -513,7 +520,7 @@ async def check_connection_async(self) -> bool: Returns ------- - ``bool`` + : ``True`` if the table exists, ``False`` otherwise. """ result = await self.connector.execute_query_one_async( @@ -544,22 +551,22 @@ async def list_jobs_async( Parameters ---------- - id : ``int`` + id: Filter by job ID - queue : ``str`` + queue: Filter by job queue name - task : ``str`` + task: Filter by job task name - status : ``str`` + status: Filter by job status (``todo``/``doing``/``succeeded``/``failed``) - lock : ``str`` + lock: Filter by job lock - queueing_lock : ``str`` + queueing_lock: Filter by job queueing_lock Returns ------- - ``Iterable[jobs.Job]`` + : """ rows = await self.connector.execute_query_all_async( query=sql.queries["list_jobs"], @@ -607,18 +614,18 @@ async def list_queues_async( Parameters ---------- - queue : ``str`` + queue: Filter by job queue name - task : ``str`` + task: Filter by job task name - status : ``str`` + status: Filter by job status (``todo``/``doing``/``succeeded``/``failed``) - lock : ``str`` + lock: Filter by job lock Returns ------- - ``List[Dict[str, Any]]`` + : A list of dictionaries representing queues stats (``name``, ``jobs_count``, ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborted``). """ @@ -684,18 +691,18 @@ async def list_tasks_async( Parameters ---------- - queue : ``str`` + queue: Filter by job queue name - task : ``str`` + task: Filter by job task name - status : ``str`` + status: Filter by job status (``todo``/``doing``/``succeeded``/``failed``) - lock : ``str`` + lock: Filter by job lock Returns ------- - ``List[Dict[str, Any]]`` + : A list of dictionaries representing tasks stats (``name``, ``jobs_count``, ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborted``). """ @@ -761,18 +768,18 @@ async def list_locks_async( Parameters ---------- - queue : ``str`` + queue: Filter by job queue name - task : ``str`` + task: Filter by job task name - status : ``str`` + status: Filter by job status (``todo``/``doing``/``succeeded``/``failed``) - lock : ``str`` + lock: Filter by job lock Returns ------- - ``List[Dict[str, Any]]`` + : A list of dictionaries representing locks stats (``name``, ``jobs_count``, ``todo``, ``doing``, ``succeeded``, ``failed``, ``cancelled``, ``aborted``). """ diff --git a/procrastinate/retry.py b/procrastinate/retry.py index 827a614df..a07341666 100644 --- a/procrastinate/retry.py +++ b/procrastinate/retry.py @@ -53,20 +53,20 @@ def __init__( Parameters ---------- - retry_at : ``Optional[datetime.datetime]`` + retry_at: If set at present time or in the past, the job may be retried immediately. Otherwise, the job will be retried no sooner than this date & time. Should be timezone-aware (even if UTC). Defaults to present time. - retry_in : ``Optional[types.TimeDeltaParams]`` + retry_in: If set, the job will be retried after this duration. If not set, the job will be retried immediately. - priority : ``Optional[int]`` + priority: If set, the job will be retried with this priority. If not set, the priority remains unchanged. - queue : ``Optional[int]`` + queue: If set, the job will be retried on this queue. If not set, the queue remains unchanged. - lock : ``Optional[int]`` + lock: If set, the job will be retried with this lock. If not set, the lock remains unchanged. """ @@ -127,7 +127,7 @@ def get_schedule_in(self, *, exception: BaseException, attempts: int) -> int | N Returns ------- - ``Optional[int]`` + : If a job should not be retried, this function should return None. Otherwise, it should return the duration after which to schedule the new job run, *in seconds*. diff --git a/procrastinate/tasks.py b/procrastinate/tasks.py index 55658a20b..7ae3afb68 100644 --- a/procrastinate/tasks.py +++ b/procrastinate/tasks.py @@ -66,31 +66,6 @@ class Task(Generic[P, Args]): """ A task is a function that should be executed later. It is linked to a default queue, and expects keyword arguments. - - Attributes - ---------- - name : ``str`` - Name of the task, usually the dotted path of the decorated function. - aliases : ``List[str]`` - Additional names for the task. - retry_strategy : `RetryStrategy` - Value indicating the retry conditions in case of - :py:class:`procrastinate.jobs.Job` error. - pass_context : ``bool`` - If ``True``, passes the task execution context as first positional argument on - :py:class:`procrastinate.jobs.Job` execution. - queue : ``str`` - Default queue to send deferred jobs to. The queue can be overridden when a - job is deferred. - priority : - Default priority (an integer) of jobs that are deferred from this task. - Jobs with higher priority are run first. Priority can be positive or negative. - If no default priority is set then the default priority is 0. - lock : ``Optional[str]`` - Default lock. The lock can be overridden when a job is deferred. - queueing_lock : ``Optional[str]`` - Default queueing lock. The queuing lock can be overridden when a job is - deferred. """ def __init__( @@ -110,16 +85,33 @@ def __init__( lock: str | None = None, queueing_lock: str | None = None, ): - self.queue = queue - self.priority = priority - self.blueprint = blueprint + #: Default queue to send deferred jobs to. The queue can be overridden + #: when a job is deferred. + self.queue: str = queue + #: Default priority (an integer) of jobs that are deferred from this + #: task. Jobs with higher priority are run first. Priority can be + #: positive or negative. If no default priority is set then the default + #: priority is 0. + self.priority: int = priority + self.blueprint: blueprints.Blueprint = blueprint self.func: Callable[P] = func - self.aliases = aliases if aliases else [] - self.retry_strategy = retry_module.get_retry_strategy(retry) + #: Additional names for the task. + self.aliases: list[str] = aliases if aliases else [] + #: Value indicating the retry conditions in case of + #: :py:class:`procrastinate.jobs.Job` error. + self.retry_strategy: retry_module.BaseRetryStrategy | None = ( + retry_module.get_retry_strategy(retry) + ) + #: Name of the task, usually the dotted path of the decorated function. self.name: str = name if name else self.full_path - self.pass_context = pass_context - self.lock = lock - self.queueing_lock = queueing_lock + #: If ``True``, passes the task execution context as first positional + #: argument on :py:class:`procrastinate.jobs.Job` execution. + self.pass_context: bool = pass_context + #: Default lock. The lock can be overridden when a job is deferred. + self.lock: str | None = lock + #: Default queueing lock. The queuing lock can be overridden when a job + #: is deferred. + self.queueing_lock: str | None = queueing_lock def add_namespace(self, namespace: str) -> None: """ @@ -190,7 +182,7 @@ def configure(self, **options: Unpack[ConfigureTaskOptions]) -> jobs.JobDeferrer Returns ------- - ``jobs.JobDeferrer`` + : An object with a ``defer`` method, identical to `Task.defer` Raises diff --git a/procrastinate/testing.py b/procrastinate/testing.py index 51ab384ee..db80f4aa6 100644 --- a/procrastinate/testing.py +++ b/procrastinate/testing.py @@ -22,15 +22,11 @@ class InMemoryConnector(connector.BaseAsyncConnector): """ def __init__(self): - """ - Attributes - ---------- - jobs : ``Dict[int, Dict]`` - Mapping of ``{: }`` - """ self.reset() self.reverse_queries = {value: key for key, value in sql.queries.items()} self.reverse_queries[schema.SchemaManager.get_schema()] = "apply_schema" + #: Mapping of ``{: }`` + self.jobs: dict[int, JobRow] = {} def reset(self) -> None: """ diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 250858d37..3eac3e770 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -104,7 +104,7 @@ async def sync_to_async(func: Callable[..., T], *args, **kwargs) -> T: Given a callable, return a callable that will call the original one in an async context. """ - return await sync.sync_to_async(func)(*args, **kwargs) + return await sync.sync_to_async(func, thread_sensitive=False)(*args, **kwargs) def causes(exc: BaseException | None): diff --git a/procrastinate_demos/demo_async/tasks.py b/procrastinate_demos/demo_async/tasks.py index 2b7e54cf7..49f6be684 100644 --- a/procrastinate_demos/demo_async/tasks.py +++ b/procrastinate_demos/demo_async/tasks.py @@ -5,9 +5,11 @@ @app.task(queue="sums") async def sum(a, b): + """Sum two numbers.""" return a + b @app.task(queue="defer") async def defer(): + """Defer a another task.""" await sum.defer_async(a=1, b=2) diff --git a/pyproject.toml b/pyproject.toml index 1ff26d963..3ac25d99b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "poetry_dynamic_versioning.backend" name = "procrastinate" version = "0.0.0" description = "Postgres-based distributed task processing library" -authors = ["Joachim Jablon", "Eric Lemoine"] +authors = ["Joachim Jablon", "Eric Lemoine", "Kai Schlamp"] license = "MIT License" classifiers = [ "Development Status :: 4 - Beta", @@ -31,19 +31,20 @@ attrs = "*" contextlib2 = { version = "*", python = "<3.10" } croniter = "*" django = { version = ">=2.2", optional = true } -importlib-metadata = { version = "*", python = "<3.8" } importlib-resources = { version = ">=1.4", python = "<3.9" } -psycopg = { extras = ["pool"], version = "^3.1.13" } +psycopg = { extras = ["pool"], version = "*" } psycopg2-binary = { version = "*", optional = true } python-dateutil = "*" sqlalchemy = { version = "^2.0", optional = true } typing-extensions = { version = "*", python = "<3.8" } +sphinx = { version = "*", optional = true } [tool.poetry.extras] django = ["django"] sqlalchemy = ["sqlalchemy"] aiopg = ["aiopg", "psycopg2-binary"] psycopg2 = ["psycopg2-binary"] +sphinx = ["sphinx"] [tool.poetry.group.types] optional = true @@ -62,9 +63,17 @@ aiopg = "*" sqlalchemy = { extras = ["mypy"], version = "*" } psycopg2-binary = "*" psycopg = [ - { version = "^3.1.13", extras = ["binary", "pool"], markers = "sys_platform != 'darwin' or platform_machine != 'arm64'"}, - { version = "^3.1.13", extras = ["binary", "pool"], markers = "sys_platform == 'darwin' and platform_machine == 'arm64'", python = ">=3.10"}, - { version = "^3.1.13", extras = ["pool"], markers = "sys_platform == 'darwin' and platform_machine == 'arm64'", python = "<3.10" } + { version = "*", extras = [ + "binary", + "pool", + ], markers = "sys_platform != 'darwin' or platform_machine != 'arm64'" }, + { version = "*", extras = [ + "binary", + "pool", + ], markers = "sys_platform == 'darwin' and platform_machine == 'arm64'", python = ">=3.10" }, + { version = "*", extras = [ + "pool", + ], markers = "sys_platform == 'darwin' and platform_machine == 'arm64'", python = "<3.10" }, ] [tool.poetry.group.django.dependencies] @@ -78,13 +87,16 @@ pytest-mock = "*" migra = "*" # migra depends on schemainspect, which has an implicit dependency on setuptools # (pkg_resources). -setuptools = { version = "*", python = ">=3.12" } +setuptools = { version = "*" } + +[tool.poetry.group.docs] +optional = true [tool.poetry.group.docs.dependencies] + django = ">=2.2" furo = "*" Sphinx = "*" -sphinx-autodoc-typehints = "*" sphinx-copybutton = "*" sphinx-github-changelog = "*" sphinxcontrib-programoutput = "*" diff --git a/tests/acceptance/django_settings.py b/tests/acceptance/django_settings.py index c2b87cb5a..97a4d761c 100644 --- a/tests/acceptance/django_settings.py +++ b/tests/acceptance/django_settings.py @@ -11,6 +11,12 @@ }, } INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", "procrastinate.contrib.django", ] USE_TZ = True # To avoid RemovedInDjango50Warning diff --git a/tests/acceptance/test_sync.py b/tests/acceptance/test_sync.py index f856db2b5..1de3be071 100644 --- a/tests/acceptance/test_sync.py +++ b/tests/acceptance/test_sync.py @@ -1,6 +1,10 @@ from __future__ import annotations +import asyncio +import time + import pytest +from asgiref.sync import sync_to_async import procrastinate from procrastinate.contrib import psycopg2 @@ -54,6 +58,53 @@ async def product_task(a, b): assert product_results == [12] +async def test_nested_sync_to_async(sync_app, async_app): + sum_results = [] + + @sync_app.task(queue="default", name="sum_task") + def sum_task_sync(a, b): + async def _sum_task_async(a, b): + def _inner_sum_task_sync(a, b): + sum_results.append(a + b) + + # Only works if the worker runs the sync task in a separate thread + await sync_to_async(_inner_sum_task_sync)(a, b) + + asyncio.run(_sum_task_async(a, b)) + + sum_task_sync.defer(a=1, b=2) + + # We need to run the async app to execute the tasks + async_app.tasks = sync_app.tasks + await async_app.run_worker_async(queues=["default"], wait=False) + + assert sum_results == [3] + + +async def test_sync_task_runs_in_parallel(sync_app, async_app): + results = [] + + @sync_app.task(queue="default", name="sync_task_1") + def sync_task_1(): + for i in range(3): + time.sleep(0.1) + results.append(i) + + @sync_app.task(queue="default", name="sync_task_2") + def sync_task_2(): + for i in range(3): + time.sleep(0.1) + results.append(i) + + sync_task_1.defer() + sync_task_2.defer() + + async_app.tasks = sync_app.tasks + await async_app.run_worker_async(queues=["default"], concurrency=2, wait=False) + + assert results == [0, 0, 1, 1, 2, 2] + + async def test_cancel(sync_app, async_app): sum_results = [] diff --git a/tests/conftest.py b/tests/conftest.py index 0da0291e4..9e190a1b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,10 @@ if key.startswith("PROCRASTINATE_"): os.environ.pop(key) +# Unfortunately, we need the sphinx fixtures even though they generate an "app" fixture +# that conflicts with our own "app" fixture +pytest_plugins = ["sphinx.testing.fixtures"] + def cursor_execute(cursor, query, *identifiers): if identifiers: diff --git a/tests/integration/contrib/django/test_django_connector.py b/tests/integration/contrib/django/test_django_connector.py index 59c5cfd15..f523f6613 100644 --- a/tests/integration/contrib/django/test_django_connector.py +++ b/tests/integration/contrib/django/test_django_connector.py @@ -1,9 +1,10 @@ from __future__ import annotations import pytest -from django.core import exceptions +from django.core import exceptions as django_exceptions -from procrastinate import psycopg_connector +import procrastinate.contrib.django +from procrastinate import exceptions, psycopg_connector from procrastinate.contrib.aiopg import aiopg_connector from procrastinate.contrib.django import django_connector as django_connector_module @@ -13,6 +14,16 @@ def django_connector(db): return django_connector_module.DjangoConnector(alias="default") +def test_wrap_exceptions__integrity_error(db): + @procrastinate.contrib.django.app.task(queueing_lock="foo") + def foo(): + pass + + with pytest.raises(exceptions.AlreadyEnqueued): + foo.defer() + foo.defer() + + def test_get_sync_connector(django_connector): assert django_connector.get_sync_connector() is django_connector @@ -22,7 +33,7 @@ def test_open(django_connector): def test_open_pool(django_connector): - with pytest.raises(exceptions.ImproperlyConfigured): + with pytest.raises(django_exceptions.ImproperlyConfigured): django_connector.open(pool=object()) @@ -35,7 +46,7 @@ async def test_open_async(django_connector): async def test_open_pool_async(django_connector): - with pytest.raises(exceptions.ImproperlyConfigured): + with pytest.raises(django_exceptions.ImproperlyConfigured): await django_connector.open_async(pool=object()) @@ -112,5 +123,5 @@ async def test_get_worker_connector__error(django_connector, mocker): return_value=False, ) - with pytest.raises(exceptions.ImproperlyConfigured): + with pytest.raises(django_exceptions.ImproperlyConfigured): django_connector.get_worker_connector() diff --git a/tests/integration/contrib/django/test_models.py b/tests/integration/contrib/django/test_models.py index 52c65ace7..21bc818e7 100644 --- a/tests/integration/contrib/django/test_models.py +++ b/tests/integration/contrib/django/test_models.py @@ -8,6 +8,7 @@ import procrastinate import procrastinate.contrib.django import procrastinate.contrib.django.exceptions +from procrastinate import jobs as jobs_module from procrastinate.contrib.django import models @@ -31,6 +32,33 @@ def test_procrastinate_job(db): } +def test_procrastinate_job__property(db): + job = models.ProcrastinateJob( + id=1, + queue_name="foo", + task_name="test_task", + priority=0, + lock="bar", + args={"a": 1, "b": 2}, + status="todo", + scheduled_at=datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc), + attempts=0, + queueing_lock="baz", + ) + assert job.procrastinate_job == jobs_module.Job( + id=1, + queue="foo", + task_name="test_task", + task_kwargs={"a": 1, "b": 2}, + priority=0, + lock="bar", + status="todo", + scheduled_at=datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc), + attempts=0, + queueing_lock="baz", + ) + + def test_procrastinate_job__no_create(db): with pytest.raises(procrastinate.contrib.django.exceptions.ReadOnlyModel): models.ProcrastinateJob.objects.create(task_name="test_task") diff --git a/tests/integration/contrib/sphinx/__init__.py b/tests/integration/contrib/sphinx/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/contrib/sphinx/conftest.py b/tests/integration/contrib/sphinx/conftest.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/contrib/sphinx/test-root/conf.py b/tests/integration/contrib/sphinx/test-root/conf.py new file mode 100644 index 000000000..abbececb4 --- /dev/null +++ b/tests/integration/contrib/sphinx/test-root/conf.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +extensions = [ + "sphinx.ext.autodoc", + "procrastinate.contrib.sphinx", +] + +buildername = "html" diff --git a/tests/integration/contrib/sphinx/test-root/index.rst b/tests/integration/contrib/sphinx/test-root/index.rst new file mode 100644 index 000000000..0f287a494 --- /dev/null +++ b/tests/integration/contrib/sphinx/test-root/index.rst @@ -0,0 +1,5 @@ +Tasks +===== + +.. automodule:: procrastinate_demos.demo_async.tasks + :members: diff --git a/tests/integration/contrib/sphinx/test_autodoc.py b/tests/integration/contrib/sphinx/test_autodoc.py new file mode 100644 index 000000000..30b191355 --- /dev/null +++ b/tests/integration/contrib/sphinx/test_autodoc.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import pytest +from sphinx.testing.path import path + + +@pytest.fixture +def rootdir(): + return path(__file__).parent + + +@pytest.fixture +def sphinx_app(app_params, make_app): + """ + Provides the 'sphinx.application.Sphinx' object + """ + args, kwargs = app_params + return make_app(*args, **kwargs) + + +@pytest.mark.sphinx(buildername="html", testroot="root") +def test_autodoc_task(sphinx_app): + sphinx_app.build() + content = (sphinx_app.outdir / "index.html").read_text() + # Check that the docstring of one of the task appears in the generated documentation + assert "Sum two numbers." in content diff --git a/tests/unit/contrib/django/test_admin.py b/tests/unit/contrib/django/test_admin.py new file mode 100644 index 000000000..53b25056c --- /dev/null +++ b/tests/unit/contrib/django/test_admin.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from procrastinate import jobs +from procrastinate.contrib.django import admin + + +def test_emoji_mapping(): + assert set(admin.JOB_STATUS_EMOJI_MAPPING) == {e.value for e in jobs.Status} diff --git a/tests/unit/contrib/django/test_django_connector.py b/tests/unit/contrib/django/test_django_connector.py new file mode 100644 index 000000000..40c8e32e3 --- /dev/null +++ b/tests/unit/contrib/django/test_django_connector.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import pytest +from django import db as django_db +from psycopg import errors as psycopg_errors + +from procrastinate import exceptions +from procrastinate.contrib.django import django_connector as django_connector_module + + +def test_wrap_exceptions__no_cause(): + with pytest.raises(django_db.DatabaseError): + with django_connector_module.wrap_exceptions(): + raise django_db.DatabaseError + + +def test_wrap_exceptions__with_cause(): + with pytest.raises(exceptions.ConnectorException): + with django_connector_module.wrap_exceptions(): + raise django_db.DatabaseError from psycopg_errors.Error() diff --git a/tests/unit/test_builtin_tasks.py b/tests/unit/test_builtin_tasks.py index 2679dc980..e25d253c7 100644 --- a/tests/unit/test_builtin_tasks.py +++ b/tests/unit/test_builtin_tasks.py @@ -14,12 +14,18 @@ async def test_remove_old_jobs(app: App, job_factory): max_hours=2, queue="queue_a", remove_error=True, + remove_cancelled=True, + remove_aborted=True, ) connector = cast(InMemoryConnector, app.connector) assert connector.queries == [ ( "delete_old_jobs", - {"nb_hours": 2, "queue": "queue_a", "statuses": ["succeeded", "failed"]}, + { + "nb_hours": 2, + "queue": "queue_a", + "statuses": ["succeeded", "failed", "cancelled", "aborted"], + }, ) ] From 57c990c9a08385731112e5529caaa8bd81dcbd05 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Mon, 19 Aug 2024 18:36:01 +0000 Subject: [PATCH 047/375] Rename remove_error and include_error to remove_failed and include_failed --- docs/howto/production/delete_finished_jobs.md | 2 +- procrastinate/builtin_tasks.py | 6 +++--- procrastinate/manager.py | 6 +++--- tests/integration/test_manager.py | 8 ++++---- tests/unit/test_builtin_tasks.py | 2 +- tests/unit/test_manager.py | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/howto/production/delete_finished_jobs.md b/docs/howto/production/delete_finished_jobs.md index 14a7502a7..eadcedc17 100644 --- a/docs/howto/production/delete_finished_jobs.md +++ b/docs/howto/production/delete_finished_jobs.md @@ -87,7 +87,7 @@ async def remove_old_jobs(context, timestamp): return await builtin_tasks.remove_old_jobs( context, max_hours=72, - remove_error=True, + remove_failed=True, remove_cancelled=True, remove_aborted=True, ) diff --git a/procrastinate/builtin_tasks.py b/procrastinate/builtin_tasks.py index ca7fb3e29..358459af5 100644 --- a/procrastinate/builtin_tasks.py +++ b/procrastinate/builtin_tasks.py @@ -11,7 +11,7 @@ async def remove_old_jobs( *, max_hours: int, queue: str | None = None, - remove_error: bool | None = False, + remove_failed: bool | None = False, remove_cancelled: bool | None = False, remove_aborted: bool | None = False, ) -> None: @@ -26,7 +26,7 @@ async def remove_old_jobs( queue : The name of the queue in which jobs will be deleted. If not specified, the task will delete jobs from all queues. - remove_error: + remove_failed: By default only successful jobs will be removed. When this parameter is True failed jobs will also be deleted. remove_cancelled: @@ -39,7 +39,7 @@ async def remove_old_jobs( await context.app.job_manager.delete_old_jobs( nb_hours=max_hours, queue=queue, - include_error=remove_error, + include_failed=remove_failed, include_cancelled=remove_cancelled, include_aborted=remove_aborted, ) diff --git a/procrastinate/manager.py b/procrastinate/manager.py index d2f4db67d..7fc360d0e 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -177,7 +177,7 @@ async def delete_old_jobs( self, nb_hours: int, queue: str | None = None, - include_error: bool | None = False, + include_failed: bool | None = False, include_cancelled: bool | None = False, include_aborted: bool | None = False, ) -> None: @@ -192,7 +192,7 @@ async def delete_old_jobs( Consider jobs that been in a final state for more than ``nb_hours`` queue: Filter by job queue name - include_error: + include_failed: If ``True``, also consider errored jobs. ``False`` by default include_cancelled: If ``True``, also consider cancelled jobs. ``False`` by default. @@ -201,7 +201,7 @@ async def delete_old_jobs( """ # We only consider finished jobs by default statuses = [jobs.Status.SUCCEEDED.value] - if include_error: + if include_failed: statuses.append(jobs.Status.FAILED.value) if include_cancelled: statuses.append(jobs.Status.CANCELLED.value) diff --git a/tests/integration/test_manager.py b/tests/integration/test_manager.py index 48656bf32..19b07d8db 100644 --- a/tests/integration/test_manager.py +++ b/tests/integration/test_manager.py @@ -198,7 +198,7 @@ async def test_delete_old_jobs_job_doing( @pytest.mark.parametrize( - "status, nb_hours, queue, include_error, expected_job_count", + "status, nb_hours, queue, include_failed, expected_job_count", [ # nb_hours (jobs.Status.SUCCEEDED, 1, None, False, 0), @@ -208,7 +208,7 @@ async def test_delete_old_jobs_job_doing( (jobs.Status.SUCCEEDED, 3, "queue_a", False, 1), (jobs.Status.SUCCEEDED, 1, "queue_b", False, 1), (jobs.Status.SUCCEEDED, 1, "queue_b", False, 1), - # include_error + # include_failed (jobs.Status.FAILED, 1, None, False, 1), (jobs.Status.FAILED, 1, None, True, 0), ], @@ -220,7 +220,7 @@ async def test_delete_old_jobs_parameters( status, nb_hours, queue, - include_error, + include_failed, expected_job_count, fetched_job_factory, ): @@ -236,7 +236,7 @@ async def test_delete_old_jobs_parameters( ) await pg_job_manager.delete_old_jobs( - nb_hours=nb_hours, queue=queue, include_error=include_error + nb_hours=nb_hours, queue=queue, include_failed=include_failed ) jobs_count = len(await get_all("procrastinate_jobs", "id")) assert jobs_count == expected_job_count diff --git a/tests/unit/test_builtin_tasks.py b/tests/unit/test_builtin_tasks.py index e25d253c7..d9d17de65 100644 --- a/tests/unit/test_builtin_tasks.py +++ b/tests/unit/test_builtin_tasks.py @@ -13,7 +13,7 @@ async def test_remove_old_jobs(app: App, job_factory): job_context.JobContext(app=app, job=job), max_hours=2, queue="queue_a", - remove_error=True, + remove_failed=True, remove_cancelled=True, remove_aborted=True, ) diff --git a/tests/unit/test_manager.py b/tests/unit/test_manager.py index 27d296330..ef8c8c32d 100644 --- a/tests/unit/test_manager.py +++ b/tests/unit/test_manager.py @@ -132,14 +132,14 @@ async def test_get_stalled_jobs_stalled(job_manager, job_factory, connector): @pytest.mark.parametrize( - "include_error, statuses", + "include_failed, statuses", [(False, ["succeeded"]), (True, ["succeeded", "failed"])], ) async def test_delete_old_jobs( - job_manager, job_factory, connector, include_error, statuses, mocker + job_manager, job_factory, connector, include_failed, statuses, mocker ): await job_manager.delete_old_jobs( - nb_hours=5, queue="marsupilami", include_error=include_error + nb_hours=5, queue="marsupilami", include_failed=include_failed ) assert connector.queries == [ ( From 2d9d5b337ad07b86a6acaaa1cd14be1de56cd6ad Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Fri, 6 Sep 2024 00:38:16 +0200 Subject: [PATCH 048/375] Fix dev-env: use docker compose instead of docker-compose --- CONTRIBUTING.md | 45 ++++++++++++++++++++++----------------------- dev-env | 2 +- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a7f9171d..219664b6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,13 +13,13 @@ Of course, feel free to read the script before launching it. This script is intended to be a one-liner that sets up everything you need. It makes the following assumptions: -- You're using `MacOS` or `Linux`, and `bash` or `zsh`. -- You already have `python3` available -- You have `poetry` [installed](https://python-poetry.org/docs/#installation) -- Either you've already setup a PostgreSQL database and environment variables (`PG*`) - are set or you have `docker-compose` available and port 5432 is free. -- Either `psql` and other `libpq` executables are available in the `PATH` or they - are located in `usr/local/opt/libpq/bin` (`Homebrew`). +- You're using `MacOS` or `Linux`, and `bash` or `zsh`. +- You already have `python3` available +- You have `poetry` [installed](https://python-poetry.org/docs/#installation) +- Either you've already setup a PostgreSQL database and environment variables (`PG*`) + are set or you have `docker compose` available and port 5432 is free. +- Either `psql` and other `libpq` executables are available in the `PATH` or they + are located in `usr/local/opt/libpq/bin` (`Homebrew`). The `dev-env` script will add the `scripts` folder to your `$PATH` for the current shell, so in the following documentation, if you see `scripts/foo`, you're welcome @@ -46,7 +46,7 @@ The PostgreSQL database we used is a fresh standard out-of-the-box database on the latest stable version. ```console -$ docker-compose up -d postgres +$ docker compose up -d postgres ``` If you want to try out the project locally, it's useful to have `postgresql-client` @@ -129,7 +129,6 @@ In addition, an [editorconfig] file will help your favorite editor to respect procrastinate coding style. It is automatically used by most famous IDEs, such as Pycharm and VS Code. - ### Write the documentation The documentation is written in `Markdown` and built with `Sphinx` and `MyST`. @@ -301,23 +300,23 @@ Python environment on the host system. Alternatively, they can be installed in a image, and Procrastinate and all the development tools can be run in Docker containers. Docker is useful when you can't, or don't want to, install system requirements. -This section shows, through `docker-compose` command examples, how to test and run +This section shows, through `docker compose` command examples, how to test and run Procrastinate in Docker. Build the `procrastinate` Docker image: ```console $ export UID GID -$ docker-compose build procrastinate +$ docker compose build procrastinate ``` Run the automated tests: ```console -$ docker-compose run --rm procrastinate pytest +$ docker compose run --rm procrastinate pytest ``` -Docker Compose is configured (in `docker-compose.yml`) to mount the local directory on +Docker Compose is configured (in `docker compose.yml`) to mount the local directory on the host system onto `/src` in the container. This means that local changes made to the Procrastinate code are visible in Procrastinate containers. @@ -326,7 +325,7 @@ container to be run with the current user id and group id. If not set or exporte Procrastinate container will run as root, and files owned by root may be created in the developer's working directory. -In the definition of the `procrastinate` service in `docker-compose.yml` the +In the definition of the `procrastinate` service in `docker compose.yml` the `PROCRASTINATE_APP` variable is set to `procrastinate_demo.app.app` (the Procrastinate demo application). So `procrastinate` commands run in Procrastinate containers are always run as if they were passed `--app procrastinate_demo.app.app`. @@ -334,55 +333,55 @@ containers are always run as if they were passed `--app procrastinate_demo.app.a Run the `procrastinate` command : ```console -$ docker-compose run --rm procrastinate procrastinate -h +$ docker compose run --rm procrastinate procrastinate -h ``` Apply the Procrastinate database schema: ```console -$ docker-compose run --rm procrastinate procrastinate schema --apply +$ docker compose run --rm procrastinate procrastinate schema --apply ``` Run the Procrastinate healthchecks: ```console -$ docker-compose run --rm procrastinate procrastinate healthchecks +$ docker compose run --rm procrastinate procrastinate healthchecks ``` Start a Procrastinate worker (`-d` used to start the container in detached mode): ```console -$ docker-compose up -d procrastinate +$ docker compose up -d procrastinate ``` Run a command (`bash` here) in the Procrastinate worker container just started: ```console -$ docker-compose exec procrastinate bash +$ docker compose exec procrastinate bash ``` Watch the Procrastinate worker logs: ```console -$ docker-compose logs -ft procrastinate +$ docker compose logs -ft procrastinate ``` Use the `procrastinate defer` command to create a job: ```console -$ docker-compose run --rm procrastinate procrastinate defer procrastinate_demo.tasks.sum '{"a":3, "b": 5}' +$ docker compose run --rm procrastinate procrastinate defer procrastinate_demo.tasks.sum '{"a":3, "b": 5}' ``` Or run the demo main file: ```console -$ docker-compose run --rm procrastinate python -m procrastinate_demo +$ docker compose run --rm procrastinate python -m procrastinate_demo ``` Stop and remove all the containers (including the `postgres` container): ```console -$ docker-compose down +$ docker compose down ``` ## Wait, there are `async` and `await` keywords everywhere!? diff --git a/dev-env b/dev-env index 4543c2ffd..001a86fa0 100755 --- a/dev-env +++ b/dev-env @@ -26,7 +26,7 @@ export GID=$(id -g) if ! pg_isready ; then echo "Starting database" export PGDATABASE=procrastinate PGHOST=127.0.0.1 PGUSER=postgres PGPASSWORD=password - docker-compose up -d postgres || return + docker compose up -d postgres || return sleep 3 fi From 657941e0dec319622c13bc338d150f5afda83cc4 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Fri, 6 Sep 2024 00:44:04 +0200 Subject: [PATCH 049/375] tentative: fix test --- tests/unit/test_worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 030e79a11..24815ce58 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -207,9 +207,11 @@ async def test_worker_run_respects_polling(worker, app): await start_worker(worker) connector = cast(InMemoryConnector, app.connector) + await asyncio.sleep(0.01) + assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 1 - await asyncio.sleep(0.05) + await asyncio.sleep(0.07) assert len([query for query in connector.queries if query[0] == "fetch_job"]) == 2 From 2fd31bbc5f3b915275dd11c34a3605b9db365bec Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 26 Aug 2024 00:15:19 +1000 Subject: [PATCH 050/375] add support for abort notification --- docs/discussions.md | 11 ++- docs/howto/advanced/cancellation.md | 29 ++----- procrastinate/app.py | 31 ++++--- procrastinate/connector.py | 13 ++- .../contrib/aiopg/aiopg_connector.py | 19 ++-- .../contrib/django/django_connector.py | 3 +- .../migrations/0032_cancel_notification.py | 15 ++++ procrastinate/contrib/django/models.py | 1 + procrastinate/job_context.py | 14 +-- procrastinate/jobs.py | 20 ++++- procrastinate/manager.py | 84 ++++++++---------- procrastinate/psycopg_connector.py | 17 ++-- .../03.00.00_01_cancel_notification.sql | 87 +++++++++++++++++++ procrastinate/sql/queries.sql | 14 +-- procrastinate/sql/schema.sql | 64 +++++++++++--- procrastinate/testing.py | 56 +++++++++--- procrastinate/utils.py | 2 +- procrastinate/worker.py | 68 ++++++++++++--- tests/acceptance/test_async.py | 73 +++++++++++----- tests/conftest.py | 4 +- tests/integration/contrib/aiopg/conftest.py | 2 +- .../contrib/aiopg/test_aiopg_connector.py | 42 +++++++-- .../integration/contrib/django/test_models.py | 1 + tests/integration/test_psycopg_connector.py | 36 ++++++-- tests/unit/test_app.py | 1 + tests/unit/test_builtin_tasks.py | 2 +- tests/unit/test_connector.py | 2 +- tests/unit/test_job_context.py | 27 ++---- tests/unit/test_jobs.py | 1 + tests/unit/test_manager.py | 8 +- tests/unit/test_testing.py | 9 +- tests/unit/test_worker.py | 43 ++++++--- tests/unit/test_worker_sync.py | 7 +- 33 files changed, 565 insertions(+), 241 deletions(-) create mode 100644 procrastinate/contrib/django/migrations/0032_cancel_notification.py create mode 100644 procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql diff --git a/docs/discussions.md b/docs/discussions.md index b0310c874..6758e5a55 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -201,14 +201,21 @@ many factors to take into account when [sizing your pool](https://wiki.postgresq ### How the `polling_interval` works -Even when the database doesn't notify workers regarding newly deferred jobs, idle -workers still poll the database every now and then, just in case. +Even when the database doesn't notify workers regarding newly deferred jobs, each worker still poll the database every now and then, just in case. There could be previously locked jobs that are now free, or scheduled jobs that have reached the ETA. `polling_interval` is the {py:meth}`App.run_worker` parameter (or the equivalent CLI flag) that sizes this "every now and then". A worker will keep fetching new jobs as long as they have capacity to process them. The polling interval starts from the moment the last attempt to fetch a new job yields no result. + +The `polling_interval` also defines how often the worker will poll the database for jobs to abort. +When `listen_notify=True`, the worker will likely be notified "instantly" of each abort request prior to polling the database. + +However, in the event `listen_notify=False` or if the abort notification was missed, `polling_interval` will represent the maximum delay before the worker reacts to an abort request. + +Note that the worker will not poll the database for jobs to be aborted if it is idle (i.e. it has no running job). + :::{note} The polling interval was previously called `timeout` in pre-v3 versions of Procrastinate. It was renamed to `polling_interval` for clarity. ::: diff --git a/docs/howto/advanced/cancellation.md b/docs/howto/advanced/cancellation.md index 0743c21e8..d5d924bb7 100644 --- a/docs/howto/advanced/cancellation.md +++ b/docs/howto/advanced/cancellation.md @@ -24,11 +24,10 @@ app.job_manager.cancel_job_by_id(33, delete_job=True) await app.job_manager.cancel_job_by_id_async(33, delete_job=True) ``` -## Mark a currently being processed job for abortion +## Mark a running job for abortion If a worker has not picked up the job yet, the below command behaves like the -command without the `abort` option. But if a job is already in the middle of -being processed, the `abort` option marks this job for abortion (see below +command without the `abort` option. But if a job is already running, the `abort` option marks this job for abortion (see below how to handle this request). ```python @@ -38,10 +37,10 @@ app.job_manager.cancel_job_by_id(33, abort=True) await app.job_manager.cancel_job_by_id_async(33, abort=True) ``` -## Handle a abortion request inside the task +## Handle an abortion request inside the task In our task, we can check (for example, periodically) if the task should be -aborted. If we want to respect that request (we don't have to), we raise a +aborted. If we want to respect that abortion request (we don't have to), we raise a `JobAborted` error. Any message passed to `JobAborted` (e.g. `raise JobAborted("custom message")`) will end up in the logs. @@ -54,24 +53,10 @@ def my_task(context): do_something_expensive() ``` -There is also an async API +Behind the scenes, the worker receives a Postgres notification every time a job is requested to abort, (unless `listen_notify=False`). -```python -@app.task(pass_context=True) -async def my_task(context): - for i in range(100): - if await context.should_abort_async(): - raise exceptions.JobAborted - do_something_expensive() -``` - -:::{warning} -`context.should_abort()` and `context.should_abort_async()` does poll the -database and might flood the database. Ensure you do it only sometimes and -not from too many parallel tasks. -::: +The worker also polls (respecting `polling_interval`) the database for abortion requests, as long as the worker is running at least one job (in the absence of running job, there is nothing to abort). :::{note} -When a task of a job that was requested to be aborted raises an error, the job -is marked as failed (regardless of the retry strategy). +When a job is requested to abort and that job fails, it will not be retried (regardless of the retry strategy). ::: diff --git a/procrastinate/app.py b/procrastinate/app.py index 711006574..3eaaeff73 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -270,22 +270,33 @@ async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: Name of the worker. Will be passed in the `JobContext` and used in the logs (defaults to ``None`` which will result in the worker named ``worker``). - polling_interval: ``float`` - Indicates the maximum duration (in seconds) the worker waits between - each database job poll. Raising this parameter can lower the rate at which - the worker makes queries to the database for requesting jobs. + polling_interval : ``float`` + Maximum time (in seconds) between database job polls. + + Controls the frequency of database queries for: + - Checking for new jobs to start + - Fetching updates for running jobs + - Checking for abort requests + + When `listen_notify` is True, the polling interval acts as a fallback + mechanism and can reasonably be set to a higher value. + (defaults to 5.0) shutdown_timeout: ``float`` Indicates the maximum duration (in seconds) the worker waits for jobs to complete when requested stop. Jobs that have not been completed by that time are aborted. A value of None corresponds to no timeout. (defaults to None) - listen_notify: ``bool`` - If ``True``, the worker will dedicate a connection from the pool to - listening to database events, notifying of newly available jobs. - If ``False``, the worker will just poll the database periodically - (see ``polling_interval``). (defaults to ``True``) - delete_jobs: ``str`` + listen_notify : ``bool`` + If ``True``, allocates a connection from the pool to + listen for: + - new job availability + - job abort requests + + Provides lower latency for job updates compared to polling alone. + + Note: Worker polls the database regardless of this setting. (defaults to ``True``) + delete_jobs : ``str`` If ``always``, the worker will automatically delete all jobs on completion. If ``successful`` the worker will only delete successful jobs. If ``never``, the worker will keep the jobs in the database. diff --git a/procrastinate/connector.py b/procrastinate/connector.py index 50615d2b4..541a772a9 100644 --- a/procrastinate/connector.py +++ b/procrastinate/connector.py @@ -1,7 +1,6 @@ from __future__ import annotations -import asyncio -from typing import Any, Callable, Iterable +from typing import Any, Awaitable, Callable, Iterable, Protocol from typing_extensions import LiteralString @@ -13,6 +12,10 @@ LISTEN_TIMEOUT = 30.0 +class Notify(Protocol): + def __call__(self, *, channel: str, payload: str) -> Awaitable[None]: ... + + class BaseConnector: json_dumps: Callable | None = None json_loads: Callable | None = None @@ -59,7 +62,9 @@ async def execute_query_all_async( raise exceptions.SyncConnectorConfigurationError async def listen_notify( - self, event: asyncio.Event, channels: Iterable[str] + self, + on_notification: Notify, + channels: Iterable[str], ) -> None: raise exceptions.SyncConnectorConfigurationError @@ -98,6 +103,6 @@ def execute_query_all( return utils.async_to_sync(self.execute_query_all_async, query, **arguments) async def listen_notify( - self, event: asyncio.Event, channels: Iterable[str] + self, on_notification: Notify, channels: Iterable[str] ) -> None: raise NotImplementedError diff --git a/procrastinate/contrib/aiopg/aiopg_connector.py b/procrastinate/contrib/aiopg/aiopg_connector.py index c3ef93976..962ff46fa 100644 --- a/procrastinate/contrib/aiopg/aiopg_connector.py +++ b/procrastinate/contrib/aiopg/aiopg_connector.py @@ -283,7 +283,7 @@ def _make_dynamic_query(self, query: str, **identifiers: str) -> Any: @wrap_exceptions() async def listen_notify( - self, event: asyncio.Event, channels: Iterable[str] + self, on_notification: connector.Notify, channels: Iterable[str] ) -> None: # We need to acquire a dedicated connection, and use the listen # query @@ -304,14 +304,14 @@ async def listen_notify( query=sql.queries["listen_queue"], channel_name=channel_name ), ) - # Initial set() lets caller know that we're ready to listen - event.set() - await self._loop_notify(event=event, connection=connection) + await self._loop_notify( + on_notification=on_notification, connection=connection + ) @wrap_exceptions() async def _loop_notify( self, - event: asyncio.Event, + on_notification: connector.Notify, connection: aiopg.Connection, timeout: float = connector.LISTEN_TIMEOUT, ) -> None: @@ -324,12 +324,15 @@ async def _loop_notify( if connection.closed: return try: - await asyncio.wait_for(connection.notifies.get(), timeout) + notification = await asyncio.wait_for( + connection.notifies.get(), timeout + ) + await on_notification( + channel=notification.channel, payload=notification.payload + ) except asyncio.TimeoutError: continue except psycopg2.Error: # aiopg>=1.3.1 will raise if the connection is closed while # we wait continue - - event.set() diff --git a/procrastinate/contrib/django/django_connector.py b/procrastinate/contrib/django/django_connector.py index 909c7ac8b..8f178c379 100644 --- a/procrastinate/contrib/django/django_connector.py +++ b/procrastinate/contrib/django/django_connector.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import contextlib from typing import ( TYPE_CHECKING, @@ -141,7 +140,7 @@ def execute_query_all( return list(self._dictfetch(cursor)) async def listen_notify( - self, event: asyncio.Event, channels: Iterable[str] + self, on_notification: connector.Notify, channels: Iterable[str] ) -> None: raise NotImplementedError( "listen/notify is not supported with Django connector" diff --git a/procrastinate/contrib/django/migrations/0032_cancel_notification.py b/procrastinate/contrib/django/migrations/0032_cancel_notification.py new file mode 100644 index 000000000..617265857 --- /dev/null +++ b/procrastinate/contrib/django/migrations/0032_cancel_notification.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from django.db import migrations + +from .. import migrations_utils + + +class Migration(migrations.Migration): + operations = [ + migrations_utils.RunProcrastinateSQL( + name="03.00.00_01_cancel_notification.sql" + ), + ] + name = "0032_cancel_notification" + dependencies = [("procrastinate", "0031_add_abort_on_procrastinate_jobs")] diff --git a/procrastinate/contrib/django/models.py b/procrastinate/contrib/django/models.py index 9ae4bd10c..64ce3c319 100644 --- a/procrastinate/contrib/django/models.py +++ b/procrastinate/contrib/django/models.py @@ -99,6 +99,7 @@ def procrastinate_job(self) -> jobs.Job: status=self.status, scheduled_at=self.scheduled_at, attempts=self.attempts, + abort_requested=self.abort_requested, queueing_lock=self.queueing_lock, ) diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index 38abc4ec3..d03a10304 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -1,7 +1,7 @@ from __future__ import annotations import time -from typing import Any, Iterable +from typing import Any, Callable, Iterable import attr @@ -54,6 +54,8 @@ class JobContext: additional_context: dict = attr.ib(factory=dict) task_result: Any = None + should_abort: Callable[[], bool] + def evolve(self, **update: Any) -> JobContext: return attr.evolve(self, **update) @@ -68,13 +70,3 @@ def job_description(self, current_timestamp: float) -> str: message += f" (started {duration:.3f} s ago)" return message - - def should_abort(self) -> bool: - assert self.job.id - job_id = self.job.id - return self.app.job_manager.get_job_abort_requested(job_id) - - async def should_abort_async(self) -> bool: - assert self.job.id - job_id = self.job.id - return await self.app.job_manager.get_job_abort_requested_async(job_id) diff --git a/procrastinate/jobs.py b/procrastinate/jobs.py index 1127c370f..5c16d67ac 100644 --- a/procrastinate/jobs.py +++ b/procrastinate/jobs.py @@ -4,9 +4,10 @@ import functools import logging from enum import Enum -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict, Union import attr +from typing_extensions import Literal from procrastinate import types @@ -22,6 +23,19 @@ cached_property = getattr(functools, "cached_property", property) +class JobInserted(TypedDict): + type: Literal["job_inserted"] + job_id: int + + +class AbortJobRequested(TypedDict): + type: Literal["abort_job_requested"] + job_id: int + + +Notification = Union[JobInserted, AbortJobRequested] + + def check_aware( instance: Job, attribute: attr.Attribute, value: datetime.datetime ) -> None: @@ -82,6 +96,9 @@ class Job: #: Number of times the job has been tried. attempts: int = 0 + # True if the job is requested to abort + abort_requested: bool = False + @classmethod def from_row(cls, row: dict[str, Any]) -> Job: return cls( @@ -95,6 +112,7 @@ def from_row(cls, row: dict[str, Any]) -> Job: scheduled_at=row["scheduled_at"], queue=row["queue_name"], attempts=row["attempts"], + abort_requested=row.get("abort_requested", False), ) def asdict(self) -> dict[str, Any]: diff --git a/procrastinate/manager.py b/procrastinate/manager.py index 7fc360d0e..a80c3bf75 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -1,9 +1,9 @@ from __future__ import annotations -import asyncio import datetime +import json import logging -from typing import Any, Iterable, NoReturn +from typing import Any, Awaitable, Iterable, NoReturn, Protocol from procrastinate import connector, exceptions, jobs, sql, utils @@ -12,6 +12,12 @@ QUEUEING_LOCK_CONSTRAINT = "procrastinate_jobs_queueing_lock_idx" +class NotificationCallback(Protocol): + def __call__( + self, *, channel: str, notification: jobs.Notification + ) -> Awaitable[None]: ... + + def get_channel_for_queues(queues: Iterable[str] | None = None) -> Iterable[str]: if queues is None: return ["procrastinate_any_queue"] @@ -360,42 +366,6 @@ async def get_job_status_async(self, job_id: int) -> jobs.Status: ) return jobs.Status(result["status"]) - def get_job_abort_requested(self, job_id: int) -> bool: - """ - Check if a job is requested for abortion. - - Parameters - ---------- - job_id : ``int`` - The id of the job to get the abortion request of - - Returns - ------- - ``bool`` - """ - result = self.connector.get_sync_connector().execute_query_one( - query=sql.queries["get_job_abort_requested"], job_id=job_id - ) - return bool(result["abort_requested"]) - - async def get_job_abort_requested_async(self, job_id: int) -> bool: - """ - Check if a job is requested for abortion. - - Parameters - ---------- - job_id : ``int`` - The id of the job to get the abortion request of - - Returns - ------- - ``bool`` - """ - result = await self.connector.execute_query_one_async( - query=sql.queries["get_job_abort_requested"], job_id=job_id - ) - return bool(result["abort_requested"]) - async def retry_job( self, job: jobs.Job, @@ -491,26 +461,39 @@ def retry_job_by_id( ) async def listen_for_jobs( - self, *, event: asyncio.Event, queues: Iterable[str] | None = None + self, + *, + on_notification: NotificationCallback, + queues: Iterable[str] | None = None, ) -> None: """ - Listens to defer operation in the database, and raises the event each time an - defer operation is seen. + Listens to job notifications from the database, and invokes the callback each time an + notification is received. This coroutine either returns ``None`` upon calling if it cannot start listening or does not return and needs to be cancelled to end. Parameters ---------- - event: - This event will be set each time a defer operation occurs - queues: - If ``None``, all defer operations will be considered. If an iterable of + on_notification : ``connector.Notify`` + A coroutine that will be called and awaited every time a notification is received + queues : ``Optional[Iterable[str]]`` + If ``None``, all notification will be considered. If an iterable of queue names is passed, only defer operations on those queues will be considered. Defaults to ``None`` """ + + async def handle_notification(channel: str, payload: str): + notification: jobs.Notification = json.loads(payload) + logger.debug( + f"Received {notification['type']} notification from channel", + extra={channel: channel, payload: payload}, + ) + await on_notification(channel=channel, notification=notification) + await self.connector.listen_notify( - event=event, channels=get_channel_for_queues(queues=queues) + on_notification=handle_notification, + channels=get_channel_for_queues(queues=queues), ) async def check_connection_async(self) -> bool: @@ -836,3 +819,12 @@ def list_locks( } ) return result + + async def list_jobs_to_abort_async(self, queue: str | None = None) -> Iterable[int]: + """ + List ids of running jobs to abort + """ + rows = await self.connector.execute_query_all_async( + query=sql.queries["list_jobs_to_abort"], queue_name=queue + ) + return [row["id"] for row in rows] diff --git a/procrastinate/psycopg_connector.py b/procrastinate/psycopg_connector.py index 90a07fd28..f2e0dd2f1 100644 --- a/procrastinate/psycopg_connector.py +++ b/procrastinate/psycopg_connector.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import contextlib import logging from typing import ( @@ -249,7 +248,7 @@ async def _get_standalone_connection( @wrap_exceptions() async def listen_notify( - self, event: asyncio.Event, channels: Iterable[str] + self, on_notification: connector.Notify, channels: Iterable[str] ) -> None: while True: async with self._get_standalone_connection() as connection: @@ -260,14 +259,14 @@ async def listen_notify( channel_name=channel_name, ), ) - # Initial set() lets caller know that we're ready to listen - event.set() - await self._loop_notify(event=event, connection=connection) + await self._loop_notify( + on_notification=on_notification, connection=connection + ) @wrap_exceptions() async def _loop_notify( self, - event: asyncio.Event, + on_notification: connector.Notify, connection: psycopg.AsyncConnection, timeout: float = connector.LISTEN_TIMEOUT, ) -> None: @@ -275,12 +274,14 @@ async def _loop_notify( while True: try: - async for _ in utils.gen_with_timeout( + async for notification in utils.gen_with_timeout( aiterable=connection.notifies(), timeout=timeout, raise_timeout=False, ): - event.set() + await on_notification( + channel=notification.channel, payload=notification.payload + ) await connection.execute("SELECT 1") except psycopg.OperationalError: diff --git a/procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql b/procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql new file mode 100644 index 000000000..c2925b1f7 --- /dev/null +++ b/procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql @@ -0,0 +1,87 @@ +CREATE OR REPLACE FUNCTION procrastinate_notify_queue_job_inserted() +RETURNS trigger + LANGUAGE plpgsql +AS $$ +DECLARE + payload TEXT; +BEGIN + SELECT json_object('type': 'job_inserted', 'job_id': NEW.id)::text INTO payload; + PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue', payload); + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS procrastinate_jobs_notify_queue ON procrastinate_jobs; + +CREATE TRIGGER procrastinate_jobs_notify_queue_job_inserted + AFTER INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue_job_inserted(); + +DROP FUNCTION IF EXISTS procrastinate_notify_queue; + +CREATE OR REPLACE FUNCTION procrastinate_notify_queue_abort_job() +RETURNS trigger + LANGUAGE plpgsql +AS $$ +DECLARE + payload TEXT; +BEGIN + SELECT json_object('type': 'abort_job_requested', 'job_id': NEW.id)::text INTO payload; + PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue', payload); + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_jobs_notify_queue_abort_job + AFTER UPDATE OF abort_requested ON procrastinate_jobs + FOR EACH ROW WHEN ((old.abort_requested = false AND new.abort_requested = true AND new.status = 'doing'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue_abort_job(); + +CREATE OR REPLACE FUNCTION procrastinate_retry_job( + job_id bigint, + retry_at timestamp with time zone, + new_priority integer, + new_queue_name character varying, + new_lock character varying +) + RETURNS void + LANGUAGE plpgsql +AS $$ +DECLARE + _job_id bigint; +BEGIN + UPDATE procrastinate_jobs + SET status = CASE + WHEN NOT abort_requested THEN 'todo'::procrastinate_job_status + ELSE 'failed'::procrastinate_job_status + END, + attempts = CASE + WHEN NOT abort_requested THEN attempts + 1 + ELSE attempts + END, + scheduled_at = CASE + WHEN NOT abort_requested THEN retry_at + ELSE scheduled_at + END, + priority = CASE + WHEN NOT abort_requested THEN COALESCE(new_priority, priority) + ELSE priority + END, + queue_name = CASE + WHEN NOT abort_requested THEN COALESCE(new_queue_name, queue_name) + ELSE queue_name + END, + lock = CASE + WHEN NOT abort_requested THEN COALESCE(new_lock, lock) + ELSE lock + END + WHERE id = job_id AND status = 'doing' + RETURNING id INTO _job_id; + IF _job_id IS NULL THEN + RAISE 'Job was not found or not in "doing" status (job id: %)', job_id; + END IF; +END; +$$; diff --git a/procrastinate/sql/queries.sql b/procrastinate/sql/queries.sql index ce4908f7d..fd851f152 100644 --- a/procrastinate/sql/queries.sql +++ b/procrastinate/sql/queries.sql @@ -58,10 +58,6 @@ SELECT procrastinate_cancel_job(%(job_id)s, %(abort)s, %(delete_job)s) AS id; -- Get the status of a job SELECT status FROM procrastinate_jobs WHERE id = %(job_id)s; --- get_job_abort_requested -- --- Check if an abortion of a job was requested -SELECT abort_requested FROM procrastinate_jobs WHERE id = %(job_id)s; - -- retry_job -- -- Retry a job, changing it from "doing" to "todo" SELECT procrastinate_retry_job(%(job_id)s, %(retry_at)s, %(new_priority)s, %(new_queue_name)s, %(new_lock)s); @@ -89,7 +85,8 @@ SELECT id, args, status, scheduled_at, - attempts + attempts, + abort_requested FROM procrastinate_jobs WHERE (%(id)s::bigint IS NULL OR id = %(id)s) AND (%(queue_name)s::varchar IS NULL OR queue_name = %(queue_name)s) @@ -191,3 +188,10 @@ SELECT FROM locks GROUP BY name ORDER BY name; + +-- list_jobs_to_abort -- +-- Get list of running jobs that are requested to be aborted +SELECT id from procrastinate_jobs +WHERE status = 'doing' +AND abort_requested = true +AND (%(queue_name)s::varchar IS NULL OR queue_name = %(queue_name)s) diff --git a/procrastinate/sql/schema.sql b/procrastinate/sql/schema.sql index c75b67ba6..d7ee49cd0 100644 --- a/procrastinate/sql/schema.sql +++ b/procrastinate/sql/schema.sql @@ -269,12 +269,30 @@ DECLARE _job_id bigint; BEGIN UPDATE procrastinate_jobs - SET status = 'todo', - attempts = attempts + 1, - scheduled_at = retry_at, - priority = COALESCE(new_priority, priority), - queue_name = COALESCE(new_queue_name, queue_name), - lock = COALESCE(new_lock, lock) + SET status = CASE + WHEN NOT abort_requested THEN 'todo'::procrastinate_job_status + ELSE 'failed'::procrastinate_job_status + END, + attempts = CASE + WHEN NOT abort_requested THEN attempts + 1 + ELSE attempts + END, + scheduled_at = CASE + WHEN NOT abort_requested THEN retry_at + ELSE scheduled_at + END, + priority = CASE + WHEN NOT abort_requested THEN COALESCE(new_priority, priority) + ELSE priority + END, + queue_name = CASE + WHEN NOT abort_requested THEN COALESCE(new_queue_name, queue_name) + ELSE queue_name + END, + lock = CASE + WHEN NOT abort_requested THEN COALESCE(new_lock, lock) + ELSE lock + END WHERE id = job_id AND status = 'doing' RETURNING id INTO _job_id; IF _job_id IS NULL THEN @@ -283,13 +301,30 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_notify_queue() +CREATE FUNCTION procrastinate_notify_queue_job_inserted() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE + payload TEXT; BEGIN - PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, NEW.task_name); - PERFORM pg_notify('procrastinate_any_queue', NEW.task_name); + SELECT json_object('type': 'job_inserted', 'job_id': NEW.id)::text INTO payload; + PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue', payload); + RETURN NEW; +END; +$$; + +CREATE FUNCTION procrastinate_notify_queue_abort_job() +RETURNS trigger + LANGUAGE plpgsql +AS $$ +DECLARE + payload TEXT; +BEGIN + SELECT json_object('type': 'abort_job_requested', 'job_id': NEW.id)::text INTO payload; + PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue', payload); RETURN NEW; END; $$; @@ -382,10 +417,15 @@ $$; -- Triggers -CREATE TRIGGER procrastinate_jobs_notify_queue +CREATE TRIGGER procrastinate_jobs_notify_queue_job_inserted AFTER INSERT ON procrastinate_jobs FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_notify_queue(); + EXECUTE PROCEDURE procrastinate_notify_queue_job_inserted(); + +CREATE TRIGGER procrastinate_jobs_notify_queue_abort_job + AFTER UPDATE OF abort_requested ON procrastinate_jobs + FOR EACH ROW WHEN ((old.abort_requested = false AND new.abort_requested = true AND new.status = 'doing'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue_abort_job(); CREATE TRIGGER procrastinate_trigger_status_events_update AFTER UPDATE OF status ON procrastinate_jobs @@ -440,7 +480,7 @@ $$; -- procrastinate_finish_job -- the next_scheduled_at argument is kept for compatibility reasons -CREATE OR REPLACE FUNCTION procrastinate_finish_job(job_id integer, end_status procrastinate_job_status, next_scheduled_at timestamp with time zone, delete_job boolean) +CREATE FUNCTION procrastinate_finish_job(job_id integer, end_status procrastinate_job_status, next_scheduled_at timestamp with time zone, delete_job boolean) RETURNS void LANGUAGE plpgsql AS $$ diff --git a/procrastinate/testing.py b/procrastinate/testing.py index db80f4aa6..283474efb 100644 --- a/procrastinate/testing.py +++ b/procrastinate/testing.py @@ -1,12 +1,12 @@ from __future__ import annotations -import asyncio import datetime +import json from collections import Counter from itertools import count from typing import Any, Dict, Iterable -from procrastinate import connector, exceptions, schema, sql, types, utils +from procrastinate import connector, exceptions, jobs, schema, sql, types, utils JobRow = Dict[str, Any] EventRow = Dict[str, Any] @@ -37,7 +37,7 @@ def reset(self) -> None: self.events: dict[int, list[EventRow]] = {} self.job_counter = count(1) self.queries: list[tuple[str, dict[str, Any]]] = [] - self.notify_event: asyncio.Event | None = None + self.on_notification: connector.Notify | None = None self.notify_channels: list[str] = [] self.periodic_defers: dict[tuple[str, str], int] = {} self.table_exists = True @@ -73,9 +73,9 @@ async def execute_query_all_async( return await self.generic_execute(query, "all", **arguments) async def listen_notify( - self, event: asyncio.Event, channels: Iterable[str] + self, on_notification: connector.Notify, channels: Iterable[str] ) -> None: - self.notify_event = event + self.on_notification = on_notification self.notify_channels = list(channels) def open(self, pool: connector.Pool | None = None) -> None: @@ -131,11 +131,14 @@ async def defer_job_one( if scheduled_at: self.events[id].append({"type": "scheduled", "at": scheduled_at}) self.events[id].append({"type": "deferred", "at": utils.utcnow()}) - if self.notify_event: - if "procrastinate_any_queue" in self.notify_channels or ( - f"procrastinate_queue#{queue}" in self.notify_channels - ): - self.notify_event.set() + + await self._notify( + queue, + { + "type": "job_inserted", + "job_id": id, + }, + ) return job_row async def defer_periodic_job_one( @@ -178,6 +181,21 @@ def finished_jobs(self) -> list[JobRow]: if job["status"] in {"failed", "succeeded"} ] + async def _notify(self, queue_name: str, notification: jobs.Notification): + if not self.on_notification: + return + + destination_channels = { + "procrastinate_any_queue", + f"procrastinate_queue#{queue_name}", + } + + for channel in set(self.notify_channels).intersection(destination_channels): + await self.on_notification( + channel=channel, + payload=json.dumps(notification), + ) + async def fetch_job_one(self, queues: Iterable[str] | None) -> dict: # Creating a copy of the iterable so that we can modify it while we iterate @@ -226,6 +244,14 @@ async def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> di if abort: job_row["abort_requested"] = True + await self._notify( + job_row["queue_name"], + { + "type": "abort_job_requested", + "job_id": job_id, + }, + ) + return {"id": job_id} return {"id": None} @@ -233,9 +259,6 @@ async def cancel_job_one(self, job_id: int, abort: bool, delete_job: bool) -> di async def get_job_status_one(self, job_id: int) -> dict: return {"status": self.jobs[job_id]["status"]} - async def get_job_abort_requested_one(self, job_id: int) -> dict: - return {"abort_requested": self.jobs[job_id]["abort_requested"]} - async def retry_job_run( self, job_id: int, @@ -328,6 +351,13 @@ async def list_locks_all(self, **kwargs): result.append({"name": lock, "jobs_count": len(lock_jobs), "stats": stats}) return result + async def list_jobs_to_abort_all(self, queue_name: str | None): + return list( + await self.list_jobs_all( + status="doing", abort_requested=True, queue_name=queue_name + ) + ) + async def set_job_status_run(self, id, status): id = int(id) self.jobs[id]["status"] = status diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 3eac3e770..4a3422ddb 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -229,7 +229,7 @@ def log_task_exception(task: asyncio.Task, error: BaseException): if error: log_task_exception(task, error=error) else: - logger.debug(f"Cancelled task ${task.get_name()}") + logger.debug(f"Cancelled task {task.get_name()}") async def wait_any(*coros_or_futures: Coroutine | asyncio.Future): diff --git a/procrastinate/worker.py b/procrastinate/worker.py index f9c7bb658..6c4fd2d86 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -64,11 +64,12 @@ def __init__( self.logger = logger self._loop_task: asyncio.Future | None = None - self._notify_event = asyncio.Event() + self._new_job_event = asyncio.Event() self._running_jobs: dict[asyncio.Task, job_context.JobContext] = {} self._job_semaphore = asyncio.Semaphore(self.concurrency) self._stop_event = asyncio.Event() self.shutdown_timeout = shutdown_timeout + self._job_ids_to_abort = set() def stop(self): if self._stop_event.is_set(): @@ -80,7 +81,7 @@ def stop(self): self._stop_event.set() - async def periodic_deferrer(self): + async def _periodic_deferrer(self): deferrer = periodic.PeriodicDeferrer( registry=self.app.periodic_registry, **self.app.periodic_defaults, @@ -224,12 +225,7 @@ async def ensure_async() -> Callable[..., Awaitable]: except BaseException as e: exc_info = e - assert job.id - abort_requested = await self.app.job_manager.get_job_abort_requested_async( - job_id=job.id - ) - - if not isinstance(e, exceptions.JobAborted) and not abort_requested: + if not isinstance(e, exceptions.JobAborted): job_retry = ( task.get_retry_exception(exception=e, job=job) if task else None ) @@ -265,6 +261,8 @@ async def ensure_async() -> Callable[..., Awaitable]: job=job, status=status, retry_decision=retry_decision ) + self._job_ids_to_abort.discard(job.id) + self.logger.debug( f"Acknowledged job completion {job.call_string}", extra=self._log_extra( @@ -285,11 +283,14 @@ async def _fetch_and_process_jobs(self): finally: if (not job or self._stop_event.is_set()) and acquire_sem_task.done(): self._job_semaphore.release() - self._notify_event.clear() + self._new_job_event.clear() if not job: break + job_id = job.id + assert job_id + context = job_context.JobContext( app=self.app, worker_name=self.worker_name, @@ -299,6 +300,7 @@ async def _fetch_and_process_jobs(self): else {}, job=job, task=self.app.tasks.get(job.task_name), + should_abort=lambda: job_id in self._job_ids_to_abort, ) job_task = asyncio.create_task( self._process_job(context), @@ -338,6 +340,41 @@ async def run(self): pass raise + async def _handle_notification( + self, *, channel: str, notification: jobs.Notification + ): + if notification["type"] == "job_inserted": + self._new_job_event.set() + elif notification["type"] == "abort_job_requested": + self._handle_abort_jobs_requested([notification["job_id"]]) + + async def _poll_jobs_to_abort(self): + while True: + logger.debug( + f"waiting for {self.polling_interval}s before querying jobs to abort" + ) + await asyncio.sleep(self.polling_interval) + if not self._running_jobs: + logger.debug("Not querying jobs to abort because no job is running") + continue + try: + job_ids = await self.app.job_manager.list_jobs_to_abort_async() + self._handle_abort_jobs_requested(job_ids) + except Exception as error: + logger.exception( + f"poll_jobs_to_abort error: {error!r}", + exc_info=error, + extra={ + "action": "poll_jobs_to_abort_error", + }, + ) + # recover from errors and continue polling + + def _handle_abort_jobs_requested(self, job_ids: Iterable[int]): + running_job_ids = {c.job.id for c in self._running_jobs.values() if c.job.id} + self._job_ids_to_abort |= set(job_ids) + self._job_ids_to_abort &= running_job_ids + async def _shutdown(self, side_tasks: list[asyncio.Task]): """ Gracefully shutdown the worker by cancelling side tasks @@ -365,10 +402,13 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): def _start_side_tasks(self) -> list[asyncio.Task]: """Start side tasks such as periodic deferrer and notification listener""" - side_tasks = [asyncio.create_task(self.periodic_deferrer(), name="deferrer")] - if self.wait and self.listen_notify: + side_tasks = [ + asyncio.create_task(self._periodic_deferrer(), name="deferrer"), + asyncio.create_task(self._poll_jobs_to_abort(), name="poll_jobs_to_abort"), + ] + if self.listen_notify: listener_coro = self.app.job_manager.listen_for_jobs( - event=self._notify_event, + on_notification=self._handle_notification, queues=self.queues, ) side_tasks.append(asyncio.create_task(listener_coro, name="listener")) @@ -384,7 +424,7 @@ async def _run_loop(self): action="start_worker", context=None, queues=self.queues ), ) - self._notify_event.clear() + self._new_job_event.clear() self._stop_event.clear() self._running_jobs = {} self._job_semaphore = asyncio.Semaphore(self.concurrency) @@ -413,7 +453,7 @@ async def _run_loop(self): while not self._stop_event.is_set(): # wait for a new job notification, a stop even or the next polling interval await utils.wait_any( - self._notify_event.wait(), + self._new_job_event.wait(), asyncio.sleep(self.polling_interval), self._stop_event.wait(), ) diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index 70f60cfdf..ce4c9109a 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -108,47 +108,76 @@ def example_task(): assert len(jobs) == 1 -async def test_abort(async_app: app_module.App): +@pytest.mark.parametrize("mode", ["listen", "poll"]) +async def test_abort_async_task(async_app: app_module.App, mode): @async_app.task(queue="default", name="task1", pass_context=True) async def task1(context): while True: await asyncio.sleep(0.02) - if await context.should_abort_async(): + if context.should_abort(): raise JobAborted - @async_app.task(queue="default", name="task2", pass_context=True) - def task2(context): + job_id = await task1.defer_async() + + polling_interval = 0.1 + + worker_task = asyncio.create_task( + async_app.run_worker_async( + queues=["default"], + wait=False, + polling_interval=polling_interval, + listen_notify=True if mode == "listen" else False, + ) + ) + + await asyncio.sleep(0.05) + result = await async_app.job_manager.cancel_job_by_id_async(job_id, abort=True) + assert result is True + + # when listening for notifications, job should cancel within ms + # if notifications are disabled, job will only cancel after polling_interval + await asyncio.wait_for( + worker_task, timeout=0.1 if mode == "listen" else polling_interval * 2 + ) + + status = await async_app.job_manager.get_job_status_async(job_id) + assert status == Status.ABORTED + + +@pytest.mark.parametrize("mode", ["listen", "poll"]) +async def test_abort_sync_task(async_app: app_module.App, mode): + @async_app.task(queue="default", name="task1", pass_context=True) + def task1(context): while True: time.sleep(0.02) if context.should_abort(): raise JobAborted - job1_id = await task1.defer_async() - job2_id = await task2.defer_async() + job_id = await task1.defer_async() + + polling_interval = 0.1 worker_task = asyncio.create_task( - async_app.run_worker_async(queues=["default"], wait=False) + async_app.run_worker_async( + queues=["default"], + wait=False, + polling_interval=polling_interval, + listen_notify=True if mode == "listen" else False, + ) ) - await asyncio.sleep(0.1) - result = await async_app.job_manager.cancel_job_by_id_async(job1_id, abort=True) - assert result is True - - await asyncio.sleep(0.1) - result = await async_app.job_manager.cancel_job_by_id_async(job2_id, abort=True) + await asyncio.sleep(0.05) + result = await async_app.job_manager.cancel_job_by_id_async(job_id, abort=True) assert result is True - await worker_task - - status = await async_app.job_manager.get_job_status_async(job1_id) - assert status == Status.ABORTED - abort_requested = await async_app.job_manager.get_job_abort_requested_async(job1_id) - assert abort_requested is False + # when listening for notifications, job should cancel within ms + # if notifications are disabled, job will only cancel after polling_interval + await asyncio.wait_for( + worker_task, timeout=0.1 if mode == "listen" else polling_interval * 2 + ) - status = await async_app.job_manager.get_job_status_async(job2_id) + status = await async_app.job_manager.get_job_status_async(job_id) assert status == Status.ABORTED - abort_requested = await async_app.job_manager.get_job_abort_requested_async(job2_id) - assert abort_requested is False async def test_concurrency(async_app: app_module.App): diff --git a/tests/conftest.py b/tests/conftest.py index 9e190a1b6..e8bbf4086 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -126,7 +126,9 @@ def not_opened_sync_psycopg_connector(psycopg_connection_params): @pytest.fixture -async def psycopg_connector(not_opened_psycopg_connector): +async def psycopg_connector( + not_opened_psycopg_connector: async_psycopg_connector_module.PsycopgConnector, +): await not_opened_psycopg_connector.open_async() yield not_opened_psycopg_connector await not_opened_psycopg_connector.close_async() diff --git a/tests/integration/contrib/aiopg/conftest.py b/tests/integration/contrib/aiopg/conftest.py index 353b773c5..701b6379f 100644 --- a/tests/integration/contrib/aiopg/conftest.py +++ b/tests/integration/contrib/aiopg/conftest.py @@ -27,5 +27,5 @@ async def _(*, open: bool = True, **kwargs): @pytest.fixture -async def aiopg_connector(aiopg_connector_factory): +async def aiopg_connector(aiopg_connector_factory) -> aiopg.AiopgConnector: return await aiopg_connector_factory() diff --git a/tests/integration/contrib/aiopg/test_aiopg_connector.py b/tests/integration/contrib/aiopg/test_aiopg_connector.py index ad7adf745..b413bdb0b 100644 --- a/tests/integration/contrib/aiopg/test_aiopg_connector.py +++ b/tests/integration/contrib/aiopg/test_aiopg_connector.py @@ -156,15 +156,26 @@ async def test_get_connection_no_psycopg2_adapter_registration( async def test_listen_notify(aiopg_connector): channel = "somechannel" event = asyncio.Event() + received_args: list[dict] = [] + + async def handle_notification(*, channel: str, payload: str): + event.set() + received_args.append({"channel": channel, "payload": payload}) task = asyncio.ensure_future( - aiopg_connector.listen_notify(channels=[channel], event=event) + aiopg_connector.listen_notify( + channels=[channel], on_notification=handle_notification + ) ) try: - await event.wait() - event.clear() - await aiopg_connector.execute_query_async(f"""NOTIFY "{channel}" """) + await asyncio.sleep(0.1) + await aiopg_connector.execute_query_async( + f"""NOTIFY "{channel}", 'somepayload' """ + ) await asyncio.wait_for(event.wait(), timeout=1) + args = received_args.pop() + assert args["channel"] == "somechannel" + assert args["payload"] == "somepayload" except asyncio.TimeoutError: pytest.fail("Notify not received within 1 sec") finally: @@ -174,9 +185,15 @@ async def test_listen_notify(aiopg_connector): async def test_loop_notify_stop_when_connection_closed_old_aiopg(aiopg_connector): # We want to make sure that the when the connection is closed, the loop end. event = asyncio.Event() + + async def handle_notification(channel: str, payload: str): + event.set() + await aiopg_connector.open_async() async with aiopg_connector._pool.acquire() as connection: - coro = aiopg_connector._loop_notify(event=event, connection=connection) + coro = aiopg_connector._loop_notify( + on_notification=handle_notification, connection=connection + ) await asyncio.sleep(0.1) # Currently, the the connection closes, the notifies queue is not # awaken. This test validates the "normal" stopping condition, there is @@ -192,9 +209,15 @@ async def test_loop_notify_stop_when_connection_closed_old_aiopg(aiopg_connector async def test_loop_notify_stop_when_connection_closed(aiopg_connector): # We want to make sure that the when the connection is closed, the loop end. event = asyncio.Event() + + async def handle_notification(channel: str, payload: str): + event.set() + await aiopg_connector.open_async() async with aiopg_connector._pool.acquire() as connection: - coro = aiopg_connector._loop_notify(event=event, connection=connection) + coro = aiopg_connector._loop_notify( + on_notification=handle_notification, connection=connection + ) await asyncio.sleep(0.1) # Currently, the the connection closes, the notifies queue is not # awaken. This test validates the "normal" stopping condition, there is @@ -211,11 +234,15 @@ async def test_loop_notify_timeout(aiopg_connector): # We want to make sure that when the listen starts, we don't listen forever. If the # connection closes, we eventually finish the coroutine. event = asyncio.Event() + + async def handle_notification(channel: str, payload: str): + event.set() + await aiopg_connector.open_async() async with aiopg_connector._pool.acquire() as connection: task = asyncio.ensure_future( aiopg_connector._loop_notify( - event=event, connection=connection, timeout=0.01 + on_notification=handle_notification, connection=connection, timeout=0.01 ) ) await asyncio.sleep(0.1) @@ -234,6 +261,7 @@ async def test_destructor(connection_params, capsys): await connector.open_async() await connector.execute_query_async("SELECT 1") + assert connector._pool assert len(connector._pool._free) == 1 # "del connector" causes a ResourceWarning from aiopg.Pool if the diff --git a/tests/integration/contrib/django/test_models.py b/tests/integration/contrib/django/test_models.py index 66f434fd3..fcee72f46 100644 --- a/tests/integration/contrib/django/test_models.py +++ b/tests/integration/contrib/django/test_models.py @@ -44,6 +44,7 @@ def test_procrastinate_job__property(db): scheduled_at=datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc), attempts=0, queueing_lock="baz", + abort_requested=False, ) assert job.procrastinate_job == jobs_module.Job( id=1, diff --git a/tests/integration/test_psycopg_connector.py b/tests/integration/test_psycopg_connector.py index b36cbe8a2..9539e50ef 100644 --- a/tests/integration/test_psycopg_connector.py +++ b/tests/integration/test_psycopg_connector.py @@ -166,15 +166,26 @@ async def test_close_async(psycopg_connector): async def test_listen_notify(psycopg_connector): channel = "somechannel" event = asyncio.Event() + received_args: list[dict] = [] + + async def handle_notification(*, channel: str, payload: str): + event.set() + received_args.append({"channel": channel, "payload": payload}) task = asyncio.ensure_future( - psycopg_connector.listen_notify(channels=[channel], event=event) + psycopg_connector.listen_notify( + channels=[channel], on_notification=handle_notification + ) ) try: - await asyncio.wait_for(event.wait(), timeout=0.2) - event.clear() - await psycopg_connector.execute_query_async(f"""NOTIFY "{channel}" """) + await asyncio.sleep(0.1) + await psycopg_connector.execute_query_async( + f"""NOTIFY "{channel}", 'somepayload' """ + ) await asyncio.wait_for(event.wait(), timeout=1) + args = received_args.pop() + assert args["channel"] == "somechannel" + assert args["payload"] == "somepayload" except asyncio.TimeoutError: pytest.fail("Notify not received within 1 sec") finally: @@ -193,10 +204,14 @@ async def configure(connection): async def test_loop_notify_stop_when_connection_closed(psycopg_connector): # We want to make sure that the when the connection is closed, the loop end. - event = asyncio.Event() + async def handle_notification(channel: str, payload: str): + pass + await psycopg_connector.open_async() async with psycopg_connector._async_pool.connection() as connection: - coro = psycopg_connector._loop_notify(event=event, connection=connection) + coro = psycopg_connector._loop_notify( + on_notification=handle_notification, connection=connection + ) await psycopg_connector._async_pool.close() assert connection.closed @@ -210,11 +225,18 @@ async def test_loop_notify_stop_when_connection_closed(psycopg_connector): async def test_loop_notify_timeout(psycopg_connector): # We want to make sure that when the listen starts, we don't listen forever. If the # connection closes, we eventually finish the coroutine. + event = asyncio.Event() + + async def handle_notification(channel: str, payload: str): + event.set() + await psycopg_connector.open_async() async with psycopg_connector._async_pool.connection() as connection: task = asyncio.ensure_future( - psycopg_connector._loop_notify(event=event, connection=connection) + psycopg_connector._loop_notify( + on_notification=handle_notification, connection=connection + ) ) assert not task.done() diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 1aa11db3f..14aa86fdb 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -96,6 +96,7 @@ async def my_task(a): result.append(a) task = asyncio.create_task(app.run_worker_async()) + await asyncio.sleep(0.01) await my_task.defer_async(a=1) await asyncio.sleep(0.01) task.cancel() diff --git a/tests/unit/test_builtin_tasks.py b/tests/unit/test_builtin_tasks.py index d9d17de65..0d49b9f04 100644 --- a/tests/unit/test_builtin_tasks.py +++ b/tests/unit/test_builtin_tasks.py @@ -10,7 +10,7 @@ async def test_remove_old_jobs(app: App, job_factory): job = job_factory() await builtin_tasks.remove_old_jobs( - job_context.JobContext(app=app, job=job), + job_context.JobContext(app=app, job=job, should_abort=lambda: False), max_hours=2, queue="queue_a", remove_failed=True, diff --git a/tests/unit/test_connector.py b/tests/unit/test_connector.py index e561b6787..a63db018f 100644 --- a/tests/unit/test_connector.py +++ b/tests/unit/test_connector.py @@ -30,7 +30,7 @@ async def test_close_async(connector): ["execute_query_async", {"query": ""}], ["execute_query_one_async", {"query": ""}], ["execute_query_all_async", {"query": ""}], - ["listen_notify", {"event": None, "channels": []}], + ["listen_notify", {"on_notification": None, "channels": []}], ], ) async def test_missing_app_async(method_name, kwargs): diff --git a/tests/unit/test_job_context.py b/tests/unit/test_job_context.py index 6d514bf86..2b4e27bcd 100644 --- a/tests/unit/test_job_context.py +++ b/tests/unit/test_job_context.py @@ -47,15 +47,17 @@ def test_job_result_as_dict(job_result, expected, mocker): def test_evolve(app: App, job_factory): job = job_factory() - context = job_context.JobContext(app=app, job=job, worker_name="a") + context = job_context.JobContext( + app=app, job=job, worker_name="a", should_abort=lambda: False + ) assert context.evolve(worker_name="b").worker_name == "b" def test_job_description_job_no_time(app: App, job_factory): job = job_factory(task_name="some_task", id=12, task_kwargs={"a": "b"}) - descr = job_context.JobContext(worker_name="a", job=job, app=app).job_description( - current_timestamp=0 - ) + descr = job_context.JobContext( + worker_name="a", job=job, app=app, should_abort=lambda: False + ).job_description(current_timestamp=0) assert descr == "worker: some_task[12](a='b')" @@ -66,21 +68,6 @@ def test_job_description_job_time(app: App, job_factory): job=job, app=app, job_result=job_context.JobResult(start_timestamp=20.0), + should_abort=lambda: False, ).job_description(current_timestamp=30.0) assert descr == "worker: some_task[12](a='b') (started 10.000 s ago)" - - -async def test_should_abort(app, job_factory): - await app.job_manager.defer_job_async(job=job_factory()) - job = await app.job_manager.fetch_job(queues=None) - await app.job_manager.cancel_job_by_id_async(job.id, abort=True) - context = job_context.JobContext(app=app, job=job) - assert await context.should_abort_async() is True - - -async def test_should_not_abort(app, job_factory): - await app.job_manager.defer_job_async(job=job_factory()) - job = await app.job_manager.fetch_job(queues=None) - await app.job_manager.cancel_job_by_id_async(job.id) - context = job_context.JobContext(app=app, job=job) - assert await context.should_abort_async() is False diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index b87ea49e1..c321ffd22 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -42,6 +42,7 @@ def test_job_get_context(job_factory, scheduled_at, context_scheduled_at): "scheduled_at": context_scheduled_at, "attempts": 42, "call_string": "mytask[12](a='b')", + "abort_requested": False, } diff --git a/tests/unit/test_manager.py b/tests/unit/test_manager.py index ef8c8c32d..3a1e695a5 100644 --- a/tests/unit/test_manager.py +++ b/tests/unit/test_manager.py @@ -300,17 +300,17 @@ async def test_retry_job(job_manager, job_factory, connector): ], ) async def test_listen_for_jobs(job_manager, connector, mocker, queues, channels): - event = mocker.Mock() + on_notification = mocker.Mock() - await job_manager.listen_for_jobs(queues=queues, event=event) - assert connector.notify_event is event + await job_manager.listen_for_jobs(queues=queues, on_notification=on_notification) + assert connector.on_notification assert connector.notify_channels == channels @pytest.fixture def configure(app): @app.task - def foo(timestamp): + def foo(timestamp: int): pass return foo.configure diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py index 537318437..7196963a7 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/test_testing.py @@ -417,8 +417,15 @@ async def test_listen_for_jobs_run(connector): async def test_defer_no_notify(connector): # This test is there to check that if the deferred queue doesn't match the # listened queue, the testing connector doesn't notify. + event = asyncio.Event() - await connector.listen_notify(event=event, channels="some_other_channel") + + async def on_notification(*, channel: str, payload: str): + event.set() + + await connector.listen_notify( + on_notification=on_notification, channels="some_other_channel" + ) await connector.defer_job_one( task_name="foo", priority=0, diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 24815ce58..72a6101b3 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -97,7 +97,6 @@ async def test_worker_run_wait_listen(worker): await start_worker(worker) connector = cast(InMemoryConnector, worker.app.connector) - assert connector.notify_event assert connector.notify_channels == ["procrastinate_any_queue"] @@ -166,8 +165,6 @@ async def perform_job(sleep: float): await perform_job.defer_async(sleep=0.05) await perform_job.defer_async(sleep=0.05) - worker._notify_event.set() - await asyncio.sleep(0.2) assert max_parallelism == 2 assert parallel_jobs == 0 @@ -533,6 +530,37 @@ async def task_func(): assert "Aborted" in record.message +@pytest.mark.parametrize( + "worker", + [ + ({"listen_notify": False, "polling_interval": 0.05}), + ({"listen_notify": True, "polling_interval": 1}), + ], + indirect=["worker"], +) +async def test_run_job_abort(app: App, worker: Worker): + @app.task(queue="yay", name="task_func", pass_context=True) + async def task_func(job_context: JobContext): + while True: + await asyncio.sleep(0.01) + if job_context.should_abort(): + raise JobAborted() + + job_id = await task_func.defer_async() + + await start_worker(worker) + + await app.job_manager.cancel_job_by_id_async(job_id, abort=True) + + await asyncio.sleep(0.01 if worker.listen_notify else 0.05) + + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.ABORTED + assert ( + worker._job_ids_to_abort == set() + ), "Expected cancelled job id to be removed from set" + + @pytest.mark.parametrize( "critical_error, recover_on_attempt_number, expected_status, expected_attempts", [ @@ -596,7 +624,7 @@ def t(): "fetch_job", ] - logs = {(r.action, r.levelname) for r in caplog.records} + logs = {(r.action, r.levelname) for r in caplog.records if hasattr(r, "action")} # remove the periodic_deferrer_no_task log record because that makes the test flaky assert { ("about_to_defer_job", "DEBUG"), @@ -633,13 +661,6 @@ async def t(): ) -async def test_run_no_listen_notify(app: App, worker): - worker.listen_notify = False - await start_worker(worker) - connector = cast(InMemoryConnector, app.connector) - assert connector.notify_event is None - - async def test_run_no_signal_handlers(worker, kill_own_pid): worker.install_signal_handlers = False await start_worker(worker) diff --git a/tests/unit/test_worker_sync.py b/tests/unit/test_worker_sync.py index 7aaadce72..7a916fb50 100644 --- a/tests/unit/test_worker_sync.py +++ b/tests/unit/test_worker_sync.py @@ -2,7 +2,7 @@ import pytest -from procrastinate import exceptions, job_context, worker +from procrastinate import exceptions, worker from procrastinate.app import App @@ -11,11 +11,6 @@ def test_worker(app: App) -> worker.Worker: return worker.Worker(app=app, queues=["yay"]) -@pytest.fixture -def context(app: App, job_factory): - return job_context.JobContext(app=app, job=job_factory()) - - def test_worker_find_task_missing(test_worker): with pytest.raises(exceptions.TaskNotFound): test_worker.find_task("foobarbaz") From f9bf4e75c3997c9bee876ea8cac8dab0ffd1b9b6 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 8 Sep 2024 21:17:09 +1000 Subject: [PATCH 051/375] cancel async task through asyncio --- docs/howto/advanced/cancellation.md | 42 ++++++++++++++--- procrastinate/worker.py | 27 +++++++++-- tests/acceptance/test_async.py | 9 ++-- tests/unit/test_worker.py | 70 ++++++++++++++++++++++++++++- 4 files changed, 131 insertions(+), 17 deletions(-) diff --git a/docs/howto/advanced/cancellation.md b/docs/howto/advanced/cancellation.md index d5d924bb7..e11ae1eb4 100644 --- a/docs/howto/advanced/cancellation.md +++ b/docs/howto/advanced/cancellation.md @@ -37,9 +37,19 @@ app.job_manager.cancel_job_by_id(33, abort=True) await app.job_manager.cancel_job_by_id_async(33, abort=True) ``` +Behind the scenes, the worker receives a Postgres notification every time a job is requested to abort, (unless `listen_notify=False`). + +The worker also polls (respecting `polling_interval`) the database for abortion requests, as long as the worker is running at least one job (in the absence of running job, there is nothing to abort). + +:::{note} +When a job is requested to abort and that job fails, it will not be retried (regardless of the retry strategy). +::: + ## Handle an abortion request inside the task -In our task, we can check (for example, periodically) if the task should be +## Sync tasks + +In a sync task, we can check (for example, periodically) if the task should be aborted. If we want to respect that abortion request (we don't have to), we raise a `JobAborted` error. Any message passed to `JobAborted` (e.g. `raise JobAborted("custom message")`) will end up in the logs. @@ -53,10 +63,30 @@ def my_task(context): do_something_expensive() ``` -Behind the scenes, the worker receives a Postgres notification every time a job is requested to abort, (unless `listen_notify=False`). +# Async tasks -The worker also polls (respecting `polling_interval`) the database for abortion requests, as long as the worker is running at least one job (in the absence of running job, there is nothing to abort). +For async tasks (coroutines), the async taks will be cancelled via [asyncio cancellation](https://docs.python.org/3/library/asyncio-task.html#task-cancellation) mechasnism. +As such, the task will be cancelled at the next opportunity. -:::{note} -When a job is requested to abort and that job fails, it will not be retried (regardless of the retry strategy). -::: +```python +@app.task() +async def my_task(): + do_something_synchronous() + # if the job is aborted while it waits for do_something to complete, asyncio.CancelledError will be raised here + await do_something() +``` + +It is possible to prevent the job from aborting by capturing asyncio.CancelledError. + +```python +@app.task() +async def my_task(): + try: + # shield something_important from being cancelled + important_task = asyncio.create_task(something_important()) + await asyncio.shield(important_task) + except asyncio.CancelledError: + # capture the error and waits for something important to complete + await important_task + # if the job should be marked as aborted, rethrow. Otherwise continue for job to succeed +``` diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 6c4fd2d86..235a3bbbb 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -257,9 +257,17 @@ async def ensure_async() -> Callable[..., Awaitable]: job_retry=job_retry, exc_info=exc_info, ) - await self._persist_job_status( - job=job, status=status, retry_decision=retry_decision + + persist_job_status_task = asyncio.create_task( + self._persist_job_status( + job=job, status=status, retry_decision=retry_decision + ) ) + try: + await asyncio.shield(persist_job_status_task) + except asyncio.CancelledError: + await persist_job_status_task + raise self._job_ids_to_abort.discard(job.id) @@ -372,8 +380,19 @@ async def _poll_jobs_to_abort(self): def _handle_abort_jobs_requested(self, job_ids: Iterable[int]): running_job_ids = {c.job.id for c in self._running_jobs.values() if c.job.id} - self._job_ids_to_abort |= set(job_ids) - self._job_ids_to_abort &= running_job_ids + new_job_ids_to_abort = (running_job_ids & set(job_ids)) - self._job_ids_to_abort + self._job_ids_to_abort |= new_job_ids_to_abort + + tasks_to_cancel = ( + task + for (task, context) in self._running_jobs.items() + if context.job.id in new_job_ids_to_abort + and context.task + and asyncio.iscoroutinefunction(context.task.func) + ) + + for task in tasks_to_cancel: + task.cancel() async def _shutdown(self, side_tasks: list[asyncio.Task]): """ diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index ce4c9109a..c854917d3 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -110,12 +110,9 @@ def example_task(): @pytest.mark.parametrize("mode", ["listen", "poll"]) async def test_abort_async_task(async_app: app_module.App, mode): - @async_app.task(queue="default", name="task1", pass_context=True) - async def task1(context): - while True: - await asyncio.sleep(0.02) - if context.should_abort(): - raise JobAborted + @async_app.task(queue="default", name="task1") + async def task1(): + await asyncio.sleep(0.5) job_id = await task1.defer_async() diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 72a6101b3..f9913836c 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -5,6 +5,7 @@ from typing import cast import pytest +from pytest_mock import MockerFixture from procrastinate.app import App from procrastinate.exceptions import JobAborted @@ -509,7 +510,7 @@ def task_func(a, b): assert "to retry" not in record.message -async def test_run_job_aborted(app: App, worker, caplog): +async def test_run_job_raising_job_aborted(app: App, worker, caplog): caplog.set_level("INFO") @app.task(queue="yay", name="task_func") @@ -530,6 +531,73 @@ async def task_func(): assert "Aborted" in record.message +async def test_abort_async_job(app: App, worker): + @app.task(queue="yay", name="task_func") + async def task_func(): + await asyncio.sleep(0.2) + + job_id = await task_func.defer_async() + + await start_worker(worker) + await app.job_manager.cancel_job_by_id_async(job_id, abort=True) + await asyncio.sleep(0.01) + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.ABORTED + + +async def test_abort_async_job_while_finishing(app: App, worker, mocker: MockerFixture): + """ + Tests that aborting a job after that job completes but before the job status is updated + does not prevent the job status from being updated + """ + connector = cast(InMemoryConnector, app.connector) + original_finish_job_run = connector.finish_job_run + + complete_finish_job_event = asyncio.Event() + + async def delayed_finish_job_run(**arguments): + await complete_finish_job_event.wait() + return await original_finish_job_run(**arguments) + + connector.finish_job_run = mocker.AsyncMock(name="finish_job_run") + connector.finish_job_run.side_effect = delayed_finish_job_run + + @app.task(queue="yay", name="task_func") + async def task_func(): + pass + + job_id = await task_func.defer_async() + + await start_worker(worker) + await app.job_manager.cancel_job_by_id_async(job_id, abort=True) + await asyncio.sleep(0.01) + complete_finish_job_event.set() + await asyncio.sleep(0.01) + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + +async def test_abort_async_job_preventing_cancellation(app: App, worker): + """ + Tests that an async job can prevent itself from being aborted + """ + + @app.task(queue="yay", name="task_func") + async def task_func(): + try: + await asyncio.sleep(0.2) + except asyncio.CancelledError: + pass + + job_id = await task_func.defer_async() + + await start_worker(worker) + await app.job_manager.cancel_job_by_id_async(job_id, abort=True) + await asyncio.sleep(0.01) + status = await app.job_manager.get_job_status_async(job_id) + assert status == Status.SUCCEEDED + + @pytest.mark.parametrize( "worker", [ From 6ace0f50731c44b3eb52ab24941bae3ae22c2a0c Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 8 Sep 2024 21:27:43 +1000 Subject: [PATCH 052/375] update documentation --- docs/howto/advanced/cancellation.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/howto/advanced/cancellation.md b/docs/howto/advanced/cancellation.md index e11ae1eb4..668e8b22d 100644 --- a/docs/howto/advanced/cancellation.md +++ b/docs/howto/advanced/cancellation.md @@ -47,7 +47,7 @@ When a job is requested to abort and that job fails, it will not be retried (reg ## Handle an abortion request inside the task -## Sync tasks +### Sync tasks In a sync task, we can check (for example, periodically) if the task should be aborted. If we want to respect that abortion request (we don't have to), we raise a @@ -63,10 +63,9 @@ def my_task(context): do_something_expensive() ``` -# Async tasks +### Async tasks -For async tasks (coroutines), the async taks will be cancelled via [asyncio cancellation](https://docs.python.org/3/library/asyncio-task.html#task-cancellation) mechasnism. -As such, the task will be cancelled at the next opportunity. +For async tasks (coroutines), they are cancelled via the [asyncio cancellation](https://docs.python.org/3/library/asyncio-task.html#task-cancellation) mechasnism. ```python @app.task() @@ -76,14 +75,14 @@ async def my_task(): await do_something() ``` -It is possible to prevent the job from aborting by capturing asyncio.CancelledError. +It is possible to prevent the job from aborting by capturing asyncio.CancelledError. ```python @app.task() async def my_task(): try: - # shield something_important from being cancelled important_task = asyncio.create_task(something_important()) + # shield something_important from being cancelled await asyncio.shield(important_task) except asyncio.CancelledError: # capture the error and waits for something important to complete From fa20d60cd59f467867a6655716235bb36c4cab62 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 9 Sep 2024 00:05:20 +1000 Subject: [PATCH 053/375] move some logic into _persist_job_status --- procrastinate/worker.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 235a3bbbb..7097652ee 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -119,6 +119,7 @@ async def _persist_job_status( job: jobs.Job, status: jobs.Status, retry_decision: retry.RetryDecision | None, + context: job_context.JobContext, ): if retry_decision: await self.app.job_manager.retry_job( @@ -138,6 +139,13 @@ async def _persist_job_status( job=job, status=status, delete_job=delete_job ) + self._job_ids_to_abort.discard(job.id) + + self.logger.debug( + f"Acknowledged job completion {job.call_string}", + extra=self._log_extra(action="finish_task", context=context, status=status), + ) + def _log_job_outcome( self, status: jobs.Status, @@ -260,7 +268,10 @@ async def ensure_async() -> Callable[..., Awaitable]: persist_job_status_task = asyncio.create_task( self._persist_job_status( - job=job, status=status, retry_decision=retry_decision + job=job, + status=status, + retry_decision=retry_decision, + context=context, ) ) try: @@ -269,15 +280,6 @@ async def ensure_async() -> Callable[..., Awaitable]: await persist_job_status_task raise - self._job_ids_to_abort.discard(job.id) - - self.logger.debug( - f"Acknowledged job completion {job.call_string}", - extra=self._log_extra( - action="finish_task", context=context, status=status - ), - ) - async def _fetch_and_process_jobs(self): """Fetch and process jobs until there is no job left or asked to stop""" while not self._stop_event.is_set(): From d78a66428355a921199c2fa6b79f58ff2508991b Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 12 Sep 2024 15:03:52 +1000 Subject: [PATCH 054/375] add more logs and update documentation wording --- docs/howto/advanced/cancellation.md | 6 ++++-- procrastinate/worker.py | 32 +++++++++++++++++++---------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/docs/howto/advanced/cancellation.md b/docs/howto/advanced/cancellation.md index 668e8b22d..4726294ee 100644 --- a/docs/howto/advanced/cancellation.md +++ b/docs/howto/advanced/cancellation.md @@ -75,7 +75,7 @@ async def my_task(): await do_something() ``` -It is possible to prevent the job from aborting by capturing asyncio.CancelledError. +If you want to have some custom behavior at cancellation time, use a combination of [shielding](https://docs.python.org/3/library/asyncio-task.html#shielding-from-cancellation) and capturing `except asyncio.CancelledError`. ```python @app.task() @@ -87,5 +87,7 @@ async def my_task(): except asyncio.CancelledError: # capture the error and waits for something important to complete await important_task - # if the job should be marked as aborted, rethrow. Otherwise continue for job to succeed + # raise if the job should be marked as aborted, or swallow CancelledError if the job should be + # marked as suceeeded + raise ``` diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 7097652ee..52e700bbf 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -383,18 +383,28 @@ async def _poll_jobs_to_abort(self): def _handle_abort_jobs_requested(self, job_ids: Iterable[int]): running_job_ids = {c.job.id for c in self._running_jobs.values() if c.job.id} new_job_ids_to_abort = (running_job_ids & set(job_ids)) - self._job_ids_to_abort - self._job_ids_to_abort |= new_job_ids_to_abort - - tasks_to_cancel = ( - task - for (task, context) in self._running_jobs.items() - if context.job.id in new_job_ids_to_abort - and context.task - and asyncio.iscoroutinefunction(context.task.func) - ) - for task in tasks_to_cancel: - task.cancel() + for process_job_task, context in self._running_jobs.items(): + if context.job.id in new_job_ids_to_abort: + self._abort_job(process_job_task, context) + + def _abort_job( + self, process_job_task: asyncio.Task, context: job_context.JobContext + ): + self._job_ids_to_abort.add(context.job.id) + + log_message: str + if not context.task: + log_message = "Received a request to abort a job but the job has no associated task. No action to perform" + elif not asyncio.iscoroutinefunction(context.task.func): + log_message = "Received a request to abort a synchronous job. Job is responsible for aborting by checking context.should_abort" + else: + log_message = "Received a request to abort an asynchronous job. Cancelling asyncio task" + process_job_task.cancel() + + self.logger.debug( + log_message, extra=self._log_extra(action="abort_job", context=context) + ) async def _shutdown(self, side_tasks: list[asyncio.Task]): """ From 31c1a9dda7a9b5c1f54821ae86f21e095ea91364 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Thu, 12 Sep 2024 21:57:26 +1000 Subject: [PATCH 055/375] Trigger CI From 5039b9b393f96abb9f00dd351bfbba6f9d7504cd Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sat, 14 Sep 2024 08:53:17 +1000 Subject: [PATCH 056/375] add abort_polling_interval --- docs/discussions.md | 22 +++++++++++++--------- docs/howto/advanced/cancellation.md | 2 +- procrastinate/app.py | 19 +++++++++++++------ procrastinate/cli.py | 12 ++++++++++-- procrastinate/worker.py | 15 +++++++++------ tests/acceptance/test_async.py | 21 ++++++++++++--------- tests/integration/test_cli.py | 5 +++-- tests/integration/test_wait_stop.py | 8 ++++---- tests/unit/test_app.py | 4 ++-- tests/unit/test_worker.py | 6 +++--- 10 files changed, 70 insertions(+), 44 deletions(-) diff --git a/docs/discussions.md b/docs/discussions.md index 6758e5a55..16d6de502 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -199,26 +199,30 @@ Having sub-workers wait for an available connection in the pool is suboptimal. Y resources will be better used with fewer sub-workers or a larger pool, but there are many factors to take into account when [sizing your pool](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections). -### How the `polling_interval` works +### How polling works + +#### `fetch_job_polling_interval` Even when the database doesn't notify workers regarding newly deferred jobs, each worker still poll the database every now and then, just in case. There could be previously locked jobs that are now free, or scheduled jobs that have -reached the ETA. `polling_interval` is the {py:meth}`App.run_worker` parameter (or the +reached the ETA. `fetch_job_polling_interval` is the {py:meth}`App.run_worker` parameter (or the equivalent CLI flag) that sizes this "every now and then". A worker will keep fetching new jobs as long as they have capacity to process them. The polling interval starts from the moment the last attempt to fetch a new job yields no result. -The `polling_interval` also defines how often the worker will poll the database for jobs to abort. -When `listen_notify=True`, the worker will likely be notified "instantly" of each abort request prior to polling the database. +:::{note} +The polling interval was previously called `timeout` in pre-v3 versions of Procrastinate. It was renamed to `fetch_job_polling_interval` for clarity. +::: -However, in the event `listen_notify=False` or if the abort notification was missed, `polling_interval` will represent the maximum delay before the worker reacts to an abort request. +#### `abort_job_polling_interval` -Note that the worker will not poll the database for jobs to be aborted if it is idle (i.e. it has no running job). +Another polling interval is the `abort_job_polling_interval`. It defines how often the worker will poll the database for jobs to abort. +When `listen_notify=True`, the worker will likely be notified "instantly" of each abort request prior to polling the database. -:::{note} -The polling interval was previously called `timeout` in pre-v3 versions of Procrastinate. It was renamed to `polling_interval` for clarity. -::: +However, when `listen_notify=False` or the abort notification was missed, `abort_job_polling_interval` will represent the maximum delay before the worker reacts to an abort request. + +Note that the worker will only poll the database for abort requests when at least one job is running. ## Procrastinate's usage of PostgreSQL functions and procedures diff --git a/docs/howto/advanced/cancellation.md b/docs/howto/advanced/cancellation.md index 4726294ee..1f193c485 100644 --- a/docs/howto/advanced/cancellation.md +++ b/docs/howto/advanced/cancellation.md @@ -39,7 +39,7 @@ await app.job_manager.cancel_job_by_id_async(33, abort=True) Behind the scenes, the worker receives a Postgres notification every time a job is requested to abort, (unless `listen_notify=False`). -The worker also polls (respecting `polling_interval`) the database for abortion requests, as long as the worker is running at least one job (in the absence of running job, there is nothing to abort). +The worker also polls (respecting `fetch_job_polling_interval`) the database for abortion requests, as long as the worker is running at least one job (in the absence of running job, there is nothing to abort). :::{note} When a job is requested to abort and that job fails, it will not be retried (regardless of the retry strategy). diff --git a/procrastinate/app.py b/procrastinate/app.py index 3eaaeff73..d6f18ff81 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -28,7 +28,8 @@ class WorkerOptions(TypedDict): name: NotRequired[str] concurrency: NotRequired[int] wait: NotRequired[bool] - polling_interval: NotRequired[float] + fetch_job_polling_interval: NotRequired[float] + abort_job_polling_interval: NotRequired[float] shutdown_timeout: NotRequired[float] listen_notify: NotRequired[bool] delete_jobs: NotRequired[str | jobs.DeleteJobCondition] @@ -270,13 +271,19 @@ async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: Name of the worker. Will be passed in the `JobContext` and used in the logs (defaults to ``None`` which will result in the worker named ``worker``). - polling_interval : ``float`` + fetch_job_polling_interval : ``float`` Maximum time (in seconds) between database job polls. - Controls the frequency of database queries for: - - Checking for new jobs to start - - Fetching updates for running jobs - - Checking for abort requests + Controls the frequency of database queries for new jobs to start. + + When `listen_notify` is True, the polling interval acts as a fallback + mechanism and can reasonably be set to a higher value. + + (defaults to 5.0) + abort_job_polling_interval : ``float`` + Maximum time (in seconds) between database abort requet polls. + + Controls the frequency of database queries for abort requests When `listen_notify` is True, the polling interval acts as a fallback mechanism and can reasonably be set to a higher value. diff --git a/procrastinate/cli.py b/procrastinate/cli.py index fda2af678..60a1719fa 100644 --- a/procrastinate/cli.py +++ b/procrastinate/cli.py @@ -292,10 +292,18 @@ def configure_worker_parser(subparsers: argparse._SubParsersAction): add_argument( worker_parser, "-p", - "--polling-interval", + "--fetch-job-polling-interval", type=float, help="How long to wait for database event push before polling", - envvar="WORKER_POLLING_INTERVAL", + envvar="WORKER_FETCH_JOB_POLLING_INTERVAL", + ) + add_argument( + worker_parser, + "-a", + "--abort-job-polling-interval", + type=float, + help="How often to polling for abort requests", + envvar="WORKER_ABORT_JOB_POLLING_INTERVAL", ) add_argument( worker_parser, diff --git a/procrastinate/worker.py b/procrastinate/worker.py index 52e700bbf..ea51813b3 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -25,7 +25,8 @@ WORKER_NAME = "worker" WORKER_CONCURRENCY = 1 # maximum number of parallel jobs -POLLING_INTERVAL = 5.0 # seconds +FETCH_JOB_POLLING_INTERVAL = 5.0 # seconds +ABORT_JOB_POLLING_INTERVAL = 5.0 # seconds class Worker: @@ -36,7 +37,8 @@ def __init__( name: str | None = WORKER_NAME, concurrency: int = WORKER_CONCURRENCY, wait: bool = True, - polling_interval: float = POLLING_INTERVAL, + fetch_job_polling_interval: float = FETCH_JOB_POLLING_INTERVAL, + abort_job_polling_interval: float = ABORT_JOB_POLLING_INTERVAL, shutdown_timeout: float | None = None, listen_notify: bool = True, delete_jobs: str | jobs.DeleteJobCondition | None = None, @@ -48,7 +50,8 @@ def __init__( self.worker_name = name self.concurrency = concurrency self.wait = wait - self.polling_interval = polling_interval + self.fetch_job_polling_interval = fetch_job_polling_interval + self.abort_job_polling_interval = abort_job_polling_interval self.listen_notify = listen_notify self.delete_jobs = ( jobs.DeleteJobCondition(delete_jobs) @@ -361,9 +364,9 @@ async def _handle_notification( async def _poll_jobs_to_abort(self): while True: logger.debug( - f"waiting for {self.polling_interval}s before querying jobs to abort" + f"waiting for {self.abort_job_polling_interval}s before querying jobs to abort" ) - await asyncio.sleep(self.polling_interval) + await asyncio.sleep(self.abort_job_polling_interval) if not self._running_jobs: logger.debug("Not querying jobs to abort because no job is running") continue @@ -485,7 +488,7 @@ async def _run_loop(self): # wait for a new job notification, a stop even or the next polling interval await utils.wait_any( self._new_job_event.wait(), - asyncio.sleep(self.polling_interval), + asyncio.sleep(self.fetch_job_polling_interval), self._stop_event.wait(), ) await self._fetch_and_process_jobs() diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index c854917d3..37b1d860f 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -116,13 +116,13 @@ async def task1(): job_id = await task1.defer_async() - polling_interval = 0.1 + abort_job_polling_interval = 0.1 worker_task = asyncio.create_task( async_app.run_worker_async( queues=["default"], wait=False, - polling_interval=polling_interval, + abort_job_polling_interval=abort_job_polling_interval, listen_notify=True if mode == "listen" else False, ) ) @@ -132,9 +132,9 @@ async def task1(): assert result is True # when listening for notifications, job should cancel within ms - # if notifications are disabled, job will only cancel after polling_interval + # if notifications are disabled, job will only cancel after abort_job_polling_interval await asyncio.wait_for( - worker_task, timeout=0.1 if mode == "listen" else polling_interval * 2 + worker_task, timeout=0.1 if mode == "listen" else abort_job_polling_interval * 2 ) status = await async_app.job_manager.get_job_status_async(job_id) @@ -152,13 +152,13 @@ def task1(context): job_id = await task1.defer_async() - polling_interval = 0.1 + abort_job_polling_interval = 0.1 worker_task = asyncio.create_task( async_app.run_worker_async( queues=["default"], wait=False, - polling_interval=polling_interval, + abort_job_polling_interval=abort_job_polling_interval, listen_notify=True if mode == "listen" else False, ) ) @@ -168,9 +168,9 @@ def task1(context): assert result is True # when listening for notifications, job should cancel within ms - # if notifications are disabled, job will only cancel after polling_interval + # if notifications are disabled, job will only cancel after abort_job_polling_interval await asyncio.wait_for( - worker_task, timeout=0.1 if mode == "listen" else polling_interval * 2 + worker_task, timeout=0.1 if mode == "listen" else abort_job_polling_interval * 2 ) status = await async_app.job_manager.get_job_status_async(job_id) @@ -218,7 +218,10 @@ async def sum(a: int, b: int): # rely on polling to fetch new jobs worker_task = asyncio.create_task( async_app.run_worker_async( - concurrency=1, wait=True, listen_notify=False, polling_interval=0.3 + concurrency=1, + wait=True, + listen_notify=False, + fetch_job_polling_interval=0.3, ) ) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 5849faa11..06dd60ab7 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -69,7 +69,7 @@ async def test_worker(entrypoint, cli_app, mocker): cli_app.run_worker_async = mocker.AsyncMock() result = await entrypoint( "worker " - "--queues a,b --name=w1 --polling-interval=8.3 " + "--queues a,b --name=w1 --fetch-job-polling-interval=8.3 --abort-job-polling-interval=20 " "--one-shot --concurrency=10 --no-listen-notify --delete-jobs=always" ) @@ -79,7 +79,8 @@ async def test_worker(entrypoint, cli_app, mocker): concurrency=10, name="w1", queues=["a", "b"], - polling_interval=8.3, + fetch_job_polling_interval=8.3, + abort_job_polling_interval=20, wait=False, listen_notify=False, delete_jobs=jobs.DeleteJobCondition.ALWAYS, diff --git a/tests/integration/test_wait_stop.py b/tests/integration/test_wait_stop.py index 852aa23c6..adf26d79f 100644 --- a/tests/integration/test_wait_stop.py +++ b/tests/integration/test_wait_stop.py @@ -13,7 +13,7 @@ async def test_wait_for_activity_cancelled(psycopg_connector): Testing that the work can be cancelled """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, polling_interval=2) + worker = worker_module.Worker(app=pg_app, fetch_job_polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting @@ -31,7 +31,7 @@ async def test_wait_for_activity_timeout(psycopg_connector): Testing that we timeout if nothing happens """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, polling_interval=2) + worker = worker_module.Worker(app=pg_app, fetch_job_polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting with pytest.raises(asyncio.TimeoutError): @@ -43,7 +43,7 @@ async def test_wait_for_activity_stop_from_signal(psycopg_connector, kill_own_pi Testing than ctrl+c interrupts the wait """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, polling_interval=2) + worker = worker_module.Worker(app=pg_app, fetch_job_polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting @@ -60,7 +60,7 @@ async def test_wait_for_activity_stop(psycopg_connector): Testing than calling worker.stop() interrupts the wait """ pg_app = app.App(connector=psycopg_connector) - worker = worker_module.Worker(app=pg_app, polling_interval=2) + worker = worker_module.Worker(app=pg_app, fetch_job_polling_interval=2) task = asyncio.ensure_future(worker.run()) await asyncio.sleep(0.2) # should be enough so that we're waiting diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 14aa86fdb..b310dc61c 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -51,11 +51,11 @@ def test_app_register(app: app_module.App): def test_app_worker(app: app_module.App, mocker): Worker = mocker.patch("procrastinate.worker.Worker") - app.worker_defaults["polling_interval"] = 12 + app.worker_defaults["fetch_job_polling_interval"] = 12 app._worker(queues=["yay"], name="w1", wait=False) Worker.assert_called_once_with( - queues=["yay"], app=app, name="w1", polling_interval=12, wait=False + queues=["yay"], app=app, name="w1", fetch_job_polling_interval=12, wait=False ) diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index f9913836c..2fe6a7103 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -198,7 +198,7 @@ async def perform_job(): @pytest.mark.parametrize( "worker", - [({"polling_interval": 0.05})], + [({"fetch_job_polling_interval": 0.05})], indirect=["worker"], ) async def test_worker_run_respects_polling(worker, app): @@ -601,8 +601,8 @@ async def task_func(): @pytest.mark.parametrize( "worker", [ - ({"listen_notify": False, "polling_interval": 0.05}), - ({"listen_notify": True, "polling_interval": 1}), + ({"listen_notify": False, "abort_job_polling_interval": 0.05}), + ({"listen_notify": True, "abort_job_polling_interval": 1}), ], indirect=["worker"], ) From 9eebbdac4a8e885a66e6382ce8f21f3ad8c8c261 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Mon, 30 Sep 2024 22:22:10 +0000 Subject: [PATCH 057/375] Add state graph (using mermaid) to the discussion page --- docs/conf.py | 8 ++++++++ docs/discussions.md | 31 +++++++++++++++++++++++++++++++ poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d85b9409e..65f72dfbc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,7 @@ "sphinxcontrib.programoutput", "sphinx_github_changelog", "sphinx_copybutton", + "sphinxcontrib.mermaid", "procrastinate.contrib.sphinx", ] @@ -98,6 +99,13 @@ html_favicon = "favicon.ico" +mermaid_init_js = """ +mermaid.initialize({ + startOnLoad: true, + theme: "neutral" +}); +""" + # -- Options for sphinx.ext.autodoc ------------------------------------------ autodoc_typehints = "both" diff --git a/docs/discussions.md b/docs/discussions.md index 16d6de502..2b003c127 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -134,6 +134,37 @@ For a more practical approach, see {doc}`howto/advanced/locks`. (discussion-async)= +## What are the different states of a Job and how do they go from one to another? + +A job can be in one of the following states: + +```{mermaid} +flowchart LR + START:::hidden + todo[TODO] + doing[DOING] + succeeded[SUCCEEDED] + failed[FAILED] + cancelled[CANCELLED] + aborted[ABORTED] + START -- a --> todo + todo -- b --> doing + doing -- c --> succeeded + doing -- d --> todo + doing -- e --> failed + todo -- f --> cancelled + doing -- g --> aborted + classDef hidden display: none; +``` + +a: A job was deferred by `my_task.defer()` or `await my_task.defer_async()`\ +b: A worker fetched a job from the database and started processing it\ +c: A worker finished processing a job successfully\ +d: A job failed but will be retried\ +e: A job failed and won't be retried\ +f: A job was cancelled before processing was started\ +g: A job was aborted that was already processing\ + ## Asynchronous operations & concurrency Here, asynchronous (or async) means "using the Python `async/await` keywords, to diff --git a/poetry.lock b/poetry.lock index 83c155c1d..a5b49008e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1629,6 +1629,17 @@ files = [ [package.extras] test = ["flake8", "mypy", "pytest"] +[[package]] +name = "sphinxcontrib-mermaid" +version = "0.9.2" +description = "Mermaid diagrams in yours Sphinx powered docs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinxcontrib-mermaid-0.9.2.tar.gz", hash = "sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af"}, + {file = "sphinxcontrib_mermaid-0.9.2-py3-none-any.whl", hash = "sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d"}, +] + [[package]] name = "sphinxcontrib-programoutput" version = "0.17" @@ -1887,4 +1898,4 @@ sqlalchemy = ["sqlalchemy"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e88d066984b0c6d684aa21f007b9eee8cb3c0fa48eb7f55d4a39b5598822e648" +content-hash = "1b96e2c5ab73a198d5ec1f410cbd3f27d0fd28c5e3a7caac32c6567e76efb048" diff --git a/pyproject.toml b/pyproject.toml index c260f50e1..05b35b534 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,7 @@ Sphinx = "*" sphinx-copybutton = "*" sphinx-github-changelog = "*" sphinxcontrib-programoutput = "*" +sphinxcontrib-mermaid = "*" myst-parser = "*" [tool.poetry-dynamic-versioning] From 041d6be1e9182b22c503f0890604888e3f433592 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Tue, 1 Oct 2024 21:01:38 +0000 Subject: [PATCH 058/375] Improve legend below state graph in docs --- docs/discussions.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/discussions.md b/docs/discussions.md index 2b003c127..84d6f9e7d 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -157,13 +157,19 @@ flowchart LR classDef hidden display: none; ``` -a: A job was deferred by `my_task.defer()` or `await my_task.defer_async()`\ -b: A worker fetched a job from the database and started processing it\ -c: A worker finished processing a job successfully\ -d: A job failed but will be retried\ -e: A job failed and won't be retried\ -f: A job was cancelled before processing was started\ -g: A job was aborted that was already processing\ +- **a**: The job was deferred by `my_task.defer()` or `await my_task.defer_async()` +- **b**: A worker fetched the job from the database and started processing it +- **c**: A worker finished processing a job successfully +- **d**: The job failed by raising an error but will be retried +- **e**: The job failed by raising an error and won't be retried +- **f**: The job was cancelled by calling `job_manager.cancel_job_by_id(job_id)` or + `await job_manager.cancel_job_by_id_async(job_id)` before its processing was started +- **g**: The job was aborted during being processed by calling + `job_manager.cancel_job_by_id(job_id, abort=True)` or + `await job_manager.cancel_job_by_id_async(job_id, abort=True)`. A sync job must also + handle the abort request by checking `context.should_abort()` and raising a + `JobAborted` exception. An async job handles it automatically by internally raising a + `CancelledError` exception. ## Asynchronous operations & concurrency From c51f70df92d47cb9e70322ef9f2ce7e304cfc7d8 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Tue, 1 Oct 2024 23:37:26 +0200 Subject: [PATCH 059/375] Update docs/discussions.md Co-authored-by: Joachim Jablon --- docs/discussions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/discussions.md b/docs/discussions.md index 84d6f9e7d..608c2f9d6 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -157,7 +157,7 @@ flowchart LR classDef hidden display: none; ``` -- **a**: The job was deferred by `my_task.defer()` or `await my_task.defer_async()` +- **a**: The job was deferred by `my_task.defer()` (or the async equivalent) - **b**: A worker fetched the job from the database and started processing it - **c**: A worker finished processing a job successfully - **d**: The job failed by raising an error but will be retried From c7d20f9827b3c1198841de636b6bd2dff76126f9 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Tue, 1 Oct 2024 23:37:33 +0200 Subject: [PATCH 060/375] Update docs/discussions.md Co-authored-by: Joachim Jablon --- docs/discussions.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/discussions.md b/docs/discussions.md index 608c2f9d6..51cfb8ce8 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -162,8 +162,7 @@ flowchart LR - **c**: A worker finished processing a job successfully - **d**: The job failed by raising an error but will be retried - **e**: The job failed by raising an error and won't be retried -- **f**: The job was cancelled by calling `job_manager.cancel_job_by_id(job_id)` or - `await job_manager.cancel_job_by_id_async(job_id)` before its processing was started +- **f**: The job was cancelled by calling `job_manager.cancel_job_by_id(job_id)` (or the async equivalent) before its processing was started - **g**: The job was aborted during being processed by calling `job_manager.cancel_job_by_id(job_id, abort=True)` or `await job_manager.cancel_job_by_id_async(job_id, abort=True)`. A sync job must also From 94b74fd7f6331e6a5c9ae3c6b45ba953cbc9fa72 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Tue, 1 Oct 2024 23:37:40 +0200 Subject: [PATCH 061/375] Update docs/discussions.md Co-authored-by: Joachim Jablon --- docs/discussions.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/discussions.md b/docs/discussions.md index 51cfb8ce8..b2202a96a 100644 --- a/docs/discussions.md +++ b/docs/discussions.md @@ -164,8 +164,7 @@ flowchart LR - **e**: The job failed by raising an error and won't be retried - **f**: The job was cancelled by calling `job_manager.cancel_job_by_id(job_id)` (or the async equivalent) before its processing was started - **g**: The job was aborted during being processed by calling - `job_manager.cancel_job_by_id(job_id, abort=True)` or - `await job_manager.cancel_job_by_id_async(job_id, abort=True)`. A sync job must also + `job_manager.cancel_job_by_id(job_id, abort=True)` (or the async equivalent). A sync job must also handle the abort request by checking `context.should_abort()` and raising a `JobAborted` exception. An async job handles it automatically by internally raising a `CancelledError` exception. From 35a31e2111b216c9ecc6069434334b701aacbd56 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Sun, 6 Oct 2024 14:24:27 +1000 Subject: [PATCH 062/375] remove job_result and task from JobContext --- docs/reference.rst | 2 +- procrastinate/job_context.py | 35 ++++++--------- procrastinate/worker.py | 74 +++++++++++++++++++++----------- tests/unit/test_builtin_tasks.py | 5 ++- tests/unit/test_job_context.py | 30 +++---------- 5 files changed, 73 insertions(+), 73 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index a0e9f60f3..566a46959 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -32,7 +32,7 @@ When tasks are created with argument ``pass_context``, they are provided a `JobContext` argument: .. autoclass:: procrastinate.JobContext - :members: app, worker_name, worker_queues, job, task + :members: app, worker_name, worker_queues, job Blueprints ---------- diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index d03a10304..8d843e3e4 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -6,29 +6,27 @@ import attr from procrastinate import app as app_module -from procrastinate import jobs, tasks, utils +from procrastinate import jobs, utils @attr.dataclass(kw_only=True) class JobResult: - start_timestamp: float | None = None + start_timestamp: float end_timestamp: float | None = None result: Any = None def duration(self, current_timestamp: float) -> float | None: - if self.start_timestamp is None: - return None return (self.end_timestamp or current_timestamp) - self.start_timestamp def as_dict(self): result = {} - if self.start_timestamp: - result.update( - { - "start_timestamp": self.start_timestamp, - "duration": self.duration(current_timestamp=time.time()), - } - ) + result.update( + { + "start_timestamp": self.start_timestamp, + "duration": self.duration(current_timestamp=time.time()), + } + ) + if self.end_timestamp: result.update({"end_timestamp": self.end_timestamp, "result": self.result}) return result @@ -48,11 +46,10 @@ class JobContext: worker_queues: Iterable[str] | None = None #: Corresponding :py:class:`~jobs.Job` job: jobs.Job - #: Corresponding :py:class:`~tasks.Task` - task: tasks.Task | None = None - job_result: JobResult = attr.ib(factory=JobResult) + #: Time the job started to be processed + start_timestamp: float + additional_context: dict = attr.ib(factory=dict) - task_result: Any = None should_abort: Callable[[], bool] @@ -62,11 +59,3 @@ def evolve(self, **update: Any) -> JobContext: @property def queues_display(self) -> str: return utils.queues_display(self.worker_queues) - - def job_description(self, current_timestamp: float) -> str: - message = f"worker: {self.job.call_string}" - duration = self.job_result.duration(current_timestamp) - if duration is not None: - message += f" (started {duration:.3f} s ago)" - - return message diff --git a/procrastinate/worker.py b/procrastinate/worker.py index ea51813b3..ab73c130d 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -79,7 +79,9 @@ def stop(self): return self.logger.info( "Stop requested", - extra=self._log_extra(context=None, action="stopping_worker"), + extra=self._log_extra( + context=None, action="stopping_worker", job_result=None + ), ) self._stop_event.set() @@ -98,7 +100,11 @@ def find_task(self, task_name: str) -> tasks.Task: raise exceptions.TaskNotFound from exc def _log_extra( - self, action: str, context: job_context.JobContext | None, **kwargs: Any + self, + action: str, + context: job_context.JobContext | None, + job_result: job_context.JobResult | None, + **kwargs: Any, ) -> types.JSONDict: extra: types.JSONDict = { "action": action, @@ -113,7 +119,7 @@ def _log_extra( return { **extra, - **(context.job_result if context else job_context.JobResult()).as_dict(), + **(job_result.as_dict() if job_result else {}), **kwargs, } @@ -123,6 +129,7 @@ async def _persist_job_status( status: jobs.Status, retry_decision: retry.RetryDecision | None, context: job_context.JobContext, + job_result: job_context.JobResult | None, ): if retry_decision: await self.app.job_manager.retry_job( @@ -146,13 +153,19 @@ async def _persist_job_status( self.logger.debug( f"Acknowledged job completion {job.call_string}", - extra=self._log_extra(action="finish_task", context=context, status=status), + extra=self._log_extra( + action="finish_task", + context=context, + status=status, + job_result=job_result, + ), ) def _log_job_outcome( self, status: jobs.Status, context: job_context.JobContext, + job_result: job_context.JobResult | None, job_retry: exceptions.JobRetry | None, exc_info: bool | BaseException = False, ): @@ -168,15 +181,15 @@ def _log_job_outcome( text = f"Job {context.job.call_string} ended with status: {log_title}, " # in practice we should always have a start and end timestamp here # but in theory the JobResult class allows it to be None - if context.job_result.start_timestamp and context.job_result.end_timestamp: - duration = ( - context.job_result.end_timestamp - context.job_result.start_timestamp - ) + if job_result and job_result.start_timestamp and job_result.end_timestamp: + duration = job_result.end_timestamp - job_result.start_timestamp text += f"lasted {duration:.3f} s" - if context.job_result.result: - text += f" - Result: {context.job_result.result}"[:250] + if job_result and job_result.result: + text += f" - Result: {job_result.result}"[:250] - extra = self._log_extra(context=context, action=log_action) + extra = self._log_extra( + context=context, action=log_action, job_result=job_result + ) log_level = logging.ERROR if status == jobs.Status.FAILED else logging.INFO logger.log(log_level, text, extra=extra, exc_info=exc_info) @@ -184,14 +197,13 @@ async def _process_job(self, context: job_context.JobContext): """ Processes a given job and persists its status """ - task = context.task + task = self.app.tasks.get(context.job.task_name) job_retry = None exc_info = False retry_decision = None job = context.job - job_result = context.job_result - job_result.start_timestamp = time.time() + job_result = job_context.JobResult(start_timestamp=context.start_timestamp) try: if not task: @@ -199,12 +211,16 @@ async def _process_job(self, context: job_context.JobContext): self.logger.debug( f"Loaded job info, about to start job {job.call_string}", - extra=self._log_extra(context=context, action="loaded_job_info"), + extra=self._log_extra( + context=context, action="loaded_job_info", job_result=job_result + ), ) self.logger.info( f"Starting job {job.call_string}", - extra=self._log_extra(context=context, action="start_job"), + extra=self._log_extra( + context=context, action="start_job", job_result=job_result + ), ) exc_info: bool | BaseException = False @@ -248,6 +264,7 @@ async def ensure_async() -> Callable[..., Awaitable]: context=context, action="task_not_found", exception=str(e), + job_result=job_result, ), ) finally: @@ -265,6 +282,7 @@ async def ensure_async() -> Callable[..., Awaitable]: self._log_job_outcome( status=status, context=context, + job_result=job_result, job_retry=job_retry, exc_info=exc_info, ) @@ -275,6 +293,7 @@ async def ensure_async() -> Callable[..., Awaitable]: status=status, retry_decision=retry_decision, context=context, + job_result=job_result, ) ) try: @@ -312,8 +331,8 @@ async def _fetch_and_process_jobs(self): if self.additional_context else {}, job=job, - task=self.app.tasks.get(job.task_name), should_abort=lambda: job_id in self._job_ids_to_abort, + start_timestamp=time.time(), ) job_task = asyncio.create_task( self._process_job(context), @@ -397,16 +416,18 @@ def _abort_job( self._job_ids_to_abort.add(context.job.id) log_message: str - if not context.task: + task = self.app.tasks.get(context.job.task_name) + if not task: log_message = "Received a request to abort a job but the job has no associated task. No action to perform" - elif not asyncio.iscoroutinefunction(context.task.func): + elif not asyncio.iscoroutinefunction(task.func): log_message = "Received a request to abort a synchronous job. Job is responsible for aborting by checking context.should_abort" else: log_message = "Received a request to abort an asynchronous job. Cancelling asyncio task" process_job_task.cancel() self.logger.debug( - log_message, extra=self._log_extra(action="abort_job", context=context) + log_message, + extra=self._log_extra(action="abort_job", context=context, job_result=None), ) async def _shutdown(self, side_tasks: list[asyncio.Task]): @@ -418,10 +439,12 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): now = time.time() for context in self._running_jobs.values(): + duration = now - context.start_timestamp self.logger.info( - "Waiting for job to finish: " - + context.job_description(current_timestamp=now), - extra=self._log_extra(context=None, action="ending_job"), + f"Waiting for job to finish: worker: {context.job.call_string} (started {duration:.3f} s ago)", + extra=self._log_extra( + context=None, action="ending_job", job_result=None + ), ) # wait for any in progress job to complete processing @@ -430,7 +453,7 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): self.logger.info( f"Stopped worker on {utils.queues_display(self.queues)}", extra=self._log_extra( - action="stop_worker", queues=self.queues, context=None + action="stop_worker", queues=self.queues, context=None, job_result=None ), ) @@ -455,7 +478,7 @@ async def _run_loop(self): self.logger.info( f"Starting worker on {utils.queues_display(self.queues)}", extra=self._log_extra( - action="start_worker", context=None, queues=self.queues + action="start_worker", context=None, queues=self.queues, job_result=None ), ) self._new_job_event.clear() @@ -480,6 +503,7 @@ async def _run_loop(self): context=None, action="stop_worker", queues=self.queues, + job_result=None, ), ) self._stop_event.set() diff --git a/tests/unit/test_builtin_tasks.py b/tests/unit/test_builtin_tasks.py index 0d49b9f04..d09d22440 100644 --- a/tests/unit/test_builtin_tasks.py +++ b/tests/unit/test_builtin_tasks.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from typing import cast from procrastinate import builtin_tasks, job_context @@ -10,7 +11,9 @@ async def test_remove_old_jobs(app: App, job_factory): job = job_factory() await builtin_tasks.remove_old_jobs( - job_context.JobContext(app=app, job=job, should_abort=lambda: False), + job_context.JobContext( + app=app, job=job, should_abort=lambda: False, start_timestamp=time.time() + ), max_hours=2, queue="queue_a", remove_failed=True, diff --git a/tests/unit/test_job_context.py b/tests/unit/test_job_context.py index 2b4e27bcd..8cd71ca87 100644 --- a/tests/unit/test_job_context.py +++ b/tests/unit/test_job_context.py @@ -1,5 +1,7 @@ from __future__ import annotations +import time + import pytest from procrastinate import job_context @@ -9,7 +11,6 @@ @pytest.mark.parametrize( "job_result, expected", [ - (job_context.JobResult(), None), (job_context.JobResult(start_timestamp=10), 20), (job_context.JobResult(start_timestamp=10, end_timestamp=15), 5), ], @@ -21,7 +22,6 @@ def test_job_result_duration(job_result, expected): @pytest.mark.parametrize( "job_result, expected", [ - (job_context.JobResult(), {}), ( job_context.JobResult(start_timestamp=10), { @@ -48,26 +48,10 @@ def test_job_result_as_dict(job_result, expected, mocker): def test_evolve(app: App, job_factory): job = job_factory() context = job_context.JobContext( - app=app, job=job, worker_name="a", should_abort=lambda: False - ) - assert context.evolve(worker_name="b").worker_name == "b" - - -def test_job_description_job_no_time(app: App, job_factory): - job = job_factory(task_name="some_task", id=12, task_kwargs={"a": "b"}) - descr = job_context.JobContext( - worker_name="a", job=job, app=app, should_abort=lambda: False - ).job_description(current_timestamp=0) - assert descr == "worker: some_task[12](a='b')" - - -def test_job_description_job_time(app: App, job_factory): - job = job_factory(task_name="some_task", id=12, task_kwargs={"a": "b"}) - descr = job_context.JobContext( - worker_name="a", - job=job, + start_timestamp=time.time(), app=app, - job_result=job_context.JobResult(start_timestamp=20.0), + job=job, + worker_name="a", should_abort=lambda: False, - ).job_description(current_timestamp=30.0) - assert descr == "worker: some_task[12](a='b') (started 10.000 s ago)" + ) + assert context.evolve(worker_name="b").worker_name == "b" From e94a5a2361ba6f4269abd3c7dbe81a89396c7414 Mon Sep 17 00:00:00 2001 From: Yann Normand Date: Mon, 7 Oct 2024 14:27:58 +1000 Subject: [PATCH 063/375] handle aborting sync jobs on shutdown --- docs/howto/advanced/shutdown.md | 26 +++++--- procrastinate/app.py | 7 +- procrastinate/cli.py | 7 ++ procrastinate/job_context.py | 17 ++++- procrastinate/manager.py | 2 +- procrastinate/worker.py | 80 +++++++++++++++-------- tests/acceptance/test_async.py | 106 ++++++++++++++++++++++++++++++- tests/acceptance/test_sync.py | 14 ++-- tests/unit/test_app.py | 2 +- tests/unit/test_builtin_tasks.py | 2 +- tests/unit/test_job_context.py | 2 +- tests/unit/test_worker.py | 6 +- 12 files changed, 220 insertions(+), 51 deletions(-) diff --git a/docs/howto/advanced/shutdown.md b/docs/howto/advanced/shutdown.md index 8abc1cae2..6ce32b787 100644 --- a/docs/howto/advanced/shutdown.md +++ b/docs/howto/advanced/shutdown.md @@ -6,13 +6,23 @@ A worker will keep running until: - [task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) is called on the task created from `app.run_worker_async` When a worker is requested to stop, it will attempt to gracefully shut down by waiting for all running jobs to complete. -If a `shutdown_timeout` option is specified, the worker will attempt to abort all jobs that have not completed by that time. Cancelling the `run_worker_async` task a second time also results in the worker aborting running jobs. +If a `shutdown_graceful_timeout` option is specified, the worker will attempt to abort all jobs that have not completed by that time. Cancelling the `run_worker_async` task a second time also results in the worker aborting running jobs. + +The worker will then wait for all jobs to complete. + :::{note} -The worker aborts its remaining jobs by calling [task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) on the underlying asyncio task that runs the job. +The worker aborts its remaining jobs by: +- setting the context so that `JobContext.should_abort` returns `AbortReason.SHUTDOWN` +- calling [task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) on the underlying asyncio task that runs the job when the job is asynchronous + +Jobs that do not respect the request to abort will prevent the worker from shutting down until they complete. In a way, it will remain a graceful shutdown for those jobs even after `shutdown_graceful_timeout`. -It is possible for that task to handle `asyncio.CancelledError` and even suppress the cancellation. +For more information, see {doc}`./cancellation`. + +Currently, Procrastinate does not provide a built-in method to forcefully terminate a worker. This is something you would want to do with your process manager (e.g. systemd, Docker, Kubernetes), which typically offers options to control process termination. In that case, your jobs will be considered stale, see {doc}`../production/retry_stalled_jobs`. ::: + ## Examples ### Run a worker until no job is left @@ -29,15 +39,15 @@ async with app.open_async(): async with app.open_async(): # give jobs up to 10 seconds to complete when a stop signal is received # all jobs still running after 10 seconds are aborted - # In the absence of shutdown_timeout, the task will complete when all jobs have completed. - await app.run_worker_async(shutdown_timeout=10) + # In the absence of shutdown_graceful_timeout, the task will complete when all jobs have completed. + await app.run_worker_async(shutdown_graceful_timeout=10) ``` ### Run a worker until its Task is cancelled ```python async with app.open_async(): - worker = asyncio.create_task(app run_worker_async()) + worker = asyncio.create_task(app.run_worker_async()) # eventually worker.cancel() try: @@ -51,7 +61,7 @@ async with app.open_async(): ```python async with app.open_async(): - worker = asyncio.create_task(app.run_worker_async(shutdown_timeout=10)) + worker = asyncio.create_task(app.run_worker_async(shutdown_graceful_timeout=10)) # eventually worker.cancel() try: @@ -66,7 +76,7 @@ async with app.open_async(): ```python async with app.open_async(): - # Notice that shutdown_timeout is not specified + # Notice that shutdown_graceful_timeout is not specified worker = asyncio.create_task(app.run_worker_async()) # eventually diff --git a/procrastinate/app.py b/procrastinate/app.py index d6f18ff81..0def8bba2 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -30,7 +30,7 @@ class WorkerOptions(TypedDict): wait: NotRequired[bool] fetch_job_polling_interval: NotRequired[float] abort_job_polling_interval: NotRequired[float] - shutdown_timeout: NotRequired[float] + shutdown_graceful_timeout: NotRequired[float] listen_notify: NotRequired[bool] delete_jobs: NotRequired[str | jobs.DeleteJobCondition] additional_context: NotRequired[dict[str, Any]] @@ -289,10 +289,11 @@ async def run_worker_async(self, **kwargs: Unpack[WorkerOptions]) -> None: mechanism and can reasonably be set to a higher value. (defaults to 5.0) - shutdown_timeout: ``float`` + shutdown_graceful_timeout: ``float`` Indicates the maximum duration (in seconds) the worker waits for jobs to - complete when requested stop. Jobs that have not been completed by that time + complete when requested to stop. Jobs that have not been completed by that time are aborted. A value of None corresponds to no timeout. + (defaults to None) listen_notify : ``bool`` If ``True``, allocates a connection from the pool to diff --git a/procrastinate/cli.py b/procrastinate/cli.py index 60a1719fa..df058ab4e 100644 --- a/procrastinate/cli.py +++ b/procrastinate/cli.py @@ -305,6 +305,13 @@ def configure_worker_parser(subparsers: argparse._SubParsersAction): help="How often to polling for abort requests", envvar="WORKER_ABORT_JOB_POLLING_INTERVAL", ) + add_argument( + worker_parser, + "--shutdown-graceful-timeout", + type=float, + help="How long to wait for jobs to complete when shutting down before aborting them", + envvar="WORKER_SHUTDOWN_GRACEFUL_TIMEOUT", + ) add_argument( worker_parser, "-w", diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index 8d843e3e4..f77e98834 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -1,6 +1,7 @@ from __future__ import annotations import time +from enum import Enum from typing import Any, Callable, Iterable import attr @@ -32,6 +33,17 @@ def as_dict(self): return result +class AbortReason(Enum): + """ + An enumeration of reasons a job is being aborted + """ + + USER_REQUEST = "user_request" #: The user requested to abort the job + SHUTDOWN = ( + "shutdown" #: The job is being aborted as part of shutting down the worker + ) + + @attr.dataclass(frozen=True, kw_only=True) class JobContext: """ @@ -51,7 +63,10 @@ class JobContext: additional_context: dict = attr.ib(factory=dict) - should_abort: Callable[[], bool] + abort_reason: Callable[[], AbortReason | None] + + def should_abort(self) -> bool: + return bool(self.abort_reason()) def evolve(self, **update: Any) -> JobContext: return attr.evolve(self, **update) diff --git a/procrastinate/manager.py b/procrastinate/manager.py index a80c3bf75..b51e266d1 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -570,7 +570,7 @@ def list_jobs( status: str | None = None, lock: str | None = None, queueing_lock: str | None = None, - ) -> Iterable[jobs.Job]: + ) -> list[jobs.Job]: """ Sync version of `list_jobs_async` """ diff --git a/procrastinate/worker.py b/procrastinate/worker.py index ab73c130d..c52aa34b7 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -39,7 +39,7 @@ def __init__( wait: bool = True, fetch_job_polling_interval: float = FETCH_JOB_POLLING_INTERVAL, abort_job_polling_interval: float = ABORT_JOB_POLLING_INTERVAL, - shutdown_timeout: float | None = None, + shutdown_graceful_timeout: float | None = None, listen_notify: bool = True, delete_jobs: str | jobs.DeleteJobCondition | None = None, additional_context: dict[str, Any] | None = None, @@ -71,8 +71,8 @@ def __init__( self._running_jobs: dict[asyncio.Task, job_context.JobContext] = {} self._job_semaphore = asyncio.Semaphore(self.concurrency) self._stop_event = asyncio.Event() - self.shutdown_timeout = shutdown_timeout - self._job_ids_to_abort = set() + self.shutdown_graceful_timeout = shutdown_graceful_timeout + self._job_ids_to_abort: dict[int, job_context.AbortReason] = dict() def stop(self): if self._stop_event.is_set(): @@ -149,7 +149,8 @@ async def _persist_job_status( job=job, status=status, delete_job=delete_job ) - self._job_ids_to_abort.discard(job.id) + assert job.id + self._job_ids_to_abort.pop(job.id, None) self.logger.debug( f"Acknowledged job completion {job.call_string}", @@ -171,8 +172,10 @@ def _log_job_outcome( ): if status == jobs.Status.SUCCEEDED: log_action, log_title = "job_success", "Success" - elif status == jobs.Status.ABORTED: + elif status == jobs.Status.ABORTED and not job_retry: log_action, log_title = "job_aborted", "Aborted" + elif status == jobs.Status.ABORTED and job_retry: + log_action, log_title = "job_aborted_retry", "Aborted, to retry" elif job_retry: log_action, log_title = "job_error_retry", "Error, to retry" else: @@ -252,7 +255,10 @@ async def ensure_async() -> Callable[..., Awaitable]: except BaseException as e: exc_info = e - if not isinstance(e, exceptions.JobAborted): + # aborted job can be retried if it is caused by a shutdown. + if not (isinstance(e, exceptions.JobAborted)) or ( + context.abort_reason() == job_context.AbortReason.SHUTDOWN + ): job_retry = ( task.get_retry_exception(exception=e, job=job) if task else None ) @@ -321,7 +327,6 @@ async def _fetch_and_process_jobs(self): break job_id = job.id - assert job_id context = job_context.JobContext( app=self.app, @@ -331,7 +336,9 @@ async def _fetch_and_process_jobs(self): if self.additional_context else {}, job=job, - should_abort=lambda: job_id in self._job_ids_to_abort, + abort_reason=lambda: self._job_ids_to_abort.get(job_id) + if job_id + else None, start_timestamp=time.time(), ) job_task = asyncio.create_task( @@ -357,19 +364,11 @@ async def run(self): try: # shield the loop task from cancellation # instead, a stop event is set to enable graceful shutdown - await utils.wait_any(asyncio.shield(loop_task), self._stop_event.wait()) - if self._stop_event.is_set(): - try: - await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) - except asyncio.TimeoutError: - pass + await asyncio.shield(loop_task) except asyncio.CancelledError: # worker.run is cancelled, usually by cancelling app.run_worker_async self.stop() - try: - await asyncio.wait_for(loop_task, timeout=self.shutdown_timeout) - except asyncio.TimeoutError: - pass + await loop_task raise async def _handle_notification( @@ -404,16 +403,24 @@ async def _poll_jobs_to_abort(self): def _handle_abort_jobs_requested(self, job_ids: Iterable[int]): running_job_ids = {c.job.id for c in self._running_jobs.values() if c.job.id} - new_job_ids_to_abort = (running_job_ids & set(job_ids)) - self._job_ids_to_abort + new_job_ids_to_abort = (running_job_ids & set(job_ids)) - set( + self._job_ids_to_abort + ) for process_job_task, context in self._running_jobs.items(): if context.job.id in new_job_ids_to_abort: - self._abort_job(process_job_task, context) + self._abort_job( + process_job_task, context, job_context.AbortReason.USER_REQUEST + ) def _abort_job( - self, process_job_task: asyncio.Task, context: job_context.JobContext + self, + process_job_task: asyncio.Task, + context: job_context.JobContext, + reason: job_context.AbortReason, ): - self._job_ids_to_abort.add(context.job.id) + assert context.job.id + self._job_ids_to_abort[context.job.id] = reason log_message: str task = self.app.tasks.get(context.job.task_name) @@ -447,9 +454,26 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): ), ) - # wait for any in progress job to complete processing - # use return_exceptions to not cancel other job tasks if one was to fail - await asyncio.gather(*self._running_jobs, return_exceptions=True) + if self._running_jobs: + await asyncio.wait( + self._running_jobs, timeout=self.shutdown_graceful_timeout + ) + + # As a reminder, tasks have a done callback that + # removes them from the self._running_jobs dict, + # so as the tasks stop, this dict will shrink. + if self._running_jobs: + self.logger.info( + f"{len(self._running_jobs)} jobs still running after graceful timeout. Aborting them", + extra=self._log_extra( + action="stop_worker", + queues=self.queues, + context=None, + job_result=None, + ), + ) + await self._abort_running_jobs() + self.logger.info( f"Stopped worker on {utils.queues_display(self.queues)}", extra=self._log_extra( @@ -457,6 +481,12 @@ async def _shutdown(self, side_tasks: list[asyncio.Task]): ), ) + async def _abort_running_jobs(self): + for task, context in self._running_jobs.items(): + self._abort_job(task, context, job_context.AbortReason.SHUTDOWN) + + await asyncio.gather(*self._running_jobs, return_exceptions=True) + def _start_side_tasks(self) -> list[asyncio.Task]: """Start side tasks such as periodic deferrer and notification listener""" side_tasks = [ diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index 37b1d860f..5a2e2f875 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -8,6 +8,7 @@ from procrastinate import app as app_module from procrastinate.contrib import aiopg from procrastinate.exceptions import JobAborted +from procrastinate.job_context import JobContext from procrastinate.jobs import Status @@ -298,7 +299,9 @@ async def appender(a: int): assert status == Status.SUCCEEDED -async def test_stop_worker_aborts_jobs_past_shutdown_timeout(async_app: app_module.App): +async def test_stop_worker_aborts_async_jobs_past_shutdown_graceful_timeout( + async_app: app_module.App, +): slow_job_cancelled = False @async_app.task(queue="default", name="fast_job") @@ -318,7 +321,7 @@ async def slow_job(): slow_job_id = await slow_job.defer_async() run_task = asyncio.create_task( - async_app.run_worker_async(wait=False, shutdown_timeout=0.3) + async_app.run_worker_async(wait=False, shutdown_graceful_timeout=0.3) ) await asyncio.sleep(0.05) @@ -332,3 +335,102 @@ async def slow_job(): assert slow_job_status == Status.ABORTED assert slow_job_cancelled + + +async def test_stop_worker_retries_async_jobs_past_shutdown_graceful_timeout( + async_app: app_module.App, +): + slow_job_cancelled = False + + @async_app.task(queue="default", name="slow_job", retry=1) + async def slow_job(): + nonlocal slow_job_cancelled + try: + await asyncio.sleep(2) + except asyncio.CancelledError: + slow_job_cancelled = True + raise + + slow_job_id = await slow_job.defer_async() + + run_task = asyncio.create_task( + async_app.run_worker_async(wait=False, shutdown_graceful_timeout=0.3) + ) + await asyncio.sleep(0.05) + + with pytest.raises(asyncio.CancelledError): + run_task.cancel() + await run_task + + slow_job_status = await async_app.job_manager.get_job_status_async(slow_job_id) + assert slow_job_cancelled + assert slow_job_status == Status.TODO + + +async def test_stop_worker_aborts_sync_jobs_past_shutdown_graceful_timeout( + async_app: app_module.App, +): + slow_job_cancelled = False + + @async_app.task(queue="default", name="fast_job") + async def fast_job(): + pass + + @async_app.task(queue="default", name="slow_job", pass_context=True) + def slow_job(context: JobContext): + nonlocal slow_job_cancelled + while True: + time.sleep(0.05) + if context.should_abort(): + slow_job_cancelled = True + raise JobAborted() + + fast_job_id = await fast_job.defer_async() + slow_job_id = await slow_job.defer_async() + + run_task = asyncio.create_task( + async_app.run_worker_async(wait=False, shutdown_graceful_timeout=0.3) + ) + await asyncio.sleep(0.05) + + with pytest.raises(asyncio.CancelledError): + run_task.cancel() + await run_task + + fast_job_status = await async_app.job_manager.get_job_status_async(fast_job_id) + slow_job_status = await async_app.job_manager.get_job_status_async(slow_job_id) + assert fast_job_status == Status.SUCCEEDED + assert slow_job_status == Status.ABORTED + + assert slow_job_cancelled + + +async def test_stop_worker_retries_sync_jobs_past_shutdown_graceful_timeout( + async_app: app_module.App, +): + slow_job_cancelled = False + + @async_app.task(queue="default", name="slow_job", retry=1, pass_context=True) + def slow_job(context: JobContext): + nonlocal slow_job_cancelled + while True: + time.sleep(0.05) + if context.should_abort(): + slow_job_cancelled = True + raise JobAborted() + + slow_job_id = await slow_job.defer_async() + + run_task = asyncio.create_task( + async_app.run_worker_async(wait=False, shutdown_graceful_timeout=0.3) + ) + await asyncio.sleep(0.05) + + with pytest.raises(asyncio.CancelledError): + run_task.cancel() + await run_task + + slow_job_status = await async_app.job_manager.get_job_status_async(slow_job_id) + assert slow_job_status == Status.TODO + + assert slow_job_cancelled diff --git a/tests/acceptance/test_sync.py b/tests/acceptance/test_sync.py index 1de3be071..f74607743 100644 --- a/tests/acceptance/test_sync.py +++ b/tests/acceptance/test_sync.py @@ -33,7 +33,7 @@ async def async_app(not_opened_psycopg_connector): # Even if we test the purely sync parts, we'll still need an async worker to execute # the tasks -async def test_defer(sync_app, async_app): +async def test_defer(sync_app: procrastinate.App, async_app: procrastinate.App): sum_results = [] product_results = [] @@ -58,7 +58,9 @@ async def product_task(a, b): assert product_results == [12] -async def test_nested_sync_to_async(sync_app, async_app): +async def test_nested_sync_to_async( + sync_app: procrastinate.App, async_app: procrastinate.App +): sum_results = [] @sync_app.task(queue="default", name="sum_task") @@ -81,7 +83,9 @@ def _inner_sum_task_sync(a, b): assert sum_results == [3] -async def test_sync_task_runs_in_parallel(sync_app, async_app): +async def test_sync_task_runs_in_parallel( + sync_app: procrastinate.App, async_app: procrastinate.App +): results = [] @sync_app.task(queue="default", name="sync_task_1") @@ -105,7 +109,7 @@ def sync_task_2(): assert results == [0, 0, 1, 1, 2, 2] -async def test_cancel(sync_app, async_app): +async def test_cancel(sync_app: procrastinate.App, async_app: procrastinate.App): sum_results = [] @sync_app.task(queue="default", name="sum_task") @@ -131,7 +135,7 @@ def sum_task(a, b): assert sum_results == [7] -def test_no_job_to_cancel_found(sync_app): +def test_no_job_to_cancel_found(sync_app: procrastinate.App): @sync_app.task(queue="default", name="example_task") def example_task(): pass diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index b310dc61c..1f3aeb1bb 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -114,7 +114,7 @@ async def my_task(a): await asyncio.sleep(3) result.append(a) - task = asyncio.create_task(app.run_worker_async(shutdown_timeout=0.1)) + task = asyncio.create_task(app.run_worker_async(shutdown_graceful_timeout=0.1)) await my_task.defer_async(a=1) await asyncio.sleep(0.01) task.cancel() diff --git a/tests/unit/test_builtin_tasks.py b/tests/unit/test_builtin_tasks.py index d09d22440..716ffdf99 100644 --- a/tests/unit/test_builtin_tasks.py +++ b/tests/unit/test_builtin_tasks.py @@ -12,7 +12,7 @@ async def test_remove_old_jobs(app: App, job_factory): job = job_factory() await builtin_tasks.remove_old_jobs( job_context.JobContext( - app=app, job=job, should_abort=lambda: False, start_timestamp=time.time() + app=app, job=job, abort_reason=lambda: None, start_timestamp=time.time() ), max_hours=2, queue="queue_a", diff --git a/tests/unit/test_job_context.py b/tests/unit/test_job_context.py index 8cd71ca87..f6eab0957 100644 --- a/tests/unit/test_job_context.py +++ b/tests/unit/test_job_context.py @@ -52,6 +52,6 @@ def test_evolve(app: App, job_factory): app=app, job=job, worker_name="a", - should_abort=lambda: False, + abort_reason=lambda: None, ) assert context.evolve(worker_name="b").worker_name == "b" diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 2fe6a7103..2871d0441 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -291,7 +291,7 @@ async def task_func(): @pytest.mark.parametrize("mode", [("stop"), ("cancel")]) async def test_stopping_worker_aborts_job_after_timeout(app: App, worker, mode): complete_task_event = asyncio.Event() - worker.shutdown_timeout = 0.02 + worker.shutdown_graceful_timeout = 0.02 task_cancelled = False @@ -336,7 +336,7 @@ async def task_func(): async def test_stopping_worker_job_suppresses_cancellation(app: App, worker): complete_task_event = asyncio.Event() - worker.shutdown_timeout = 0.02 + worker.shutdown_graceful_timeout = 0.02 @app.task() async def task_func(): @@ -625,7 +625,7 @@ async def task_func(job_context: JobContext): status = await app.job_manager.get_job_status_async(job_id) assert status == Status.ABORTED assert ( - worker._job_ids_to_abort == set() + worker._job_ids_to_abort == {} ), "Expected cancelled job id to be removed from set" From 772b41abc50dbf3d98e01f11847358cf3cc7240c Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Thu, 17 Oct 2024 22:23:48 +0000 Subject: [PATCH 064/375] Drop Python 3.8 support --- .pre-commit-config.yaml | 1 - poetry.lock | 59 +------------------ .../contrib/django/migrations_utils.py | 14 +---- procrastinate/schema.py | 19 ++---- procrastinate/sql/__init__.py | 15 +---- pyproject.toml | 5 +- 6 files changed, 14 insertions(+), 99 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d9423e4c5..ade087ef0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,6 @@ repos: - croniter==3.0.3 - django-stubs==5.1.0 - django==5.1.1 - - importlib-resources==6.4.5 - psycopg2-binary==2.9.9 - psycopg[pool]==3.2.2 - python-dateutil==2.9.0.post0 diff --git a/poetry.lock b/poetry.lock index a5b49008e..1074696b3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -109,40 +109,9 @@ files = [ {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -411,7 +380,6 @@ files = [ [package.dependencies] asgiref = ">=3.6.0,<4" -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -668,28 +636,6 @@ perf = ["ipython"] test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] -[[package]] -name = "importlib-resources" -version = "6.4.5" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, - {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -982,7 +928,6 @@ files = [ ] [package.dependencies] -"backports.zoneinfo" = {version = ">=0.2.0", markers = "python_version < \"3.9\""} psycopg-binary = {version = "3.2.2", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} @@ -1897,5 +1842,5 @@ sqlalchemy = ["sqlalchemy"] [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "1b96e2c5ab73a198d5ec1f410cbd3f27d0fd28c5e3a7caac32c6567e76efb048" +python-versions = "^3.9" +content-hash = "8d4350e830593b495e8caa03a3be60037cdcd221d240b26adf75199a2eb02ff1" diff --git a/procrastinate/contrib/django/migrations_utils.py b/procrastinate/contrib/django/migrations_utils.py index 1b3ff018b..e678d12a4 100644 --- a/procrastinate/contrib/django/migrations_utils.py +++ b/procrastinate/contrib/django/migrations_utils.py @@ -1,20 +1,10 @@ from __future__ import annotations import functools -import sys -from typing import TYPE_CHECKING +from importlib import resources from django.db import migrations -if TYPE_CHECKING: - import importlib_resources -else: - # https://github.com/pypa/twine/pull/551 - if sys.version_info[:2] < (3, 9): # coverage: exclude - import importlib_resources - else: # coverage: exclude - import importlib.resources as importlib_resources - @functools.lru_cache(maxsize=None) def list_migration_files() -> dict[str, str]: @@ -23,7 +13,7 @@ def list_migration_files() -> dict[str, str]: """ return { p.name: p.read_text(encoding="utf-8") - for p in importlib_resources.files("procrastinate.sql.migrations").iterdir() + for p in resources.files("procrastinate.sql.migrations").iterdir() if p.name.endswith(".sql") } diff --git a/procrastinate/schema.py b/procrastinate/schema.py index b32aef2bd..f393fae63 100644 --- a/procrastinate/schema.py +++ b/procrastinate/schema.py @@ -1,20 +1,11 @@ from __future__ import annotations import pathlib -import sys -from typing import TYPE_CHECKING, cast +from importlib import resources +from typing import cast from typing_extensions import LiteralString -if TYPE_CHECKING: - import importlib_resources -else: - # https://github.com/pypa/twine/pull/551 - if sys.version_info[:2] < (3, 9): # coverage: exclude - import importlib_resources - else: # coverage: exclude - import importlib.resources as importlib_resources - from procrastinate import connector as connector_module migrations_path = pathlib.Path(__file__).parent / "sql" / "migrations" @@ -29,9 +20,9 @@ def get_schema() -> LiteralString: # procrastinate takes full responsibility for the queries, we # can safely vouch for them being as safe as if they were # defined in the code itself. - schema_sql = ( - importlib_resources.files("procrastinate.sql") / "schema.sql" - ).read_text(encoding="utf-8") + schema_sql = (resources.files("procrastinate.sql") / "schema.sql").read_text( + encoding="utf-8" + ) return cast(LiteralString, schema_sql) @staticmethod diff --git a/procrastinate/sql/__init__.py b/procrastinate/sql/__init__.py index fd4c42ccf..e777650f0 100644 --- a/procrastinate/sql/__init__.py +++ b/procrastinate/sql/__init__.py @@ -1,20 +1,11 @@ from __future__ import annotations import re -import sys -from typing import TYPE_CHECKING, cast +from importlib import resources +from typing import cast from typing_extensions import LiteralString -if TYPE_CHECKING: - import importlib_resources -else: - # https://github.com/pypa/twine/pull/551 - if sys.version_info[:2] < (3, 9): # coverage: exclude - import importlib_resources - else: # coverage: exclude - import importlib.resources as importlib_resources - QUERIES_REGEX = re.compile(r"(?:\n|^)-- ([a-z0-9_]+) --\n(?:-- .+\n)*", re.MULTILINE) @@ -37,7 +28,7 @@ def parse_query_file(query_file: str) -> dict[str, LiteralString]: def get_queries() -> dict[str, LiteralString]: return parse_query_file( - (importlib_resources.files("procrastinate.sql") / "queries.sql").read_text( + (resources.files("procrastinate.sql") / "queries.sql").read_text( encoding="utf-8" ) ) diff --git a/pyproject.toml b/pyproject.toml index 05b35b534..63a014496 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ documentation = "https://procrastinate.readthedocs.io/" procrastinate = 'procrastinate.cli:main' [tool.poetry.dependencies] -python = "^3.8" +python = "^3.9" aiopg = { version = "*", optional = true } anyio = "*" asgiref = "*" @@ -31,12 +31,11 @@ attrs = "*" contextlib2 = { version = "*", python = "<3.10" } croniter = "*" django = { version = ">=2.2", optional = true } -importlib-resources = { version = ">=1.4", python = "<3.9" } psycopg = { extras = ["pool"], version = "*" } psycopg2-binary = { version = "*", optional = true } python-dateutil = "*" sqlalchemy = { version = "^2.0", optional = true } -typing-extensions = { version = "*", python = "<3.8" } +typing-extensions = "*" sphinx = { version = "*", optional = true } [tool.poetry.extras] From 5694f8639cbed29d3b9f2b23079d75e529d3176e Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 19:30:45 +0100 Subject: [PATCH 065/375] Upgrade deps --- .pre-commit-config.yaml | 20 +- poetry.lock | 1232 +++++++++-------- .../contrib/sphinx/test_autodoc.py | 5 +- 3 files changed, 658 insertions(+), 599 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ade087ef0..13f178e43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,24 +34,24 @@ repos: - id: pyright additional_dependencies: - aiopg==1.4.0 - - anyio==4.5.0 + - anyio==4.6.2.post1 - asgiref==3.8.1 - attrs==24.2.0 - contextlib2==21.6.0 - - croniter==3.0.3 - - django-stubs==5.1.0 - - django==5.1.1 - - psycopg2-binary==2.9.9 - - psycopg[pool]==3.2.2 + - croniter==5.0.1 + - django-stubs==5.1.1 + - django==5.1.3 + - psycopg2-binary==2.9.10 + - psycopg[pool]==3.2.3 - python-dateutil==2.9.0.post0 - - sphinx==7.1.2 - - sqlalchemy==2.0.35 + - sphinx==7.4.7 + - sqlalchemy==2.0.36 - typing-extensions==4.12.2 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.7 + rev: v0.8.1 hooks: - id: ruff - args: [--fix, --unsafe-fixes] + args: [--fix, --unsafe-fixes, --show-fixes] - id: ruff-format - repo: https://github.com/PyCQA/doc8 rev: v1.1.2 diff --git a/poetry.lock b/poetry.lock index 1074696b3..4d8c28903 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiopg" @@ -20,24 +20,24 @@ sa = ["sqlalchemy[postgresql-psycopg2binary] (>=1.3,<1.5)"] [[package]] name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.16" +description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] name = "anyio" -version = "4.5.0" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78"}, - {file = "anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9"}, + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [package.dependencies] @@ -48,7 +48,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -146,101 +146,116 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -267,83 +282,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.8" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, ] [package.dependencies] @@ -354,13 +359,13 @@ toml = ["tomli"] [[package]] name = "croniter" -version = "3.0.3" +version = "5.0.1" description = "croniter provides iteration for datetime object with cron like format" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" files = [ - {file = "croniter-3.0.3-py2.py3-none-any.whl", hash = "sha256:b3bd11f270dc54ccd1f2397b813436015a86d30ffc5a7a9438eec1ed916f2101"}, - {file = "croniter-3.0.3.tar.gz", hash = "sha256:34117ec1741f10a7bd0ec3ad7d8f0eb8fa457a2feb9be32e6a2250e158957668"}, + {file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"}, + {file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"}, ] [package.dependencies] @@ -389,13 +394,13 @@ bcrypt = ["bcrypt"] [[package]] name = "django" -version = "5.1.1" +version = "5.1.3" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"}, - {file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"}, + {file = "Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818"}, + {file = "Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a"}, ] [package.dependencies] @@ -409,37 +414,37 @@ bcrypt = ["bcrypt"] [[package]] name = "django-stubs" -version = "5.1.0" +version = "5.1.1" description = "Mypy stubs for Django" optional = false python-versions = ">=3.8" files = [ - {file = "django_stubs-5.1.0-py3-none-any.whl", hash = "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40"}, - {file = "django_stubs-5.1.0.tar.gz", hash = "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5"}, + {file = "django_stubs-5.1.1-py3-none-any.whl", hash = "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac"}, + {file = "django_stubs-5.1.1.tar.gz", hash = "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b"}, ] [package.dependencies] asgiref = "*" django = "*" -django-stubs-ext = ">=5.1.0" +django-stubs-ext = ">=5.1.1" tomli = {version = "*", markers = "python_version < \"3.11\""} types-PyYAML = "*" typing-extensions = ">=4.11.0" [package.extras] -compatible-mypy = ["mypy (>=1.11.0,<1.12.0)"] +compatible-mypy = ["mypy (>=1.12,<1.14)"] oracle = ["oracledb"] redis = ["redis"] [[package]] name = "django-stubs-ext" -version = "5.1.0" +version = "5.1.1" description = "Monkey-patching and extensions for django-stubs" optional = false python-versions = ">=3.8" files = [ - {file = "django_stubs_ext-5.1.0-py3-none-any.whl", hash = "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d"}, - {file = "django_stubs_ext-5.1.0.tar.gz", hash = "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926"}, + {file = "django_stubs_ext-5.1.1-py3-none-any.whl", hash = "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c"}, + {file = "django_stubs_ext-5.1.1.tar.gz", hash = "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c"}, ] [package.dependencies] @@ -448,24 +453,24 @@ typing-extensions = "*" [[package]] name = "docutils" -version = "0.20.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] name = "dunamai" -version = "1.22.0" +version = "1.23.0" description = "Dynamic version generation" optional = false python-versions = ">=3.5" files = [ - {file = "dunamai-1.22.0-py3-none-any.whl", hash = "sha256:eab3894b31e145bd028a74b13491c57db01986a7510482c9b5fff3b4e53d77b7"}, - {file = "dunamai-1.22.0.tar.gz", hash = "sha256:375a0b21309336f0d8b6bbaea3e038c36f462318c68795166e31f9873fdad676"}, + {file = "dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041"}, + {file = "dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4"}, ] [package.dependencies] @@ -690,71 +695,72 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -808,38 +814,43 @@ pg = ["psycopg2-binary"] [[package]] name = "mypy" -version = "1.11.2" +version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, - {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, - {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, - {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, - {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, - {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, - {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, - {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, - {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, - {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, - {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, - {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, - {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, - {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, - {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, - {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, - {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, - {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, - {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, ] [package.dependencies] @@ -849,6 +860,7 @@ typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -892,13 +904,13 @@ testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0, [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -918,24 +930,24 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "psycopg" -version = "3.2.2" +version = "3.2.3" description = "PostgreSQL database adapter for Python" optional = false python-versions = ">=3.8" files = [ - {file = "psycopg-3.2.2-py3-none-any.whl", hash = "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2"}, - {file = "psycopg-3.2.2.tar.gz", hash = "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0"}, + {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, + {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, ] [package.dependencies] -psycopg-binary = {version = "3.2.2", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +psycopg-binary = {version = "3.2.3", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] -binary = ["psycopg-binary (==3.2.2)"] -c = ["psycopg-c (==3.2.2)"] +binary = ["psycopg-binary (==3.2.3)"] +c = ["psycopg-c (==3.2.3)"] dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] pool = ["psycopg-pool"] @@ -943,86 +955,86 @@ test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", [[package]] name = "psycopg-binary" -version = "3.2.2" +version = "3.2.3" description = "PostgreSQL database adapter for Python -- C optimisation distribution" optional = false python-versions = ">=3.8" files = [ - {file = "psycopg_binary-3.2.2-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8eacbf58d4f8d7bc82e0a60476afa2622b5a58f639a3cc2710e3e37b72aff3cb"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d07e62476ee8c54853b2b8cfdf3858a574218103b4cd213211f64326c7812437"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c22e615ee0ecfc6687bb8a39a4ed9d6bac030b5e72ac15e7324fd6e48979af71"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec29c7ec136263628e3f09a53e51d0a4b1ad765a6e45135707bfa848b39113f9"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:035753f80cbbf6aceca6386f53e139df70c7aca057b0592711047b5a8cfef8bb"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ee99336151ff7c30682f2ef9cb1174d235bc1471322faabba97f9db1398167"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a60674dff4a4194e88312b463fb84ac80924c2b9e25d0e0460f3176bf1af4a6b"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3c701507a49340de422d77a6ce95918a0019990bbf27daec35aa40050c6eadb6"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b3c5a04eaf8866e399315cff2e810260cce10b797437a9f49fd71b5f4b94d0a"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ad9c09de4c262f516ae6891d042a4325649b18efa39dd82bbe0f7bc95c37bfb"}, - {file = "psycopg_binary-3.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:bf1d3582185cb43ecc27403bee2f5405b7a45ccaab46c8508d9a9327341574fc"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:554d208757129d34fa47b7c890f9ef922f754e99c6b089cb3a209aa0fe282682"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:71dc3cc10d1fd7d26a3079d0a5b4a8e8ad0d7b89a702ceb7605a52e4395be122"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86f578d63f2e1fdf87c9adaed4ff23d7919bda8791cf1380fa4cf3a857ccb8b"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a4eb737682c02a602a12aa85a492608066f77793dab681b1c4e885fedc160b1"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e120a576e74e4e612c48f4b021e322e320ca102534d78a0ca4db2ffd058ae8d"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849d518e7d4c6186e1e48ea2ac2671912edf7e732fffe6f01dfed61cf0245de4"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ee2b19152bcec8f356f989c31768702be5f139b4d51094273c4a9ddc8c55380"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:00273dd011892e8216fcef76b42f775ddaa6348664a7fffae2a27c9557f45bfa"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4bcb489615d7e56d1de42937e6a0fc13f766505729afdb54c2947a52db295220"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06963f88916a177df95aaed27101af0989ba206654743b1a0e050b9d8e734686"}, - {file = "psycopg_binary-3.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed1ad836a0c21890c7f84e73c7ef1ed0950e0e4b0d8e49b609b6fd9c13f2ca21"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0dd314229885a81f9497875295d8788e651b78945627540f1e78ed71595e614a"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:989acbe2f552769cdb780346cea32d86e7c117044238d5172ac10b025fe47194"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566b1c530898590f0ac9d949cf94351c08d73c89f8800c74c0a63ffd89a383c8"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68d03efab7e2830a0df3aa4c29a708930e3f6b9fd98774ff9c4fd1f33deafecc"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e1f013bfb744023df23750fde51edcb606def8328473361db3c192c392c6060"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a06136aab55a2de7dd4e2555badae276846827cfb023e6ba1b22f7a7b88e3f1b"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:020c5154be144a1440cf87eae012b9004fb414ae4b9e7b1b9fb808fe39e96e83"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef341c556aeaa43a2729b07b04e20bfffdcf3d96c4a96e728ca94fe4ce632d8c"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66de2dd7d37bf66eb234ca9d907f5cd8caca43ff8d8a50dd5c15844d1cf0390c"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2eb6f8f410dbbb71b8c633f283b8588b63bee0a7321f00ab76e9c800c593f732"}, - {file = "psycopg_binary-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b45553c6b614d02e1486585980afdfd18f0000aac668e2e87c6e32da1adb051a"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:1ee891287c2da57e7fee31fbe2fbcdf57125768133d811b02e9523d5a052eb28"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5e95e4a8076ac7611e571623e1113fa84fd48c0459601969ffbf534d7aa236e7"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6269d79a3d7d76b6fcf0fafae8444da00e83777a6c68c43851351a571ad37155"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6dd5d21a298c3c53af20ced8da4ae4cd038c6fe88c80842a8888fa3660b2094"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cf64e41e238620f05aad862f06bc8424f8f320d8075f1499bd85a225d18bd57"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c482c3236ded54add31136a91d5223b233ec301f297fa2db79747404222dca6"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0718be095cefdad712542169d16fa58b3bd9200a3de1b0217ae761cdec1cf569"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fb303b03c243a9041e1873b596e246f7caaf01710b312fafa65b1db5cd77dd6f"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:705da5bc4364bd7529473225fca02b795653bc5bd824dbe43e1df0b1a40fe691"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:05406b96139912574571b1c56bb023839a9146cf4b57c4548f36251dd5909fa1"}, - {file = "psycopg_binary-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:7c357cf87e8d7612cfe781225be7669f35038a765d1b53ec9605f6c5aef9ee85"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:059aa5e8fa119de328b4cb02ee80775443763b25682a02dd7d026b8d4f565834"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05a50f94e1e4fa37a0074b09263b83b0aa038c3c72068a61f1ad61ea449ef9d5"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:951507b3d77a64c907afe893e01e09b41051fd7e27e9462f450fb8bb64bc22b0"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ec4986c4ac2503e865acd3943d179531c3bbfa5a1c8ee81fcfccb551dad645f"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b32b0e838841d5b109d32fc706b8bc64e50c161fee3f1371ccf696e5598bc49"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fdc74a83348477b28bea9e7b391c9fc189b480fe3cd0e46bb989514410b64d60"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9efe0ca78be4a573b4b81226904c711cfadc4783d64bfdf58a3394da7c1a1354"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:51f56ae2898acaa33623adad96ddc5acbb5e2f72f2fc020065c8be05c0e01dce"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:43b209be0424e8abece428a884cb711f504e3526dfbcb0bf51529907a55eda15"}, - {file = "psycopg_binary-3.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:d3c147eea9f3950a34133dc187e8d3534e54ff4a178a4ebd8993b2c97e123200"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6c7b6a8d4e1b77cdb50192b61235b33fc2f1d28c67627fc93a1d43e9130dd479"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e234edc4bb746d8ac3daae8753ee38eaa7af2ee333a1d35ce6b02a02874aed18"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f12640ba92c538b3b64a199a918d3bb0cc0d7f7123c6ba93cb065e1a2d049f0"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8937dc548621b336b0d8383a3470fb7192b42a108c760a152282909867bf5b26"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4afbb97d64cd8078edec859b07859a18ef3de7261a3a873ba52f32548373ae92"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c432710bdf8ccfdd75b0bc9cdf1fd21ff394363e4daec099c667f3c5f1721e2b"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:366cc4e194f7feb4e3038d6775fd4b69835e7d923972aee5baec986de972abd6"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b286ed65a891928bd457ffa0cd5fec09b9b5208bfd096d087e45369f07c5cb85"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fee41c99312002e5d1f7462b1954aefed44c6efe5f021c3eac311640c16f6b7"}, - {file = "psycopg_binary-3.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:87cceaf07760a04023596f9ca1d4e929d38ae8d778161cb3e8d27a0f990dd264"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749"}, ] [[package]] name = "psycopg-pool" -version = "3.2.3" +version = "3.2.4" description = "Connection Pool for Psycopg" optional = false python-versions = ">=3.8" files = [ - {file = "psycopg_pool-3.2.3-py3-none-any.whl", hash = "sha256:53bd8e640625e01b2927b2ad96df8ed8e8f91caea4597d45e7673fc7bbb85eb1"}, - {file = "psycopg_pool-3.2.3.tar.gz", hash = "sha256:bb942f123bef4b7fbe4d55421bd3fb01829903c95c0f33fd42b7e94e5ac9b52a"}, + {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"}, + {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"}, ] [package.dependencies] @@ -1030,83 +1042,78 @@ typing-extensions = ">=4.6" [[package]] name = "psycopg2-binary" -version = "2.9.9" +version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, ] [[package]] @@ -1125,13 +1132,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -1165,17 +1172,17 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] @@ -1326,29 +1333,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.6.7" +version = "0.8.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"}, - {file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"}, - {file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"}, - {file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"}, - {file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"}, - {file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"}, - {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"}, - {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"}, - {file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"}, - {file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"}, - {file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"}, - {file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"}, - {file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"}, - {file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"}, - {file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"}, - {file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"}, - {file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"}, - {file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"}, + {file = "ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5"}, + {file = "ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087"}, + {file = "ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5"}, + {file = "ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790"}, + {file = "ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6"}, + {file = "ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737"}, + {file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"}, ] [[package]] @@ -1367,23 +1374,23 @@ sqlalchemy = "*" [[package]] name = "setuptools" -version = "75.1.0" +version = "75.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, - {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, + {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, + {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] [[package]] name = "six" @@ -1431,38 +1438,39 @@ files = [ [[package]] name = "sphinx" -version = "7.1.2" +version = "7.4.7" description = "Python documentation generator" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, - {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, + {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, + {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.21" +alabaster = ">=0.7.14,<0.8.0" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.13" -requests = ">=2.25.0" -snowballstemmer = ">=2.0" +importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] name = "sphinx-basic-ng" @@ -1517,47 +1525,50 @@ Sphinx = "*" [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] @@ -1576,15 +1587,22 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-mermaid" -version = "0.9.2" +version = "1.0.0" description = "Mermaid diagrams in yours Sphinx powered docs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "sphinxcontrib-mermaid-0.9.2.tar.gz", hash = "sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af"}, - {file = "sphinxcontrib_mermaid-0.9.2-py3-none-any.whl", hash = "sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d"}, + {file = "sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3"}, + {file = "sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146"}, ] +[package.dependencies] +pyyaml = "*" +sphinx = "*" + +[package.extras] +test = ["defusedxml", "myst-parser", "pytest", "ruff", "sphinx"] + [[package]] name = "sphinxcontrib-programoutput" version = "0.17" @@ -1601,90 +1619,100 @@ Sphinx = ">=1.7.0" [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sqlalchemy" -version = "2.0.35" +version = "2.0.36" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b"}, - {file = "SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e"}, - {file = "SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf"}, - {file = "SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f021d334f2ca692523aaf7bbf7592ceff70c8594fad853416a81d66b35e3abf9"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05c3f58cf91683102f2f0265c0db3bd3892e9eedabe059720492dbaa4f922da1"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032d979ce77a6c2432653322ba4cbeabf5a6837f704d16fa38b5a05d8e21fa00"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2e795c2f7d7249b75bb5f479b432a51b59041580d20599d4e112b5f2046437a3"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:cc32b2990fc34380ec2f6195f33a76b6cdaa9eecf09f0c9404b74fc120aef36f"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-win32.whl", hash = "sha256:9509c4123491d0e63fb5e16199e09f8e262066e58903e84615c301dde8fa2e87"}, - {file = "SQLAlchemy-2.0.35-cp37-cp37m-win_amd64.whl", hash = "sha256:3655af10ebcc0f1e4e06c5900bb33e080d6a1fa4228f502121f28a3b1753cde5"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4c31943b61ed8fdd63dfd12ccc919f2bf95eefca133767db6fbbd15da62078ec"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a62dd5d7cc8626a3634208df458c5fe4f21200d96a74d122c83bc2015b333bc1"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0630774b0977804fba4b6bbea6852ab56c14965a2b0c7fc7282c5f7d90a1ae72"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d625eddf7efeba2abfd9c014a22c0f6b3796e0ffb48f5d5ab106568ef01ff5a"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ada603db10bb865bbe591939de854faf2c60f43c9b763e90f653224138f910d9"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c41411e192f8d3ea39ea70e0fae48762cd11a2244e03751a98bd3c0ca9a4e936"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-win32.whl", hash = "sha256:d299797d75cd747e7797b1b41817111406b8b10a4f88b6e8fe5b5e59598b43b0"}, - {file = "SQLAlchemy-2.0.35-cp38-cp38-win_amd64.whl", hash = "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-win32.whl", hash = "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3"}, - {file = "SQLAlchemy-2.0.35-cp39-cp39-win_amd64.whl", hash = "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f"}, - {file = "SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1"}, - {file = "sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, + {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, + {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, ] [package.dependencies] @@ -1698,7 +1726,7 @@ aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] @@ -1740,13 +1768,13 @@ pg = ["psycopg2"] [[package]] name = "sqlparse" -version = "0.5.1" +version = "0.5.2" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, - {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, + {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, + {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, ] [package.extras] @@ -1755,13 +1783,43 @@ doc = ["sphinx"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -1816,13 +1874,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] diff --git a/tests/integration/contrib/sphinx/test_autodoc.py b/tests/integration/contrib/sphinx/test_autodoc.py index 30b191355..d49d51dac 100644 --- a/tests/integration/contrib/sphinx/test_autodoc.py +++ b/tests/integration/contrib/sphinx/test_autodoc.py @@ -1,12 +1,13 @@ from __future__ import annotations +import pathlib + import pytest -from sphinx.testing.path import path @pytest.fixture def rootdir(): - return path(__file__).parent + return pathlib.Path(__file__).parent @pytest.fixture From 05f5a02e972c8de378fba5e72483dc0f0309346e Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 19:31:05 +0100 Subject: [PATCH 066/375] New Migrations procedure: documentation --- CONTRIBUTING.md | 126 ++++++++++++++-------------- docs/howto/django/migrations.md | 14 ++-- docs/howto/production/migrations.md | 83 +++++++++--------- 3 files changed, 114 insertions(+), 109 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 219664b6f..c5a418d1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,11 +15,17 @@ the following assumptions: - You're using `MacOS` or `Linux`, and `bash` or `zsh`. - You already have `python3` available -- You have `poetry` [installed](https://python-poetry.org/docs/#installation) -- Either you've already setup a PostgreSQL database and environment variables (`PG*`) - are set or you have `docker compose` available and port 5432 is free. -- Either `psql` and other `libpq` executables are available in the `PATH` or they - are located in `usr/local/opt/libpq/bin` (`Homebrew`). +- Either: + - you already have `poetry`, `pre-commit` and `nox` installed + - or you have `pipx` installed and you're ok installing those 3 tools with `pipx` + - or you don't have `pipx` installed but it's ok if we install it for you +- Either: + - you've already setup a PostgreSQL database and environment variables (`PG*`) + are set + - or you have `docker compose` available and port 5432 is free. +- Either: + - `psql` and other `libpq` executables are available in the `PATH` + - or they are located in `usr/local/opt/libpq/bin` (`Homebrew`). The `dev-env` script will add the `scripts` folder to your `$PATH` for the current shell, so in the following documentation, if you see `scripts/foo`, you're welcome @@ -172,26 +178,30 @@ ALTER TABLE procrastinate_jobs ADD COLUMN extra TEXT; The name of migration scripts must follow a specific pattern: ``` -xx.yy.zz_ab_very_short_description_of_your_changes.sql +xx.yy.zz_ab_{pre|post}_very_short_description_of_your_changes.sql ``` -`xx.yy.zz` is the number of the latest released version of Procrastinate. (The latest -release is the one marked `Latest release` on the [Procrastinate releases] page.) -`xx`, `yy` and `zz` must be 2-digit numbers, with leading zeros if necessary. -`ab` is the 2-digit migration script's serial number, `01` being the first number in -the series. And, finally, `very_short_description_of_your_changes` is a very short -description of the changes (wow). It is important to use underscores between the -different parts, and between words in the short description. +`xx.yy.zz` is the number of the latest released version of Procrastinate. (The +latest release is the one marked `Latest release` on the [Procrastinate +releases] page.) `xx`, `yy` and `zz` must be 2-digit numbers, with leading +zeros if necessary. `ab` is the 2-digit migration script's serial number, the +first number for each release being `01` for pre-migrations and `50` for +post-migrations. `pre` is if the migration should be applied before upgrading +the code, `post` is if the migration should be applied after upgrading the +code. And, finally, `very_short_description_of_your_changes` is a very short +description of the changes (wow). It is important to use underscores between +the different parts, and between words in the short description. -For example, let's say the latest released version of Procrastinate is `1.0.1`, and -that the `migrations` directory already includes a migration script whose serial -number is `01` for that release number. In that case, if you need to add a migration -script, its name will start with `01.00.01_02_`. +For example, let's say the latest released version of Procrastinate is `1.0.1`, +that the `migrations` directory already includes a post-migration script whose +serial number for that release number and your migration should be +applied after deploying the corresponding python code. In that case, if you +need to add a migration script, its name will start with `01.00.01_51_post_`. ### Backward-compatibility -As a Procrastinate developer, the changes that you make to the Procrastinate database -schema must be compatible with the Python code of previous Procrastinate versions. +As a Procrastinate developer, you must ensure you use pre-migrations and post-migrations +to maintain backward compatibility with previous versions of Procrastinate. For example, let's say that the current Procrastinate database schema includes an SQL function @@ -211,8 +221,8 @@ replace the old function by the new one, and add a migration script that removes function and adds the new one: ```sql -DROP FUNCTION procrastinate_func(integer, text, timestamp); -CREATE FUNCTION procrastinate_func(arg1 integer, arg2 text) +DROP FUNCTION procrastinate_func_v3(integer, text, timestamp); +CREATE FUNCTION procrastinate_func_v3(arg1 integer, arg2 text) RETURNS INT ... ``` @@ -226,57 +236,20 @@ So when you make changes to the Procrastinate database schema you must ensure th new schema still works with old versions of the Procrastinate Python code. Going back to our `procrastinate_func` example. Instead of replacing the old function -by the new one in `schema.sql`, you will leave the old function, and just add the new -one. And your migration script will just involve adding the new version of the function: +by the new one in `schema.sql`, you add a new function in pre-migrations and remove the +old function in post-migrations: ```sql -CREATE FUNCTION procrastinate_func(arg1 integer, arg2 text) +-- xx_xx_xx_01_pre_add_new_version_procrastinate_func.sql +CREATE FUNCTION procrastinate_func_v4(arg1 integer, arg2 text) RETURNS INT ... -``` - -The question that comes next is: when can the old version of `procrastinate_func` be -removed? Or more generally, when can the SQL compatibility layer be removed? - -The answer is some time after the next major version of Procrastinate! - -For example, if the current Procrastinate version is 1.5.0, the SQL compatibility layer -will be removed after 2.0.0 is released. The 2.0.0 release will be a pivot release, in -the sense that Procrastinate users who want to upgrade from, say, 1.5.0 to 2.5.0, will -need to upgrade from 1.5.0 to 2.0.0 first, and then from 2.0.0 to 2.5.0. And they will -always migrate the database schema before updating the code. -The task of removing the SQL compatibility layer after the release of a major version -(e.g. 2.0.0) is the responsibility of Procrastinate maintainers. More specifically, for -the 2.1.0 release, Procrastinate maintainers will need to edit `schema.sql` and remove -the SQL compatibility layer. - -But, as a standard developer, when you make changes to the Procrastinate database schema -that involves leaving or adding SQL statements for compatibility reasons, it's a good -idea to add a migration script for the removal of the SQL compatibility layer. This will -greatly help the Procrastinate maintainers. - -For example, let's say the current released version of Procrastinate is 1.5.0, and you -want to change the signature of `procrastinate_func` as described above. You will add -a `1.5.0` migration script (e.g. -`01.05.00_01_add_new_version_procrastinate_func.sql`) that adds the new version of -the function, as already described above. And you will also add a `2.0.0` migration -script (e.g. `02.00.00_01_remove_old_version_procrastinate_func.sql`) that takes -care of removing the old version of the function: - -```sql +-- xx_xx_xx_50_post_remove_old_version_procrastinate_func.sql DROP FUNCTION procrastinate_func(integer, text, timestamp); -``` - -In this way, you provide the new SQL code, the compatibility layer, and the migration -for the removal of the compatibility layer. +... -:::{note} -The migration scripts that remove the SQL compatibility code are to be added to the -`future_migrations` directory instead of the `migrations` directory. And it will -be the responsibility of Procrastinate maintainers to move them to the -`migrations` directory after the next major release. -::: +``` ### Migration tests @@ -288,6 +261,29 @@ included in the normal test suite, but you can run them specifically with: (venv) $ pytest tests/migration ``` +We run the `acceptance` tests on 3 different configurations: + +- Without the post-migrations applied and with the last released version of + Procrastinate +- Without the post-migrations applied and with the current checked out code +- With all migrations applied and with the current checked out code (this is + just part of the normal test suite) + +This is to ensure that the migrations are backward-compatible and that the database +schema can be upgraded without downtime. We simulate all stages of the upgrade process: + +- (the initial situation being that Procrastinate is running with the last + released version of the code and all migrations of the last released + version have been applied) +- First, the user would apply pre-migrations while the old version of the + code is still running. +- Then, the user would upgrade the code to the new version. +- Finally, the user would apply post-migrations. + +There are cases where new acceptance tests cannot work on the last released version. +In that case, the tests can be skipped by adding `@pytest.mark.skip_before_version("x.y.z")`, +where `x.y.z` is the version of Procrastinate where the test would start running. + ## Try our demos See the demos page for instructions on how to run the demos ({doc}`demos`). diff --git a/docs/howto/django/migrations.md b/docs/howto/django/migrations.md index ea80166ec..ca32e5d86 100644 --- a/docs/howto/django/migrations.md +++ b/docs/howto/django/migrations.md @@ -4,14 +4,18 @@ Procrastinate comes with its own migrations so don't forget to run `./manage.py migrate`. Procrastinate provides 2 kinds of migrations: -- The Django equivalent of the `procrastinate` normal migrations, which are - used to create all of the PostgreSQL DDL objects used by Procrastinate. -- Specific noop migrations used for Django to understand the Procrastinate - Models (see {doc}`models`). + +- The Django equivalent of the `procrastinate` normal migrations, which are + used to create all of the PostgreSQL DDL objects used by Procrastinate. +- Specific noop migrations used for Django to understand the Procrastinate + Models (see {doc}`models`). Procrastinate's Django migrations are always kept in sync with your current version of Procrastinate, it's always a good idea to check the release notes and read the migrations when upgrading so that you know what will be happening to the database. -See {doc}`../production/migrations` for more information on migrations. +See {doc}`../production/migrations` for more information on migrations, especially +around `pre` and `post` migrations: if you deploy while the code is running, you'll +want to ensure you run the `pre-` migrations before you deploy the code and the +`post-` migrations after. diff --git a/docs/howto/production/migrations.md b/docs/howto/production/migrations.md index 3543a7225..04a1e69eb 100644 --- a/docs/howto/production/migrations.md +++ b/docs/howto/production/migrations.md @@ -1,5 +1,10 @@ # Migrate the Procrastinate schema +:::{warning} +v3 introduces a new way to handle migrations. Hopefully, easier both for users +and maintainers. Read about pre- and post-migrations below. +::: + When the Procrastinate database schema evolves in new Procrastinate releases, new migrations are released alongside. Look at the [Release notes](https://github.com/procrastinate-org/procrastinate/releases) @@ -31,17 +36,22 @@ on PyPI. A simple way to list all the migrations is to use the command: $ procrastinate schema --migrations-path /home/me/my_venv/lib/python3.x/lib/site-packages/procrastinate/sql/migrations ``` + It's your responsibility to keep track of which migrations have been applied yet or not. Thankfully, the names of procrastinate migrations should help you: they follow a specific pattern: ``` -xx.yy.zz_ab_very_short_description_of_the_migration.sql +{xx.yy.zz}_{ab}_{pre|post}_very_short_description_of_the_migration.sql ``` -- `xx.yy.zz` is the version of Procrastinate the migration script can be applied to. -- `ab` is the migration script's serial number, `01` being the first number in the - series. +- `xx.yy.zz` is the version of Procrastinate the migration script can be applied to. +- `ab` is the migration script's serial number, `01` being the first number in the + series. +- `pre` / `post`: indicates wether the migration should be applied before + upgrading the code (`pre`) or after upgrading the code (`post`) in the context + of a blue-green deployment. On old migrations, if `pre` or `post` is not + specified, it's a `post` migration. :::{note} There is a [debate](https://github.com/procrastinate-org/procrastinate/issues/1040) @@ -50,51 +60,46 @@ directions for how to use classic ones (apart from Django), please feel free to and/or contribute code or documentation if you have an opinion on this. ::: -Let's say you are currently using Procrastinate 1.9.0, and you want to update to -Procrastinate 1.15.0. In that case, before upgrading the Procrastinate Python package -(from 1.9.0 to 1.15.0), you will need to apply all the migration scripts whose versions -are greater than or equal to 1.9.0, and lower than 1.15.0 (1.9.0 ≤ version \< 1.15.0). -And you will apply them in version order, and, for a version, in serial number order. -For example, you will apply the following migration scripts, in that order: - -1. `01.09.00_01_xxxxx.sql` -2. `01.10.00_01_xxxxx.sql` -3. `01.11.00_01_xxxxx.sql` -4. `01.11.00_02_xxxxx.sql` -5. `01.12.00_01_xxxxx.sql` -6. `01.14.00_01_xxxxx.sql` -7. `01.14.00_02_xxxxx.sql` +## How to apply migrations -If you want to upgrade from one Procrastinate major version to another, say from -Procrastinate 1.6.0 to 3.2.0, there are two options, depending on whether you can -interrupt the service to do the migration or not. - -## The easier way, with service interruption +##1 The easier way, with service interruption 1. Shut down the services that use Procrastinate: both the services that defer tasks and the workers. -2. Apply all the migration scripts (1.6.0 ≤ version \< 3.2.0). -3. Upgrade your code to the new Procrastinate version (3.2.0). +2. Apply all the migration scripts (`pre` & `post`), e.g. with: + +```console +$ MIGRATION_TO_APPLY="02.00.00_01_pre_some_migration.sql" +$ cat $(procrastinate schema --migrations-path)/${MIGRATION_TO_APPLY} | psql +$ MIGRATION_TO_APPLY="02.00.00_01_post_some_migration.sql" +$ cat $(procrastinate schema --migrations-path)/${MIGRATION_TO_APPLY} | psql +$ ... +``` + +3. Upgrade your code to the new Procrastinate version. 4. Start all the services. This, as you've noticed, only works if you're able to stop the services. ## The safer way, without service interruption -:::{note} -This only applies starting at Procrastinate 0.17.0. For previous versions, -you will have to interrupt the service or write custom migrations. -::: +If you need to ensure service continuity, you'll need to make intermediate upgrades. +Basically, you'll need to stop at every version that provides migrations. + +```console +$ MIGRATION_TO_APPLY="02.01.00_01_pre_some_migration.sql" +$ cat $(procrastinate schema --migrations-path)/${MIGRATION_TO_APPLY} | psql + +$ yoursystem/deploy procrastinate 2.1.0 -If you care about service continuity, you'll need to make intermediate upgrades. For -example, to upgrade from Procrastinate 1.6.0 to 3.2.0, here are the steps you will need -to follow: +$ MIGRATION_TO_APPLY="02.01.00_01_post_some_migration.sql" +$ cat $(procrastinate schema --migrations-path)/${MIGRATION_TO_APPLY} | psql -1. Apply all the migration scripts between 1.6.0 and 2.0.0 (1.6.0 ≤ version \< 2.0.0). -2. Live-upgrade the Procrastinate version used in your services, from 1.6.0 to 2.0.0. -3. Apply all the migration scripts between 2.0.0 and 3.0.0 (2.0.0 ≤ version \< 3.0.0). -4. Live-upgrade the Procrastinate version used in your services, from 2.0.0 to 3.0.0. -5. Apply all the migration scripts between 3.0.0 and 3.2.0 (3.0.0 ≤ version \< 3.2.0). -6. Live-upgrade the Procrastinate version used in your services, from 3.0.0 and 3.2.0. +$ MIGRATION_TO_APPLY="02.02.00_01_pre_some_migration.sql" +$ cat $(procrastinate schema --migrations-path)/${MIGRATION_TO_APPLY} | psql -Following this process you can go from 1.6.0 to 3.2.0 with no service discontinuity. +$ yoursystem/deploy procrastinate 2.2.0 + +$ MIGRATION_TO_APPLY="02.02.00_01_post_some_migration.sql" +$ cat $(procrastinate schema --migrations-path)/${MIGRATION_TO_APPLY} | psql +``` From 7e9fc8e9be64738e8da94e085eba532b5bc21e3f Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 19:56:02 +0100 Subject: [PATCH 067/375] Cosmetics - just pre-commit reorganizing things --- procrastinate/__init__.py | 6 +++--- procrastinate/app.py | 3 +-- procrastinate/cli.py | 3 ++- procrastinate/connector.py | 3 ++- procrastinate/contrib/aiopg/aiopg_connector.py | 3 ++- procrastinate/contrib/django/apps.py | 2 +- procrastinate/contrib/django/django_connector.py | 3 +-- procrastinate/contrib/django/migrations_utils.py | 2 +- procrastinate/contrib/psycopg2/psycopg2_connector.py | 3 ++- procrastinate/contrib/sqlalchemy/psycopg2_connector.py | 3 ++- procrastinate/job_context.py | 3 ++- procrastinate/manager.py | 3 ++- procrastinate/metadata.py | 2 +- procrastinate/periodic.py | 5 +++-- procrastinate/psycopg_connector.py | 4 +--- procrastinate/retry.py | 3 ++- procrastinate/sync_psycopg_connector.py | 3 ++- procrastinate/testing.py | 7 ++++--- procrastinate/types.py | 4 ++-- procrastinate/utils.py | 10 ++++++---- procrastinate/worker.py | 3 ++- tests/acceptance/test_nominal.py | 4 ++-- 22 files changed, 46 insertions(+), 36 deletions(-) diff --git a/procrastinate/__init__.py b/procrastinate/__init__.py index d0dbffb78..4c4cc0f1b 100644 --- a/procrastinate/__init__.py +++ b/procrastinate/__init__.py @@ -21,14 +21,14 @@ __all__ = [ "App", - "Blueprint", - "JobContext", "BaseConnector", "BaseRetryStrategy", + "Blueprint", + "JobContext", "PsycopgConnector", - "SyncPsycopgConnector", "RetryDecision", "RetryStrategy", + "SyncPsycopgConnector", ] diff --git a/procrastinate/app.py b/procrastinate/app.py index 0def8bba2..ac2d50c35 100644 --- a/procrastinate/app.py +++ b/procrastinate/app.py @@ -4,11 +4,10 @@ import contextlib import functools import logging +from collections.abc import Iterable, Iterator from typing import ( TYPE_CHECKING, Any, - Iterable, - Iterator, TypedDict, ) diff --git a/procrastinate/cli.py b/procrastinate/cli.py index df058ab4e..0ea894549 100644 --- a/procrastinate/cli.py +++ b/procrastinate/cli.py @@ -8,7 +8,8 @@ import os import shlex import sys -from typing import Any, Awaitable, Callable, Literal, Union +from collections.abc import Awaitable +from typing import Any, Callable, Literal, Union import procrastinate from procrastinate import connector, exceptions, jobs, shell, types, utils diff --git a/procrastinate/connector.py b/procrastinate/connector.py index 541a772a9..8ca8676f6 100644 --- a/procrastinate/connector.py +++ b/procrastinate/connector.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Awaitable, Callable, Iterable, Protocol +from collections.abc import Awaitable, Iterable +from typing import Any, Callable, Protocol from typing_extensions import LiteralString diff --git a/procrastinate/contrib/aiopg/aiopg_connector.py b/procrastinate/contrib/aiopg/aiopg_connector.py index 962ff46fa..80fe31718 100644 --- a/procrastinate/contrib/aiopg/aiopg_connector.py +++ b/procrastinate/contrib/aiopg/aiopg_connector.py @@ -3,7 +3,8 @@ import asyncio import functools import logging -from typing import Any, AsyncGenerator, Callable, Coroutine, Iterable +from collections.abc import AsyncGenerator, Coroutine, Iterable +from typing import Any, Callable import aiopg import psycopg2 diff --git a/procrastinate/contrib/django/apps.py b/procrastinate/contrib/django/apps.py index 6ea960947..a436cebd2 100644 --- a/procrastinate/contrib/django/apps.py +++ b/procrastinate/contrib/django/apps.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable +from collections.abc import Iterable from django import apps from django.utils import module_loading diff --git a/procrastinate/contrib/django/django_connector.py b/procrastinate/contrib/django/django_connector.py index 8f178c379..9d0dc4175 100644 --- a/procrastinate/contrib/django/django_connector.py +++ b/procrastinate/contrib/django/django_connector.py @@ -1,11 +1,10 @@ from __future__ import annotations import contextlib +from collections.abc import Generator, Iterable from typing import ( TYPE_CHECKING, Any, - Generator, - Iterable, ) import asgiref.sync diff --git a/procrastinate/contrib/django/migrations_utils.py b/procrastinate/contrib/django/migrations_utils.py index e678d12a4..6fb8a3d27 100644 --- a/procrastinate/contrib/django/migrations_utils.py +++ b/procrastinate/contrib/django/migrations_utils.py @@ -6,7 +6,7 @@ from django.db import migrations -@functools.lru_cache(maxsize=None) +@functools.cache def list_migration_files() -> dict[str, str]: """ Returns a list of filenames and file contents for all migration files diff --git a/procrastinate/contrib/psycopg2/psycopg2_connector.py b/procrastinate/contrib/psycopg2/psycopg2_connector.py index bb847e95d..fb38999ab 100644 --- a/procrastinate/contrib/psycopg2/psycopg2_connector.py +++ b/procrastinate/contrib/psycopg2/psycopg2_connector.py @@ -3,7 +3,8 @@ import contextlib import functools import logging -from typing import Any, Callable, Generator, Iterator +from collections.abc import Generator, Iterator +from typing import Any, Callable import psycopg2 import psycopg2.errors diff --git a/procrastinate/contrib/sqlalchemy/psycopg2_connector.py b/procrastinate/contrib/sqlalchemy/psycopg2_connector.py index e80ecc5c4..1459c5caa 100644 --- a/procrastinate/contrib/sqlalchemy/psycopg2_connector.py +++ b/procrastinate/contrib/sqlalchemy/psycopg2_connector.py @@ -3,7 +3,8 @@ import contextlib import functools import re -from typing import Any, Callable, Generator, Mapping +from collections.abc import Generator, Mapping +from typing import Any, Callable import psycopg2.errors import sqlalchemy diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index f77e98834..b224dfba1 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -1,8 +1,9 @@ from __future__ import annotations import time +from collections.abc import Iterable from enum import Enum -from typing import Any, Callable, Iterable +from typing import Any, Callable import attr diff --git a/procrastinate/manager.py b/procrastinate/manager.py index b51e266d1..532f86ede 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -3,7 +3,8 @@ import datetime import json import logging -from typing import Any, Awaitable, Iterable, NoReturn, Protocol +from collections.abc import Awaitable, Iterable +from typing import Any, NoReturn, Protocol from procrastinate import connector, exceptions, jobs, sql, utils diff --git a/procrastinate/metadata.py b/procrastinate/metadata.py index 874b1d70e..42589e26e 100644 --- a/procrastinate/metadata.py +++ b/procrastinate/metadata.py @@ -1,7 +1,7 @@ from __future__ import annotations import importlib.metadata as importlib_metadata -from typing import Mapping +from collections.abc import Mapping def extract_metadata() -> Mapping[str, str]: diff --git a/procrastinate/periodic.py b/procrastinate/periodic.py index 78048bffb..94f4cd83a 100644 --- a/procrastinate/periodic.py +++ b/procrastinate/periodic.py @@ -4,7 +4,8 @@ import functools import logging import time -from typing import Callable, Generic, Iterable, Tuple, cast +from collections.abc import Iterable +from typing import Callable, Generic, cast import attr import croniter @@ -40,7 +41,7 @@ def croniter(self) -> croniter.croniter: return croniter.croniter(self.cron) -TaskAtTime = Tuple[PeriodicTask, int] +TaskAtTime = tuple[PeriodicTask, int] class PeriodicRegistry: diff --git a/procrastinate/psycopg_connector.py b/procrastinate/psycopg_connector.py index f2e0dd2f1..24e15728f 100644 --- a/procrastinate/psycopg_connector.py +++ b/procrastinate/psycopg_connector.py @@ -2,13 +2,11 @@ import contextlib import logging +from collections.abc import AsyncGenerator, AsyncIterator, Iterable from typing import ( TYPE_CHECKING, Any, - AsyncGenerator, - AsyncIterator, Callable, - Iterable, ) from typing_extensions import LiteralString diff --git a/procrastinate/retry.py b/procrastinate/retry.py index a07341666..c1e43e947 100644 --- a/procrastinate/retry.py +++ b/procrastinate/retry.py @@ -7,7 +7,8 @@ import datetime import warnings -from typing import Iterable, Union, overload +from collections.abc import Iterable +from typing import Union, overload import attr diff --git a/procrastinate/sync_psycopg_connector.py b/procrastinate/sync_psycopg_connector.py index c16489d89..e8905647b 100644 --- a/procrastinate/sync_psycopg_connector.py +++ b/procrastinate/sync_psycopg_connector.py @@ -2,7 +2,8 @@ import contextlib import logging -from typing import Any, Callable, Generator, Iterator +from collections.abc import Generator, Iterator +from typing import Any, Callable import psycopg import psycopg.rows diff --git a/procrastinate/testing.py b/procrastinate/testing.py index 283474efb..4cb4c7481 100644 --- a/procrastinate/testing.py +++ b/procrastinate/testing.py @@ -3,13 +3,14 @@ import datetime import json from collections import Counter +from collections.abc import Iterable from itertools import count -from typing import Any, Dict, Iterable +from typing import Any from procrastinate import connector, exceptions, jobs, schema, sql, types, utils -JobRow = Dict[str, Any] -EventRow = Dict[str, Any] +JobRow = dict[str, Any] +EventRow = dict[str, Any] class InMemoryConnector(connector.BaseAsyncConnector): diff --git a/procrastinate/types.py b/procrastinate/types.py index eb604a4e1..9c373aff9 100644 --- a/procrastinate/types.py +++ b/procrastinate/types.py @@ -4,8 +4,8 @@ from typing_extensions import NotRequired -JSONValue = t.Union[str, int, float, bool, None, t.Dict[str, t.Any], t.List[t.Any]] -JSONDict = t.Dict[str, JSONValue] +JSONValue = t.Union[str, int, float, bool, None, dict[str, t.Any], list[t.Any]] +JSONDict = dict[str, JSONValue] class TimeDeltaParams(t.TypedDict): diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 4a3422ddb..37d100634 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -9,15 +9,17 @@ import pathlib import sys import types -from typing import ( - Any, +from collections.abc import ( AsyncGenerator, AsyncIterator, Awaitable, - Callable, Coroutine, - Generic, Iterable, +) +from typing import ( + Any, + Callable, + Generic, TypeVar, ) diff --git a/procrastinate/worker.py b/procrastinate/worker.py index c52aa34b7..f4227d9d8 100644 --- a/procrastinate/worker.py +++ b/procrastinate/worker.py @@ -6,7 +6,8 @@ import inspect import logging import time -from typing import Any, Awaitable, Callable, Iterable +from collections.abc import Awaitable, Iterable +from typing import Any, Callable from procrastinate import ( app, diff --git a/tests/acceptance/test_nominal.py b/tests/acceptance/test_nominal.py index 8f9e91cd6..243c806f5 100644 --- a/tests/acceptance/test_nominal.py +++ b/tests/acceptance/test_nominal.py @@ -3,7 +3,7 @@ import signal import subprocess import time -from typing import Protocol, Tuple, cast +from typing import Protocol, cast import pytest @@ -211,7 +211,7 @@ def test_periodic_deferrer(worker: Worker): # We're making a dict from the output results = dict( - cast(Tuple[int, int], (int(a) for a in e[5:].split())) + cast(tuple[int, int], (int(a) for a in e[5:].split())) for e in stdout.splitlines() if e.startswith("tick ") ) From ecc7fc5a6b7008429850146e8078b5186641786e Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 20:08:29 +0100 Subject: [PATCH 068/375] Make nominal test less flaky --- tests/acceptance/test_nominal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/test_nominal.py b/tests/acceptance/test_nominal.py index 243c806f5..36a7879b8 100644 --- a/tests/acceptance/test_nominal.py +++ b/tests/acceptance/test_nominal.py @@ -77,7 +77,7 @@ def test_nominal(defer, worker, app): assert stdout.strip() == "20" defer("two_fails") - stdout, stderr = worker(app=app) + stdout, stderr = worker(app=app, sleep=1.5) print(stdout, stderr) assert "Print something to stdout" in stdout assert stderr.count("Exception: This should fail") == 2 From d34af176e0044549bfebf9c1a28f3104525dae16 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 19:56:26 +0100 Subject: [PATCH 069/375] Rework migrations, queries, version SQL objects, don't remove ABORTING status just yet --- procrastinate/contrib/django/admin.py | 1 + .../0031_pre_cancel_notification.py | 20 ++ .../migrations/0032_cancel_notification.py | 15 - ...bs.py => 0032_post_cancel_notification.py} | 11 +- procrastinate/jobs.py | 1 + procrastinate/manager.py | 15 +- ....02_01_add_abort_on_procrastinate_jobs.sql | 271 ---------------- .../03.00.00_01_cancel_notification.sql | 87 ------ .../03.00.00_01_pre_cancel_notification.sql | 295 ++++++++++++++++++ .../03.00.00_50_post_cancel_notification.sql | 202 ++++++++++++ procrastinate/sql/queries.sql | 12 +- procrastinate/sql/schema.sql | 271 ++++------------ procrastinate/testing.py | 4 +- tests/integration/test_manager.py | 8 +- tests/integration/test_psycopg_connector.py | 4 +- tests/unit/test_manager.py | 8 +- tests/unit/test_worker.py | 2 +- 17 files changed, 616 insertions(+), 611 deletions(-) create mode 100644 procrastinate/contrib/django/migrations/0031_pre_cancel_notification.py delete mode 100644 procrastinate/contrib/django/migrations/0032_cancel_notification.py rename procrastinate/contrib/django/migrations/{0031_add_abort_on_procrastinate_jobs.py => 0032_post_cancel_notification.py} (67%) delete mode 100644 procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql delete mode 100644 procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql create mode 100644 procrastinate/sql/migrations/03.00.00_01_pre_cancel_notification.sql create mode 100644 procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql diff --git a/procrastinate/contrib/django/admin.py b/procrastinate/contrib/django/admin.py index b606ffec7..714a73861 100644 --- a/procrastinate/contrib/django/admin.py +++ b/procrastinate/contrib/django/admin.py @@ -16,6 +16,7 @@ "failed": "❌", "succeeded": "✅", "cancelled": "🤚", + "aborting": "🔌🕑️", # legacy, not used anymore "aborted": "🔌", } diff --git a/procrastinate/contrib/django/migrations/0031_pre_cancel_notification.py b/procrastinate/contrib/django/migrations/0031_pre_cancel_notification.py new file mode 100644 index 000000000..6ec17db85 --- /dev/null +++ b/procrastinate/contrib/django/migrations/0031_pre_cancel_notification.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from django.db import migrations, models + +from .. import migrations_utils + + +class Migration(migrations.Migration): + operations = [ + migrations_utils.RunProcrastinateSQL( + name="03.00.00_01_pre_cancel_notification.sql" + ), + migrations.AddField( + "procrastinatejob", + "abort_requested", + models.BooleanField(), + ), + ] + name = "0031_pre_cancel_notification" + dependencies = [("procrastinate", "0030_alter_procrastinateevent_options")] diff --git a/procrastinate/contrib/django/migrations/0032_cancel_notification.py b/procrastinate/contrib/django/migrations/0032_cancel_notification.py deleted file mode 100644 index 617265857..000000000 --- a/procrastinate/contrib/django/migrations/0032_cancel_notification.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -from django.db import migrations - -from .. import migrations_utils - - -class Migration(migrations.Migration): - operations = [ - migrations_utils.RunProcrastinateSQL( - name="03.00.00_01_cancel_notification.sql" - ), - ] - name = "0032_cancel_notification" - dependencies = [("procrastinate", "0031_add_abort_on_procrastinate_jobs")] diff --git a/procrastinate/contrib/django/migrations/0031_add_abort_on_procrastinate_jobs.py b/procrastinate/contrib/django/migrations/0032_post_cancel_notification.py similarity index 67% rename from procrastinate/contrib/django/migrations/0031_add_abort_on_procrastinate_jobs.py rename to procrastinate/contrib/django/migrations/0032_post_cancel_notification.py index f968078d5..3be9f78b0 100644 --- a/procrastinate/contrib/django/migrations/0031_add_abort_on_procrastinate_jobs.py +++ b/procrastinate/contrib/django/migrations/0032_post_cancel_notification.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): operations = [ migrations_utils.RunProcrastinateSQL( - name="02.09.02_01_add_abort_on_procrastinate_jobs.sql" + name="03.00.00_50_post_cancel_notification.sql" ), migrations.AlterField( "procrastinatejob", @@ -25,11 +25,6 @@ class Migration(migrations.Migration): max_length=32, ), ), - migrations.AddField( - "procrastinatejob", - "abort_requested", - models.BooleanField(), - ), ] - name = "0031_add_abort_on_procrastinate_jobs" - dependencies = [("procrastinate", "0030_alter_procrastinateevent_options")] + name = "0032_post_cancel_notification" + dependencies = [("procrastinate", "0031_pre_cancel_notification")] diff --git a/procrastinate/jobs.py b/procrastinate/jobs.py index 5c16d67ac..ac2dd0a83 100644 --- a/procrastinate/jobs.py +++ b/procrastinate/jobs.py @@ -53,6 +53,7 @@ class Status(Enum): SUCCEEDED = "succeeded" #: The job ended successfully FAILED = "failed" #: The job ended with an error CANCELLED = "cancelled" #: The job was cancelled + ABORTING = "aborting" #: legacy, not used anymore ABORTED = "aborted" #: The job was aborted diff --git a/procrastinate/manager.py b/procrastinate/manager.py index 532f86ede..d83e7a0df 100644 --- a/procrastinate/manager.py +++ b/procrastinate/manager.py @@ -10,7 +10,11 @@ logger = logging.getLogger(__name__) -QUEUEING_LOCK_CONSTRAINT = "procrastinate_jobs_queueing_lock_idx" +QUEUEING_LOCK_CONSTRAINT = "procrastinate_jobs_queueing_lock_idx_v1" + +# TODO: Only necessary to make it work with the pre-migration of v3.0.0. +# We can remove this in the next minor version. +QUEUEING_LOCK_CONSTRAINT_LEGACY = "procrastinate_jobs_queueing_lock_idx" class NotificationCallback(Protocol): @@ -21,9 +25,9 @@ def __call__( def get_channel_for_queues(queues: Iterable[str] | None = None) -> Iterable[str]: if queues is None: - return ["procrastinate_any_queue"] + return ["procrastinate_any_queue_v1"] else: - return ["procrastinate_queue#" + queue for queue in queues] + return ["procrastinate_queue_v1#" + queue for queue in queues] class JobManager: @@ -82,7 +86,10 @@ def _defer_job_query_kwargs(self, job: jobs.Job) -> dict[str, Any]: def _raise_already_enqueued( self, exc: exceptions.UniqueViolation, queueing_lock: str | None ) -> NoReturn: - if exc.constraint_name == QUEUEING_LOCK_CONSTRAINT: + if exc.constraint_name in [ + QUEUEING_LOCK_CONSTRAINT, + QUEUEING_LOCK_CONSTRAINT_LEGACY, + ]: raise exceptions.AlreadyEnqueued( "Job cannot be enqueued: there is already a job in the queue " f"with the queueing lock {queueing_lock}" diff --git a/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql b/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql deleted file mode 100644 index 1a0b7dc58..000000000 --- a/procrastinate/sql/migrations/02.09.02_01_add_abort_on_procrastinate_jobs.sql +++ /dev/null @@ -1,271 +0,0 @@ --- Add an 'abort_requested' column to the procrastinate_jobs table -ALTER TABLE procrastinate_jobs ADD COLUMN abort_requested boolean DEFAULT false NOT NULL; - --- Set abort requested flag on all jobs with 'aborting' status -UPDATE procrastinate_jobs SET abort_requested = true WHERE status = 'aborting'; - --- Delete the indexes that depends on the old status and enum type -DROP INDEX IF EXISTS procrastinate_jobs_queueing_lock_idx; -DROP INDEX IF EXISTS procrastinate_jobs_lock_idx; -DROP INDEX IF EXISTS procrastinate_jobs_id_lock_idx; - --- Delete the triggers that depends on the old status type (to recreate them later) -DROP TRIGGER IF EXISTS procrastinate_jobs_notify_queue ON procrastinate_jobs; -DROP TRIGGER IF EXISTS procrastinate_trigger_status_events_update ON procrastinate_jobs; -DROP TRIGGER IF EXISTS procrastinate_trigger_status_events_insert ON procrastinate_jobs; -DROP TRIGGER IF EXISTS procrastinate_trigger_scheduled_events ON procrastinate_jobs; - --- Delete the functions that depends on the old status type -DROP FUNCTION IF EXISTS procrastinate_fetch_job; -DROP FUNCTION IF EXISTS procrastinate_finish_job(bigint, procrastinate_job_status, boolean); -DROP FUNCTION IF EXISTS procrastinate_cancel_job; -DROP FUNCTION IF EXISTS procrastinate_trigger_status_events_procedure_update; -DROP FUNCTION IF EXISTS procrastinate_finish_job(integer, procrastinate_job_status, timestamp with time zone, boolean); - --- Create a new enum type without 'aborting' -CREATE TYPE procrastinate_job_status_new AS ENUM ( - 'todo', - 'doing', - 'succeeded', - 'failed', - 'cancelled', - 'aborted' -); - --- We need to drop the default temporarily as otherwise DatatypeMismatch would occur -ALTER TABLE procrastinate_jobs ALTER COLUMN status DROP DEFAULT; - --- Alter the table to use the new type -ALTER TABLE procrastinate_jobs -ALTER COLUMN status TYPE procrastinate_job_status_new -USING ( - CASE status::text - WHEN 'aborting' THEN 'doing'::procrastinate_job_status_new - ELSE status::text::procrastinate_job_status_new - END -); - --- Recreate the default -ALTER TABLE procrastinate_jobs ALTER COLUMN status SET DEFAULT 'todo'::procrastinate_job_status_new; - --- Drop the old type -DROP TYPE procrastinate_job_status; - --- Rename the new type to the original name -ALTER TYPE procrastinate_job_status_new RENAME TO procrastinate_job_status; - --- Recreate the indexes -CREATE UNIQUE INDEX procrastinate_jobs_queueing_lock_idx ON procrastinate_jobs (queueing_lock) WHERE status = 'todo'; -CREATE UNIQUE INDEX procrastinate_jobs_lock_idx ON procrastinate_jobs (lock) WHERE status = 'doing'; -CREATE INDEX procrastinate_jobs_id_lock_idx ON procrastinate_jobs (id, lock) WHERE status = ANY (ARRAY['todo'::procrastinate_job_status, 'doing'::procrastinate_job_status]); - --- Recreate and update the functions -CREATE OR REPLACE FUNCTION procrastinate_fetch_job( - target_queue_names character varying[] -) - RETURNS procrastinate_jobs - LANGUAGE plpgsql -AS $$ -DECLARE - found_jobs procrastinate_jobs; -BEGIN - WITH candidate AS ( - SELECT jobs.* - FROM procrastinate_jobs AS jobs - WHERE - -- reject the job if its lock has earlier jobs - NOT EXISTS ( - SELECT 1 - FROM procrastinate_jobs AS earlier_jobs - WHERE - jobs.lock IS NOT NULL - AND earlier_jobs.lock = jobs.lock - AND earlier_jobs.status IN ('todo', 'doing') - AND earlier_jobs.id < jobs.id) - AND jobs.status = 'todo' - AND (target_queue_names IS NULL OR jobs.queue_name = ANY( target_queue_names )) - AND (jobs.scheduled_at IS NULL OR jobs.scheduled_at <= now()) - ORDER BY jobs.priority DESC, jobs.id ASC LIMIT 1 - FOR UPDATE OF jobs SKIP LOCKED - ) - UPDATE procrastinate_jobs - SET status = 'doing' - FROM candidate - WHERE procrastinate_jobs.id = candidate.id - RETURNING procrastinate_jobs.* INTO found_jobs; - - RETURN found_jobs; -END; -$$; - -CREATE FUNCTION procrastinate_finish_job(job_id bigint, end_status procrastinate_job_status, delete_job boolean) - RETURNS void - LANGUAGE plpgsql -AS $$ -DECLARE - _job_id bigint; -BEGIN - IF end_status NOT IN ('succeeded', 'failed', 'aborted') THEN - RAISE 'End status should be either "succeeded", "failed" or "aborted" (job id: %)', job_id; - END IF; - IF delete_job THEN - DELETE FROM procrastinate_jobs - WHERE id = job_id AND status IN ('todo', 'doing') - RETURNING id INTO _job_id; - ELSE - UPDATE procrastinate_jobs - SET status = end_status, - abort_requested = false, - attempts = CASE status - WHEN 'doing' THEN attempts + 1 ELSE attempts - END - WHERE id = job_id AND status IN ('todo', 'doing') - RETURNING id INTO _job_id; - END IF; - IF _job_id IS NULL THEN - RAISE 'Job was not found or not in "doing" or "todo" status (job id: %)', job_id; - END IF; -END; -$$; - -CREATE FUNCTION procrastinate_cancel_job(job_id bigint, abort boolean, delete_job boolean) - RETURNS bigint - LANGUAGE plpgsql -AS $$ -DECLARE - _job_id bigint; -BEGIN - IF delete_job THEN - DELETE FROM procrastinate_jobs - WHERE id = job_id AND status = 'todo' - RETURNING id INTO _job_id; - END IF; - IF _job_id IS NULL THEN - IF abort THEN - UPDATE procrastinate_jobs - SET abort_requested = true, - status = CASE status - WHEN 'todo' THEN 'cancelled'::procrastinate_job_status ELSE status - END - WHERE id = job_id AND status IN ('todo', 'doing') - RETURNING id INTO _job_id; - ELSE - UPDATE procrastinate_jobs - SET status = 'cancelled'::procrastinate_job_status - WHERE id = job_id AND status = 'todo' - RETURNING id INTO _job_id; - END IF; - END IF; - RETURN _job_id; -END; -$$; - -CREATE FUNCTION procrastinate_trigger_status_events_procedure_update() - RETURNS trigger - LANGUAGE plpgsql -AS $$ -BEGIN - WITH t AS ( - SELECT CASE - WHEN OLD.status = 'todo'::procrastinate_job_status - AND NEW.status = 'doing'::procrastinate_job_status - THEN 'started'::procrastinate_job_event_type - WHEN OLD.status = 'doing'::procrastinate_job_status - AND NEW.status = 'todo'::procrastinate_job_status - THEN 'deferred_for_retry'::procrastinate_job_event_type - WHEN OLD.status = 'doing'::procrastinate_job_status - AND NEW.status = 'failed'::procrastinate_job_status - THEN 'failed'::procrastinate_job_event_type - WHEN OLD.status = 'doing'::procrastinate_job_status - AND NEW.status = 'succeeded'::procrastinate_job_status - THEN 'succeeded'::procrastinate_job_event_type - WHEN OLD.status = 'todo'::procrastinate_job_status - AND ( - NEW.status = 'cancelled'::procrastinate_job_status - OR NEW.status = 'failed'::procrastinate_job_status - OR NEW.status = 'succeeded'::procrastinate_job_status - ) - THEN 'cancelled'::procrastinate_job_event_type - WHEN OLD.status = 'doing'::procrastinate_job_status - AND NEW.status = 'aborted'::procrastinate_job_status - THEN 'aborted'::procrastinate_job_event_type - ELSE NULL - END as event_type - ) - INSERT INTO procrastinate_events(job_id, type) - SELECT NEW.id, t.event_type - FROM t - WHERE t.event_type IS NOT NULL; - RETURN NEW; -END; -$$; - -CREATE FUNCTION procrastinate_finish_job(job_id integer, end_status procrastinate_job_status, next_scheduled_at timestamp with time zone, delete_job boolean) - RETURNS void - LANGUAGE plpgsql -AS $$ -DECLARE - _job_id bigint; -BEGIN - IF end_status NOT IN ('succeeded', 'failed') THEN - RAISE 'End status should be either "succeeded" or "failed" (job id: %)', job_id; - END IF; - IF delete_job THEN - DELETE FROM procrastinate_jobs - WHERE id = job_id AND status IN ('todo', 'doing') - RETURNING id INTO _job_id; - ELSE - UPDATE procrastinate_jobs - SET status = end_status, - abort_requested = false, - attempts = - CASE - WHEN status = 'doing' THEN attempts + 1 - ELSE attempts - END - WHERE id = job_id AND status IN ('todo', 'doing') - RETURNING id INTO _job_id; - END IF; - IF _job_id IS NULL THEN - RAISE 'Job was not found or not in "doing" or "todo" status (job id: %)', job_id; - END IF; -END; -$$; - --- Recreate the deleted triggers -CREATE TRIGGER procrastinate_jobs_notify_queue - AFTER INSERT ON procrastinate_jobs - FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_notify_queue(); - -CREATE TRIGGER procrastinate_trigger_status_events_update - AFTER UPDATE OF status ON procrastinate_jobs - FOR EACH ROW - EXECUTE PROCEDURE procrastinate_trigger_status_events_procedure_update(); - -CREATE TRIGGER procrastinate_trigger_status_events_insert - AFTER INSERT ON procrastinate_jobs - FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_trigger_status_events_procedure_insert(); - -CREATE TRIGGER procrastinate_trigger_scheduled_events - AFTER UPDATE OR INSERT ON procrastinate_jobs - FOR EACH ROW WHEN ((new.scheduled_at IS NOT NULL AND new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_trigger_scheduled_events_procedure(); - --- Create additional function and trigger for abortion requests -CREATE FUNCTION procrastinate_trigger_abort_requested_events_procedure() - RETURNS trigger - LANGUAGE plpgsql -AS $$ -BEGIN - INSERT INTO procrastinate_events(job_id, type) - VALUES (NEW.id, 'abort_requested'::procrastinate_job_event_type); - RETURN NEW; -END; -$$; - -CREATE TRIGGER procrastinate_trigger_abort_requested_events - AFTER UPDATE OF abort_requested ON procrastinate_jobs - FOR EACH ROW WHEN ((new.abort_requested = true)) - EXECUTE PROCEDURE procrastinate_trigger_abort_requested_events_procedure(); diff --git a/procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql b/procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql deleted file mode 100644 index c2925b1f7..000000000 --- a/procrastinate/sql/migrations/03.00.00_01_cancel_notification.sql +++ /dev/null @@ -1,87 +0,0 @@ -CREATE OR REPLACE FUNCTION procrastinate_notify_queue_job_inserted() -RETURNS trigger - LANGUAGE plpgsql -AS $$ -DECLARE - payload TEXT; -BEGIN - SELECT json_object('type': 'job_inserted', 'job_id': NEW.id)::text INTO payload; - PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); - PERFORM pg_notify('procrastinate_any_queue', payload); - RETURN NEW; -END; -$$; - -DROP TRIGGER IF EXISTS procrastinate_jobs_notify_queue ON procrastinate_jobs; - -CREATE TRIGGER procrastinate_jobs_notify_queue_job_inserted - AFTER INSERT ON procrastinate_jobs - FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_notify_queue_job_inserted(); - -DROP FUNCTION IF EXISTS procrastinate_notify_queue; - -CREATE OR REPLACE FUNCTION procrastinate_notify_queue_abort_job() -RETURNS trigger - LANGUAGE plpgsql -AS $$ -DECLARE - payload TEXT; -BEGIN - SELECT json_object('type': 'abort_job_requested', 'job_id': NEW.id)::text INTO payload; - PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); - PERFORM pg_notify('procrastinate_any_queue', payload); - RETURN NEW; -END; -$$; - -CREATE TRIGGER procrastinate_jobs_notify_queue_abort_job - AFTER UPDATE OF abort_requested ON procrastinate_jobs - FOR EACH ROW WHEN ((old.abort_requested = false AND new.abort_requested = true AND new.status = 'doing'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_notify_queue_abort_job(); - -CREATE OR REPLACE FUNCTION procrastinate_retry_job( - job_id bigint, - retry_at timestamp with time zone, - new_priority integer, - new_queue_name character varying, - new_lock character varying -) - RETURNS void - LANGUAGE plpgsql -AS $$ -DECLARE - _job_id bigint; -BEGIN - UPDATE procrastinate_jobs - SET status = CASE - WHEN NOT abort_requested THEN 'todo'::procrastinate_job_status - ELSE 'failed'::procrastinate_job_status - END, - attempts = CASE - WHEN NOT abort_requested THEN attempts + 1 - ELSE attempts - END, - scheduled_at = CASE - WHEN NOT abort_requested THEN retry_at - ELSE scheduled_at - END, - priority = CASE - WHEN NOT abort_requested THEN COALESCE(new_priority, priority) - ELSE priority - END, - queue_name = CASE - WHEN NOT abort_requested THEN COALESCE(new_queue_name, queue_name) - ELSE queue_name - END, - lock = CASE - WHEN NOT abort_requested THEN COALESCE(new_lock, lock) - ELSE lock - END - WHERE id = job_id AND status = 'doing' - RETURNING id INTO _job_id; - IF _job_id IS NULL THEN - RAISE 'Job was not found or not in "doing" status (job id: %)', job_id; - END IF; -END; -$$; diff --git a/procrastinate/sql/migrations/03.00.00_01_pre_cancel_notification.sql b/procrastinate/sql/migrations/03.00.00_01_pre_cancel_notification.sql new file mode 100644 index 000000000..8715a43a6 --- /dev/null +++ b/procrastinate/sql/migrations/03.00.00_01_pre_cancel_notification.sql @@ -0,0 +1,295 @@ +-- Note: starting with v3, there are 2 changes in the migration system: +-- - We now have pre- and post-migration scripts. pre-migrations are safe to +-- apply before upgrading the code. post-migrations are safe to apply after +-- upgrading he code. +-- This is a pre-migration script. +-- - Whenever we recreate an immutable object (function, trigger, indexes), we +-- will suffix its name with a version number. + +-- Add an 'abort_requested' column to the procrastinate_jobs table +ALTER TABLE procrastinate_jobs ADD COLUMN abort_requested boolean DEFAULT false NOT NULL; + +-- Set abort requested flag on all jobs with 'aborting' status +UPDATE procrastinate_jobs SET abort_requested = true WHERE status = 'aborting'; + +-- Add temporary triggers to sync the abort_requested flag with the status +-- so that blue-green deployments can work +CREATE OR REPLACE FUNCTION procrastinate_sync_abort_requested_with_status_temp() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + IF NEW.status = 'aborting' THEN + NEW.abort_requested = true; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_trigger_sync_abort_requested_with_status_temp + BEFORE UPDATE OF status ON procrastinate_jobs + FOR EACH ROW + EXECUTE FUNCTION procrastinate_sync_abort_requested_with_status_temp(); + +-- Create the new versioned functions + +CREATE FUNCTION procrastinate_defer_job_v1( + queue_name character varying, + task_name character varying, + priority integer, + lock text, + queueing_lock text, + args jsonb, + scheduled_at timestamp with time zone +) + RETURNS bigint + LANGUAGE plpgsql +AS $$ +DECLARE + job_id bigint; +BEGIN + INSERT INTO procrastinate_jobs (queue_name, task_name, priority, lock, queueing_lock, args, scheduled_at) + VALUES (queue_name, task_name, priority, lock, queueing_lock, args, scheduled_at) + RETURNING id INTO job_id; + + RETURN job_id; +END; +$$; + +CREATE FUNCTION procrastinate_defer_periodic_job_v1( + _queue_name character varying, + _lock character varying, + _queueing_lock character varying, + _task_name character varying, + _priority integer, + _periodic_id character varying, + _defer_timestamp bigint, + _args jsonb +) + RETURNS bigint + LANGUAGE plpgsql +AS $$ +DECLARE + _job_id bigint; + _defer_id bigint; +BEGIN + + INSERT + INTO procrastinate_periodic_defers (task_name, periodic_id, defer_timestamp) + VALUES (_task_name, _periodic_id, _defer_timestamp) + ON CONFLICT DO NOTHING + RETURNING id into _defer_id; + + IF _defer_id IS NULL THEN + RETURN NULL; + END IF; + + UPDATE procrastinate_periodic_defers + SET job_id = procrastinate_defer_job_v1( + _queue_name, + _task_name, + _priority, + _lock, + _queueing_lock, + _args, + NULL + ) + WHERE id = _defer_id + RETURNING job_id INTO _job_id; + + DELETE + FROM procrastinate_periodic_defers + USING ( + SELECT id + FROM procrastinate_periodic_defers + WHERE procrastinate_periodic_defers.task_name = _task_name + AND procrastinate_periodic_defers.periodic_id = _periodic_id + AND procrastinate_periodic_defers.defer_timestamp < _defer_timestamp + ORDER BY id + FOR UPDATE + ) to_delete + WHERE procrastinate_periodic_defers.id = to_delete.id; + + RETURN _job_id; +END; +$$; + +CREATE FUNCTION procrastinate_fetch_job_v1( + target_queue_names character varying[] +) + RETURNS procrastinate_jobs + LANGUAGE plpgsql +AS $$ +DECLARE + found_jobs procrastinate_jobs; +BEGIN + WITH candidate AS ( + SELECT jobs.* + FROM procrastinate_jobs AS jobs + WHERE + -- reject the job if its lock has earlier jobs + NOT EXISTS ( + SELECT 1 + FROM procrastinate_jobs AS earlier_jobs + WHERE + jobs.lock IS NOT NULL + AND earlier_jobs.lock = jobs.lock + AND earlier_jobs.status IN ('todo', 'doing') + AND earlier_jobs.id < jobs.id) + AND jobs.status = 'todo' + AND (target_queue_names IS NULL OR jobs.queue_name = ANY( target_queue_names )) + AND (jobs.scheduled_at IS NULL OR jobs.scheduled_at <= now()) + ORDER BY jobs.priority DESC, jobs.id ASC LIMIT 1 + FOR UPDATE OF jobs SKIP LOCKED + ) + UPDATE procrastinate_jobs + SET status = 'doing' + FROM candidate + WHERE procrastinate_jobs.id = candidate.id + RETURNING procrastinate_jobs.* INTO found_jobs; + + RETURN found_jobs; +END; +$$; + +CREATE FUNCTION procrastinate_finish_job_v1(job_id bigint, end_status procrastinate_job_status, delete_job boolean) + RETURNS void + LANGUAGE plpgsql +AS $$ +DECLARE + _job_id bigint; +BEGIN + IF end_status NOT IN ('succeeded', 'failed', 'aborted') THEN + RAISE 'End status should be either "succeeded", "failed" or "aborted" (job id: %)', job_id; + END IF; + IF delete_job THEN + DELETE FROM procrastinate_jobs + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + ELSE + UPDATE procrastinate_jobs + SET status = end_status, + abort_requested = false, + attempts = CASE status + WHEN 'doing' THEN attempts + 1 ELSE attempts + END + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + END IF; + IF _job_id IS NULL THEN + RAISE 'Job was not found or not in "doing" or "todo" status (job id: %)', job_id; + END IF; +END; +$$; + +CREATE FUNCTION procrastinate_cancel_job_v1(job_id bigint, abort boolean, delete_job boolean) + RETURNS bigint + LANGUAGE plpgsql +AS $$ +DECLARE + _job_id bigint; +BEGIN + IF delete_job THEN + DELETE FROM procrastinate_jobs + WHERE id = job_id AND status = 'todo' + RETURNING id INTO _job_id; + END IF; + IF _job_id IS NULL THEN + IF abort THEN + UPDATE procrastinate_jobs + SET abort_requested = true, + status = CASE status + WHEN 'todo' THEN 'cancelled'::procrastinate_job_status ELSE status + END + WHERE id = job_id AND status IN ('todo', 'doing') + RETURNING id INTO _job_id; + ELSE + UPDATE procrastinate_jobs + SET status = 'cancelled'::procrastinate_job_status + WHERE id = job_id AND status = 'todo' + RETURNING id INTO _job_id; + END IF; + END IF; + RETURN _job_id; +END; +$$; + +-- The retry_job function now has specific behaviour when a job is set to be +-- retried while it's aborting: in that case it's marked as failed. +CREATE FUNCTION procrastinate_retry_job_v1( + job_id bigint, + retry_at timestamp with time zone, + new_priority integer, + new_queue_name character varying, + new_lock character varying +) RETURNS void LANGUAGE plpgsql AS $$ +DECLARE + _job_id bigint; + _abort_requested boolean; +BEGIN + SELECT abort_requested FROM procrastinate_jobs + WHERE id = job_id AND status = 'doing' + FOR UPDATE + INTO _abort_requested; + IF _abort_requested THEN + UPDATE procrastinate_jobs + SET status = 'failed'::procrastinate_job_status + WHERE id = job_id AND status = 'doing' + RETURNING id INTO _job_id; + ELSE + UPDATE procrastinate_jobs + SET status = 'todo'::procrastinate_job_status, + attempts = attempts + 1, + scheduled_at = retry_at, + priority = COALESCE(new_priority, priority), + queue_name = COALESCE(new_queue_name, queue_name), + lock = COALESCE(new_lock, lock) + WHERE id = job_id AND status = 'doing' + RETURNING id INTO _job_id; + END IF; + + IF _job_id IS NULL THEN + RAISE 'Job was not found or not in "doing" status (job id: %)', job_id; + END IF; +END; +$$; + +-- Create new versioned trigger functions and triggers + +CREATE FUNCTION procrastinate_notify_queue_job_inserted_v1() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +DECLARE + payload TEXT; +BEGIN + SELECT json_object('type': 'job_inserted', 'job_id': NEW.id)::text INTO payload; + PERFORM pg_notify('procrastinate_queue_v1#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue_v1', payload); + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_jobs_notify_queue_job_inserted_temp + AFTER INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue_job_inserted_v1(); + +CREATE FUNCTION procrastinate_notify_queue_abort_job_v1() +RETURNS trigger + LANGUAGE plpgsql +AS $$ +DECLARE + payload TEXT; +BEGIN + SELECT json_object('type': 'abort_job_requested', 'job_id': NEW.id)::text INTO payload; + PERFORM pg_notify('procrastinate_queue_v1#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue_v1', payload); + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_jobs_notify_queue_job_aborted_temp + AFTER UPDATE OF abort_requested ON procrastinate_jobs + FOR EACH ROW WHEN ((old.abort_requested = false AND new.abort_requested = true AND new.status = 'doing'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue_abort_job_v1(); diff --git a/procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql b/procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql new file mode 100644 index 000000000..38e40da86 --- /dev/null +++ b/procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql @@ -0,0 +1,202 @@ +-- These are old versions of functions, that we needed to keep around for +-- backwards compatibility. We can now safely drop them. +DROP FUNCTION IF EXISTS procrastinate_finish_job( + integer, + procrastinate_job_status, + timestamp with time zone, + boolean +); +DROP FUNCTION IF EXISTS procrastinate_defer_job( + character varying, + character varying, + text, + text, + jsonb, + timestamp with time zone +); +DROP FUNCTION IF EXISTS procrastinate_defer_periodic_job( + character varying, + character varying, + character varying, + character varying, + character varying, + bigint, + jsonb +); +DROP FUNCTION IF EXISTS procrastinate_retry_job( + bigint, + timestamp with time zone +); +DROP FUNCTION IF EXISTS procrastinate_retry_job( + bigint, + timestamp with time zone, + integer, + character varying, + character varying +); + +-- Remove all traces of the "aborting" status +-- Last sanity update in case the trigger didn't work 100% of the time +UPDATE procrastinate_jobs SET abort_requested = true WHERE status = 'aborting'; + +-- Delete the indexes that depend on the old status and enum type +DROP INDEX IF EXISTS procrastinate_jobs_queueing_lock_idx; +DROP INDEX IF EXISTS procrastinate_jobs_lock_idx; +DROP INDEX IF EXISTS procrastinate_jobs_id_lock_idx; + +-- Delete the unversioned triggers +DROP TRIGGER IF EXISTS procrastinate_trigger_status_events_update ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_trigger_status_events_insert ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_trigger_scheduled_events ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_trigger_status_events_update ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_jobs_notify_queue ON procrastinate_jobs; + +-- Delete the unversioned functions +DROP FUNCTION IF EXISTS procrastinate_defer_job; +DROP FUNCTION IF EXISTS procrastinate_defer_periodic_job; +DROP FUNCTION IF EXISTS procrastinate_fetch_job; +DROP FUNCTION IF EXISTS procrastinate_finish_job(bigint, procrastinate_job_status, boolean); +DROP FUNCTION IF EXISTS procrastinate_cancel_job; +DROP FUNCTION IF EXISTS procrastinate_trigger_status_events_procedure_update; +DROP FUNCTION IF EXISTS procrastinate_finish_job(integer, procrastinate_job_status, timestamp with time zone, boolean); +DROP FUNCTION IF EXISTS procrastinate_notify_queue; + +-- Delete the functions that depend on the old event type +DROP FUNCTION IF EXISTS procrastinate_trigger_status_events_procedure_insert; +DROP FUNCTION IF EXISTS procrastinate_trigger_scheduled_events_procedure; + +-- Delete temporary triggers and functions +DROP TRIGGER IF EXISTS procrastinate_jobs_notify_queue_job_inserted_temp ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_jobs_notify_queue_job_aborted_temp ON procrastinate_jobs; +DROP TRIGGER IF EXISTS procrastinate_trigger_sync_abort_requested_with_status_temp ON procrastinate_jobs; +DROP FUNCTION IF EXISTS procrastinate_sync_abort_requested_with_status_temp; + +-- Alter the table to not use the 'aborting' status anymore +ALTER TABLE procrastinate_jobs +ALTER COLUMN status TYPE procrastinate_job_status +USING ( + CASE status::text + WHEN 'aborting' THEN 'doing'::procrastinate_job_status + ELSE status::procrastinate_job_status + END +); + +-- Recreate the dropped temporary triggers +CREATE TRIGGER procrastinate_jobs_notify_queue_job_inserted_v1 + AFTER INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue_job_inserted_v1(); +CREATE TRIGGER procrastinate_jobs_notify_queue_job_aborted_v1 + AFTER UPDATE OF abort_requested ON procrastinate_jobs + FOR EACH ROW WHEN ((old.abort_requested = false AND new.abort_requested = true AND new.status = 'doing'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_notify_queue_abort_job_v1(); + +-- Recreate the dropped indexes (with version suffix) +CREATE UNIQUE INDEX procrastinate_jobs_queueing_lock_idx_v1 ON procrastinate_jobs (queueing_lock) WHERE status = 'todo'; +CREATE UNIQUE INDEX procrastinate_jobs_lock_idx_v1 ON procrastinate_jobs (lock) WHERE status = 'doing'; +CREATE INDEX procrastinate_jobs_id_lock_idx_v1 ON procrastinate_jobs (id, lock) WHERE status = ANY (ARRAY['todo'::procrastinate_job_status, 'doing'::procrastinate_job_status]); + +-- Rename existing indexes +ALTER INDEX procrastinate_jobs_queue_name_idx RENAME TO procrastinate_jobs_queue_name_idx_v1; +ALTER INDEX procrastinate_events_job_id_fkey RENAME TO procrastinate_events_job_id_fkey_v1; +ALTER INDEX procrastinate_periodic_defers_job_id_fkey RENAME TO procrastinate_periodic_defers_job_id_fkey_v1; + +-- Recreate or rename the other triggers & their associated functions + +CREATE FUNCTION procrastinate_trigger_function_status_events_insert_v1() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO procrastinate_events(job_id, type) + VALUES (NEW.id, 'deferred'::procrastinate_job_event_type); + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_trigger_status_events_insert_v1 + AFTER INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_trigger_function_status_events_insert_v1(); + +CREATE FUNCTION procrastinate_trigger_function_status_events_update_v1() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + WITH t AS ( + SELECT CASE + WHEN OLD.status = 'todo'::procrastinate_job_status + AND NEW.status = 'doing'::procrastinate_job_status + THEN 'started'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'todo'::procrastinate_job_status + THEN 'deferred_for_retry'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'failed'::procrastinate_job_status + THEN 'failed'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'succeeded'::procrastinate_job_status + THEN 'succeeded'::procrastinate_job_event_type + WHEN OLD.status = 'todo'::procrastinate_job_status + AND ( + NEW.status = 'cancelled'::procrastinate_job_status + OR NEW.status = 'failed'::procrastinate_job_status + OR NEW.status = 'succeeded'::procrastinate_job_status + ) + THEN 'cancelled'::procrastinate_job_event_type + WHEN OLD.status = 'doing'::procrastinate_job_status + AND NEW.status = 'aborted'::procrastinate_job_status + THEN 'aborted'::procrastinate_job_event_type + ELSE NULL + END as event_type + ) + INSERT INTO procrastinate_events(job_id, type) + SELECT NEW.id, t.event_type + FROM t + WHERE t.event_type IS NOT NULL; + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_trigger_status_events_update_v1 + AFTER UPDATE OF status ON procrastinate_jobs + FOR EACH ROW + EXECUTE PROCEDURE procrastinate_trigger_function_status_events_update_v1(); + +CREATE FUNCTION procrastinate_trigger_function_scheduled_events_v1() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO procrastinate_events(job_id, type, at) + VALUES (NEW.id, 'scheduled'::procrastinate_job_event_type, NEW.scheduled_at); + + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_trigger_scheduled_events_v1 + AFTER UPDATE OR INSERT ON procrastinate_jobs + FOR EACH ROW WHEN ((new.scheduled_at IS NOT NULL AND new.status = 'todo'::procrastinate_job_status)) + EXECUTE PROCEDURE procrastinate_trigger_function_scheduled_events_v1(); + +CREATE FUNCTION procrastinate_trigger_abort_requested_events_procedure_v1() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO procrastinate_events(job_id, type) + VALUES (NEW.id, 'abort_requested'::procrastinate_job_event_type); + RETURN NEW; +END; +$$; + +CREATE TRIGGER procrastinate_trigger_abort_requested_events_v1 + AFTER UPDATE OF abort_requested ON procrastinate_jobs + FOR EACH ROW WHEN ((new.abort_requested = true)) + EXECUTE PROCEDURE procrastinate_trigger_abort_requested_events_procedure_v1(); + +-- Rename remaining functions to use version suffix +ALTER FUNCTION procrastinate_unlink_periodic_defers RENAME TO procrastinate_unlink_periodic_defers_v1; +ALTER TRIGGER procrastinate_trigger_delete_jobs ON procrastinate_jobs RENAME TO procrastinate_trigger_delete_jobs_v1; diff --git a/procrastinate/sql/queries.sql b/procrastinate/sql/queries.sql index fd851f152..ddb1b92c2 100644 --- a/procrastinate/sql/queries.sql +++ b/procrastinate/sql/queries.sql @@ -5,17 +5,17 @@ -- defer_job -- -- Create and enqueue a job -SELECT procrastinate_defer_job(%(queue)s, %(task_name)s, %(priority)s, %(lock)s, %(queueing_lock)s, %(args)s, %(scheduled_at)s) AS id; +SELECT procrastinate_defer_job_v1(%(queue)s, %(task_name)s, %(priority)s, %(lock)s, %(queueing_lock)s, %(args)s, %(scheduled_at)s) AS id; -- defer_periodic_job -- -- Create a periodic job if it doesn't already exist, and delete periodic metadata -- for previous jobs in the same task. -SELECT procrastinate_defer_periodic_job(%(queue)s, %(lock)s, %(queueing_lock)s, %(task_name)s, %(periodic_id)s, %(defer_timestamp)s, %(args)s) AS id; +SELECT procrastinate_defer_periodic_job_v1(%(queue)s, %(lock)s, %(queueing_lock)s, %(task_name)s, %(priority)s, %(periodic_id)s, %(defer_timestamp)s, %(args)s) AS id; -- fetch_job -- -- Get the first awaiting job SELECT id, status, task_name, priority, lock, queueing_lock, args, scheduled_at, queue_name, attempts - FROM procrastinate_fetch_job(%(queues)s); + FROM procrastinate_fetch_job_v1(%(queues)s::varchar[]); -- select_stalled_jobs -- -- Get running jobs that started more than a given time ago @@ -48,11 +48,11 @@ WHERE id IN ( -- finish_job -- -- Finish a job, changing it from "doing" to "succeeded" or "failed" -SELECT procrastinate_finish_job(%(job_id)s, %(status)s, %(delete_job)s); +SELECT procrastinate_finish_job_v1(%(job_id)s, %(status)s, %(delete_job)s); -- cancel_job -- -- Cancel a job, changing it from "todo" to "cancelled" or mark for abortion -SELECT procrastinate_cancel_job(%(job_id)s, %(abort)s, %(delete_job)s) AS id; +SELECT procrastinate_cancel_job_v1(%(job_id)s, %(abort)s, %(delete_job)s) AS id; -- get_job_status -- -- Get the status of a job @@ -60,7 +60,7 @@ SELECT status FROM procrastinate_jobs WHERE id = %(job_id)s; -- retry_job -- -- Retry a job, changing it from "doing" to "todo" -SELECT procrastinate_retry_job(%(job_id)s, %(retry_at)s, %(new_priority)s, %(new_queue_name)s, %(new_lock)s); +SELECT procrastinate_retry_job_v1(%(job_id)s, %(retry_at)s, %(new_priority)s, %(new_queue_name)s, %(new_lock)s); -- listen_queue -- -- In this one, the argument is an identifier, shoud not be escaped the same way diff --git a/procrastinate/sql/schema.sql b/procrastinate/sql/schema.sql index d7ee49cd0..67306e5ef 100644 --- a/procrastinate/sql/schema.sql +++ b/procrastinate/sql/schema.sql @@ -10,6 +10,7 @@ CREATE TYPE procrastinate_job_status AS ENUM ( 'succeeded', -- The job ended successfully 'failed', -- The job ended with an error 'cancelled', -- The job was cancelled + 'aborting', -- legacy, not used anymore since v3.0.0 'aborted' -- The job was aborted ); @@ -60,21 +61,20 @@ CREATE TABLE procrastinate_events ( -- Constraints & Indices -- this prevents from having several jobs with the same queueing lock in the "todo" state -CREATE UNIQUE INDEX procrastinate_jobs_queueing_lock_idx ON procrastinate_jobs (queueing_lock) WHERE status = 'todo'; +CREATE UNIQUE INDEX procrastinate_jobs_queueing_lock_idx_v1 ON procrastinate_jobs (queueing_lock) WHERE status = 'todo'; -- this prevents from having several jobs with the same lock in the "doing" state -CREATE UNIQUE INDEX procrastinate_jobs_lock_idx ON procrastinate_jobs (lock) WHERE status = 'doing'; +CREATE UNIQUE INDEX procrastinate_jobs_lock_idx_v1 ON procrastinate_jobs (lock) WHERE status = 'doing'; -CREATE INDEX procrastinate_jobs_queue_name_idx ON procrastinate_jobs(queue_name); -CREATE INDEX procrastinate_jobs_id_lock_idx ON procrastinate_jobs (id, lock) WHERE status = ANY (ARRAY['todo'::procrastinate_job_status, 'doing'::procrastinate_job_status]); +CREATE INDEX procrastinate_jobs_queue_name_idx_v1 ON procrastinate_jobs(queue_name); +CREATE INDEX procrastinate_jobs_id_lock_idx_v1 ON procrastinate_jobs (id, lock) WHERE status = ANY (ARRAY['todo'::procrastinate_job_status, 'doing'::procrastinate_job_status]); -CREATE INDEX procrastinate_events_job_id_fkey ON procrastinate_events(job_id); +CREATE INDEX procrastinate_events_job_id_fkey_v1 ON procrastinate_events(job_id); -CREATE INDEX procrastinate_periodic_defers_job_id_fkey ON procrastinate_periodic_defers(job_id); +CREATE INDEX procrastinate_periodic_defers_job_id_fkey_v1 ON procrastinate_periodic_defers(job_id); -- Functions - -CREATE FUNCTION procrastinate_defer_job( +CREATE FUNCTION procrastinate_defer_job_v1( queue_name character varying, task_name character varying, priority integer, @@ -97,7 +97,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_defer_periodic_job( +CREATE FUNCTION procrastinate_defer_periodic_job_v1( _queue_name character varying, _lock character varying, _queueing_lock character varying, @@ -126,7 +126,7 @@ BEGIN END IF; UPDATE procrastinate_periodic_defers - SET job_id = procrastinate_defer_job( + SET job_id = procrastinate_defer_job_v1( _queue_name, _task_name, _priority, @@ -155,7 +155,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_fetch_job( +CREATE FUNCTION procrastinate_fetch_job_v1( target_queue_names character varying[] ) RETURNS procrastinate_jobs @@ -193,7 +193,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_finish_job(job_id bigint, end_status procrastinate_job_status, delete_job boolean) +CREATE FUNCTION procrastinate_finish_job_v1(job_id bigint, end_status procrastinate_job_status, delete_job boolean) RETURNS void LANGUAGE plpgsql AS $$ @@ -223,7 +223,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_cancel_job(job_id bigint, abort boolean, delete_job boolean) +CREATE FUNCTION procrastinate_cancel_job_v1(job_id bigint, abort boolean, delete_job boolean) RETURNS bigint LANGUAGE plpgsql AS $$ @@ -255,53 +255,45 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_retry_job( +CREATE FUNCTION procrastinate_retry_job_v1( job_id bigint, retry_at timestamp with time zone, new_priority integer, new_queue_name character varying, new_lock character varying -) - RETURNS void - LANGUAGE plpgsql -AS $$ +) RETURNS void LANGUAGE plpgsql AS $$ DECLARE _job_id bigint; + _abort_requested boolean; BEGIN - UPDATE procrastinate_jobs - SET status = CASE - WHEN NOT abort_requested THEN 'todo'::procrastinate_job_status - ELSE 'failed'::procrastinate_job_status - END, - attempts = CASE - WHEN NOT abort_requested THEN attempts + 1 - ELSE attempts - END, - scheduled_at = CASE - WHEN NOT abort_requested THEN retry_at - ELSE scheduled_at - END, - priority = CASE - WHEN NOT abort_requested THEN COALESCE(new_priority, priority) - ELSE priority - END, - queue_name = CASE - WHEN NOT abort_requested THEN COALESCE(new_queue_name, queue_name) - ELSE queue_name - END, - lock = CASE - WHEN NOT abort_requested THEN COALESCE(new_lock, lock) - ELSE lock - END + SELECT abort_requested FROM procrastinate_jobs WHERE id = job_id AND status = 'doing' - RETURNING id INTO _job_id; + FOR UPDATE + INTO _abort_requested; + IF _abort_requested THEN + UPDATE procrastinate_jobs + SET status = 'failed'::procrastinate_job_status + WHERE id = job_id AND status = 'doing' + RETURNING id INTO _job_id; + ELSE + UPDATE procrastinate_jobs + SET status = 'todo'::procrastinate_job_status, + attempts = attempts + 1, + scheduled_at = retry_at, + priority = COALESCE(new_priority, priority), + queue_name = COALESCE(new_queue_name, queue_name), + lock = COALESCE(new_lock, lock) + WHERE id = job_id AND status = 'doing' + RETURNING id INTO _job_id; + END IF; + IF _job_id IS NULL THEN RAISE 'Job was not found or not in "doing" status (job id: %)', job_id; END IF; END; $$; -CREATE FUNCTION procrastinate_notify_queue_job_inserted() +CREATE FUNCTION procrastinate_notify_queue_job_inserted_v1() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -309,13 +301,13 @@ DECLARE payload TEXT; BEGIN SELECT json_object('type': 'job_inserted', 'job_id': NEW.id)::text INTO payload; - PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); - PERFORM pg_notify('procrastinate_any_queue', payload); + PERFORM pg_notify('procrastinate_queue_v1#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue_v1', payload); RETURN NEW; END; $$; -CREATE FUNCTION procrastinate_notify_queue_abort_job() +CREATE FUNCTION procrastinate_notify_queue_abort_job_v1() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -323,13 +315,13 @@ DECLARE payload TEXT; BEGIN SELECT json_object('type': 'abort_job_requested', 'job_id': NEW.id)::text INTO payload; - PERFORM pg_notify('procrastinate_queue#' || NEW.queue_name, payload); - PERFORM pg_notify('procrastinate_any_queue', payload); + PERFORM pg_notify('procrastinate_queue_v1#' || NEW.queue_name, payload); + PERFORM pg_notify('procrastinate_any_queue_v1', payload); RETURN NEW; END; $$; -CREATE FUNCTION procrastinate_trigger_status_events_procedure_insert() +CREATE FUNCTION procrastinate_trigger_function_status_events_insert_v1() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -340,7 +332,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_trigger_status_events_procedure_update() +CREATE FUNCTION procrastinate_trigger_function_status_events_update_v1() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -380,7 +372,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_trigger_scheduled_events_procedure() +CREATE FUNCTION procrastinate_trigger_function_scheduled_events_v1() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -392,7 +384,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_trigger_abort_requested_events_procedure() +CREATE FUNCTION procrastinate_trigger_abort_requested_events_procedure_v1() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -403,7 +395,7 @@ BEGIN END; $$; -CREATE FUNCTION procrastinate_unlink_periodic_defers() +CREATE FUNCTION procrastinate_unlink_periodic_defers_v1() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -417,177 +409,36 @@ $$; -- Triggers -CREATE TRIGGER procrastinate_jobs_notify_queue_job_inserted +CREATE TRIGGER procrastinate_jobs_notify_queue_job_inserted_v1 AFTER INSERT ON procrastinate_jobs FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_notify_queue_job_inserted(); + EXECUTE PROCEDURE procrastinate_notify_queue_job_inserted_v1(); -CREATE TRIGGER procrastinate_jobs_notify_queue_abort_job +CREATE TRIGGER procrastinate_jobs_notify_queue_job_aborted_v1 AFTER UPDATE OF abort_requested ON procrastinate_jobs FOR EACH ROW WHEN ((old.abort_requested = false AND new.abort_requested = true AND new.status = 'doing'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_notify_queue_abort_job(); + EXECUTE PROCEDURE procrastinate_notify_queue_abort_job_v1(); -CREATE TRIGGER procrastinate_trigger_status_events_update +CREATE TRIGGER procrastinate_trigger_status_events_update_v1 AFTER UPDATE OF status ON procrastinate_jobs FOR EACH ROW - EXECUTE PROCEDURE procrastinate_trigger_status_events_procedure_update(); + EXECUTE PROCEDURE procrastinate_trigger_function_status_events_update_v1(); -CREATE TRIGGER procrastinate_trigger_status_events_insert +CREATE TRIGGER procrastinate_trigger_status_events_insert_v1 AFTER INSERT ON procrastinate_jobs FOR EACH ROW WHEN ((new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_trigger_status_events_procedure_insert(); + EXECUTE PROCEDURE procrastinate_trigger_function_status_events_insert_v1(); -CREATE TRIGGER procrastinate_trigger_scheduled_events +CREATE TRIGGER procrastinate_trigger_scheduled_events_v1 AFTER UPDATE OR INSERT ON procrastinate_jobs FOR EACH ROW WHEN ((new.scheduled_at IS NOT NULL AND new.status = 'todo'::procrastinate_job_status)) - EXECUTE PROCEDURE procrastinate_trigger_scheduled_events_procedure(); + EXECUTE PROCEDURE procrastinate_trigger_function_scheduled_events_v1(); -CREATE TRIGGER procrastinate_trigger_abort_requested_events +CREATE TRIGGER procrastinate_trigger_abort_requested_events_v1 AFTER UPDATE OF abort_requested ON procrastinate_jobs FOR EACH ROW WHEN ((new.abort_requested = true)) - EXECUTE PROCEDURE procrastinate_trigger_abort_requested_events_procedure(); + EXECUTE PROCEDURE procrastinate_trigger_abort_requested_events_procedure_v1(); -CREATE TRIGGER procrastinate_trigger_delete_jobs +CREATE TRIGGER procrastinate_trigger_delete_jobs_v1 BEFORE DELETE ON procrastinate_jobs - FOR EACH ROW EXECUTE PROCEDURE procrastinate_unlink_periodic_defers(); - - --- Old versions of functions, for backwards compatibility (to be removed in a future release) - --- procrastinate_defer_job --- the function without the priority argument is kept for compatibility reasons -CREATE FUNCTION procrastinate_defer_job( - queue_name character varying, - task_name character varying, - lock text, - queueing_lock text, - args jsonb, - scheduled_at timestamp with time zone -) - RETURNS bigint - LANGUAGE plpgsql -AS $$ -DECLARE - job_id bigint; -BEGIN - INSERT INTO procrastinate_jobs (queue_name, task_name, lock, queueing_lock, args, scheduled_at) - VALUES (queue_name, task_name, lock, queueing_lock, args, scheduled_at) - RETURNING id INTO job_id; - - RETURN job_id; -END; -$$; - --- procrastinate_finish_job --- the next_scheduled_at argument is kept for compatibility reasons -CREATE FUNCTION procrastinate_finish_job(job_id integer, end_status procrastinate_job_status, next_scheduled_at timestamp with time zone, delete_job boolean) - RETURNS void - LANGUAGE plpgsql -AS $$ -DECLARE - _job_id bigint; -BEGIN - IF end_status NOT IN ('succeeded', 'failed') THEN - RAISE 'End status should be either "succeeded" or "failed" (job id: %)', job_id; - END IF; - IF delete_job THEN - DELETE FROM procrastinate_jobs - WHERE id = job_id AND status IN ('todo', 'doing') - RETURNING id INTO _job_id; - ELSE - UPDATE procrastinate_jobs - SET status = end_status, - abort_requested = false, - attempts = - CASE - WHEN status = 'doing' THEN attempts + 1 - ELSE attempts - END - WHERE id = job_id AND status IN ('todo', 'doing') - RETURNING id INTO _job_id; - END IF; - IF _job_id IS NULL THEN - RAISE 'Job was not found or not in "doing" or "todo" status (job id: %)', job_id; - END IF; -END; -$$; - --- procrastinate_defer_periodic_job --- the function without the priority argument is kept for compatibility reasons -CREATE FUNCTION procrastinate_defer_periodic_job( - _queue_name character varying, - _lock character varying, - _queueing_lock character varying, - _task_name character varying, - _periodic_id character varying, - _defer_timestamp bigint, - _args jsonb -) - RETURNS bigint - LANGUAGE plpgsql -AS $$ -DECLARE - _job_id bigint; - _defer_id bigint; -BEGIN - - INSERT - INTO procrastinate_periodic_defers (task_name, periodic_id, defer_timestamp) - VALUES (_task_name, _periodic_id, _defer_timestamp) - ON CONFLICT DO NOTHING - RETURNING id into _defer_id; - - IF _defer_id IS NULL THEN - RETURN NULL; - END IF; - - UPDATE procrastinate_periodic_defers - SET job_id = procrastinate_defer_job( - _queue_name, - _task_name, - 0, - _lock, - _queueing_lock, - _args, - NULL - ) - WHERE id = _defer_id - RETURNING job_id INTO _job_id; - - DELETE - FROM procrastinate_periodic_defers - USING ( - SELECT id - FROM procrastinate_periodic_defers - WHERE procrastinate_periodic_defers.task_name = _task_name - AND procrastinate_periodic_defers.periodic_id = _periodic_id - AND procrastinate_periodic_defers.defer_timestamp < _defer_timestamp - ORDER BY id - FOR UPDATE - ) to_delete - WHERE procrastinate_periodic_defers.id = to_delete.id; - - RETURN _job_id; -END; -$$; - --- procrastinate_retry_job --- the function without the new_* arguments is kept for compatibility reasons -CREATE FUNCTION procrastinate_retry_job(job_id bigint, retry_at timestamp with time zone) - RETURNS void - LANGUAGE plpgsql -AS $$ -DECLARE - _job_id bigint; -BEGIN - UPDATE procrastinate_jobs - SET status = 'todo', - attempts = attempts + 1, - scheduled_at = retry_at - WHERE id = job_id AND status = 'doing' - RETURNING id INTO _job_id; - IF _job_id IS NULL THEN - RAISE 'Job was not found or not in "doing" status (job id: %)', job_id; - END IF; -END; -$$; + FOR EACH ROW EXECUTE PROCEDURE procrastinate_unlink_periodic_defers_v1(); diff --git a/procrastinate/testing.py b/procrastinate/testing.py index 4cb4c7481..b356af9ae 100644 --- a/procrastinate/testing.py +++ b/procrastinate/testing.py @@ -187,8 +187,8 @@ async def _notify(self, queue_name: str, notification: jobs.Notification): return destination_channels = { - "procrastinate_any_queue", - f"procrastinate_queue#{queue_name}", + "procrastinate_any_queue_v1", + f"procrastinate_queue_v1#{queue_name}", } for channel in set(self.notify_channels).intersection(destination_channels): diff --git a/tests/integration/test_manager.py b/tests/integration/test_manager.py index 19b07d8db..39cf07699 100644 --- a/tests/integration/test_manager.py +++ b/tests/integration/test_manager.py @@ -396,7 +396,13 @@ async def test_defer_job_violate_queueing_lock(pg_job_manager, job_factory): ) cause = excinfo.value.__cause__ assert isinstance(cause, exceptions.UniqueViolation) - assert cause.constraint_name == "procrastinate_jobs_queueing_lock_idx" + + # TODO: When QUEUEING_LOCK_CONSTRAINT_LEGACY in manager.py is removed, we can + # also remove the check for the old constraint name "procrastinate_jobs_queueing_lock_idx" + assert cause.constraint_name in [ + "procrastinate_jobs_queueing_lock_idx", + "procrastinate_jobs_queueing_lock_idx_v1", + ] async def test_check_connection(pg_job_manager): diff --git a/tests/integration/test_psycopg_connector.py b/tests/integration/test_psycopg_connector.py index 9539e50ef..48fe3e0c3 100644 --- a/tests/integration/test_psycopg_connector.py +++ b/tests/integration/test_psycopg_connector.py @@ -99,13 +99,13 @@ async def test_execute_query(psycopg_connector): async def test_wrap_exceptions(psycopg_connector): await psycopg_connector.execute_query_async( - """SELECT procrastinate_defer_job( + """SELECT procrastinate_defer_job_v1( 'queue', 'foo', 0, NULL, 'lock', '{}', NULL ) AS id;""" ) with pytest.raises(exceptions.UniqueViolation): await psycopg_connector.execute_query_async( - """SELECT procrastinate_defer_job( + """SELECT procrastinate_defer_job_v1( 'queue', 'foo', 0, NULL, 'lock', '{}', NULL ) AS id;""" ) diff --git a/tests/unit/test_manager.py b/tests/unit/test_manager.py index 3a1e695a5..fb91f1b0b 100644 --- a/tests/unit/test_manager.py +++ b/tests/unit/test_manager.py @@ -62,7 +62,7 @@ async def test_manager_defer_job_unique_violation_exception( ): connector.execute_query_one_async = mocker.Mock( side_effect=exceptions.UniqueViolation( - constraint_name="procrastinate_jobs_queueing_lock_idx" + constraint_name="procrastinate_jobs_queueing_lock_idx_v1" ) ) @@ -86,7 +86,7 @@ async def test_manager_defer_job_unique_violation_exception_sync( ): connector.execute_query_one = mocker.Mock( side_effect=exceptions.UniqueViolation( - constraint_name="procrastinate_jobs_queueing_lock_idx" + constraint_name="procrastinate_jobs_queueing_lock_idx_v1" ) ) @@ -295,8 +295,8 @@ async def test_retry_job(job_manager, job_factory, connector): @pytest.mark.parametrize( "queues, channels", [ - (None, ["procrastinate_any_queue"]), - (["a", "b"], ["procrastinate_queue#a", "procrastinate_queue#b"]), + (None, ["procrastinate_any_queue_v1"]), + (["a", "b"], ["procrastinate_queue_v1#a", "procrastinate_queue_v1#b"]), ], ) async def test_listen_for_jobs(job_manager, connector, mocker, queues, channels): diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 2871d0441..da00e9cb9 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -98,7 +98,7 @@ async def test_worker_run_wait_listen(worker): await start_worker(worker) connector = cast(InMemoryConnector, worker.app.connector) - assert connector.notify_channels == ["procrastinate_any_queue"] + assert connector.notify_channels == ["procrastinate_any_queue_v1"] @pytest.mark.parametrize( From e49b5d5a82e5fe8222bd26ca5e1bc14f6181334f Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 20:05:40 +0100 Subject: [PATCH 070/375] Remove coverage by default in pytest --- .github/workflows/ci.yml | 2 +- pyproject.toml | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36e31446b..cbd238186 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: - run: poetry install --all-extras - name: Run tests - run: scripts/tests + run: scripts/tests --cov=procrastinate --cov-branch env: COVERAGE_FILE: ".coverage.${{ matrix.python-version }}" PGHOST: localhost diff --git a/pyproject.toml b/pyproject.toml index 63a014496..82641bbc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,16 +106,7 @@ enable = true pattern = '(?P\d+(\.\d+)*)([-._]?((?P[a-zA-Z]+)[-._]?(?P\d+)?))?$' [tool.pytest.ini_options] -addopts = [ - "--cov-report=term-missing", - "--cov-report=html", - "--cov-branch", - "--cov=procrastinate", - "-vv", - "--strict-markers", - "-rfE", - "--reuse-db", -] +addopts = ["-vv", "--strict-markers", "-rfE", "--reuse-db"] testpaths = [ "tests/unit", "tests/integration", From 3c0bc03a8e47712f75433daea17d0cb3b48879c3 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 20:06:38 +0100 Subject: [PATCH 071/375] New acceptance tests system: running 3 times --- .github/workflows/ci.yml | 48 ++++++++++++++++++ noxfile.py | 103 +++++++++++++++++++++++++++++++++++++++ scripts/bootstrap | 5 +- tests/conftest.py | 65 ++++++++++++++++++++++-- 4 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 noxfile.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbd238186..31bebaa15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,54 @@ jobs: path: .coverage.${{ matrix.python-version }} include-hidden-files: true + acceptance: + strategy: + matrix: + mode: + - "current_version_without_post_migration" + - "stable_version_without_post_migration" + + name: "acceptance-test" + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + # Set health checks to wait until postgres has started + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + + - name: Install poetry + run: pipx install poetry + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "poetry" + + - name: Get latest version + id: get-latest-version + run: gh release list --limit 1 --json tagName --jq '"checkout_ref="+.[0].tagName' >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run tests + run: pipx run nox -s ${{ matrix.mode }} + env: + PGHOST: localhost + PGUSER: postgres + PGPASSWORD: postgres + LATEST_VERSION: ${{ steps.get-latest-version.outputs.checkout_ref }} + static-typing: name: Run Pyright runs-on: ubuntu-latest diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..528170e59 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import os +import pathlib +import shutil +import tempfile + +import nox # type: ignore +import packaging.version + + +def fetch_latest_tag(session: nox.Session) -> packaging.version.Version: + if "LATEST_TAG" in os.environ: + return packaging.version.Version(os.environ["LATEST_TAG"]) + + session.run("git", "fetch", "--tags", external=True) + out = session.run("git", "tag", "--list", external=True, silent=True) + assert out + return max(packaging.version.Version(tag) for tag in out.splitlines()) + + +def get_pre_migration(latest_tag: packaging.version.Version) -> str: + migrations_folder = ( + pathlib.Path(__file__).parent / "procrastinate" / "sql" / "migrations" + ) + migrations = sorted(migrations_folder.glob("*.sql")) + pre_migration: pathlib.Path | None = None + for migration in migrations: + mig_version = packaging.version.Version(migration.name.split("_")[0]) + if mig_version > latest_tag and "_post_" in migration.name: + break + + pre_migration = migration + + assert pre_migration is not None + return pre_migration.name + + +@nox.session +def current_version_with_post_migration(session: nox.Session): + session.install("poetry") + session.run("poetry", "install", "--all-extras") + session.run( + "poetry", + "run", + "pytest", + *session.posargs, + ) + + +@nox.session +def current_version_without_post_migration(session: nox.Session): + latest_tag = fetch_latest_tag(session) + pre_migration = get_pre_migration(latest_tag) + + session.install("poetry") + session.run("poetry", "install", "--all-extras") + session.run( + "poetry", + "run", + "pytest", + f"--migrate-until={pre_migration}", + "./tests/acceptance", + *session.posargs, + ) + + +@nox.session +def stable_version_without_post_migration(session: nox.Session): + latest_tag = fetch_latest_tag(session) + pre_migration = get_pre_migration(latest_tag) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = pathlib.Path(temp_dir) + base_path = pathlib.Path(__file__).parent + shutil.copytree(base_path / "tests", temp_path / "tests") + shutil.copy(base_path / "pyproject.toml", temp_path / "pyproject.toml") + shutil.copy(base_path / "poetry.lock", temp_path / "poetry.lock") + session.chdir(temp_dir) + + session.install("poetry") + session.install("poetry-plugin-export") + session.run( + "poetry", + "export", + "--output", + "requirements.txt", + "--with", + "test", + "--all-extras", + "--without-hashes", + env={"POETRY_WARNINGS_EXPORT": "false"}, + ) + session.install("-r", "requirements.txt") + session.install("procrastinate") + session.run( + "pytest", + f"--migrate-until={pre_migration}", + f"--latest-version={latest_tag}", + "./tests/acceptance", + *session.posargs, + env={"PYTHONPATH": temp_dir}, + ) diff --git a/scripts/bootstrap b/scripts/bootstrap index 3aadf7063..e2719a14e 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -8,7 +8,7 @@ set -eu # between projects. -if ! which pre-commit || ! which poetry; then +if ! which pre-commit || ! which poetry || ! which nox; then if ! which pipx; then python3 -m pip install --user pipx python3 -m pipx ensurepath @@ -19,6 +19,9 @@ if ! which pre-commit || ! which poetry; then if ! which poetry; then pipx install poetry fi + if ! which nox; then + pipx install nox + fi fi pre-commit install diff --git a/tests/conftest.py b/tests/conftest.py index e8bbf4086..09de9d254 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,9 @@ import signal as stdlib_signal import string import uuid +from pathlib import Path +import packaging.version import psycopg import psycopg.conninfo import psycopg.sql @@ -30,6 +32,49 @@ pytest_plugins = ["sphinx.testing.fixtures"] +def pytest_addoption(parser: pytest.Parser) -> None: + parser.addoption( + "--migrate-until", + action="store", + help="Migrate until a specific migration (including it), " + "otherwise the full schema is applied", + ) + + parser.addoption( + "--latest-version", + action="store", + help="Tells pytest what the latest version is so that " + "@pytest.mark.skip_before_version works", + ) + + +def pytest_configure(config): + # register an additional marker + config.addinivalue_line( + "markers", + "skip_before_version(version: str): mark test to run only on versions " + "strictly higher than param. Useful for acceptance tests running on the " + "stable version", + ) + + +def pytest_runtest_setup(item): + required_version = next( + (mark.args[0] for mark in item.iter_markers(name="skip_before_version")), None + ) + latest_version_str = item.config.getoption("--latest-version") + if required_version is None or latest_version_str is None: + return + + latest_version = packaging.version.Version(latest_version_str) + required_version = packaging.version.Version(required_version) + + if latest_version < required_version: + pytest.skip( + f"Skipping test on version {latest_version}, requires {required_version}" + ) + + def cursor_execute(cursor, query, *identifiers): if identifiers: query = psycopg.sql.SQL(query).format( @@ -79,13 +124,27 @@ def _(dbname, template=None): @pytest.fixture(scope="session") -def setup_db(): +def setup_db(request: pytest.FixtureRequest): dbname = "procrastinate_test_template" db_create(dbname=dbname) connector = testing.InMemoryConnector() - with db_executor(dbname) as execute: - execute(schema.SchemaManager(connector=connector).get_schema()) + migrate_until = request.config.getoption("--migrate-until") + if migrate_until is None: + with db_executor(dbname) as execute: + execute(schema.SchemaManager(connector=connector).get_schema()) + else: + assert isinstance(migrate_until, str) + schema_manager = schema.SchemaManager(connector=connector) + migrations_path = Path(schema_manager.get_migrations_path()) + migrations = sorted(migrations_path.glob("*.sql")) + for migration in migrations: + with migration.open() as f: + with db_executor(dbname) as execute: + execute(f.read()) + + if migration.name == migrate_until: + break yield dbname From 0e83c307f4d2059ebca8c82f988518b75c9d7631 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 20:06:59 +0100 Subject: [PATCH 072/375] Skip recent acceptance tests on 2.x --- tests/acceptance/test_async.py | 7 +++++++ tests/acceptance/test_shell.py | 1 + 2 files changed, 8 insertions(+) diff --git a/tests/acceptance/test_async.py b/tests/acceptance/test_async.py index 5a2e2f875..2f0a5c9bc 100644 --- a/tests/acceptance/test_async.py +++ b/tests/acceptance/test_async.py @@ -109,6 +109,7 @@ def example_task(): assert len(jobs) == 1 +@pytest.mark.skip_before_version("3.0.0") @pytest.mark.parametrize("mode", ["listen", "poll"]) async def test_abort_async_task(async_app: app_module.App, mode): @async_app.task(queue="default", name="task1") @@ -142,6 +143,7 @@ async def task1(): assert status == Status.ABORTED +@pytest.mark.skip_before_version("3.0.0") @pytest.mark.parametrize("mode", ["listen", "poll"]) async def test_abort_sync_task(async_app: app_module.App, mode): @async_app.task(queue="default", name="task1", pass_context=True) @@ -211,6 +213,7 @@ async def appender(a: int): assert len(results) == 100, "Unexpected number of job executions" +@pytest.mark.skip_before_version("3.0.0") async def test_polling(async_app: app_module.App): @async_app.task(queue="default", name="sum") async def sum(a: int, b: int): @@ -299,6 +302,7 @@ async def appender(a: int): assert status == Status.SUCCEEDED +@pytest.mark.skip_before_version("3.0.0") async def test_stop_worker_aborts_async_jobs_past_shutdown_graceful_timeout( async_app: app_module.App, ): @@ -337,6 +341,7 @@ async def slow_job(): assert slow_job_cancelled +@pytest.mark.skip_before_version("3.0.0") async def test_stop_worker_retries_async_jobs_past_shutdown_graceful_timeout( async_app: app_module.App, ): @@ -367,6 +372,7 @@ async def slow_job(): assert slow_job_status == Status.TODO +@pytest.mark.skip_before_version("3.0.0") async def test_stop_worker_aborts_sync_jobs_past_shutdown_graceful_timeout( async_app: app_module.App, ): @@ -405,6 +411,7 @@ def slow_job(context: JobContext): assert slow_job_cancelled +@pytest.mark.skip_before_version("3.0.0") async def test_stop_worker_retries_sync_jobs_past_shutdown_graceful_timeout( async_app: app_module.App, ): diff --git a/tests/acceptance/test_shell.py b/tests/acceptance/test_shell.py index 1613bc10c..78d97d7b8 100644 --- a/tests/acceptance/test_shell.py +++ b/tests/acceptance/test_shell.py @@ -57,6 +57,7 @@ async def _(): return _ +@pytest.mark.skip_before_version("3.0.0") async def test_shell(read, write, defer): assert await read() == [ "Welcome to the procrastinate shell. Type help or ? to list commands." From 355da011ddbe29c75bc2c9dee16a7248e8119628 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 1 Dec 2024 20:12:35 +0100 Subject: [PATCH 073/375] Try simplifying the noxfile --- .github/workflows/ci.yml | 8 ++++---- noxfile.py | 34 +++++++++++----------------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31bebaa15..011ab651e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,9 @@ jobs: python-version: "3.12" cache: "poetry" - - name: Get latest version - id: get-latest-version - run: gh release list --limit 1 --json tagName --jq '"checkout_ref="+.[0].tagName' >> $GITHUB_OUTPUT + - name: Get latest tag + id: get-latest-tag + run: gh release list --limit 1 --json tagName --jq '"latest_tag="+.[0].tagName' >> $GITHUB_OUTPUT env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -109,7 +109,7 @@ jobs: PGHOST: localhost PGUSER: postgres PGPASSWORD: postgres - LATEST_VERSION: ${{ steps.get-latest-version.outputs.checkout_ref }} + LATEST_TAG: ${{ steps.get-latest-tag.outputs.latest_tag }} static-typing: name: Run Pyright diff --git a/noxfile.py b/noxfile.py index 528170e59..d22959369 100644 --- a/noxfile.py +++ b/noxfile.py @@ -39,13 +39,8 @@ def get_pre_migration(latest_tag: packaging.version.Version) -> str: @nox.session def current_version_with_post_migration(session: nox.Session): session.install("poetry") - session.run("poetry", "install", "--all-extras") - session.run( - "poetry", - "run", - "pytest", - *session.posargs, - ) + session.run("poetry", "install", "--all-extras", external=True) + session.run("poetry", "run", "pytest", *session.posargs, external=True) @nox.session @@ -53,7 +48,6 @@ def current_version_without_post_migration(session: nox.Session): latest_tag = fetch_latest_tag(session) pre_migration = get_pre_migration(latest_tag) - session.install("poetry") session.run("poetry", "install", "--all-extras") session.run( "poetry", @@ -62,6 +56,7 @@ def current_version_without_post_migration(session: nox.Session): f"--migrate-until={pre_migration}", "./tests/acceptance", *session.posargs, + external=True, ) @@ -71,33 +66,26 @@ def stable_version_without_post_migration(session: nox.Session): pre_migration = get_pre_migration(latest_tag) with tempfile.TemporaryDirectory() as temp_dir: + session.chdir(temp_dir) + temp_path = pathlib.Path(temp_dir) base_path = pathlib.Path(__file__).parent + + # Install test dependencies and copy tests shutil.copytree(base_path / "tests", temp_path / "tests") shutil.copy(base_path / "pyproject.toml", temp_path / "pyproject.toml") shutil.copy(base_path / "poetry.lock", temp_path / "poetry.lock") - session.chdir(temp_dir) + session.run("poetry", "install", "--with", "test", "--no-root", external=True) - session.install("poetry") - session.install("poetry-plugin-export") - session.run( - "poetry", - "export", - "--output", - "requirements.txt", - "--with", - "test", - "--all-extras", - "--without-hashes", - env={"POETRY_WARNINGS_EXPORT": "false"}, - ) - session.install("-r", "requirements.txt") + # Install latest procrastinate from PyPI session.install("procrastinate") + session.run( "pytest", f"--migrate-until={pre_migration}", f"--latest-version={latest_tag}", "./tests/acceptance", *session.posargs, + # This is necessary for pytest-django, due to not installing the project env={"PYTHONPATH": temp_dir}, ) From 168f1de194359bfb8e7af0c1be8547dd230ea213 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 01:34:15 +0000 Subject: [PATCH 074/375] chore(deps): update all dependencies --- .pre-commit-config.yaml | 2 +- poetry.lock | 48 ++++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7892bafd..17e4673b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - sphinx==7.4.7 - sqlalchemy==2.0.36 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.2 + rev: v0.8.3 hooks: - id: ruff args: [--fix, --unsafe-fixes] diff --git a/poetry.lock b/poetry.lock index 75584f562..614501326 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1154,20 +1154,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.24.0" +version = "0.25.0" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, + {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] @@ -1333,29 +1333,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.8.2" +version = "0.8.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, - {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, - {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, - {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, - {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, - {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, - {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, ] [[package]] From cb3b4e7d600a59ed9f4f4c92b6305ef5772e8910 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 01:09:26 +0000 Subject: [PATCH 075/375] chore(deps): lock file maintenance --- poetry.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 614501326..45bb5b217 100644 --- a/poetry.lock +++ b/poetry.lock @@ -135,13 +135,13 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -1753,13 +1753,13 @@ pg = ["psycopg2"] [[package]] name = "sqlparse" -version = "0.5.2" +version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, - {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, ] [package.extras] From 7864e21f9d7464f786e61c7e47f3aa05021b2200 Mon Sep 17 00:00:00 2001 From: Kai Schlamp Date: Tue, 17 Dec 2024 23:41:01 +0000 Subject: [PATCH 076/375] Constraint to prevent jobs in todo state with abort_requested --- .../sql/migrations/03.00.00_50_post_cancel_notification.sql | 3 +++ procrastinate/sql/schema.sql | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql b/procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql index 38e40da86..16e930051 100644 --- a/procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql +++ b/procrastinate/sql/migrations/03.00.00_50_post_cancel_notification.sql @@ -200,3 +200,6 @@ CREATE TRIGGER procrastinate_trigger_abort_requested_events_v1 -- Rename remaining functions to use version suffix ALTER FUNCTION procrastinate_unlink_periodic_defers RENAME TO procrastinate_unlink_periodic_defers_v1; ALTER TRIGGER procrastinate_trigger_delete_jobs ON procrastinate_jobs RENAME TO procrastinate_trigger_delete_jobs_v1; + +-- New constraints +ALTER TABLE procrastinate_jobs ADD CONSTRAINT check_not_todo_abort_requested CHECK (NOT (status = 'todo' AND abort_requested = true)); diff --git a/procrastinate/sql/schema.sql b/procrastinate/sql/schema.sql index 67306e5ef..687f01404 100644 --- a/procrastinate/sql/schema.sql +++ b/procrastinate/sql/schema.sql @@ -39,7 +39,8 @@ CREATE TABLE procrastinate_jobs ( status procrastinate_job_status DEFAULT 'todo'::procrastinate_job_status NOT NULL, scheduled_at timestamp with time zone NULL, attempts integer DEFAULT 0 NOT NULL, - abort_requested boolean DEFAULT false NOT NULL + abort_requested boolean DEFAULT false NOT NULL, + CONSTRAINT check_not_todo_abort_requested CHECK (NOT (status = 'todo' AND abort_requested = true)) ); CREATE TABLE procrastinate_periodic_defers ( From 131c412c9569cd2373fceb8ef664074763ad4659 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:09:57 +0000 Subject: [PATCH 077/375] fix(deps): update all dependencies --- .pre-commit-config.yaml | 4 ++-- poetry.lock | 48 ++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17e4673b5..e17959627 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: trailing-whitespace - id: mixed-line-ending - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.390 + rev: v1.1.391 hooks: - id: pyright additional_dependencies: @@ -47,7 +47,7 @@ repos: - sphinx==7.4.7 - sqlalchemy==2.0.36 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.3 + rev: v0.8.4 hooks: - id: ruff args: [--fix, --unsafe-fixes] diff --git a/poetry.lock b/poetry.lock index 45bb5b217..852853c00 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,19 +81,19 @@ files = [ [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -1333,29 +1333,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.8.3" +version = "0.8.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, - {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, - {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, - {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, - {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, - {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, - {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, + {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, + {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, + {file = "ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296"}, + {file = "ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643"}, + {file = "ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e"}, + {file = "ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3"}, + {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f"}, + {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604"}, + {file = "ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf"}, + {file = "ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720"}, + {file = "ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae"}, + {file = "ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7"}, + {file = "ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111"}, + {file = "ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8"}, + {file = "ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835"}, + {file = "ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d"}, + {file = "ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08"}, + {file = "ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8"}, ] [[package]] From f10491b7273ae3363d144c556a4789a2141f4316 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:10:11 +0000 Subject: [PATCH 078/375] fix(deps): update all dependencies to v8 --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 45bb5b217..4511598e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -359,13 +359,13 @@ toml = ["tomli"] [[package]] name = "croniter" -version = "5.0.1" +version = "6.0.0" description = "croniter provides iteration for datetime object with cron like format" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" files = [ - {file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"}, - {file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"}, + {file = "croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368"}, + {file = "croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577"}, ] [package.dependencies] From 5512442e9bcc29a9696394aa16d5ba7069cc6882 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:10:33 +0000 Subject: [PATCH 079/375] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e17959627..3cbea1ae0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: - aiopg==1.4.0 - anyio==4.7.0 - asgiref==3.8.1 - - attrs==24.2.0 + - attrs==24.3.0 - contextlib2==21.6.0 - croniter==5.0.1 - django-stubs==5.1.1 From 3ba6d5ce5f42293262369ed7efd93d7e89502452 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:10:43 +0000 Subject: [PATCH 080/375] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17e4673b5..25504a8cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - asgiref==3.8.1 - attrs==24.2.0 - contextlib2==21.6.0 - - croniter==5.0.1 + - croniter==6.0.0 - django-stubs==5.1.1 - django==5.1.4 - psycopg2-binary==2.9.10 From fbae5549a3b7458551c4376ff8744deda0df5e64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 01:49:09 +0000 Subject: [PATCH 081/375] chore(deps): lock file maintenance --- poetry.lock | 90 ++++++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/poetry.lock b/poetry.lock index b36eb3b00..71703b36f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -654,13 +654,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -814,49 +814,49 @@ pg = ["psycopg2-binary"] [[package]] name = "mypy" -version = "1.13.0" +version = "1.14.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, - {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, - {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, - {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, - {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, - {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, - {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, - {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, - {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, - {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, - {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, - {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, - {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, - {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, - {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, - {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, - {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, + {file = "mypy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e971c1c667007f9f2b397ffa80fa8e1e0adccff336e5e77e74cb5f22868bee87"}, + {file = "mypy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e86aaeaa3221a278c66d3d673b297232947d873773d61ca3ee0e28b2ff027179"}, + {file = "mypy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1628c5c3ce823d296e41e2984ff88c5861499041cb416a8809615d0c1f41740e"}, + {file = "mypy-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fadb29b77fc14a0dd81304ed73c828c3e5cde0016c7e668a86a3e0dfc9f3af3"}, + {file = "mypy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:3fa76988dc760da377c1e5069200a50d9eaaccf34f4ea18428a3337034ab5a44"}, + {file = "mypy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e73c8a154eed31db3445fe28f63ad2d97b674b911c00191416cf7f6459fd49a"}, + {file = "mypy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:273e70fcb2e38c5405a188425aa60b984ffdcef65d6c746ea5813024b68c73dc"}, + {file = "mypy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1daca283d732943731a6a9f20fdbcaa927f160bc51602b1d4ef880a6fb252015"}, + {file = "mypy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e68047bedb04c1c25bba9901ea46ff60d5eaac2d71b1f2161f33107e2b368eb"}, + {file = "mypy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a52f26b9c9b1664a60d87675f3bae00b5c7f2806e0c2800545a32c325920bcc"}, + {file = "mypy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5326ab70a6db8e856d59ad4cb72741124950cbbf32e7b70e30166ba7bbf61dd"}, + {file = "mypy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf4ec4980bec1e0e24e5075f449d014011527ae0055884c7e3abc6a99cd2c7f1"}, + {file = "mypy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390dfb898239c25289495500f12fa73aa7f24a4c6d90ccdc165762462b998d63"}, + {file = "mypy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e026d55ddcd76e29e87865c08cbe2d0104e2b3153a523c529de584759379d3d"}, + {file = "mypy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:585ed36031d0b3ee362e5107ef449a8b5dfd4e9c90ccbe36414ee405ee6b32ba"}, + {file = "mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741"}, + {file = "mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7"}, + {file = "mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8"}, + {file = "mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc"}, + {file = "mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f"}, + {file = "mypy-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b16738b1d80ec4334654e89e798eb705ac0c36c8a5c4798496cd3623aa02286"}, + {file = "mypy-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10065fcebb7c66df04b05fc799a854b1ae24d9963c8bb27e9064a9bdb43aa8ad"}, + {file = "mypy-1.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbb7d683fa6bdecaa106e8368aa973ecc0ddb79a9eaeb4b821591ecd07e9e03c"}, + {file = "mypy-1.14.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3498cb55448dc5533e438cd13d6ddd28654559c8c4d1fd4b5ca57a31b81bac01"}, + {file = "mypy-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c7b243408ea43755f3a21a0a08e5c5ae30eddb4c58a80f415ca6b118816e60aa"}, + {file = "mypy-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14117b9da3305b39860d0aa34b8f1ff74d209a368829a584eb77524389a9c13e"}, + {file = "mypy-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af98c5a958f9c37404bd4eef2f920b94874507e146ed6ee559f185b8809c44cc"}, + {file = "mypy-1.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b343a1d3989547024377c2ba0dca9c74a2428ad6ed24283c213af8dbb0710b"}, + {file = "mypy-1.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cdb5563c1726c85fb201be383168f8c866032db95e1095600806625b3a648cb7"}, + {file = "mypy-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:74e925649c1ee0a79aa7448baf2668d81cc287dc5782cff6a04ee93f40fb8d3f"}, + {file = "mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab"}, + {file = "mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -1809,13 +1809,13 @@ files = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20240917" +version = "6.0.12.20241221" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" files = [ - {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, - {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, + {file = "types_PyYAML-6.0.12.20241221-py3-none-any.whl", hash = "sha256:0657a4ff8411a030a2116a196e8e008ea679696b5b1a8e1a6aa8ebb737b34688"}, + {file = "types_pyyaml-6.0.12.20241221.tar.gz", hash = "sha256:4f149aa893ff6a46889a30af4c794b23833014c469cc57cbc3ad77498a58996f"}, ] [[package]] @@ -1842,13 +1842,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] From d1855a5bdfb250559eb32c1e0f6fee47e586f6cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 00:51:49 +0000 Subject: [PATCH 082/375] chore(deps): lock file maintenance --- poetry.lock | 329 +++++++++++++++++++++++++--------------------------- 1 file changed, 158 insertions(+), 171 deletions(-) diff --git a/poetry.lock b/poetry.lock index 71703b36f..80eb13b1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -146,116 +146,103 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] @@ -282,73 +269,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.9" +version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, - {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, - {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, - {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, - {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, - {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, - {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, - {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, - {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, - {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, - {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, - {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, - {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, - {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, - {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, - {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] From 5f9747e78ee9f1c02e685ac8bc33a2dd1bffb822 Mon Sep 17 00:00:00 2001 From: medihack Date: Sat, 4 Jan 2025 17:02:51 +0000 Subject: [PATCH 083/375] Add devcontainer setup --- .devcontainer/Dockerfile | 17 +++++++++++++++++ .devcontainer/devcontainer.json | 11 +++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..2fe5f2346 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/devcontainers/python:3.12 + +USER root + +RUN sudo apt-get update \ + && apt-get install -y --no-install-recommends \ + bash-completion \ + postgresql-common \ + && /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \ + && apt-get install -y --no-install-recommends \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +USER vscode + +RUN pipx install poetry \ + && poetry completions bash >> ~/.bash_completion diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..ffb181db8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "moby": false + } + }, + "postStartCommand": "/bin/bash -c 'source ./dev-env'" +} From 3b63b37dbefecd45b63d58bc333dee2dbcf16376 Mon Sep 17 00:00:00 2001 From: medihack Date: Sun, 5 Jan 2025 14:25:58 +0000 Subject: [PATCH 084/375] Don't use dropped python version for static type checking --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc4e97b19..54e98c799 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" cache: "poetry" - name: Install dependencies From c3c895a691ce4155c40c8e1f5f97f4b16e84254b Mon Sep 17 00:00:00 2001 From: medihack Date: Sun, 5 Jan 2025 14:35:51 +0000 Subject: [PATCH 085/375] Use first version from the python version matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54e98c799..9f8c68675 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "${{ matrix.python-version[0] }}" cache: "poetry" - name: Install dependencies From 938c8d270e81035a0cf826584653ef472f061027 Mon Sep 17 00:00:00 2001 From: medihack Date: Sun, 5 Jan 2025 15:38:34 +0000 Subject: [PATCH 086/375] Fix to lowest Python version for static type checking in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f8c68675..54e98c799 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "${{ matrix.python-version[0] }}" + python-version: "3.9" cache: "poetry" - name: Install dependencies From ee104507db68cc7e1a7ab0e49f9b7c5a641d394f Mon Sep 17 00:00:00 2001 From: medihack Date: Mon, 6 Jan 2025 14:08:24 +0000 Subject: [PATCH 087/375] Improve devcontainer setup --- .devcontainer/devcontainer.json | 13 +++++++++++- .devcontainer/postStart | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100755 .devcontainer/postStart diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ffb181db8..4eb98dbc2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,5 +7,16 @@ "moby": false } }, - "postStartCommand": "/bin/bash -c 'source ./dev-env'" + "postCreateCommand": "scripts/bootstrap", + "postStartCommand": ".devcontainer/postStart", + "remoteEnv": { + "PATH": "${containerWorkspaceFolder}/.venv/bin:${containerEnv:PATH}", + "PGDATABASE": "procrastinate", + "PGHOST": "127.0.0.1", + "PGPASSWORD": "password", + "PGUSER": "postgres", + "POETRY_VIRTUALENVS_IN_PROJECT": "true", + "PROCRASTINATE_APP": "procrastinate_demos.demo_async.app.app", + "VIRTUAL_ENV": "${containerWorkspaceFolder}/.venv" + } } diff --git a/.devcontainer/postStart b/.devcontainer/postStart new file mode 100755 index 000000000..c656c23dd --- /dev/null +++ b/.devcontainer/postStart @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +if ! pg_isready ; then + echo "Starting database" + export PGDATABASE=procrastinate PGHOST=127.0.0.1 PGUSER=postgres PGPASSWORD=password + docker-compose up -d postgres || return + sleep 3 +fi + +echo "" +echo "Database is ready!" +echo "" + +if ! pg_dump --schema-only --table=procrastinate_jobs 1>/dev/null 2>&1; then + echo "Applying migrations" + procrastinate schema --apply || return +fi + +echo "Migrations applied!" + +echo "" +echo "Welcome to the Procrastinate development container!" +echo "" +echo "You'll find the detailed instructions in the contributing documentation:" +echo " https://procrastinate.readthedocs.io/en/latest/contributing.html" +echo "" +echo "TL;DR: important commands:" +echo "- pytest: Launch the tests" +echo "- tox: Entrypoint for testing multiple python versions as well as docs, linters & formatters" +echo "- procrastinate: Test procrastinate locally." +echo "" +echo "We've gone ahead and set up a few additional commands for you:" +echo "- htmlcov: Opens the test coverage results in your browser" +echo "- htmldoc: Opens the locally built sphinx documentation in your browser" +echo "- lint: Run code formatters & linters" +echo "- docs: Build doc" From 6ef1783a883e31d745209e40ee60b7fb1ca27e46 Mon Sep 17 00:00:00 2001 From: medihack Date: Mon, 6 Jan 2025 14:38:36 +0000 Subject: [PATCH 088/375] Use docker-compose in devcontainer setup to spin up the db --- .devcontainer/devcontainer.json | 18 ++++++------------ .devcontainer/docker-compose.yml | 21 +++++++++++++++++++++ .devcontainer/{postStart => post-create.sh} | 11 +---------- 3 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 .devcontainer/docker-compose.yml rename .devcontainer/{postStart => post-create.sh} (79%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4eb98dbc2..bebb34375 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,16 +1,8 @@ { - "build": { - "dockerfile": "Dockerfile" - }, - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": { - "moby": false - } - }, - "postCreateCommand": "scripts/bootstrap", - "postStartCommand": ".devcontainer/postStart", + "dockerComposeFile": "docker-compose.yml", + "postCreateCommand": ".devcontainer/post-create.sh", "remoteEnv": { - "PATH": "${containerWorkspaceFolder}/.venv/bin:${containerEnv:PATH}", + "PATH": "${containerWorkspaceFolder}/.venv/bin:${containerWorkspaceFolder}/scripts:${containerEnv:PATH}", "PGDATABASE": "procrastinate", "PGHOST": "127.0.0.1", "PGPASSWORD": "password", @@ -18,5 +10,7 @@ "POETRY_VIRTUALENVS_IN_PROJECT": "true", "PROCRASTINATE_APP": "procrastinate_demos.demo_async.app.app", "VIRTUAL_ENV": "${containerWorkspaceFolder}/.venv" - } + }, + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..056fe570e --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,21 @@ +services: + app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ../..:/workspaces:cached + command: sleep infinity + network_mode: service:db + db: + image: postgres:latest + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_DB: procrastinate + POSTGRES_PASSWORD: password + +volumes: + postgres-data: diff --git a/.devcontainer/postStart b/.devcontainer/post-create.sh similarity index 79% rename from .devcontainer/postStart rename to .devcontainer/post-create.sh index c656c23dd..ad1e6283f 100755 --- a/.devcontainer/postStart +++ b/.devcontainer/post-create.sh @@ -1,15 +1,6 @@ #!/usr/bin/env bash -if ! pg_isready ; then - echo "Starting database" - export PGDATABASE=procrastinate PGHOST=127.0.0.1 PGUSER=postgres PGPASSWORD=password - docker-compose up -d postgres || return - sleep 3 -fi - -echo "" -echo "Database is ready!" -echo "" +scripts/bootstrap if ! pg_dump --schema-only --table=procrastinate_jobs 1>/dev/null 2>&1; then echo "Applying migrations" From 25045696104b81447555a891fad7aeb751d07e28 Mon Sep 17 00:00:00 2001 From: medihack Date: Mon, 6 Jan 2025 14:45:40 +0000 Subject: [PATCH 089/375] Add some devcontainer documentation --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3bbcc7938..2e465cfdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,13 @@ The `dev-env` script will add the `scripts` folder to your `$PATH` for the curre shell, so in the following documentation, if you see `scripts/foo`, you're welcome to call `foo` directly. +### Development Container + +Alternatively, you can utilize our development container setup. In VSCode, select +`Dev Containers: Reopen in Container` from the command palette. This action sets up a +container preconfigured with all required dependencies and automatically provisions a +database. The virtual environment is created and activated seamlessly within the container. + ## Instructions for contribution ### Environment variables From 19406566669000b437b7ade8a916c01c0abe09c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:28:58 +0000 Subject: [PATCH 090/375] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.8.6) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 550dd1fad..39819489b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - sphinx==7.4.7 - sqlalchemy==2.0.36 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.8.6 hooks: - id: ruff args: [--fix, --unsafe-fixes] From cdd8b62e00eb1e0759e12b857acda9ef32a66463 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:29:08 +0000 Subject: [PATCH 091/375] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39819489b..550dd1fad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - sphinx==7.4.7 - sqlalchemy==2.0.36 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.8.4 hooks: - id: ruff args: [--fix, --unsafe-fixes] From 6a390a41144962f96912662ca585109ac022ceef Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sat, 11 Jan 2025 19:04:21 +0100 Subject: [PATCH 092/375] Add task back as a property on job context --- docs/howto/advanced/retry.md | 115 +++++++++++++++++++-------------- docs/reference.rst | 4 +- procrastinate/job_context.py | 16 ++++- tests/unit/test_job_context.py | 16 +++++ 4 files changed, 99 insertions(+), 52 deletions(-) diff --git a/docs/howto/advanced/retry.md b/docs/howto/advanced/retry.md index d8a5ad708..9c6d6ee55 100644 --- a/docs/howto/advanced/retry.md +++ b/docs/howto/advanced/retry.md @@ -9,36 +9,36 @@ app / machine reboots. ## Simple strategies -- Retry 5 times (so 6 attempts total): - - ```python - @app.task(retry=5) - def flaky_task(): - if random.random() > 0.9: - raise Exception("Who could have seen this coming?") - print("Hello world") - ``` - -- Retry indefinitely: - - ```python - @app.task(retry=True) - def flaky_task(): - if random.random() > 0.9: - raise Exception("Who could have seen this coming?") - print("Hello world") - ``` +- Retry 5 times (so 6 attempts total): + + ```python + @app.task(retry=5) + def flaky_task(): + if random.random() > 0.9: + raise Exception("Who could have seen this coming?") + print("Hello world") + ``` + +- Retry indefinitely: + + ```python + @app.task(retry=True) + def flaky_task(): + if random.random() > 0.9: + raise Exception("Who could have seen this coming?") + print("Hello world") + ``` ## Advanced strategies Advanced strategies let you: -- define a maximum number of retries (if you don't, jobs will be retried indefinitely - until they pass) -- define the retry delay, with constant, linear and exponential backoff options (if - you don't, jobs will be retried immediately) -- define the exception types you want to retry on (if you don't, jobs will be retried - on any type of exceptions) +- define a maximum number of retries (if you don't, jobs will be retried indefinitely + until they pass) +- define the retry delay, with constant, linear and exponential backoff options (if + you don't, jobs will be retried immediately) +- define the exception types you want to retry on (if you don't, jobs will be retried + on any type of exceptions) Define your precise strategy using a {py:class}`RetryStrategy` instance: @@ -57,9 +57,9 @@ def my_other_task(): {py:class}`RetryStrategy` takes 3 parameters related to how long it will wait between retries: -- `wait=5` to wait 5 seconds before each retry -- `linear_wait=5` to wait 5 seconds then 10 then 15 and so on -- `exponential_wait=5` to wait 5 seconds then 25 then 125 and so on +- `wait=5` to wait 5 seconds before each retry +- `linear_wait=5` to wait 5 seconds then 10 then 15 and so on +- `exponential_wait=5` to wait 5 seconds then 25 then 125 and so on ## Implementing your own strategy @@ -73,28 +73,45 @@ The time to wait between retries can be specified with `retry_in` or alternative with `retry_at`. This is similar to how `schedule_in` and `schedule_at` are used when {doc}`scheduling a job in the future `. - ```python - import random - from procrastinate import Job, RetryDecision - - class RandomRetryStrategy(procrastinate.BaseRetryStrategy): - max_attempts = 3 - min = 1 - max = 10 +```python +import random +from procrastinate import Job, RetryDecision + +class RandomRetryStrategy(procrastinate.BaseRetryStrategy): + max_attempts = 3 + min = 1 + max = 10 + + def get_retry_decision(self, *, exception:Exception, job:Job) -> RetryDecision: + if job.attempts >= max_attempts: + return RetryDecision(should_retry=False) + + wait = random.uniform(self.min, self.max) + + return RetryDecision( + retry_in={"seconds": wait}, # or retry_at (a datetime object) + priority=job.priority + 1, # optional + queue="another_queue", # optional + lock="another_lock", # optional + ) +``` - def get_retry_decision(self, *, exception:Exception, job:Job) -> RetryDecision: - if job.attempts >= max_attempts: - return RetryDecision(should_retry=False) +There is also a legacy `get_schedule_in` method that is deprecated an will be +removed in a future version in favor of the above `get_retry_decision` method. - wait = random.uniform(self.min, self.max) +## Knowing whether a job is on its last attempt - return RetryDecision( - retry_in={"seconds": wait}, # or retry_at (a datetime object) - priority=job.priority + 1, # optional - queue="another_queue", # optional - lock="another_lock", # optional - ) - ``` +By using `pass_context=True`, and introspecting the task's retry strategy, +you can know whether a currently executing job is on its last attempt: -There is also a legacy `get_schedule_in` method that is deprecated an will be -removed in a future version in favor of the above `get_retry_decision` method. +```python +@app.task(retry=10, pass_context=True) +def my_task(job_context: procrastinate.JobContext) -> None: + job = job_context.job + task = job_context.task + if task.retry.get_retry_decision(exception=Exception(), job=job) is None: + print("Warning: last attempt!") + + if random.random() < 0.9: + raise Exception +``` diff --git a/docs/reference.rst b/docs/reference.rst index 566a46959..b3883ab45 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -32,7 +32,7 @@ When tasks are created with argument ``pass_context``, they are provided a `JobContext` argument: .. autoclass:: procrastinate.JobContext - :members: app, worker_name, worker_queues, job + :members: app, worker_name, worker_queues, job, task, should_abort Blueprints ---------- @@ -80,7 +80,7 @@ Exceptions .. automodule:: procrastinate.exceptions :members: ProcrastinateException, LoadFromPathError, ConnectorException, AlreadyEnqueued, AppNotOpen, TaskNotFound, - UnboundTaskError + UnboundTaskError, JobAborted Job statuses ------------ diff --git a/procrastinate/job_context.py b/procrastinate/job_context.py index b224dfba1..dd37202da 100644 --- a/procrastinate/job_context.py +++ b/procrastinate/job_context.py @@ -8,7 +8,7 @@ import attr from procrastinate import app as app_module -from procrastinate import jobs, utils +from procrastinate import jobs, tasks, utils @attr.dataclass(kw_only=True) @@ -64,9 +64,16 @@ class JobContext: additional_context: dict = attr.ib(factory=dict) + #: Callable returning the reason the job should be aborted (or None if it + #: should not be aborted) abort_reason: Callable[[], AbortReason | None] def should_abort(self) -> bool: + """ + Returns True if the job should be aborted: in that case, the job should + stop processing as soon as possible and raise raise + :py:class:`~exceptions.JobAborted` + """ return bool(self.abort_reason()) def evolve(self, **update: Any) -> JobContext: @@ -75,3 +82,10 @@ def evolve(self, **update: Any) -> JobContext: @property def queues_display(self) -> str: return utils.queues_display(self.worker_queues) + + @property + def task(self) -> tasks.Task: + """ + The :py:class:`~tasks.Task` associated to the job + """ + return self.app.tasks[self.job.task_name] diff --git a/tests/unit/test_job_context.py b/tests/unit/test_job_context.py index f6eab0957..72ed06f33 100644 --- a/tests/unit/test_job_context.py +++ b/tests/unit/test_job_context.py @@ -55,3 +55,19 @@ def test_evolve(app: App, job_factory): abort_reason=lambda: None, ) assert context.evolve(worker_name="b").worker_name == "b" + + +def test_task(app: App, job_factory): + @app.task(name="my_task") + def my_task(a, b): + return a + b + + job = job_factory(task_name="my_task") + context = job_context.JobContext( + start_timestamp=time.time(), + app=app, + job=job, + worker_name="a", + abort_reason=lambda: None, + ) + assert context.task == my_task From 0438b0bd8d546a36d8289edca70b7fd1d05a2676 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:41:37 +0000 Subject: [PATCH 093/375] Update all dependencies --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d4be1a87..d31c7792e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: services: postgres: - image: postgres:16 + image: postgres:17 # Set health checks to wait until postgres has started env: POSTGRES_PASSWORD: postgres From 5466a870e77df2b8ae398529b0b76360151fcd57 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 12 Jan 2025 16:56:03 +0100 Subject: [PATCH 094/375] Update docs/howto/production/logging.md Co-authored-by: Andrew Womeldorf --- docs/howto/production/logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/production/logging.md b/docs/howto/production/logging.md index 5df46b07f..9c2ecdadf 100644 --- a/docs/howto/production/logging.md +++ b/docs/howto/production/logging.md @@ -57,7 +57,7 @@ shared_processors = [ ] structlog.configure( - processors=shared_processors, + processors=shared_processors + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) From f14595a61ab7c0fb13c5d2482c6d0e60c6cbf368 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 12 Jan 2025 16:56:14 +0100 Subject: [PATCH 095/375] Update docs/howto/production/logging.md Co-authored-by: Andrew Womeldorf --- docs/howto/production/logging.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/howto/production/logging.md b/docs/howto/production/logging.md index 9c2ecdadf..7ed143c85 100644 --- a/docs/howto/production/logging.md +++ b/docs/howto/production/logging.md @@ -53,7 +53,6 @@ shared_processors = [ structlog.stdlib.add_log_level, structlog.processors.StackInfoRenderer(), structlog.dev.set_exc_info, - structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ] structlog.configure( From 891da5a18feb0256594579a7b97aa3da52e4215d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:24:51 +0000 Subject: [PATCH 096/375] Update dependency django to v5.1.5 [SECURITY] --- poetry.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 53eb587db..2f61ec245 100644 --- a/poetry.lock +++ b/poetry.lock @@ -361,13 +361,13 @@ pytz = ">2021.1" [[package]] name = "django" -version = "4.2.17" +version = "4.2.18" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0"}, - {file = "Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"}, + {file = "Django-4.2.18-py3-none-any.whl", hash = "sha256:ba52eff7e228f1c775d5b0db2ba53d8c49d2f8bfe6ca0234df6b7dd12fb25b19"}, + {file = "Django-4.2.18.tar.gz", hash = "sha256:52ae8eacf635617c0f13b44f749e5ea13dc34262819b2cc8c8636abb08d82c4b"}, ] [package.dependencies] @@ -381,13 +381,13 @@ bcrypt = ["bcrypt"] [[package]] name = "django" -version = "5.1.4" +version = "5.1.5" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.1.4-py3-none-any.whl", hash = "sha256:236e023f021f5ce7dee5779de7b286565fdea5f4ab86bae5338e3f7b69896cf0"}, - {file = "Django-5.1.4.tar.gz", hash = "sha256:de450c09e91879fa5a307f696e57c851955c910a438a35e6b4c895e86bedc82a"}, + {file = "Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459"}, + {file = "Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3"}, ] [package.dependencies] From b065c5a47975916c8d1712b6a18761c45f9e8622 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:26:12 +0000 Subject: [PATCH 097/375] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df41f8a8f..5979b53ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - contextlib2==21.6.0 - croniter==6.0.0 - django-stubs==5.1.1 - - django==5.1.4 + - django==5.1.5 - psycopg2-binary==2.9.10 - psycopg[pool]==3.2.3 - python-dateutil==2.9.0.post0 From 870154c3bd17b1e861783801e17fc63c89bf364b Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 19 Jan 2025 15:57:25 +0100 Subject: [PATCH 098/375] Update pyproject.toml metadata format for poetry 2.0 (closer to PEP 621) --- .pre-commit-config.yaml | 1 - poetry.lock | 240 +++++++++++++++++++--------------------- pyproject.toml | 56 +++++----- 3 files changed, 143 insertions(+), 154 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5979b53ee..f6609ea12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,6 @@ repos: - id: pyright additional_dependencies: - aiopg==1.4.0 - - anyio==4.7.0 - asgiref==3.8.1 - attrs==24.3.0 - contextlib2==21.6.0 diff --git a/poetry.lock b/poetry.lock index 2f61ec245..260a355a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "aiopg" @@ -6,6 +6,7 @@ version = "1.4.0" description = "Postgres integration with asyncio." optional = false python-versions = ">=3.7" +groups = ["main", "pg_implem"] files = [ {file = "aiopg-1.4.0-py3-none-any.whl", hash = "sha256:aea46e8aff30b039cfa818e6db4752c97656e893fc75e5a5dc57355a9e9dedbd"}, {file = "aiopg-1.4.0.tar.gz", hash = "sha256:116253bef86b4d954116716d181e9a0294037f266718b2e1c9766af995639d71"}, @@ -24,39 +25,19 @@ version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] -[[package]] -name = "anyio" -version = "4.7.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -files = [ - {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, - {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] -trio = ["trio (>=0.26.1)"] - [[package]] name = "asgiref" version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" +groups = ["main", "django", "docs", "types"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -74,6 +55,7 @@ version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" +groups = ["main", "pg_implem"] files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, @@ -85,6 +67,7 @@ version = "24.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, @@ -104,6 +87,7 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, @@ -118,6 +102,7 @@ version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" +groups = ["docs"] files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, @@ -139,6 +124,7 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "docs"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -150,6 +136,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main", "docs"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -251,6 +238,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "docs", "test"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -262,6 +251,8 @@ version = "21.6.0" description = "Backports and enhancements for the contextlib module" optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "python_version < \"3.10\"" files = [ {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, @@ -273,6 +264,7 @@ version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, @@ -350,6 +342,7 @@ version = "6.0.0" description = "croniter provides iteration for datetime object with cron like format" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" +groups = ["main"] files = [ {file = "croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368"}, {file = "croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577"}, @@ -365,6 +358,8 @@ version = "4.2.18" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" +groups = ["main", "django", "docs", "types"] +markers = "python_version < \"3.10\"" files = [ {file = "Django-4.2.18-py3-none-any.whl", hash = "sha256:ba52eff7e228f1c775d5b0db2ba53d8c49d2f8bfe6ca0234df6b7dd12fb25b19"}, {file = "Django-4.2.18.tar.gz", hash = "sha256:52ae8eacf635617c0f13b44f749e5ea13dc34262819b2cc8c8636abb08d82c4b"}, @@ -385,6 +380,8 @@ version = "5.1.5" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" +groups = ["main", "django", "docs", "types"] +markers = "python_version >= \"3.10\"" files = [ {file = "Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459"}, {file = "Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3"}, @@ -405,6 +402,7 @@ version = "5.1.1" description = "Mypy stubs for Django" optional = false python-versions = ">=3.8" +groups = ["types"] files = [ {file = "django_stubs-5.1.1-py3-none-any.whl", hash = "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac"}, {file = "django_stubs-5.1.1.tar.gz", hash = "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b"}, @@ -429,6 +427,7 @@ version = "5.1.1" description = "Monkey-patching and extensions for django-stubs" optional = false python-versions = ">=3.8" +groups = ["types"] files = [ {file = "django_stubs_ext-5.1.1-py3-none-any.whl", hash = "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c"}, {file = "django_stubs_ext-5.1.1.tar.gz", hash = "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c"}, @@ -444,6 +443,7 @@ version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -455,6 +455,7 @@ version = "1.23.0" description = "Dynamic version generation" optional = false python-versions = ">=3.5" +groups = ["release"] files = [ {file = "dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041"}, {file = "dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4"}, @@ -469,6 +470,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["test"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -483,6 +486,7 @@ version = "2024.8.6" description = "A clean customisable Sphinx documentation theme." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, @@ -500,6 +504,7 @@ version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" +groups = ["main", "pg_implem", "test"] files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -575,6 +580,7 @@ files = [ {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, ] +markers = {main = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and extra == \"sqlalchemy\"", pg_implem = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")", test = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} [package.extras] docs = ["Sphinx", "furo"] @@ -586,6 +592,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "docs"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -600,6 +607,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main", "docs"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -611,10 +619,12 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] +markers = {main = "python_version < \"3.10\""} [package.dependencies] zipp = ">=3.20" @@ -634,6 +644,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -645,6 +656,7 @@ version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main", "docs"] files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, @@ -662,6 +674,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -686,6 +699,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -756,6 +770,7 @@ version = "0.4.2" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, @@ -775,6 +790,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -782,17 +798,18 @@ files = [ [[package]] name = "migra" -version = "3.0.1663481299" +version = "2.0.1611797829" description = "Like `diff` but for PostgreSQL schemas" optional = false -python-versions = ">=3.7,<4" +python-versions = "*" +groups = ["test"] files = [ - {file = "migra-3.0.1663481299-py3-none-any.whl", hash = "sha256:061643e9af63488e085d729f267ed4af4249789979732b703ddeb2c478ec9a93"}, - {file = "migra-3.0.1663481299.tar.gz", hash = "sha256:0cf0c125d553008d9ff5402663a51703ccc474bb65b5a4f4727906dbf58e217f"}, + {file = "migra-2.0.1611797829-py2.py3-none-any.whl", hash = "sha256:1cbd4dfed5c7fc0db1801ef0ba71a842c41ceaea48a74bb902e09935ff933ad4"}, + {file = "migra-2.0.1611797829.tar.gz", hash = "sha256:637df5be1009a760cf62170336266fb29e1fe1fdc071197deb48ffaff70c6a27"}, ] [package.dependencies] -schemainspect = ">=3.1.1663480743" +schemainspect = ">=0.1.1610498640,<3" six = "*" sqlbag = "*" @@ -805,6 +822,7 @@ version = "1.14.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["pg_implem"] files = [ {file = "mypy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e971c1c667007f9f2b397ffa80fa8e1e0adccff336e5e77e74cb5f22868bee87"}, {file = "mypy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e86aaeaa3221a278c66d3d673b297232947d873773d61ca3ee0e28b2ff027179"}, @@ -858,6 +876,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["pg_implem"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -869,6 +888,7 @@ version = "3.0.1" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, @@ -895,6 +915,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "docs", "release", "test"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -906,6 +927,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -921,13 +943,13 @@ version = "3.2.3" description = "PostgreSQL database adapter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "pg_implem"] files = [ {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, ] [package.dependencies] -psycopg-binary = {version = "3.2.3", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -940,85 +962,13 @@ docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)" pool = ["psycopg-pool"] test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] -[[package]] -name = "psycopg-binary" -version = "3.2.3" -description = "PostgreSQL database adapter for Python -- C optimisation distribution" -optional = false -python-versions = ">=3.8" -files = [ - {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749"}, -] - [[package]] name = "psycopg-pool" version = "3.2.4" description = "Connection Pool for Psycopg" optional = false python-versions = ">=3.8" +groups = ["main", "pg_implem"] files = [ {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"}, {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"}, @@ -1033,6 +983,7 @@ version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" +groups = ["main", "pg_implem"] files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -1081,7 +1032,6 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -1110,6 +1060,7 @@ version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, @@ -1124,6 +1075,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1146,6 +1098,7 @@ version = "0.25.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, @@ -1164,6 +1117,7 @@ version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, @@ -1182,6 +1136,7 @@ version = "4.9.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, @@ -1200,6 +1155,7 @@ version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, @@ -1217,6 +1173,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1231,6 +1188,7 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1242,6 +1200,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1304,6 +1263,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1325,6 +1285,7 @@ version = "0.8.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["lint_format"] files = [ {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, @@ -1348,16 +1309,18 @@ files = [ [[package]] name = "schemainspect" -version = "3.1.1663587362" +version = "0.1.1611724890" description = "Schema inspection for PostgreSQL (and possibly others)" optional = false -python-versions = ">=3.7,<4" +python-versions = "*" +groups = ["test"] files = [ - {file = "schemainspect-3.1.1663587362-py3-none-any.whl", hash = "sha256:3071265712863c4d4e742940a4b44ac685135af3c93416872ec1bb6c822c4aca"}, - {file = "schemainspect-3.1.1663587362.tar.gz", hash = "sha256:a295ad56f7a19c09e5e1ef9f16dadbf6392e26196cb5f05b5afe613c99ce7468"}, + {file = "schemainspect-0.1.1611724890-py2.py3-none-any.whl", hash = "sha256:d3211054de21f0396f3a0a233c389964c65dbec30aa236a330d63521adc96837"}, + {file = "schemainspect-0.1.1611724890.tar.gz", hash = "sha256:bf1f93ff0b80ca455a6ce09b7b13237d5cf60062aeb00f7da8451c71ba5f93ff"}, ] [package.dependencies] +six = "*" sqlalchemy = "*" [[package]] @@ -1366,6 +1329,7 @@ version = "75.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, @@ -1386,28 +1350,19 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["main", "docs"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -1419,6 +1374,7 @@ version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, @@ -1430,6 +1386,7 @@ version = "7.4.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -1466,6 +1423,7 @@ version = "1.0.0b2" description = "A modern skeleton for Sphinx themes." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, @@ -1483,6 +1441,7 @@ version = "0.5.2" description = "Add a copy button to each of your code cells." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, @@ -1497,19 +1456,27 @@ rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] [[package]] name = "sphinx-github-changelog" -version = "1.4.0" +version = "1.0.2" description = "Build a sphinx changelog from GitHub Releases" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "*" +groups = ["docs"] files = [ - {file = "sphinx_github_changelog-1.4.0-py3-none-any.whl", hash = "sha256:cdf2099ea3e4587ae8637be7ba609738bfdeca4bd80c5df6fc45046735ae5c2f"}, - {file = "sphinx_github_changelog-1.4.0.tar.gz", hash = "sha256:204745e93a1f280e4664977b5fee526b0a011c92ca19c304bd01fd641ddb6393"}, + {file = "sphinx-github-changelog-1.0.2.tar.gz", hash = "sha256:8af05b7aaeb7b6ffa81ceea7ffde34e0ab39d032879c57bbefb08c0c6bb12872"}, ] [package.dependencies] docutils = "*" +importlib-metadata = "*" requests = "*" -Sphinx = "*" +sphinx = "*" + +[package.extras] +dev = ["black", "isort", "tox"] +docs = ["doc8", "sphinx (>=3.1.1)", "sphinx-github-changelog", "sphinx_rtd_theme"] +docs-spelling = ["sphinxcontrib-spelling"] +lint = ["black", "check-manifest", "flake8", "isort", "mypy"] +test = ["pytest", "pytest-cov", "pytest-mock", "requests-mock"] [[package]] name = "sphinxcontrib-applehelp" @@ -1517,6 +1484,7 @@ version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -1533,6 +1501,7 @@ version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -1549,6 +1518,7 @@ version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -1565,6 +1535,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["main", "docs"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -1579,6 +1550,7 @@ version = "1.0.0" description = "Mermaid diagrams in yours Sphinx powered docs" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3"}, {file = "sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146"}, @@ -1597,6 +1569,7 @@ version = "0.18" description = "Sphinx extension to include program output" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "sphinxcontrib_programoutput-0.18-py3-none-any.whl", hash = "sha256:8a651bc85de69a808a064ff0e48d06c12b9347da4fe5fdb1e94914b01e1b0c36"}, {file = "sphinxcontrib_programoutput-0.18.tar.gz", hash = "sha256:09e68b6411d937a80b6085f4fdeaa42e0dc5555480385938465f410589d2eed8"}, @@ -1614,6 +1587,7 @@ version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -1630,6 +1604,7 @@ version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -1646,6 +1621,7 @@ version = "2.0.36" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main", "pg_implem", "test"] files = [ {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, @@ -1705,6 +1681,7 @@ files = [ {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, ] +markers = {main = "extra == \"sqlalchemy\""} [package.dependencies] greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} @@ -1742,6 +1719,7 @@ version = "0.1.1617247075" description = "various snippets of SQL-related boilerplate" optional = false python-versions = "*" +groups = ["test"] files = [ {file = "sqlbag-0.1.1617247075-py2.py3-none-any.whl", hash = "sha256:ecdef26d661f8640711030ac6ee618deb92b91f9f0fc2efbf8a3b133af13092d"}, {file = "sqlbag-0.1.1617247075.tar.gz", hash = "sha256:b9d7862c3b2030356d796ca872907962fd54704066978d7ae89383f5123366ed"}, @@ -1763,6 +1741,7 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" +groups = ["main", "django", "docs", "types"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -1778,6 +1757,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["main", "docs", "pg_implem", "test", "types"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1812,6 +1792,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {main = "python_version < \"3.11\"", docs = "python_version < \"3.11\"", pg_implem = "python_version < \"3.11\"", test = "python_full_version <= \"3.11.0a6\"", types = "python_version < \"3.11\""} [[package]] name = "types-pyyaml" @@ -1819,6 +1800,7 @@ version = "6.0.12.20241221" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" +groups = ["types"] files = [ {file = "types_PyYAML-6.0.12.20241221-py3-none-any.whl", hash = "sha256:0657a4ff8411a030a2116a196e8e008ea679696b5b1a8e1a6aa8ebb737b34688"}, {file = "types_pyyaml-6.0.12.20241221.tar.gz", hash = "sha256:4f149aa893ff6a46889a30af4c794b23833014c469cc57cbc3ad77498a58996f"}, @@ -1830,6 +1812,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "django", "docs", "pg_implem", "test", "types"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1841,6 +1824,8 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main", "django", "docs", "pg_implem", "types"] +markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -1852,6 +1837,7 @@ version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, @@ -1869,10 +1855,12 @@ version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] +markers = {main = "python_version < \"3.10\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] @@ -1890,6 +1878,6 @@ sphinx = ["sphinx"] sqlalchemy = ["sqlalchemy"] [metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "7dc5206d167bf90d1265364436a6d3d28c038617d6ea4be08e28e06f89a147b5" +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "5239615731ce9259f885baf45eca0bd70cd4d8d03a4d8346c300afed200bbafb" diff --git a/pyproject.toml b/pyproject.toml index f3c07d0b1..8cd79001d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,49 +2,51 @@ requires = ["poetry-core", "poetry-dynamic-versioning"] build-backend = "poetry_dynamic_versioning.backend" -[tool.poetry] +[project] name = "procrastinate" version = "0.0.0" description = "Postgres-based distributed task processing library" -authors = ["Joachim Jablon", "Eric Lemoine", "Kai Schlamp"] license = "MIT License" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", ] +authors = [ + { name = "Joachim Jablon", email = "ewjoachim@gmail.com" }, + { name = "Eric Lemoine" }, + { name = "Kai Schlamp" }, +] readme = "README.md" keywords = ["postgres", "task-queue"] -homepage = "https://procrastinate.readthedocs.io/" -repository = "https://github.com/procrastinate-org/procrastinate/" -documentation = "https://procrastinate.readthedocs.io/" - -[tool.poetry.scripts] -procrastinate = 'procrastinate.cli:main' +requires-python = ">=3.9" +dependencies = [ + "psycopg[pool]", + "asgiref", + "attrs", + 'contextlib2;python_version<"3.10"', + "croniter", + "python-dateutil", + "typing-extensions", +] -[tool.poetry.dependencies] -python = "^3.9" -aiopg = { version = "*", optional = true } -anyio = "*" -asgiref = "*" -attrs = "*" -contextlib2 = { version = "*", python = "<3.10" } -croniter = "*" -django = { version = ">=2.2", optional = true } -psycopg = { extras = ["pool"], version = "*" } -psycopg2-binary = { version = "*", optional = true } -python-dateutil = "*" -sqlalchemy = { version = "^2.0", optional = true } -typing-extensions = "*" -sphinx = { version = "*", optional = true } - -[tool.poetry.extras] -django = ["django"] -sqlalchemy = ["sqlalchemy"] +[project.optional-dependencies] +django = ["django>=2.2"] +sqlalchemy = ["sqlalchemy~=2.0"] aiopg = ["aiopg", "psycopg2-binary"] psycopg2 = ["psycopg2-binary"] sphinx = ["sphinx"] +[project.urls] +homepage = "https://procrastinate.readthedocs.io/" +source = "https://github.com/procrastinate-org/procrastinate/" +documentation = "https://procrastinate.readthedocs.io/" +issues = "https://github.com/procrastinate-org/procrastinate/issues" +changelog = "https://github.com/procrastinate-org/procrastinate/releases" + +[project.scripts] +procrastinate = 'procrastinate.cli:main' + [tool.poetry.group.types] optional = true From db7bf30c4df2515c6546a01e2f457f9fa686da26 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 19 Jan 2025 17:19:22 +0100 Subject: [PATCH 099/375] Switch to uv: pyproject.toml & lockfile --- poetry.lock | 1883 ------------------------------------------------ pyproject.toml | 113 ++- uv.lock | 1561 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 1614 insertions(+), 1943 deletions(-) delete mode 100644 poetry.lock create mode 100644 uv.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 260a355a7..000000000 --- a/poetry.lock +++ /dev/null @@ -1,1883 +0,0 @@ -# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. - -[[package]] -name = "aiopg" -version = "1.4.0" -description = "Postgres integration with asyncio." -optional = false -python-versions = ">=3.7" -groups = ["main", "pg_implem"] -files = [ - {file = "aiopg-1.4.0-py3-none-any.whl", hash = "sha256:aea46e8aff30b039cfa818e6db4752c97656e893fc75e5a5dc57355a9e9dedbd"}, - {file = "aiopg-1.4.0.tar.gz", hash = "sha256:116253bef86b4d954116716d181e9a0294037f266718b2e1c9766af995639d71"}, -] - -[package.dependencies] -async-timeout = ">=3.0,<5.0" -psycopg2-binary = ">=2.9.5" - -[package.extras] -sa = ["sqlalchemy[postgresql-psycopg2binary] (>=1.3,<1.5)"] - -[[package]] -name = "alabaster" -version = "0.7.16" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, -] - -[[package]] -name = "asgiref" -version = "3.8.1" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.8" -groups = ["main", "django", "docs", "types"] -files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -groups = ["main", "pg_implem"] -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "attrs" -version = "24.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[package]] -name = "babel" -version = "2.16.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["main", "docs"] -files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -groups = ["docs"] -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "certifi" -version = "2024.12.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -groups = ["main", "docs"] -files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main", "docs"] -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "docs", "test"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "contextlib2" -version = "21.6.0" -description = "Backports and enhancements for the contextlib module" -optional = false -python-versions = ">=3.6" -groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, - {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, -] - -[[package]] -name = "coverage" -version = "7.6.10" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "croniter" -version = "6.0.0" -description = "croniter provides iteration for datetime object with cron like format" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" -groups = ["main"] -files = [ - {file = "croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368"}, - {file = "croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577"}, -] - -[package.dependencies] -python-dateutil = "*" -pytz = ">2021.1" - -[[package]] -name = "django" -version = "4.2.18" -description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -optional = false -python-versions = ">=3.8" -groups = ["main", "django", "docs", "types"] -markers = "python_version < \"3.10\"" -files = [ - {file = "Django-4.2.18-py3-none-any.whl", hash = "sha256:ba52eff7e228f1c775d5b0db2ba53d8c49d2f8bfe6ca0234df6b7dd12fb25b19"}, - {file = "Django-4.2.18.tar.gz", hash = "sha256:52ae8eacf635617c0f13b44f749e5ea13dc34262819b2cc8c8636abb08d82c4b"}, -] - -[package.dependencies] -asgiref = ">=3.6.0,<4" -sqlparse = ">=0.3.1" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -argon2 = ["argon2-cffi (>=19.1.0)"] -bcrypt = ["bcrypt"] - -[[package]] -name = "django" -version = "5.1.5" -description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -optional = false -python-versions = ">=3.10" -groups = ["main", "django", "docs", "types"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459"}, - {file = "Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3"}, -] - -[package.dependencies] -asgiref = ">=3.8.1,<4" -sqlparse = ">=0.3.1" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -argon2 = ["argon2-cffi (>=19.1.0)"] -bcrypt = ["bcrypt"] - -[[package]] -name = "django-stubs" -version = "5.1.1" -description = "Mypy stubs for Django" -optional = false -python-versions = ">=3.8" -groups = ["types"] -files = [ - {file = "django_stubs-5.1.1-py3-none-any.whl", hash = "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac"}, - {file = "django_stubs-5.1.1.tar.gz", hash = "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b"}, -] - -[package.dependencies] -asgiref = "*" -django = "*" -django-stubs-ext = ">=5.1.1" -tomli = {version = "*", markers = "python_version < \"3.11\""} -types-PyYAML = "*" -typing-extensions = ">=4.11.0" - -[package.extras] -compatible-mypy = ["mypy (>=1.12,<1.14)"] -oracle = ["oracledb"] -redis = ["redis"] - -[[package]] -name = "django-stubs-ext" -version = "5.1.1" -description = "Monkey-patching and extensions for django-stubs" -optional = false -python-versions = ">=3.8" -groups = ["types"] -files = [ - {file = "django_stubs_ext-5.1.1-py3-none-any.whl", hash = "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c"}, - {file = "django_stubs_ext-5.1.1.tar.gz", hash = "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c"}, -] - -[package.dependencies] -django = "*" -typing-extensions = "*" - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - -[[package]] -name = "dunamai" -version = "1.23.0" -description = "Dynamic version generation" -optional = false -python-versions = ">=3.5" -groups = ["release"] -files = [ - {file = "dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041"}, - {file = "dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4"}, -] - -[package.dependencies] -packaging = ">=20.9" - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["test"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "furo" -version = "2024.8.6" -description = "A clean customisable Sphinx documentation theme." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, - {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7" -sphinx = ">=6.0,<9.0" -sphinx-basic-ng = ">=1.0.0.beta2" - -[[package]] -name = "greenlet" -version = "3.1.1" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -groups = ["main", "pg_implem", "test"] -files = [ - {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, - {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, - {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, - {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, - {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, - {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, - {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, - {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, - {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, - {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, - {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, - {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, - {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, - {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, - {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, - {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, - {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, -] -markers = {main = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and extra == \"sqlalchemy\"", pg_implem = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")", test = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main", "docs"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main", "docs"] -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.5.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "docs"] -files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, -] -markers = {main = "python_version < \"3.10\""} - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -groups = ["test"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.5" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main", "docs"] -files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mdit-py-plugins" -version = "0.4.2" -description = "Collection of plugins for markdown-it-py" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, - {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, -] - -[package.dependencies] -markdown-it-py = ">=1.0.0,<4.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["myst-parser", "sphinx-book-theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["docs"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "migra" -version = "2.0.1611797829" -description = "Like `diff` but for PostgreSQL schemas" -optional = false -python-versions = "*" -groups = ["test"] -files = [ - {file = "migra-2.0.1611797829-py2.py3-none-any.whl", hash = "sha256:1cbd4dfed5c7fc0db1801ef0ba71a842c41ceaea48a74bb902e09935ff933ad4"}, - {file = "migra-2.0.1611797829.tar.gz", hash = "sha256:637df5be1009a760cf62170336266fb29e1fe1fdc071197deb48ffaff70c6a27"}, -] - -[package.dependencies] -schemainspect = ">=0.1.1610498640,<3" -six = "*" -sqlbag = "*" - -[package.extras] -pg = ["psycopg2-binary"] - -[[package]] -name = "mypy" -version = "1.14.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -groups = ["pg_implem"] -files = [ - {file = "mypy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e971c1c667007f9f2b397ffa80fa8e1e0adccff336e5e77e74cb5f22868bee87"}, - {file = "mypy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e86aaeaa3221a278c66d3d673b297232947d873773d61ca3ee0e28b2ff027179"}, - {file = "mypy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1628c5c3ce823d296e41e2984ff88c5861499041cb416a8809615d0c1f41740e"}, - {file = "mypy-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fadb29b77fc14a0dd81304ed73c828c3e5cde0016c7e668a86a3e0dfc9f3af3"}, - {file = "mypy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:3fa76988dc760da377c1e5069200a50d9eaaccf34f4ea18428a3337034ab5a44"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e73c8a154eed31db3445fe28f63ad2d97b674b911c00191416cf7f6459fd49a"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:273e70fcb2e38c5405a188425aa60b984ffdcef65d6c746ea5813024b68c73dc"}, - {file = "mypy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1daca283d732943731a6a9f20fdbcaa927f160bc51602b1d4ef880a6fb252015"}, - {file = "mypy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e68047bedb04c1c25bba9901ea46ff60d5eaac2d71b1f2161f33107e2b368eb"}, - {file = "mypy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a52f26b9c9b1664a60d87675f3bae00b5c7f2806e0c2800545a32c325920bcc"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5326ab70a6db8e856d59ad4cb72741124950cbbf32e7b70e30166ba7bbf61dd"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf4ec4980bec1e0e24e5075f449d014011527ae0055884c7e3abc6a99cd2c7f1"}, - {file = "mypy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390dfb898239c25289495500f12fa73aa7f24a4c6d90ccdc165762462b998d63"}, - {file = "mypy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e026d55ddcd76e29e87865c08cbe2d0104e2b3153a523c529de584759379d3d"}, - {file = "mypy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:585ed36031d0b3ee362e5107ef449a8b5dfd4e9c90ccbe36414ee405ee6b32ba"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7"}, - {file = "mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8"}, - {file = "mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc"}, - {file = "mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f"}, - {file = "mypy-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b16738b1d80ec4334654e89e798eb705ac0c36c8a5c4798496cd3623aa02286"}, - {file = "mypy-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10065fcebb7c66df04b05fc799a854b1ae24d9963c8bb27e9064a9bdb43aa8ad"}, - {file = "mypy-1.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbb7d683fa6bdecaa106e8368aa973ecc0ddb79a9eaeb4b821591ecd07e9e03c"}, - {file = "mypy-1.14.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3498cb55448dc5533e438cd13d6ddd28654559c8c4d1fd4b5ca57a31b81bac01"}, - {file = "mypy-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c7b243408ea43755f3a21a0a08e5c5ae30eddb4c58a80f415ca6b118816e60aa"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14117b9da3305b39860d0aa34b8f1ff74d209a368829a584eb77524389a9c13e"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af98c5a958f9c37404bd4eef2f920b94874507e146ed6ee559f185b8809c44cc"}, - {file = "mypy-1.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b343a1d3989547024377c2ba0dca9c74a2428ad6ed24283c213af8dbb0710b"}, - {file = "mypy-1.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cdb5563c1726c85fb201be383168f8c866032db95e1095600806625b3a648cb7"}, - {file = "mypy-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:74e925649c1ee0a79aa7448baf2668d81cc287dc5782cff6a04ee93f40fb8d3f"}, - {file = "mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab"}, - {file = "mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -groups = ["pg_implem"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "myst-parser" -version = "3.0.1" -description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, - {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, -] - -[package.dependencies] -docutils = ">=0.18,<0.22" -jinja2 = "*" -markdown-it-py = ">=3.0,<4.0" -mdit-py-plugins = ">=0.4,<1.0" -pyyaml = "*" -sphinx = ">=6,<8" - -[package.extras] -code-style = ["pre-commit (>=3.0,<4.0)"] -linkify = ["linkify-it-py (>=2.0,<3.0)"] -rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "docs", "release", "test"] -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "psycopg" -version = "3.2.3" -description = "PostgreSQL database adapter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "pg_implem"] -files = [ - {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, - {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, -] - -[package.dependencies] -psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} -typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} -tzdata = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -binary = ["psycopg-binary (==3.2.3)"] -c = ["psycopg-c (==3.2.3)"] -dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] -docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] -pool = ["psycopg-pool"] -test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] - -[[package]] -name = "psycopg-pool" -version = "3.2.4" -description = "Connection Pool for Psycopg" -optional = false -python-versions = ">=3.8" -groups = ["main", "pg_implem"] -files = [ - {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"}, - {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"}, -] - -[package.dependencies] -typing-extensions = ">=4.6" - -[[package]] -name = "psycopg2-binary" -version = "2.9.10" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -optional = false -python-versions = ">=3.8" -groups = ["main", "pg_implem"] -files = [ - {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, - {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, - {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, - {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, - {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, - {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, -] - -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["main", "docs"] -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.25.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, - {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, -] - -[package.dependencies] -pytest = ">=8.2,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "6.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, -] - -[package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-django" -version = "4.9.0" -description = "A Django plugin for pytest." -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, - {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[package.extras] -docs = ["sphinx", "sphinx-rtd-theme"] -testing = ["Django", "django-configurations (>=2.0)"] - -[[package]] -name = "pytest-mock" -version = "3.14.0" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, -] - -[package.dependencies] -pytest = ">=6.2.5" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2024.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main", "docs"] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "ruff" -version = "0.8.4" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["lint_format"] -files = [ - {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, - {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, - {file = "ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8"}, - {file = "ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835"}, - {file = "ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d"}, - {file = "ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08"}, - {file = "ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8"}, -] - -[[package]] -name = "schemainspect" -version = "0.1.1611724890" -description = "Schema inspection for PostgreSQL (and possibly others)" -optional = false -python-versions = "*" -groups = ["test"] -files = [ - {file = "schemainspect-0.1.1611724890-py2.py3-none-any.whl", hash = "sha256:d3211054de21f0396f3a0a233c389964c65dbec30aa236a330d63521adc96837"}, - {file = "schemainspect-0.1.1611724890.tar.gz", hash = "sha256:bf1f93ff0b80ca455a6ce09b7b13237d5cf60062aeb00f7da8451c71ba5f93ff"}, -] - -[package.dependencies] -six = "*" -sqlalchemy = "*" - -[[package]] -name = "setuptools" -version = "75.6.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, - {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "test"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -groups = ["main", "docs"] -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "soupsieve" -version = "2.6" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, -] - -[[package]] -name = "sphinx" -version = "7.4.7" -description = "Python documentation generator" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, - {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, -] - -[package.dependencies] -alabaster = ">=0.7.14,<0.8.0" -babel = ">=2.13" -colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} -docutils = ">=0.20,<0.22" -imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.1" -packaging = ">=23.0" -Pygments = ">=2.17" -requests = ">=2.30.0" -snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] - -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b2" -description = "A modern skeleton for Sphinx themes." -optional = false -python-versions = ">=3.7" -groups = ["docs"] -files = [ - {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, - {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, -] - -[package.dependencies] -sphinx = ">=4.0" - -[package.extras] -docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -description = "Add a copy button to each of your code cells." -optional = false -python-versions = ">=3.7" -groups = ["docs"] -files = [ - {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, - {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, -] - -[package.dependencies] -sphinx = ">=1.8" - -[package.extras] -code-style = ["pre-commit (==2.12.1)"] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] - -[[package]] -name = "sphinx-github-changelog" -version = "1.0.2" -description = "Build a sphinx changelog from GitHub Releases" -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "sphinx-github-changelog-1.0.2.tar.gz", hash = "sha256:8af05b7aaeb7b6ffa81ceea7ffde34e0ab39d032879c57bbefb08c0c6bb12872"}, -] - -[package.dependencies] -docutils = "*" -importlib-metadata = "*" -requests = "*" -sphinx = "*" - -[package.extras] -dev = ["black", "isort", "tox"] -docs = ["doc8", "sphinx (>=3.1.1)", "sphinx-github-changelog", "sphinx_rtd_theme"] -docs-spelling = ["sphinxcontrib-spelling"] -lint = ["black", "check-manifest", "flake8", "isort", "mypy"] -test = ["pytest", "pytest-cov", "pytest-mock", "requests-mock"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, - {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, - {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, - {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -groups = ["main", "docs"] -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-mermaid" -version = "1.0.0" -description = "Mermaid diagrams in yours Sphinx powered docs" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3"}, - {file = "sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146"}, -] - -[package.dependencies] -pyyaml = "*" -sphinx = "*" - -[package.extras] -test = ["defusedxml", "myst-parser", "pytest", "ruff", "sphinx"] - -[[package]] -name = "sphinxcontrib-programoutput" -version = "0.18" -description = "Sphinx extension to include program output" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "sphinxcontrib_programoutput-0.18-py3-none-any.whl", hash = "sha256:8a651bc85de69a808a064ff0e48d06c12b9347da4fe5fdb1e94914b01e1b0c36"}, - {file = "sphinxcontrib_programoutput-0.18.tar.gz", hash = "sha256:09e68b6411d937a80b6085f4fdeaa42e0dc5555480385938465f410589d2eed8"}, -] - -[package.dependencies] -Sphinx = ">=5.0.0" - -[package.extras] -docs = ["furo"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, - {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["defusedxml (>=0.7.1)", "pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sqlalchemy" -version = "2.0.36" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -groups = ["main", "pg_implem", "test"] -files = [ - {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, - {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, - {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, -] -markers = {main = "extra == \"sqlalchemy\""} - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} -mypy = {version = ">=0.910", optional = true, markers = "extra == \"mypy\""} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - -[[package]] -name = "sqlbag" -version = "0.1.1617247075" -description = "various snippets of SQL-related boilerplate" -optional = false -python-versions = "*" -groups = ["test"] -files = [ - {file = "sqlbag-0.1.1617247075-py2.py3-none-any.whl", hash = "sha256:ecdef26d661f8640711030ac6ee618deb92b91f9f0fc2efbf8a3b133af13092d"}, - {file = "sqlbag-0.1.1617247075.tar.gz", hash = "sha256:b9d7862c3b2030356d796ca872907962fd54704066978d7ae89383f5123366ed"}, -] - -[package.dependencies] -packaging = "*" -six = "*" -sqlalchemy = "*" - -[package.extras] -maria = ["pymysql"] -pendulum = ["pendulum", "relativedelta"] -pg = ["psycopg2"] - -[[package]] -name = "sqlparse" -version = "0.5.3" -description = "A non-validating SQL parser." -optional = false -python-versions = ">=3.8" -groups = ["main", "django", "docs", "types"] -files = [ - {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, - {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, -] - -[package.extras] -dev = ["build", "hatch"] -doc = ["sphinx"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["main", "docs", "pg_implem", "test", "types"] -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] -markers = {main = "python_version < \"3.11\"", docs = "python_version < \"3.11\"", pg_implem = "python_version < \"3.11\"", test = "python_full_version <= \"3.11.0a6\"", types = "python_version < \"3.11\""} - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20241221" -description = "Typing stubs for PyYAML" -optional = false -python-versions = ">=3.8" -groups = ["types"] -files = [ - {file = "types_PyYAML-6.0.12.20241221-py3-none-any.whl", hash = "sha256:0657a4ff8411a030a2116a196e8e008ea679696b5b1a8e1a6aa8ebb737b34688"}, - {file = "types_pyyaml-6.0.12.20241221.tar.gz", hash = "sha256:4f149aa893ff6a46889a30af4c794b23833014c469cc57cbc3ad77498a58996f"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["main", "django", "docs", "pg_implem", "test", "types"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "tzdata" -version = "2024.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main", "django", "docs", "pg_implem", "types"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "zipp" -version = "3.21.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main", "docs"] -files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, -] -markers = {main = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[extras] -aiopg = ["aiopg", "psycopg2-binary"] -django = ["django"] -psycopg2 = ["psycopg2-binary"] -sphinx = ["sphinx"] -sqlalchemy = ["sqlalchemy"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.9" -content-hash = "5239615731ce9259f885baf45eca0bd70cd4d8d03a4d8346c300afed200bbafb" diff --git a/pyproject.toml b/pyproject.toml index 8cd79001d..d746fd64a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,14 @@ [build-system] -requires = ["poetry-core", "poetry-dynamic-versioning"] -build-backend = "poetry_dynamic_versioning.backend" +requires = ["setuptools", "versioningit"] +build-backend = "setuptools.build_meta" + +[tool.versioningit] [project] name = "procrastinate" -version = "0.0.0" +dynamic = ["version"] description = "Postgres-based distributed task processing library" -license = "MIT License" +license = { file = "LICENSE.md " } classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -24,12 +26,16 @@ dependencies = [ "psycopg[pool]", "asgiref", "attrs", - 'contextlib2;python_version<"3.10"', + "contextlib2; python_version < '3.10'", "croniter", "python-dateutil", "typing-extensions", ] +[tool.setuptools.packages.find] +include = ["procrastinate"] + + [project.optional-dependencies] django = ["django>=2.2"] sqlalchemy = ["sqlalchemy~=2.0"] @@ -47,65 +53,52 @@ changelog = "https://github.com/procrastinate-org/procrastinate/releases" [project.scripts] procrastinate = 'procrastinate.cli:main' -[tool.poetry.group.types] -optional = true - -[tool.poetry.group.types.dependencies] -django-stubs = "*" - -[tool.poetry.group.release.dependencies] -dunamai = "*" - -[tool.poetry.group.lint_format.dependencies] -ruff = "*" - -[tool.poetry.group.pg_implem.dependencies] -aiopg = "*" -sqlalchemy = { extras = ["mypy"], version = "*" } -psycopg2-binary = "*" -psycopg = [ - { version = "*", extras = [ - "binary", - "pool", - ], markers = "sys_platform != 'darwin' or platform_machine != 'arm64'" }, - { version = "*", extras = [ - "binary", - "pool", - ], markers = "sys_platform == 'darwin' and platform_machine == 'arm64'", python = ">=3.10" }, - { version = "*", extras = [ - "pool", - ], markers = "sys_platform == 'darwin' and platform_machine == 'arm64'", python = "<3.10" }, +[tool.uv] +default-groups = [ + "release", + "lint_format", + "pg_implem", + "django", + "test", + "docs", ] -[tool.poetry.group.django.dependencies] +[dependency-groups] +types = ["django-stubs"] +release = ["dunamai"] +lint_format = ["ruff"] +pg_implem = [ + "aiopg", + "sqlalchemy", + "psycopg2-binary", + "psycopg[binary,pool]; sys_platform != 'darwin' or platform_machine != 'arm64'", + "psycopg[binary,pool]; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version >= '3.10'", + "psycopg[pool]; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.10'", +] django = [ - { version = "4.2.*", python = "<3.10" }, - { version = "*", python = ">=3.10" }, + "django~=4.2.0; python_version < '3.10'", + "django; python_version >= '3.10'", +] +test = [ + "pytest-asyncio", + "pytest-cov", + "pytest-django", + "pytest-mock", + "migra", + # migra depends on schemainspect, which has an implicit dependency on setuptools + # (pkg_resources). + "setuptools", +] +docs = [ + "django>=2.2", + "furo", + "Sphinx", + "sphinx-copybutton", + "sphinx-github-changelog", + "sphinxcontrib-programoutput", + "sphinxcontrib-mermaid", + "myst-parser", ] - -[tool.poetry.group.test.dependencies] -pytest-asyncio = "*" -pytest-cov = "*" -pytest-django = "*" -pytest-mock = "*" -migra = "*" -# migra depends on schemainspect, which has an implicit dependency on setuptools -# (pkg_resources). -setuptools = { version = "*" } - -[tool.poetry.group.docs.dependencies] -django = ">=2.2" -furo = "*" -Sphinx = "*" -sphinx-copybutton = "*" -sphinx-github-changelog = "*" -sphinxcontrib-programoutput = "*" -sphinxcontrib-mermaid = "*" -myst-parser = "*" - -[tool.poetry-dynamic-versioning] -enable = true -pattern = '(?P\d+(\.\d+)*)([-._]?((?P[a-zA-Z]+)[-._]?(?P\d+)?))?$' [tool.pytest.ini_options] addopts = ["-vv", "--strict-markers", "-rfE", "--reuse-db"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..69ab14683 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1561 @@ +version = 1 +requires-python = ">=3.9" +resolution-markers = [ + "(python_full_version >= '3.10' and platform_machine != 'arm64') or (python_full_version >= '3.10' and sys_platform != 'darwin')", + "python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "(python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version < '3.10' and sys_platform != 'darwin')", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] + +[[package]] +name = "aiopg" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout" }, + { name = "psycopg2-binary" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/aba75a9ffcb1704b98c39986344230eaa70c40ac28e5ca635df231db912f/aiopg-1.4.0.tar.gz", hash = "sha256:116253bef86b4d954116716d181e9a0294037f266718b2e1c9766af995639d71", size = 35593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/2f/ab8690bf995171b9a8b60b98a2ca91d4108a42422abf10bf622397437d26/aiopg-1.4.0-py3-none-any.whl", hash = "sha256:aea46e8aff30b039cfa818e6db4752c97656e893fc75e5a5dc57355a9e9dedbd", size = 34770 }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version < '3.10' and sys_platform != 'darwin')", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.10' and platform_machine != 'arm64') or (python_full_version >= '3.10' and sys_platform != 'darwin')", + "python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, +] + +[[package]] +name = "attrs" +version = "24.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, + { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, + { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, + { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, + { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, + { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, + { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, + { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, + { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, + { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, + { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, + { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, + { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "contextlib2" +version = "21.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/13/37ea7805ae3057992e96ecb1cffa2fa35c2ef4498543b846f90dd2348d8f/contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869", size = 43795 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/56/6d6872f79d14c0cb02f1646cbb4592eef935857c0951a105874b7b62a0c3/contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", size = 13277 }, +] + +[[package]] +name = "coverage" +version = "7.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 }, + { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 }, + { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 }, + { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 }, + { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 }, + { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 }, + { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 }, + { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 }, + { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 }, + { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 }, + { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, + { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, + { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, + { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, + { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, + { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, + { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, + { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, + { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, + { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, + { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, + { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, + { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, + { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, + { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, + { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, + { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, + { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, + { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, + { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, + { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, + { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, + { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, + { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, + { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, + { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, + { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, + { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, + { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, + { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, + { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, + { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, + { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, + { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, + { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, + { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, + { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, + { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, + { url = "https://files.pythonhosted.org/packages/40/41/473617aadf9a1c15bc2d56be65d90d7c29bfa50a957a67ef96462f7ebf8e/coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", size = 207978 }, + { url = "https://files.pythonhosted.org/packages/10/f6/480586607768b39a30e6910a3c4522139094ac0f1677028e1f4823688957/coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", size = 208415 }, + { url = "https://files.pythonhosted.org/packages/f1/af/439bb760f817deff6f4d38fe7da08d9dd7874a560241f1945bc3b4446550/coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", size = 236452 }, + { url = "https://files.pythonhosted.org/packages/d0/13/481f4ceffcabe29ee2332e60efb52e4694f54a402f3ada2bcec10bb32e43/coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", size = 234374 }, + { url = "https://files.pythonhosted.org/packages/c5/59/4607ea9d6b1b73e905c7656da08d0b00cdf6e59f2293ec259e8914160025/coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", size = 235505 }, + { url = "https://files.pythonhosted.org/packages/85/60/d66365723b9b7f29464b11d024248ed3523ce5aab958e4ad8c43f3f4148b/coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", size = 234616 }, + { url = "https://files.pythonhosted.org/packages/74/f8/2cf7a38e7d81b266f47dfcf137fecd8fa66c7bdbd4228d611628d8ca3437/coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", size = 233099 }, + { url = "https://files.pythonhosted.org/packages/50/2b/bff6c1c6b63c4396ea7ecdbf8db1788b46046c681b8fcc6ec77db9f4ea49/coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", size = 234089 }, + { url = "https://files.pythonhosted.org/packages/bf/b5/baace1c754d546a67779358341aa8d2f7118baf58cac235db457e1001d1b/coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", size = 210701 }, + { url = "https://files.pythonhosted.org/packages/b1/bf/9e1e95b8b20817398ecc5a1e8d3e05ff404e1b9fb2185cd71561698fe2a2/coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", size = 211482 }, + { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "croniter" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468 }, +] + +[[package]] +name = "django" +version = "4.2.18" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version < '3.10' and sys_platform != 'darwin')", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "asgiref", marker = "python_full_version < '3.10'" }, + { name = "sqlparse", marker = "python_full_version < '3.10'" }, + { name = "tzdata", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/82/470d12df22d7b56b12812539ce7bed332d8cfda51a657ab2b59f3390cae3/Django-4.2.18.tar.gz", hash = "sha256:52ae8eacf635617c0f13b44f749e5ea13dc34262819b2cc8c8636abb08d82c4b", size = 10428204 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/76/39c641b5787e5e61f35b9d29c6f19bf94506bf7be3e48249f72233c4625d/Django-4.2.18-py3-none-any.whl", hash = "sha256:ba52eff7e228f1c775d5b0db2ba53d8c49d2f8bfe6ca0234df6b7dd12fb25b19", size = 7993633 }, +] + +[[package]] +name = "django" +version = "5.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.10' and platform_machine != 'arm64') or (python_full_version >= '3.10' and sys_platform != 'darwin')", + "python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "asgiref", marker = "python_full_version >= '3.10'" }, + { name = "sqlparse", marker = "python_full_version >= '3.10'" }, + { name = "tzdata", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/17/834e3e08d590dcc27d4cc3c5cd4e2fb757b7a92bab9de8ee402455732952/Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3", size = 10700031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/e6/e92c8c788b83d109f34d933c5e817095d85722719cb4483472abc135f44e/Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459", size = 8276957 }, +] + +[[package]] +name = "django-stubs" +version = "5.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django", version = "4.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "django", version = "5.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "django-stubs-ext" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/d6/b29debed5527ead981b69cef404f7589cca7b6e4aa65fe3e60a478b4588e/django_stubs-5.1.2.tar.gz", hash = "sha256:a0fcb3659bab46a6d835cc2d9bff3fc29c36ccea41a10e8b1930427bc0f9f0df", size = 267374 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/28/137af496de2419ac521b4530f4f6340adbf709befd7d63ce590537c7432a/django_stubs-5.1.2-py3-none-any.whl", hash = "sha256:04ddc778faded6fb48468a8da9e98b8d12b9ba983faa648d37a73ebde0f024da", size = 472598 }, +] + +[[package]] +name = "django-stubs-ext" +version = "5.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "4.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "django", version = "5.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/83/b673bf5131c61949f7840b70f9d25a52d90d27416fe2692f13ade14496f1/django_stubs_ext-5.1.2.tar.gz", hash = "sha256:421c0c3025a68e3ab8e16f065fad9ba93335ecefe2dd92a0cff97a665680266c", size = 9629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/c1/5df5231c5db00e3981e71f295c7e5269cbddb0a2c666b3a6f03831b24bd1/django_stubs_ext-5.1.2-py3-none-any.whl", hash = "sha256:6c559214538d6a26f631ca638ddc3251a0a891d607de8ce01d23d3201ad8ad6c", size = 9032 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "dunamai" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/4e/a5c8c337a1d9ac0384298ade02d322741fb5998041a5ea74d1cd2a4a1d47/dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4", size = 44681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/4c/963169386309fec4f96fd61210ac0a0666887d0fb0a50205395674d20b71/dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041", size = 26342 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "furo" +version = "2024.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, + { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, + { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, + { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, + { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, + { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, + { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, + { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, + { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, + { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, + { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, + { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, + { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, + { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, + { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, + { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, + { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, + { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, + { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "migra" +version = "3.0.1663481299" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "schemainspect" }, + { name = "six" }, + { name = "sqlbag" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/fb/4761e69d6028909f4b68f175f53ac69c521b75b11e977087b6ce6ec3b006/migra-3.0.1663481299.tar.gz", hash = "sha256:0cf0c125d553008d9ff5402663a51703ccc474bb65b5a4f4727906dbf58e217f", size = 10083 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/66/79bf13b29c2c3a3e72d8dead21fde7ae15f84e78038c92a35e62b3e9c229/migra-3.0.1663481299-py3-none-any.whl", hash = "sha256:061643e9af63488e085d729f267ed4af4249789979732b703ddeb2c478ec9a93", size = 10537 }, +] + +[[package]] +name = "myst-parser" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version < '3.10' and sys_platform != 'darwin')", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", marker = "python_full_version < '3.10'" }, + { name = "mdit-py-plugins", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163 }, +] + +[[package]] +name = "myst-parser" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.10' and platform_machine != 'arm64') or (python_full_version >= '3.10' and sys_platform != 'darwin')", + "python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "docutils", marker = "python_full_version >= '3.10'" }, + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "markdown-it-py", marker = "python_full_version >= '3.10'" }, + { name = "mdit-py-plugins", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "procrastinate" +version = "3.0.0b1.post7+g73e21b9e.d20250119" +source = { editable = "." } +dependencies = [ + { name = "asgiref" }, + { name = "attrs" }, + { name = "contextlib2", marker = "python_full_version < '3.10'" }, + { name = "croniter" }, + { name = "psycopg", extra = ["pool"] }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +aiopg = [ + { name = "aiopg" }, + { name = "psycopg2-binary" }, +] +django = [ + { name = "django", version = "4.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "django", version = "5.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +psycopg2 = [ + { name = "psycopg2-binary" }, +] +sphinx = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sqlalchemy = [ + { name = "sqlalchemy" }, +] + +[package.dev-dependencies] +django = [ + { name = "django", version = "4.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "django", version = "5.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +docs = [ + { name = "django", version = "4.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "django", version = "5.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "furo" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-github-changelog" }, + { name = "sphinxcontrib-mermaid" }, + { name = "sphinxcontrib-programoutput" }, +] +lint-format = [ + { name = "ruff" }, +] +pg-implem = [ + { name = "aiopg" }, + { name = "psycopg", extra = ["binary"], marker = "python_full_version >= '3.10' or platform_machine != 'arm64' or sys_platform != 'darwin'" }, + { name = "psycopg", extra = ["pool"] }, + { name = "psycopg2-binary" }, + { name = "sqlalchemy" }, +] +release = [ + { name = "dunamai" }, +] +test = [ + { name = "migra" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-mock" }, + { name = "setuptools" }, +] +types = [ + { name = "django-stubs" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiopg", marker = "extra == 'aiopg'" }, + { name = "asgiref" }, + { name = "attrs" }, + { name = "contextlib2", marker = "python_full_version < '3.10'" }, + { name = "croniter" }, + { name = "django", marker = "extra == 'django'", specifier = ">=2.2" }, + { name = "psycopg", extras = ["pool"] }, + { name = "psycopg2-binary", marker = "extra == 'aiopg'" }, + { name = "psycopg2-binary", marker = "extra == 'psycopg2'" }, + { name = "python-dateutil" }, + { name = "sphinx", marker = "extra == 'sphinx'" }, + { name = "sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = "~=2.0" }, + { name = "typing-extensions" }, +] + +[package.metadata.requires-dev] +django = [ + { name = "django", marker = "python_full_version < '3.10'", specifier = "~=4.2.0" }, + { name = "django", marker = "python_full_version >= '3.10'" }, +] +docs = [ + { name = "django", specifier = ">=2.2" }, + { name = "furo" }, + { name = "myst-parser" }, + { name = "sphinx" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-github-changelog" }, + { name = "sphinxcontrib-mermaid" }, + { name = "sphinxcontrib-programoutput" }, +] +lint-format = [{ name = "ruff" }] +pg-implem = [ + { name = "aiopg" }, + { name = "psycopg", extras = ["binary", "pool"], marker = "platform_machine != 'arm64' or sys_platform != 'darwin'" }, + { name = "psycopg", extras = ["binary", "pool"], marker = "python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "psycopg", extras = ["pool"], marker = "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "psycopg2-binary" }, + { name = "sqlalchemy" }, +] +release = [{ name = "dunamai" }] +test = [ + { name = "migra" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-mock" }, + { name = "setuptools" }, +] +types = [{ name = "django-stubs" }] + +[[package]] +name = "psycopg" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/f2/954b1467b3e2ca5945b83b5e320268be1f4df486c3e8ffc90f4e4b707979/psycopg-3.2.4.tar.gz", hash = "sha256:f26f1346d6bf1ef5f5ef1714dd405c67fb365cfd1c6cea07de1792747b167b92", size = 156109 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/49/15114d5f7ee68983f4e1a24d47e75334568960352a07c6f0e796e912685d/psycopg-3.2.4-py3-none-any.whl", hash = "sha256:43665368ccd48180744cab26b74332f46b63b7e06e8ce0775547a3533883d381", size = 198716 }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "(python_full_version >= '3.10' and implementation_name != 'pypy') or (implementation_name != 'pypy' and platform_machine != 'arm64') or (implementation_name != 'pypy' and sys_platform != 'darwin')" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/7b/6d7a4626b49e227125f8edf6f114dd8e9a9b22fc4f0abc3b2b0068d5f2bd/psycopg_binary-3.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c716f75b5c0388fc5283b5124046292c727511dd8c6aa59ca2dc644b9a2ed0cd", size = 3862864 }, + { url = "https://files.pythonhosted.org/packages/2b/7b/bc0dbb8384997e1321ffb265f96e68ba8584c2af58229816c16809218bdf/psycopg_binary-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2e8050347018f596a63f5dccbb92fb68bca52b13912cb8fc40184b24c0e534f", size = 3934048 }, + { url = "https://files.pythonhosted.org/packages/42/c0/8a8034650e4618efc8c0be32c30469933a1ddac1656525c0c6b2b2151736/psycopg_binary-3.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04171f9af9ab567c0fd339bac06f2c75836db839cebac5bd07824778dafa7f0e", size = 4516741 }, + { url = "https://files.pythonhosted.org/packages/b8/6c/714572fc7c59295498287b9b4b965e3b1d6ff5758c310535a2f02d159688/psycopg_binary-3.2.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7ba7b2ff25a6405826f627fb7d0f1e06e5c08ae25ffabc74a5e9ec7b0a63b85", size = 4323332 }, + { url = "https://files.pythonhosted.org/packages/64/19/a807021e48719cf226a7b520fd0c9c741577ad8974ecd264efe03862d80c/psycopg_binary-3.2.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e58eeba520d405b2ad72dffaafd04d0b592bef870e718bf37c261e89a75450a", size = 4569646 }, + { url = "https://files.pythonhosted.org/packages/67/78/70c515175c623bbc505d015ef1ee55b1ee4d0878985a95d4d6317fdd6894/psycopg_binary-3.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb18cfbb1cfc8172786ceefd314f0faa05c40ea93b3db7194d0f6bbbbfedb42a", size = 4279629 }, + { url = "https://files.pythonhosted.org/packages/0f/02/8a0395ac8f69320ca26f4f7ec7fd16620671ba002072e01ed5fb13c29a38/psycopg_binary-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:769804b4f753ddec9403183a6d4577d5b696fc49c2451421013fb06d6fa2f288", size = 3868189 }, + { url = "https://files.pythonhosted.org/packages/b9/a8/fa254c48513580c9cae242b5fac4af4dd1227178061a27a2eb260ff61a27/psycopg_binary-3.2.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7d4f0c9b01eb933ce35bb32a54205f48d7bc36bf455565afe269cabcb7973955", size = 3335018 }, + { url = "https://files.pythonhosted.org/packages/d6/c1/98c239f40851c67eb4813d6a7eb90b39f717de2fd48f23fe3121899eb70b/psycopg_binary-3.2.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:26aed7ff8691ba810de95718d3bc81a43fd48a4036c3641ef711eb5f71fc7106", size = 3432703 }, + { url = "https://files.pythonhosted.org/packages/91/08/5b6fa2247bf964ac14d10cff3f7163d901dd008b7b6300e13eace8394751/psycopg_binary-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8a4b65eaf44dfed0b47e6ebd392e88cd3cff62ea11652d92db6fefeb2608ed25", size = 3457676 }, + { url = "https://files.pythonhosted.org/packages/2f/55/79db2b10f87eb7a913b59bbcdd10f794c4c964141f2db31f8eb1f567c7d9/psycopg_binary-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9fa48a2dc54c4e906d7dd781031d227d1b13966deff7e5ece5b037588643190", size = 2787324 }, + { url = "https://files.pythonhosted.org/packages/f3/9a/8013aa4ad4d76dfcf9b822da549d51aab96abfc77afc44b200ef295685dc/psycopg_binary-3.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d092b0aa80b8c3ee0701a7252cbfb0bdb742e1f74aaf0c1a13ef22c05c9266ab", size = 3871518 }, + { url = "https://files.pythonhosted.org/packages/1e/65/2422036d0169e33e5f06d868a36235340f85e42afe153d59b0edf4b4210f/psycopg_binary-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3955381dacc6d15f3838d5f25445ee99f80882876a163f8de0c01ffc54aeef4a", size = 3938511 }, + { url = "https://files.pythonhosted.org/packages/bf/ab/4f6c815862d62d9d06353abfbf36fef69ad7e6ca0763eed1629f47579e83/psycopg_binary-3.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04144d1963aa3309247980f1a742b98e15f60d68ea9745143c433f99aaeb70d7", size = 4512971 }, + { url = "https://files.pythonhosted.org/packages/27/ef/0e5e9ea6122f61f9e0c4e70b7f7a28ef51404c98bbb32096ad99f79f85b5/psycopg_binary-3.2.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eac61931bc90c1c6fdc648452894d3a434a005ffefaf12819b4709548c894bf2", size = 4318297 }, + { url = "https://files.pythonhosted.org/packages/93/cd/05d71e4f2f7f69fd185d2ec44b66de13734ff70c426ead14523e206258bb/psycopg_binary-3.2.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c09b765960480c4586758a3c16f0ee0db6f7e2f31c88cccb5e7d7024215468cd", size = 4570696 }, + { url = "https://files.pythonhosted.org/packages/af/7c/f5099ad491f78ba491e56cd686b38b0737eb09a719e919661a9f8d08e754/psycopg_binary-3.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:220de8efcc276e42ba7cc7ed613145b1274b6b5de321a1396fb6b6ce1758d34c", size = 4275069 }, + { url = "https://files.pythonhosted.org/packages/2d/95/a1a2f861d90f3394f98d032329a1e44a67c8d1f5bded0ec343b664c65ba5/psycopg_binary-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b558d3de315d18819ce477908e27518cbdd3275717c6193b58dde36f0443e167", size = 3865827 }, + { url = "https://files.pythonhosted.org/packages/ab/72/0b395ad2db2adc6009d2a1cdc2707b1764a3e870d6895cf92dc87e251aa9/psycopg_binary-3.2.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3b4c9b9a112d43533f7dbdedbb1188107d4ddcd262e2a2af41b4de0caf7d053", size = 3329276 }, + { url = "https://files.pythonhosted.org/packages/ba/5d/8e9904664e5bae3852989a0f1b0517c781ff0a9cba64416ffa68952129ac/psycopg_binary-3.2.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:870df866f789bb641a350897c1751c293b9420f46be4eb366d190ff5f2f2ffd8", size = 3426059 }, + { url = "https://files.pythonhosted.org/packages/46/6a/9abc03e01c1cb97878e6e87d5ea9e3d925790b04fa03d72b2d6e3455f124/psycopg_binary-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89506e268fb95428fb0f8f7abe48032e66cf47390469e11a4fe989f7407a5d88", size = 3456766 }, + { url = "https://files.pythonhosted.org/packages/12/c5/1be474bfa7282aa9177c3e498eb641b1441724f0155953f3872c69deddf0/psycopg_binary-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:7ddf1494cc3bf60761c01265c44dfc7a7fd63f21308c403c14f5dd91702df84d", size = 2790400 }, + { url = "https://files.pythonhosted.org/packages/48/f8/f30cf36bc9bc672894413f10f0498d5e81b0813c87f1b963d85e7c5cc9f1/psycopg_binary-3.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ac24b3d421127ebe8662eba2c1e149a12f0f5b6795e66c1811a3f59111456bb", size = 3852023 }, + { url = "https://files.pythonhosted.org/packages/2f/23/88a265ca4a35def6f53cb239e352bf52f01ea418f57f4272b3913ecd6fd2/psycopg_binary-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f702f36204127984dd212eb57bb328676abdfe8a56f179e408a806d5e520aa11", size = 3935919 }, + { url = "https://files.pythonhosted.org/packages/0f/2b/2ac3456208c255a6fad9fec4fea0e411e34a0b4b0ecd1e60c0ba36fb78c4/psycopg_binary-3.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:610cd2013ee0849154fcff34b0cca17f720c91c7430ca094a61f1e5ff1d38e15", size = 4493108 }, + { url = "https://files.pythonhosted.org/packages/55/f5/725b786b7cf1b91f1afbe03545f0b14857c0a5cc03b4f8a6735ec289ff89/psycopg_binary-3.2.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95da59edd95f6b6488799c9710fafc2d5750e3ec6328ec991f7a9be04efe6886", size = 4300141 }, + { url = "https://files.pythonhosted.org/packages/09/80/72b3a1ec912b8be51e6af858fcd2a016d25145aca400e75bba6ab91025c4/psycopg_binary-3.2.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b71e98e3186f08473962e1ea4bfbc4387ecc398644b794cb112ad0a4276e3789", size = 4540559 }, + { url = "https://files.pythonhosted.org/packages/0b/8e/6cd6643f04e033bcdab008d5175c9356ade1eecff53fa4558d383dd9866c/psycopg_binary-3.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ccf4f71c3a0d46bc74207bf7997f010a6586414161dd10f3dd026ec059942ef", size = 4253687 }, + { url = "https://files.pythonhosted.org/packages/85/47/50d93bef98d32eba1f7b95e3c4e671a7f59b1d0b9ed01fdb43e951d6012b/psycopg_binary-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:244e1dd33b694792b7bc7a3d412a535ba39116218b07d8936b4591567f4121e9", size = 3842084 }, + { url = "https://files.pythonhosted.org/packages/2e/a0/2cf0dda5634d14219a24c05bc85cb928a5b2ea29684d167aebc974df016c/psycopg_binary-3.2.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f8dc8f4de5130c6278dd5e34b18ad8324a74658a7adb72d4e67ca97f9aeaaf3c", size = 3315357 }, + { url = "https://files.pythonhosted.org/packages/14/65/13b3dd91dd62f6e4ee3cb00bd24ab60a251592c03a8fb090c28057f21e38/psycopg_binary-3.2.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c336e58a48061a9189d3ba8c19f00fe5d9570219e6f7f954b923ad5c33e5bc71", size = 3394512 }, + { url = "https://files.pythonhosted.org/packages/07/cc/90b5307ff833892c8985aefd73c1894b1a9d8b5df4965650e95636ba8161/psycopg_binary-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9633c5dc6796d11766d2475e62335b67e5f99f119f40ba1675c1d23208d7709d", size = 3431893 }, + { url = "https://files.pythonhosted.org/packages/40/dc/5ab8fec2fc2e0599fd7a60abe046c853477bbb7cd978b818f795c5423848/psycopg_binary-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:295c25e56b430d786a475c5c2cef266b0b27c0a6fcaadf9d83a4cdcfb76f971f", size = 2778464 }, + { url = "https://files.pythonhosted.org/packages/25/e2/f56675aada063762f08559b6969e47e1313f269fc1682c16457c13da8186/psycopg_binary-3.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:81ab801c0d35830c876bf0d1edc8e7dd2f73aa2b04fe24eb812159c0b054d149", size = 3846854 }, + { url = "https://files.pythonhosted.org/packages/7b/8b/8c4a66b2b3db494367df0299535b7d2df78f303334228c517b8d00c411d5/psycopg_binary-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c09e02ce1124eb6638b3381df050a8cf88aedfad4522f939945cda49050a990c", size = 3932292 }, + { url = "https://files.pythonhosted.org/packages/84/e8/618d45f77cebce73d75497c95685a0902aea3783386d9335ce486c69e13a/psycopg_binary-3.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a249cdc6a5c2b5088a8677acba66b291e5237524739ab3d27498e1ef189312f5", size = 4493785 }, + { url = "https://files.pythonhosted.org/packages/c4/87/fc30318e6b97e723e017e7dc88d0f721bbfb749de1a6e414e52d4ac54c9a/psycopg_binary-3.2.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2960ba8a5c0ad75e184f6d8bf76bdf023708999efe75fe4e13445136c1cd206", size = 4304874 }, + { url = "https://files.pythonhosted.org/packages/91/30/1d127e651c21cd77befaf361c7c3b9001bfff51ac38027e8fce598ba0701/psycopg_binary-3.2.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dae2e50b0d3425c167eebbedc3553f7c811dbc0dbfc737b6877f68a03be7daf", size = 4541296 }, + { url = "https://files.pythonhosted.org/packages/0d/5e/22c824cb38745c1c744cec85d227190727c564afb75960ce0057ca15fd84/psycopg_binary-3.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bf7ee7e0002c2cce43ecb923ec510358056eb2e44a96afaeb0424518f35206", size = 4255756 }, + { url = "https://files.pythonhosted.org/packages/b3/83/ae8783dec3f7e39df8a4056e4d383926ffec531970c0b415d48d9fd4a2c2/psycopg_binary-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f5c85eeb63b1a8a6b026eef57f5da36ff215ce9a6a3bb8e20a409670d6cfbda", size = 3845918 }, + { url = "https://files.pythonhosted.org/packages/be/f7/fb7bffb0c4c45a5a82fe324e4f7b176075a4c5372e546a038858dd13c7ab/psycopg_binary-3.2.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8c7b95899d4d6d23c5cc46cb3419e8e6ca68d867509432ee1487042564a1ea55", size = 3315429 }, + { url = "https://files.pythonhosted.org/packages/81/a3/29f4993a239d6a3fb18ef8681d9990c007f5f73bdd2e21f65f07ac55ad6f/psycopg_binary-3.2.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fa4acea9ca20a567c3872a5afab2084751530bb57b8fb6b52820d5c54e7c8c3b", size = 3399388 }, + { url = "https://files.pythonhosted.org/packages/25/5b/925171cbfa2e3d1ccb7f4c005d0d5db609ba796c1d08a23c42825b09c554/psycopg_binary-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c487f35a1905bb15da927c1fc05f70f3d29f0e21fb4ba21d360a0da9c755f20", size = 3436702 }, + { url = "https://files.pythonhosted.org/packages/b6/47/25b2b85b8fcabf99bfa92b4b0d587894c01576bf0b2bf137c243d1eb1070/psycopg_binary-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:80297c3a9f7b5a6afdb0d8f220661ccd796e5c9128c44b32c41267f7daefd37f", size = 2779196 }, + { url = "https://files.pythonhosted.org/packages/2f/56/f40184d35179e433bc88d99993435e370feaa3e1dd25b670aeccdf44b321/psycopg_binary-3.2.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2ddec5deed4c93a1bd73f210bed6dadbabc470ac1f9ebf55fa260e48396fd61f", size = 3864122 }, + { url = "https://files.pythonhosted.org/packages/9f/55/3e3ef6a140aaecd4ada5fe81099ab26b380f5bc6e9dcf9ef1fca2b298071/psycopg_binary-3.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8bd54787d894261ff48d5c4b7f23e281c05c9a5ac67355eff7d29cfbcde640cd", size = 3934859 }, + { url = "https://files.pythonhosted.org/packages/02/87/58af827b8388b8218ca627b739b737d79ead688d564080a4a10277c83641/psycopg_binary-3.2.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ae8cf8694d01788be5f418f6cada813e2b86cef67efba9c60cb9371cee9eb9", size = 4516943 }, + { url = "https://files.pythonhosted.org/packages/94/3e/bbb50f5f1c3055aca8d16091c5d0f64327697d444e3078d2d2951cc7bdef/psycopg_binary-3.2.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0958dd3bfffbdef86594a6fa45255d4389ade94d17572bdf5207a900166a3cba", size = 4323772 }, + { url = "https://files.pythonhosted.org/packages/bf/32/1a3524942befe5ddc0369cf29e0e9f5ea1607d1ffa089fa4ca10fa43a5d8/psycopg_binary-3.2.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b9558f9d101907e412ea12c355e8989c811d382d893ba6a541c091e6d916164", size = 4570266 }, + { url = "https://files.pythonhosted.org/packages/50/05/19c199ea980f652a8033af5308887ea21dd929558eb16e66c482de5b310c/psycopg_binary-3.2.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279faafe9a4cdaeeee7844c19cccb865328bd55a2bf4012fef8d7040223a5245", size = 4283435 }, + { url = "https://files.pythonhosted.org/packages/bc/eb/8a3f9475ba305447e41687e031e140e2f7829a9b9cd7c8432d34d2f63df0/psycopg_binary-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:196d8426a9220d29c118eec6074034648267c176d220cb42c49b3c9c396f0dbc", size = 3868207 }, + { url = "https://files.pythonhosted.org/packages/a9/29/4f0f4b7c51cdc4ba6e72f77418a4f43500415866b4b748b08cae43f77aa7/psycopg_binary-3.2.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:166e68b1e42862b18570d636a7b615630552daeab8b129083aa094f848be64b0", size = 3335297 }, + { url = "https://files.pythonhosted.org/packages/61/b5/56042b08bf5962ac631198efe6a949e52c95cb1111c015cae7eab1eb8afc/psycopg_binary-3.2.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b84c3f51969d33266640c218ad5bb5f8487e6a991db7a95b2c3c46fbda37a77c", size = 3433536 }, + { url = "https://files.pythonhosted.org/packages/1a/fb/361e8ed5f7f79f697a6a9193b7529a7a509ef761bb33f1aeb42bd25c7329/psycopg_binary-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:501113e4d84887c03f83c7d8886c0744fe088fd6b633b919ebf7af4f0f7186be", size = 3459503 }, + { url = "https://files.pythonhosted.org/packages/c7/8d/6db8fba11a23c541186c42298e38d75e9ce45722dce3c5ee258372f74bcd/psycopg_binary-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:e889fe21c578c6c533c8550e1b3ba5d2cc5d151890458fa5fbfc2ca3b2324cfa", size = 2789153 }, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/71/01d4e589dc5fd1f21368b7d2df183ed0e5bbc160ce291d745142b229797b/psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/28/2b56ac94c236ee033c7b291bcaa6a83089d0cc0fe7830c35f6521177c199/psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224", size = 38240 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/81/331257dbf2801cdb82105306042f7a1637cc752f65f2bb688188e0de5f0b/psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", size = 3043397 }, + { url = "https://files.pythonhosted.org/packages/e7/9a/7f4f2f031010bbfe6a02b4a15c01e12eb6b9b7b358ab33229f28baadbfc1/psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", size = 3274806 }, + { url = "https://files.pythonhosted.org/packages/e5/57/8ddd4b374fa811a0b0a0f49b6abad1cde9cb34df73ea3348cc283fcd70b4/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", size = 2851361 }, + { url = "https://files.pythonhosted.org/packages/f9/66/d1e52c20d283f1f3a8e7e5c1e06851d432f123ef57b13043b4f9b21ffa1f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", size = 3080836 }, + { url = "https://files.pythonhosted.org/packages/a0/cb/592d44a9546aba78f8a1249021fe7c59d3afb8a0ba51434d6610cc3462b6/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", size = 3264552 }, + { url = "https://files.pythonhosted.org/packages/64/33/c8548560b94b7617f203d7236d6cdf36fe1a5a3645600ada6efd79da946f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", size = 3019789 }, + { url = "https://files.pythonhosted.org/packages/b0/0e/c2da0db5bea88a3be52307f88b75eec72c4de62814cbe9ee600c29c06334/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", size = 2871776 }, + { url = "https://files.pythonhosted.org/packages/15/d7/774afa1eadb787ddf41aab52d4c62785563e29949613c958955031408ae6/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", size = 2820959 }, + { url = "https://files.pythonhosted.org/packages/5e/ed/440dc3f5991a8c6172a1cde44850ead0e483a375277a1aef7cfcec00af07/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", size = 2919329 }, + { url = "https://files.pythonhosted.org/packages/03/be/2cc8f4282898306732d2ae7b7378ae14e8df3c1231b53579efa056aae887/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", size = 2957659 }, + { url = "https://files.pythonhosted.org/packages/d0/12/fb8e4f485d98c570e00dad5800e9a2349cfe0f71a767c856857160d343a5/psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", size = 1024605 }, + { url = "https://files.pythonhosted.org/packages/22/4f/217cd2471ecf45d82905dd09085e049af8de6cfdc008b6663c3226dc1c98/psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", size = 1163817 }, + { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397 }, + { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806 }, + { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370 }, + { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780 }, + { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583 }, + { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831 }, + { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822 }, + { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975 }, + { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320 }, + { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617 }, + { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618 }, + { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816 }, + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, + { url = "https://files.pythonhosted.org/packages/a2/bc/e77648009b6e61af327c607543f65fdf25bcfb4100f5a6f3bdb62ddac03c/psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", size = 3043437 }, + { url = "https://files.pythonhosted.org/packages/e0/e8/5a12211a1f5b959f3e3ccd342eace60c1f26422f53e06d687821dc268780/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", size = 2851340 }, + { url = "https://files.pythonhosted.org/packages/47/ed/5932b0458a7fc61237b653df050513c8d18a6f4083cc7f90dcef967f7bce/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", size = 3080905 }, + { url = "https://files.pythonhosted.org/packages/71/df/8047d85c3d23864aca4613c3be1ea0fe61dbe4e050a89ac189f9dce4403e/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", size = 3264640 }, + { url = "https://files.pythonhosted.org/packages/f3/de/6157e4ef242920e8f2749f7708d5cc8815414bdd4a27a91996e7cd5c80df/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", size = 3019812 }, + { url = "https://files.pythonhosted.org/packages/25/f9/0fc49efd2d4d6db3a8d0a3f5749b33a0d3fdd872cad49fbf5bfce1c50027/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", size = 2871933 }, + { url = "https://files.pythonhosted.org/packages/57/bc/2ed1bd182219065692ed458d218d311b0b220b20662d25d913bc4e8d3549/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", size = 2820990 }, + { url = "https://files.pythonhosted.org/packages/71/2a/43f77a9b8ee0b10e2de784d97ddc099d9fe0d9eec462a006e4d2cc74756d/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", size = 2919352 }, + { url = "https://files.pythonhosted.org/packages/57/86/d2943df70469e6afab3b5b8e1367fccc61891f46de436b24ddee6f2c8404/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", size = 2957614 }, + { url = "https://files.pythonhosted.org/packages/85/21/195d69371330983aa16139e60ba855d0a18164c9295f3a3696be41bbcd54/psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", size = 1025341 }, + { url = "https://files.pythonhosted.org/packages/ad/53/73196ebc19d6fbfc22427b982fbc98698b7b9c361e5e7707e3a3247cf06d/psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", size = 1163958 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/df/adcc0d60f1053d74717d21d58c0048479e9cab51464ce0d2965b086bd0e2/pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f", size = 53950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/d8/defa05ae50dcd6019a95527200d3b3980043df5aa445d40cb0ef9f7f98ab/pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075", size = 19400 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-django" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/43c8b2528c24d7f1a48a47e3f7381f5ab2ae8c64634b0c3f4bd843063955/pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314", size = 84067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "ruff" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, + { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, + { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, + { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, + { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, + { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, + { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, + { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, + { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, + { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, + { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, + { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, + { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, + { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, + { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, + { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, + { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, +] + +[[package]] +name = "schemainspect" +version = "3.1.1663587362" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/41/c2ea77a94a7dcbde0d5b9cce70018a730e4ab5504628c14ced657c87217a/schemainspect-3.1.1663587362.tar.gz", hash = "sha256:a295ad56f7a19c09e5e1ef9f16dadbf6392e26196cb5f05b5afe613c99ce7468", size = 28520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e2/eea82cd82c91c840f1f32cfc874db831ac3a742fbd9dfe713cae851441f1/schemainspect-3.1.1663587362-py3-none-any.whl", hash = "sha256:3071265712863c4d4e742940a4b44ac685135af3c93416872ec1bb6c822c4aca", size = 37373 }, +] + +[[package]] +name = "setuptools" +version = "75.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version < '3.10' and sys_platform != 'darwin')", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "babel", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "imagesize", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.10' and platform_machine != 'arm64') or (python_full_version >= '3.10' and sys_platform != 'darwin')", + "python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "babel", marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.10'" }, + { name = "imagesize", marker = "python_full_version >= '3.10'" }, + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343 }, +] + +[[package]] +name = "sphinx-github-changelog" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "requests" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/21/90bc47a6cdfff2e37bc233754f6662808975528a73dab0bf6a5215eeb2a5/sphinx_github_changelog-1.4.0.tar.gz", hash = "sha256:204745e93a1f280e4664977b5fee526b0a011c92ca19c304bd01fd641ddb6393", size = 7583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/07/a669a23f47803ad12e620b68b761bf7bf32fd9f293c5846fda8f3893d706/sphinx_github_changelog-1.4.0-py3-none-any.whl", hash = "sha256:cdf2099ea3e4587ae8637be7ba609738bfdeca4bd80c5df6fc45046735ae5c2f", size = 8513 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597 }, +] + +[[package]] +name = "sphinxcontrib-programoutput" +version = "0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/c0/834af2290f8477213ec0dd60e90104f5644aa0c37b1a0d6f0a2b5efe03c4/sphinxcontrib_programoutput-0.18.tar.gz", hash = "sha256:09e68b6411d937a80b6085f4fdeaa42e0dc5555480385938465f410589d2eed8", size = 26333 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/2c/7aec6e0580f666d4f61474a50c4995a98abfff27d827f0e7bc8c4fa528f5/sphinxcontrib_programoutput-0.18-py3-none-any.whl", hash = "sha256:8a651bc85de69a808a064ff0e48d06c12b9347da4fe5fdb1e94914b01e1b0c36", size = 20346 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.37" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/20/93ea2518df4d7a14ebe9ace9ab8bb92aaf7df0072b9007644de74172b06c/sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb", size = 9626249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/21/aaf0cd2e7ee56e464af7cba38a54f9c1203570181ec5d847711f33c9f520/SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e", size = 2102915 }, + { url = "https://files.pythonhosted.org/packages/fd/01/6615256759515f13bb7d7b49981326f1f4e80ff1bd92dccd53f99dab79ea/SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069", size = 2094095 }, + { url = "https://files.pythonhosted.org/packages/6a/f2/400252bda1bd67da7a35bb2ab84d10a8ad43975d42f15b207a9efb765446/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1", size = 3076482 }, + { url = "https://files.pythonhosted.org/packages/40/c6/e7e8e894c8f065f96ca202cdb00454d60d4962279b3eb5a81b8766dfa836/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84", size = 3084750 }, + { url = "https://files.pythonhosted.org/packages/d6/ee/1cdab04b7760e48273f2592037df156afae044e2e6589157673bd2a830c0/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f", size = 3040575 }, + { url = "https://files.pythonhosted.org/packages/4d/af/2dd456bfd8d4b9750792ceedd828bddf83860f2420545e5effbaf722dae5/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4", size = 3066113 }, + { url = "https://files.pythonhosted.org/packages/dd/d7/ad997559574f94d7bd895a8a63996afef518d07e9eaf5a2a9cbbcb877c16/SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72", size = 2075239 }, + { url = "https://files.pythonhosted.org/packages/d0/82/141fbed705a21af2d825068831da1d80d720945df60c2b97ddc5133b3714/SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636", size = 2099307 }, + { url = "https://files.pythonhosted.org/packages/7c/37/4915290c1849337be6d24012227fb3c30c575151eec2b182ee5f45e96ce7/SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c", size = 2104098 }, + { url = "https://files.pythonhosted.org/packages/4c/f5/8cce9196434014a24cc65f6c68faa9a887080932361ee285986c0a35892d/SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5", size = 2094492 }, + { url = "https://files.pythonhosted.org/packages/9c/54/2df4b3d0d11b384b6e9a8788d0f1123243f2d2356e2ccf626f93dcc1a09f/SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8", size = 3212789 }, + { url = "https://files.pythonhosted.org/packages/57/4f/e1db9475f940f1c54c365ed02d4f6390f884fc95a6a4022ece7725956664/SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b", size = 3212784 }, + { url = "https://files.pythonhosted.org/packages/89/57/d93212e827d1f03a6cd4d0ea13775957c2a95161330fa47449b91153bd09/SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087", size = 3149616 }, + { url = "https://files.pythonhosted.org/packages/5f/c2/759347419f69cf0bbb76d330fbdbd24cefb15842095fe86bca623759b9e8/SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9", size = 3169944 }, + { url = "https://files.pythonhosted.org/packages/22/04/a19ecb53aa19bb8cf491ecdb6bf8c1ac74959cd4962e119e91d4e2b8ecaa/SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989", size = 2074686 }, + { url = "https://files.pythonhosted.org/packages/7b/9d/6e030cc2c675539dbc5ef73aa97a3cbe09341e27ad38caed2b70c4273aff/SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba", size = 2099891 }, + { url = "https://files.pythonhosted.org/packages/86/62/e5de4a5e0c4f5ceffb2b461aaa2378c0ee00642930a8c38e5b80338add0f/SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef", size = 2102692 }, + { url = "https://files.pythonhosted.org/packages/01/44/3b65f4f16abeffd611da0ebab9e3aadfca45d041a78a67835c41c6d28289/SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4", size = 2093079 }, + { url = "https://files.pythonhosted.org/packages/a4/d8/e3a6622e86e3ae3a41ba470d1bb095c1f2dedf6b71feae0b4b94b5951017/SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4", size = 3242509 }, + { url = "https://files.pythonhosted.org/packages/3a/ef/5a53a6a60ac5a5d4ed28959317dac1ff72bc16773ccd9b3fe79713fe27f3/SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd", size = 3253368 }, + { url = "https://files.pythonhosted.org/packages/67/f2/30f5012379031cd5389eb06455282f926a4f99258e5ee5ccdcea27f30d67/SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098", size = 3188655 }, + { url = "https://files.pythonhosted.org/packages/fe/df/905499aa051605aeda62c1faf33d941ffb7fda291159ab1c24ef5207a079/SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb", size = 3215281 }, + { url = "https://files.pythonhosted.org/packages/94/54/f2769e7e356520f75016d82ca43ed85e47ba50e636a34124db4625ae5976/SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761", size = 2072972 }, + { url = "https://files.pythonhosted.org/packages/c2/7f/241f059e0b7edb85845368f43964d6b0b41733c2f7fffaa993f8e66548a5/SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff", size = 2098597 }, + { url = "https://files.pythonhosted.org/packages/45/d1/e63e56ceab148e69f545703a74b90c8c6dc0a04a857e4e63a4c07a23cf91/SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658", size = 2097968 }, + { url = "https://files.pythonhosted.org/packages/fd/e5/93ce63310347062bd42aaa8b6785615c78539787ef4380252fcf8e2dcee3/SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb", size = 2088445 }, + { url = "https://files.pythonhosted.org/packages/1b/8c/d0e0081c09188dd26040fc8a09c7d87f539e1964df1ac60611b98ff2985a/SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4", size = 3174880 }, + { url = "https://files.pythonhosted.org/packages/79/f7/3396038d8d4ea92c72f636a007e2fac71faae0b59b7e21af46b635243d09/SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94", size = 3188226 }, + { url = "https://files.pythonhosted.org/packages/ef/33/7a1d85716b29c86a744ed43690e243cb0e9c32e3b68a67a97eaa6b49ef66/SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0", size = 3121425 }, + { url = "https://files.pythonhosted.org/packages/27/11/fa63a77c88eb2f79bb8b438271fbacd66a546a438e4eaba32d62f11298e2/SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6", size = 3149589 }, + { url = "https://files.pythonhosted.org/packages/b6/04/fcdd103b6871f2110460b8275d1c4828daa806997b0fa5a01c1cd7fd522d/SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2", size = 2070746 }, + { url = "https://files.pythonhosted.org/packages/d4/7c/e024719205bdc1465b7b7d3d22ece8e1ad57bc7d76ef6ed78bb5f812634a/SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2", size = 2094612 }, + { url = "https://files.pythonhosted.org/packages/70/c9/f199edc09a630ac62079977cbb8a50888cb920c1f635ce254cb4d61e1dda/SQLAlchemy-2.0.37-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33", size = 2105789 }, + { url = "https://files.pythonhosted.org/packages/e7/cc/9286318598bb26af535f480636ed6cf368794f2b483122ce7f2a56acef57/SQLAlchemy-2.0.37-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b", size = 2097013 }, + { url = "https://files.pythonhosted.org/packages/db/41/efaa216b3ebe2989f233ac72abed7281c8fe45a40a2cae7a06c3b1cef870/SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b", size = 3090933 }, + { url = "https://files.pythonhosted.org/packages/65/ee/b99bb446b0dc8fa5e2dbd47bf379bc62c5f2823681732fd3a253b1c49a6e/SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a", size = 3098730 }, + { url = "https://files.pythonhosted.org/packages/dd/61/a9eac6696ca4791895784871f79b32bcf1b0dd17614479734558036af8d8/SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9", size = 3057751 }, + { url = "https://files.pythonhosted.org/packages/95/be/d70fa8a42287976dad0d590f75633ec203694d2f9bafd1cdba41d8e4db55/SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8", size = 3084290 }, + { url = "https://files.pythonhosted.org/packages/18/e9/a00e73a7e8eb620ea030592c7d3a9b66c31bc6b36effdf04f10c7ada8dc1/SQLAlchemy-2.0.37-cp39-cp39-win32.whl", hash = "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278", size = 2077561 }, + { url = "https://files.pythonhosted.org/packages/2a/52/f3fff9216b9df07e8142001e638d5ba8c298299a2a9006b9ab3b068fb0f1/SQLAlchemy-2.0.37-cp39-cp39-win_amd64.whl", hash = "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b", size = 2101760 }, + { url = "https://files.pythonhosted.org/packages/3b/36/59cc97c365f2f79ac9f3f51446cae56dfd82c4f2dd98497e6be6de20fb91/SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1", size = 1894113 }, +] + +[[package]] +name = "sqlbag" +version = "0.1.1617247075" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "six" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/6f/46171ef9ef6d177b94dff96e6403c7fb7466de5c9ee767b0218a21945fdb/sqlbag-0.1.1617247075.tar.gz", hash = "sha256:b9d7862c3b2030356d796ca872907962fd54704066978d7ae89383f5123366ed", size = 11652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/c8/1d4bd038d4b34f3810fd8ab70a48cc6f3d2373666f2797f0298a97b088c9/sqlbag-0.1.1617247075-py2.py3-none-any.whl", hash = "sha256:ecdef26d661f8640711030ac6ee618deb92b91f9f0fc2efbf8a3b133af13092d", size = 14971 }, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20241230" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] From b6d04851fe2b7d382d58c1d9ab96e97071f65bf0 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sun, 19 Jan 2025 22:50:15 +0100 Subject: [PATCH 100/375] Pre-commit --- .pre-commit-config.yaml | 72 +++++++++++++++++++++++------ pyproject.toml | 4 +- scripts/sync-pre-commit.py | 92 ++++++++++++++++++++++++++++++++++++++ uv.lock | 80 ++++++++++++++++++++++++++++++++- 4 files changed, 231 insertions(+), 17 deletions(-) create mode 100755 scripts/sync-pre-commit.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6609ea12..0bd40b558 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,23 +29,65 @@ repos: - id: trailing-whitespace - id: mixed-line-ending - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.391 + rev: v1.1.392.post0 hooks: - id: pyright additional_dependencies: - aiopg==1.4.0 + - alabaster==0.7.16 ; python_full_version < '3.10' + - alabaster==1.0.0 ; python_full_version >= '3.10' - asgiref==3.8.1 + - async-timeout==4.0.3 - attrs==24.3.0 - - contextlib2==21.6.0 + - babel==2.16.0 + - certifi==2024.12.14 + - charset-normalizer==3.4.1 + - colorama==0.4.6 ; sys_platform == 'win32' + - contextlib2==21.6.0 ; python_full_version < '3.10' - croniter==6.0.0 - - django-stubs==5.1.1 - - django==5.1.5 + - django==4.2.18 ; python_full_version < '3.10' + - django==5.1.5 ; python_full_version >= '3.10' + - django-stubs==5.1.2 + - django-stubs-ext==5.1.2 + - docutils==0.21.2 + - greenlet==3.1.1 ; (python_full_version < '3.14' and platform_machine == + 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') + or (python_full_version < '3.14' and platform_machine == 'aarch64') or + (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version + < '3.14' and platform_machine == 'ppc64le') or (python_full_version < + '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' + and platform_machine == 'x86_64') + - idna==3.10 + - imagesize==1.4.1 + - importlib-metadata==8.5.0 ; python_full_version < '3.10' + - jinja2==3.1.5 + - markupsafe==3.0.2 + - packaging==24.2 + - psycopg==3.2.4 + - psycopg-pool==3.2.4 - psycopg2-binary==2.9.10 - - psycopg[pool]==3.2.3 + - pygments==2.19.1 - python-dateutil==2.9.0.post0 - - sphinx==7.4.7 - - sqlalchemy==2.0.36 + - pytz==2024.2 + - requests==2.32.3 + - six==1.17.0 + - snowballstemmer==2.2.0 + - sphinx==7.4.7 ; python_full_version < '3.10' + - sphinx==8.1.3 ; python_full_version >= '3.10' + - sphinxcontrib-applehelp==2.0.0 + - sphinxcontrib-devhelp==2.0.0 + - sphinxcontrib-htmlhelp==2.1.0 + - sphinxcontrib-jsmath==1.0.1 + - sphinxcontrib-qthelp==2.0.0 + - sphinxcontrib-serializinghtml==2.0.0 + - sqlalchemy==2.0.37 + - sqlparse==0.5.3 + - tomli==2.2.1 ; python_full_version < '3.11' + - types-pyyaml==6.0.12.20241230 - typing-extensions==4.12.2 + - tzdata==2024.2 ; sys_platform == 'win32' + - urllib3==2.3.0 + - zipp==3.21.0 ; python_full_version < '3.10' - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.4 hooks: @@ -56,11 +98,13 @@ repos: rev: v1.1.2 hooks: - id: doc8 - - repo: https://github.com/ewjoachim/poetry-to-pre-commit - rev: 2.2.0 - hooks: - - id: sync-repos - args: [--map=pyright-python=pyright, --map=ruff-pre-commit=ruff] - - id: sync-hooks-additional-dependencies - args: ['--bind=pyright=main,types'] + - repo: local + hooks: + - id: sync-pre-commit + name: Sync pre-commit hooks + language: python + entry: scripts/sync-pre-commit.py + additional_dependencies: + - uv + - ruamel.yaml diff --git a/pyproject.toml b/pyproject.toml index d746fd64a..fd0754eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,7 @@ docs = [ "sphinxcontrib-mermaid", "myst-parser", ] +dev = ["ruff", "pyright", "doc8"] [tool.pytest.ini_options] addopts = ["-vv", "--strict-markers", "-rfE", "--reuse-db"] @@ -134,9 +135,8 @@ exclude_lines = [ "[ ]+\\.\\.\\.$", ] - [tool.pyright] -exclude = ["tests", ".venv"] +exclude = ["tests", ".venv", "scripts"] [tool.ruff] target-version = "py39" diff --git a/scripts/sync-pre-commit.py b/scripts/sync-pre-commit.py new file mode 100755 index 000000000..a42d3be95 --- /dev/null +++ b/scripts/sync-pre-commit.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# /// script +# dependencies = [ +# "ruamel.yaml", +# ] +# /// +# Usage: uv run scripts/sync-pre-commit.py +# or through pre-commit hook: pre-commit run --all-files sync-pre-commit + +from __future__ import annotations + +import contextlib +import copy +import pathlib +import subprocess +from collections.abc import Generator +from typing import Any, cast + +import ruamel.yaml + +PRE_COMMIT_PYPI_MAPPING = { + "pyright-python": "pyright", + "ruff": "ruff", + "doc8": "doc8", +} + + +@contextlib.contextmanager +def yaml_roundtrip( + path: pathlib.Path, +) -> Generator[dict[str, Any], None, None]: + yaml = ruamel.yaml.YAML() + config = cast("dict[str, Any]", yaml.load(path.read_text())) + old_config = copy.deepcopy(config) + yield config + if config != old_config: + yaml.indent(mapping=2, sequence=4, offset=2) + yaml.dump(config, path) + + +def export_from_uv_lock(group_args): + base_export_args = [ + "uv", + "export", + "--all-extras", + "--no-hashes", + "--no-header", + "--no-emit-project", + "--no-emit-workspace", + ] + packages = ( + subprocess.check_output( + [*base_export_args, *group_args], + text=True, + ) + .strip() + .split("\n") + ) + return packages + + +def main(): + groups_typing = [ + "--group=types", + "--no-group=release", + "--no-group=lint_format", + "--no-group=pg_implem", + "--no-group=django", + "--no-group=test", + "--no-group=docs", + ] + groups_dev = [ + "--only-group=dev", + ] + typing_dependencies = export_from_uv_lock(groups_typing) + dev_dependencies = export_from_uv_lock(groups_dev) + dev_versions = dict(e.split("==") for e in dev_dependencies) + + with yaml_roundtrip(pathlib.Path(".pre-commit-config.yaml")) as pre_commit_config: + for repo in pre_commit_config["repos"]: + repo_name = repo["repo"].split("/")[-1] + pypi_package = PRE_COMMIT_PYPI_MAPPING.get(repo_name) + if pypi_package: + repo["rev"] = f"v{dev_versions[pypi_package]}" + + if repo_name == "pyright-python": + repo["hooks"][0]["additional_dependencies"] = typing_dependencies + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock index 69ab14683..2dd467f10 100644 --- a/uv.lock +++ b/uv.lock @@ -353,6 +353,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/c1/5df5231c5db00e3981e71f295c7e5269cbddb0a2c666b3a6f03831b24bd1/django_stubs_ext-5.1.2-py3-none-any.whl", hash = "sha256:6c559214538d6a26f631ca638ddc3251a0a891d607de8ce01d23d3201ad8ad6c", size = 9032 }, ] +[[package]] +name = "doc8" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "restructuredtext-lint" }, + { name = "stevedore" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/28/b0a576233730b756ca1ebb422bc6199a761b826b86e93e5196dfa85331ea/doc8-1.1.2.tar.gz", hash = "sha256:1225f30144e1cc97e388dbaf7fe3e996d2897473a53a6dae268ddde21c354b98", size = 27030 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/f1/6ffd5d76578e98a8f21ae7216b88a7212c778f665f1a8f4f8ce6f9605da4/doc8-1.1.2-py3-none-any.whl", hash = "sha256:e787b3076b391b8b49400da5d018bacafe592dfc0a04f35a9be22d0122b82b59", size = 25794 }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -668,6 +684,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "packaging" version = "24.2" @@ -677,6 +702,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pbr" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/35/80cf8f6a4f34017a7fe28242dc45161a1baa55c41563c354d8147e8358b2/pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24", size = 124032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/44/6a65ecd630393d47ad3e7d5354768cb7f9a10b3a0eb2cd8c6f52b28211ee/pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a", size = 108529 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -688,7 +722,7 @@ wheels = [ [[package]] name = "procrastinate" -version = "3.0.0b1.post7+g73e21b9e.d20250119" +version = "3.0.0b1.post12+g0f42bc28" source = { editable = "." } dependencies = [ { name = "asgiref" }, @@ -721,6 +755,11 @@ sqlalchemy = [ ] [package.dev-dependencies] +dev = [ + { name = "doc8" }, + { name = "pyright" }, + { name = "ruff" }, +] django = [ { name = "django", version = "4.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "django", version = "5.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -781,6 +820,11 @@ requires-dist = [ ] [package.metadata.requires-dev] +dev = [ + { name = "doc8" }, + { name = "pyright" }, + { name = "ruff" }, +] django = [ { name = "django", marker = "python_full_version < '3.10'", specifier = "~=4.2.0" }, { name = "django", marker = "python_full_version >= '3.10'" }, @@ -985,6 +1029,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] +[[package]] +name = "pyright" +version = "1.1.392.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/df/3c6f6b08fba7ccf49b114dfc4bb33e25c299883fd763f93fad47ef8bc58d/pyright-1.1.392.post0.tar.gz", hash = "sha256:3b7f88de74a28dcfa90c7d90c782b6569a48c2be5f9d4add38472bdaac247ebd", size = 3789911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/b1/a18de17f40e4f61ca58856b9ef9b0febf74ff88978c3f7776f910071f567/pyright-1.1.392.post0-py3-none-any.whl", hash = "sha256:252f84458a46fa2f0fd4e2f91fc74f50b9ca52c757062e93f6c250c0d8329eb2", size = 5595487 }, +] + [[package]] name = "pytest" version = "8.3.4" @@ -1140,6 +1197,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] +[[package]] +name = "restructuredtext-lint" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/9c/6d8035cafa2d2d314f34e6cd9313a299de095b26e96f1c7312878f988eec/restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45", size = 16723 } + [[package]] name = "ruff" version = "0.9.2" @@ -1476,6 +1542,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, ] +[[package]] +name = "stevedore" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pbr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/e9/4eedccff8332cc40cc60ddd3b28d4c3e255ee7e9c65679fa4533ab98f598/stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d", size = 513899 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/73/d0091d22a65b55e8fb6aca7b3b6713b5a261dd01cec4cfd28ed127ac0cfc/stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857", size = 49534 }, +] + [[package]] name = "tomli" version = "2.2.1" From 3d02ceaa813bfe64870f4d3a2038dbcd9db6ad9a Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:36:54 +0100 Subject: [PATCH 101/375] Devcontainer --- .devcontainer/Dockerfile | 4 ++-- .devcontainer/devcontainer.json | 1 - uv.lock | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2fe5f2346..f08631f7d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -13,5 +13,5 @@ RUN sudo apt-get update \ USER vscode -RUN pipx install poetry \ - && poetry completions bash >> ~/.bash_completion +RUN pipx install uv \ + && uv generate-shell-completion bash >> ~/.bash_completion diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bebb34375..68f6ea33e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,6 @@ "PGHOST": "127.0.0.1", "PGPASSWORD": "password", "PGUSER": "postgres", - "POETRY_VIRTUALENVS_IN_PROJECT": "true", "PROCRASTINATE_APP": "procrastinate_demos.demo_async.app.app", "VIRTUAL_ENV": "${containerWorkspaceFolder}/.venv" }, diff --git a/uv.lock b/uv.lock index 2dd467f10..6b3cee583 100644 --- a/uv.lock +++ b/uv.lock @@ -722,7 +722,7 @@ wheels = [ [[package]] name = "procrastinate" -version = "3.0.0b1.post12+g0f42bc28" +version = "3.0.0b1.post12+g9807a35c.d20250121" source = { editable = "." } dependencies = [ { name = "asgiref" }, From 725f9c1fe7d4b72f4e4bc586d86ce4050a5fbe4d Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:37:27 +0100 Subject: [PATCH 102/375] Documentation & dev env --- CONTRIBUTING.md | 52 ++++++++++++------------------ dev-env | 4 +-- docs/howto/django/configuration.md | 6 ++-- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 134304056..80d718758 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,9 +16,9 @@ the following assumptions: - You're using `MacOS` or `Linux`, and `bash` or `zsh`. - You already have `python3` available - Either: - - you already have `poetry`, `pre-commit` and `nox` installed - - or you have `pipx` installed and you're ok installing those 3 tools with `pipx` - - or you don't have `pipx` installed but it's ok if we install it for you + - you already have `uv`, `pre-commit` and `nox` installed + - or you have `uv` installed and you're ok installing the 2 other tools with `uv` + - or you don't have `uv` installed but it's ok if we install it for you - Either: - you've already setup a PostgreSQL database and environment variables (`PG*`) are set @@ -80,26 +80,27 @@ $ /usr/local/opt/libpq/bin/createdb ### Set up your development environment -The development environment is managed by [poetry]. It's a tool that manages +The development environment is managed by [uv]. It's a tool that manages dependencies and virtual environments. We also use [pre-commit] to keep the code -clean. +clean and [nox] to run some tests. -If you don't already have `poetry` or `pre-commit` installed, you can +If you don't already have `uv`, `pre-commit` or `nox` installed, you can install them with: ```console $ scripts/bootstrap ``` -This will install [pipx] if necessary and use it to install `poetry` and +This will install [uv] if necessary and use it to install `nox` and `pre-commit`. Then, install Procrastinate with development dependencies in a virtual environment: ```console -$ poetry env use 3.{x} # Select the Python version you want to use (replace {x}) -$ poetry install -$ poetry shell # Activate the virtual environment +$ uv venv --python=3.{x} # Select the Python version you want to use (replace {x}) +$ uv sync # Install the project and its dependencies +$ uv run $SHELL # Activate the virtual environment +$ exit # Quit the virtual environment ``` You can check that your Python environment is properly activated: @@ -397,8 +398,8 @@ When possible, we're trying to avoid duplicating code, with designs such as ## Dependencies management -Dependencies for the package are handled by Poetry in -[`pyproject.toml`](https://github.com/procrastinate-org/procrastinate/blob/main/pyproject.toml#L25). +Dependencies for the package are handled by uv in +[`pyproject.toml`](https://github.com/procrastinate-org/procrastinate/blob/main/pyproject.toml). Whenever possible, we avoid pinning or putting any kind of limits on the requirements. We'll typically only do that if we know that there's a known conflict with specific versions. Typically, even if we support a subset of @@ -407,28 +408,17 @@ and if users use procrastinate with unsupported Django version and it works for them, everyone is happy. Dependencies for the development environment are kept in -[`poetry.lock`](https://github.com/procrastinate-org/procrastinate/blob/main/poetry.lock). +[`uv.lock`](https://github.com/procrastinate-org/procrastinate/blob/main/uv.lock). Those are updated regularily by [Renovate](https://docs.renovatebot.com/) which merges their own PRs. -The versions in `pre-commit-config.yaml` are kept in sync with `poetry.lock` -by the `pre-commit` hook -[poetry-to-pre-commit](https://github.com/procrastinate-org/procrastinate/blob/main/.pre-commit-config.yaml#L61). +The versions in `pre-commit-config.yaml` are kept in sync with `uv.lock` +by a local `pre-commit` hook +[script](https://github.com/procrastinate-org/procrastinate/blob/main/scripts/sync-pre-commit.py). If you need to recompute the lockfile in your PR, you can use: ```console -$ # Update all the pinned dependencies in pyproject.toml & all versions in poetry.lock -$ # (there are actually no pinned dependencies in pyproject.toml, so this only updates the -$ # lockfile). -$ poetry update - -$ # Similarly, update dependencies in the lockfile. In procrastinate, it's equivalent -$ # to the command above -$ poetry lock - -$ # Recompute the lockfile (e.g. after the pyproject.toml was updated) without trying -$ # to update anything -$ poetry lock --no-update +$ uv lock ``` ## Core contributor additional documentation @@ -451,7 +441,7 @@ automated. This works with pre-release too. When creating the release, GitHub will save the release info and create a tag with the provided version. The new tag will be seen by GitHub Actions, which will then create a wheel (using the tag as version number, thanks to -`poetry-dynamic-versioning`), and push it to PyPI (using Trusted publishing). +`versioningit`), and push it to PyPI (using Trusted publishing). That tag should also trigger a ReadTheDocs build, which will read GitHub releases (thanks to our `changelog` extension) which will write the changelog in the published documentation (transformed from `Markdown` to @@ -469,8 +459,8 @@ also rebuild the stable and latest doc on [readthedocs](https://readthedocs.org/ [editorconfig]: https://editorconfig.org/ [libpq environment variables]: https://www.postgresql.org/docs/current/libpq-envars.html -[pipx]: https://pipx.pypa.io/stable/ -[poetry]: https://python-poetry.org/ +[uv]: https://docs.astral.sh/uv [pre-commit]: https://pre-commit.com/ [Procrastinate releases]: https://github.com/procrastinate-org/procrastinate/releases [Pytest]: https://docs.pytest.org/en/latest/ +[nox]: https://nox.thea.codes/en/stable/ diff --git a/dev-env b/dev-env index 95bd758bb..9eb035ab0 100755 --- a/dev-env +++ b/dev-env @@ -36,7 +36,7 @@ echo "" export PROCRASTINATE_APP=procrastinate_demos.demo_async.app.app export PATH="$(pwd)/scripts/:$PATH" -source $(poetry env info -p)/bin/activate +source .venv/bin/activate if ! pg_dump --schema-only --table=procrastinate_jobs 1>/dev/null 2>&1; then echo "Applying migrations" @@ -63,4 +63,4 @@ echo "- htmldoc: Opens the locally built sphinx documentation in your browser" echo "- lint: Run code formatters & linters" echo "- docs: Build doc" echo "" -echo 'Quit the poetry shell with the command `deactivate`' +echo 'Quit the shell with the command `deactivate`' diff --git a/docs/howto/django/configuration.md b/docs/howto/django/configuration.md index 50cbd31c4..9d2238481 100644 --- a/docs/howto/django/configuration.md +++ b/docs/howto/django/configuration.md @@ -10,11 +10,11 @@ how. For each Python version supported by Procrastinate, Procastinate is tested with the latest Django version supported by that Python version. -As of September 2024, this means Procrastinate is tested with Django 4.2 for +As of January 2025, this means Procrastinate is tested with Django 4.2 for Python 3.8 and 3.9, and Django 5.1 for Python 3.10+. This paragraph is likely to be outdated in the future, the best way to get up-to-date info is to have a -look at the `tool.poetry.group.django.dependencies` section of the [package -configuration](https://github.com/procrastinate-org/procrastinate/blob/pydjver/pyproject.toml#L79-L83) +look at the `django` dependency group section of the [package +configuration](https://github.com/procrastinate-org/procrastinate/blob/main/pyproject.toml#L78-81) ## Installation & configuration From 3bbcbfdba9fdcf4a043356064e6fe9fb2a5cc163 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:55:54 +0100 Subject: [PATCH 103/375] Remove procrastinate's own version from the lock (needs uv 0.5.21) --- uv.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/uv.lock b/uv.lock index 6b3cee583..3dacf9edf 100644 --- a/uv.lock +++ b/uv.lock @@ -722,7 +722,6 @@ wheels = [ [[package]] name = "procrastinate" -version = "3.0.0b1.post12+g9807a35c.d20250121" source = { editable = "." } dependencies = [ { name = "asgiref" }, From fc2b50f4d30c344128c21cb58373cf0d5942b322 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:56:01 +0100 Subject: [PATCH 104/375] CI --- .github/workflows/ci.yml | 44 +++++++++++++--------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d31c7792e..fe0de7d11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: tags: - "*" +env: + UV_FROZEN: "true" + jobs: build: strategy: @@ -37,18 +40,13 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Install poetry - run: pipx install poetry - - - uses: actions/setup-python@v5 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5 with: - python-version: "${{ matrix.python-version }}" - cache: "poetry" - - - run: poetry install --all-extras + python-version: ${{ matrix.python-version }} - name: Run tests - run: scripts/tests --cov=procrastinate --cov-branch + run: uv run pytest --cov=procrastinate --cov-branch env: COVERAGE_FILE: ".coverage.${{ matrix.python-version }}" PGHOST: localhost @@ -88,13 +86,10 @@ jobs: steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - - name: Install poetry - run: pipx install poetry - - - uses: actions/setup-python@v5 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5 with: python-version: "3.12" - cache: "poetry" - name: Get latest tag id: get-latest-tag @@ -103,7 +98,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run tests - run: pipx run nox -s ${{ matrix.mode }} + run: uvx run nox -s ${{ matrix.mode }} env: PGHOST: localhost PGUSER: postgres @@ -116,19 +111,16 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Install poetry - run: pipx install poetry - - - uses: actions/setup-python@v5 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5 with: python-version: "3.9" - cache: "poetry" - name: Install dependencies - run: poetry install --all-extras --with=types + run: uv sync --all-extras --with-group=types - name: Activate virtualenv - run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH + run: echo ".venv/bin" >> $GITHUB_PATH - name: Extract pyright version from pre-commit id: pre-commit-pyright-version @@ -187,15 +179,7 @@ jobs: - build - static-typing steps: - - name: Install poetry - run: | - pipx install poetry - pipx inject poetry 'poetry-dynamic-versioning[plugin]' - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Build wheel and sdist - run: poetry build - - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 From 0ecc38c51c007bdc4169be1dfc281e19d40cba32 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:56:10 +0100 Subject: [PATCH 105/375] Scripts --- scripts/README.md | 2 +- scripts/bootstrap | 27 ++++++++++++--------------- scripts/docs | 2 +- scripts/tests | 2 +- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index da9c7d83d..51f88fbcd 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -3,6 +3,6 @@ Each individual file in this folder is a script, aimed to capture a command for the project. -These scripts are expected to be run as-is, not in poetry or a virtualenv. +These scripts are expected to be run as-is, not in uv or a virtualenv. See [CONTRIBUTING.md](../CONTRIBUTING.md) for more details. diff --git a/scripts/bootstrap b/scripts/bootstrap index e2719a14e..4ade2e871 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -1,28 +1,25 @@ #!/usr/bin/env bash set -eu -# This script will take care of installing pipx for you (mainly on debian-based -# installations). It should not be run in a virtual environment. pipx, poetry -# and pre-commit are all tools that manage their own virtual environements, and -# are useful as tools to have around, and unlikely to cause version clashes -# between projects. +# This script will take care of installing uv (though if you're in a real +# computer and not a container or anything, you may rather want to install it +# differently, e.g. via brew). +# pre-commit and nox are all tools that manage their own virtual environements, +# and are useful as tools to have around, and unlikely to cause version clashes +# between projects, so we're installing them too, via uv, if they're not around. -if ! which pre-commit || ! which poetry || ! which nox; then - if ! which pipx; then - python3 -m pip install --user pipx - python3 -m pipx ensurepath +if ! which uv || ! which pre-commit || ! which nox; then + if ! which uv; then + python3 -m pip install --user uv fi if ! which pre-commit; then - pipx install pre-commit - fi - if ! which poetry; then - pipx install poetry + uv tool install pre-commit fi if ! which nox; then - pipx install nox + uv tool install nox fi fi pre-commit install -poetry install --extras "django sqlalchemy" +uv sync --all-extras diff --git a/scripts/docs b/scripts/docs index 803a29c41..0b0d48f73 100755 --- a/scripts/docs +++ b/scripts/docs @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -eux -poetry run sphinx-build -EW docs docs/_build/html "$@" +uv run sphinx-build -EW docs docs/_build/html "$@" diff --git a/scripts/tests b/scripts/tests index a661bfdd9..7de780d79 100755 --- a/scripts/tests +++ b/scripts/tests @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -eux -poetry run pytest "$@" +uv run pytest "$@" From 41ccaa3a5a5901aa4fc442e7edf8cf7826164525 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:56:18 +0100 Subject: [PATCH 106/375] Readthedocs --- .readthedocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 19de0a4c5..3064bdd4e 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -11,9 +11,9 @@ build: python: "latest" jobs: post_create_environment: - - python -m pip install poetry + - python -m pip install uv post_install: - - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m poetry install --with docs + - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m uv sync --with-group docs sphinx: configuration: docs/conf.py From 940bf7a56b337ed360aff883a597086190b6680f Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:56:26 +0100 Subject: [PATCH 107/375] Noxfile --- .github/workflows/ci.yml | 2 +- noxfile.py | 32 ++++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe0de7d11..ae26ad4e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,7 +98,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run tests - run: uvx run nox -s ${{ matrix.mode }} + run: uvx nox -s ${{ matrix.mode }} env: PGHOST: localhost PGUSER: postgres diff --git a/noxfile.py b/noxfile.py index d22959369..cf69af18c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -38,9 +38,8 @@ def get_pre_migration(latest_tag: packaging.version.Version) -> str: @nox.session def current_version_with_post_migration(session: nox.Session): - session.install("poetry") - session.run("poetry", "install", "--all-extras", external=True) - session.run("poetry", "run", "pytest", *session.posargs, external=True) + session.run("uv", "sync", "--all-extras", external=True) + session.run("uv", "run", "pytest", *session.posargs, external=True) @nox.session @@ -48,10 +47,16 @@ def current_version_without_post_migration(session: nox.Session): latest_tag = fetch_latest_tag(session) pre_migration = get_pre_migration(latest_tag) - session.run("poetry", "install", "--all-extras") session.run( - "poetry", - "run", + "uv", + "sync", + "--all-extras", + "--group", + "test", + external=True, + env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}, + ) + session.run( "pytest", f"--migrate-until={pre_migration}", "./tests/acceptance", @@ -74,8 +79,19 @@ def stable_version_without_post_migration(session: nox.Session): # Install test dependencies and copy tests shutil.copytree(base_path / "tests", temp_path / "tests") shutil.copy(base_path / "pyproject.toml", temp_path / "pyproject.toml") - shutil.copy(base_path / "poetry.lock", temp_path / "poetry.lock") - session.run("poetry", "install", "--with", "test", "--no-root", external=True) + shutil.copy(base_path / "uv.lock", temp_path / "uv.lock") + session.run( + "uv", + "sync", + "--all-extras", + "--group", + "test", + "--no-install-project", + external=True, + env={ + "UV_PROJECT_ENVIRONMENT": session.virtualenv.location, + }, + ) # Install latest procrastinate from PyPI session.install("procrastinate") From a31a7239b844a61ed2fd691e5105988419f798db Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 21:56:40 +0100 Subject: [PATCH 108/375] Dockerfile (who uses this ???) --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 636b36d40..328e4ca9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3 -RUN pip install poetry +RUN pip install uv ARG UID=1000 ARG GID=1000 @@ -10,7 +10,7 @@ USER $UID:$GID ENV HOME="/src" COPY pyproject.toml ./ -COPY poetry.lock ./ -RUN poetry install -ENTRYPOINT ["poetry", "run"] +COPY uv.lock ./ +RUN uv sync +ENTRYPOINT ["uv", "run"] CMD ["procrastinate", "worker"] From 0dcd0817f5b7daa2561e385203f1cc1994dfbd94 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 22:02:00 +0100 Subject: [PATCH 109/375] Tell uv that version may change on every commit (also, reorder) --- pyproject.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd0754eab..8154919d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,10 +32,6 @@ dependencies = [ "typing-extensions", ] -[tool.setuptools.packages.find] -include = ["procrastinate"] - - [project.optional-dependencies] django = ["django>=2.2"] sqlalchemy = ["sqlalchemy~=2.0"] @@ -53,7 +49,12 @@ changelog = "https://github.com/procrastinate-org/procrastinate/releases" [project.scripts] procrastinate = 'procrastinate.cli:main' + +[tool.setuptools.packages.find] +include = ["procrastinate"] + [tool.uv] +cache-keys = [{ git = { commit = true, tags = true } }] default-groups = [ "release", "lint_format", From 2d14ea4fbb34c137423b6268f71663d025291f7c Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 22:04:54 +0100 Subject: [PATCH 110/375] We need a recent uv --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8154919d3..afa7437d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ include = ["procrastinate"] [tool.uv] cache-keys = [{ git = { commit = true, tags = true } }] +required-version = ">=0.5.21" default-groups = [ "release", "lint_format", From 68e09c28389ffa2c1794df2cfa9ec9c252167bd2 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 21 Jan 2025 22:41:05 +0100 Subject: [PATCH 111/375] Add default tag --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index afa7437d3..cdec79384 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,10 @@ build-backend = "setuptools.build_meta" [tool.versioningit] +[tool.versioningit.vcs] +method = "git" +default-tag = "0.0.0" + [project] name = "procrastinate" dynamic = ["version"] From 91bd89b0a02d6736b8f023e61c5dfed3faacefb7 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:15:56 +0100 Subject: [PATCH 112/375] Add . in pythonpath for pytest --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdec79384..5b09523f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,8 +121,9 @@ filterwarnings = """ """ asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +django_find_project = false DJANGO_SETTINGS_MODULE = "tests.acceptance.django_settings" - +pythonpath = ["."] [tool.coverage.run] relative_files = true From 73fe3eed8beaea36ed4922b4890757afe25f42e1 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:16:42 +0100 Subject: [PATCH 113/375] Try using hatchling instead of setuptools --- pyproject.toml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5b09523f7..b95ccd2b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,15 @@ [build-system] -requires = ["setuptools", "versioningit"] -build-backend = "setuptools.build_meta" +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" -[tool.versioningit] - -[tool.versioningit.vcs] -method = "git" -default-tag = "0.0.0" +[tool.hatch.version] +source = "uv-dynamic-versioning" [project] name = "procrastinate" dynamic = ["version"] description = "Postgres-based distributed task processing library" -license = { file = "LICENSE.md " } +license = { file = "LICENSE.md" } classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -53,10 +50,6 @@ changelog = "https://github.com/procrastinate-org/procrastinate/releases" [project.scripts] procrastinate = 'procrastinate.cli:main' - -[tool.setuptools.packages.find] -include = ["procrastinate"] - [tool.uv] cache-keys = [{ git = { commit = true, tags = true } }] required-version = ">=0.5.21" From 191ebc0d4d97af01ca35f3b052898cde4104148f Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:33:00 +0100 Subject: [PATCH 114/375] Move procrastinate demos under procrastinate --- .devcontainer/devcontainer.json | 2 +- dev-env | 2 +- docs/demos.md | 2 +- docs/howto/production/logging.md | 2 +- .../demos}/README.md | 38 +++++++++---------- .../demos}/__init__.py | 0 .../demos}/demo_async/__init__.py | 0 .../demos}/demo_async/__main__.py | 0 .../demos}/demo_async/app.py | 2 +- .../demos}/demo_async/tasks.py | 0 .../demos}/demo_django/__init__.py | 0 .../demos}/demo_django/__main__.py | 0 .../demos}/demo_django/demo/__init__.py | 0 .../demos}/demo_django/demo/admin.py | 0 .../demos}/demo_django/demo/apps.py | 2 +- .../demo/migrations/0001_initial.py | 0 .../demo_django/demo/migrations/__init__.py | 0 .../demos}/demo_django/demo/models.py | 0 .../demos}/demo_django/demo/tasks.py | 0 .../demo/templates/demo/book_form.html | 0 .../demo/templates/demo/book_list.html | 0 .../demos}/demo_django/demo/views.py | 0 .../demos}/demo_django/manage.py | 2 +- .../demos}/demo_django/project/__init__.py | 0 .../demos}/demo_django/project/asgi.py | 2 +- .../demos}/demo_django/project/settings.py | 10 ++--- .../demos}/demo_django/project/urls.py | 2 +- .../demos}/demo_django/project/wsgi.py | 2 +- .../demos}/demo_sync/__init__.py | 0 .../demos}/demo_sync/__main__.py | 0 .../demos}/demo_sync/app.py | 2 +- .../demos}/demo_sync/tasks.py | 0 .../contrib/sphinx/test-root/index.rst | 2 +- 33 files changed, 36 insertions(+), 36 deletions(-) rename {procrastinate_demos => procrastinate/demos}/README.md (69%) rename {procrastinate_demos => procrastinate/demos}/__init__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_async/__init__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_async/__main__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_async/app.py (69%) rename {procrastinate_demos => procrastinate/demos}/demo_async/tasks.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/__init__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/__main__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/__init__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/admin.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/apps.py (66%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/migrations/0001_initial.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/migrations/__init__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/models.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/tasks.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/templates/demo/book_form.html (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/templates/demo/book_list.html (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/demo/views.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/manage.py (91%) rename {procrastinate_demos => procrastinate/demos}/demo_django/project/__init__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_django/project/asgi.py (86%) rename {procrastinate_demos => procrastinate/demos}/demo_django/project/settings.py (94%) rename {procrastinate_demos => procrastinate/demos}/demo_django/project/urls.py (94%) rename {procrastinate_demos => procrastinate/demos}/demo_django/project/wsgi.py (86%) rename {procrastinate_demos => procrastinate/demos}/demo_sync/__init__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_sync/__main__.py (100%) rename {procrastinate_demos => procrastinate/demos}/demo_sync/app.py (69%) rename {procrastinate_demos => procrastinate/demos}/demo_sync/tasks.py (100%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 68f6ea33e..eb10091c9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ "PGHOST": "127.0.0.1", "PGPASSWORD": "password", "PGUSER": "postgres", - "PROCRASTINATE_APP": "procrastinate_demos.demo_async.app.app", + "PROCRASTINATE_APP": "procrastinate.demos.demo_async.app.app", "VIRTUAL_ENV": "${containerWorkspaceFolder}/.venv" }, "service": "app", diff --git a/dev-env b/dev-env index 9eb035ab0..26f807077 100755 --- a/dev-env +++ b/dev-env @@ -34,7 +34,7 @@ echo "" echo "Database is ready!" echo "" -export PROCRASTINATE_APP=procrastinate_demos.demo_async.app.app +export PROCRASTINATE_APP=procrastinate.demos.demo_async.app.app export PATH="$(pwd)/scripts/:$PATH" source .venv/bin/activate diff --git a/docs/demos.md b/docs/demos.md index 88442fe88..b99e94683 100644 --- a/docs/demos.md +++ b/docs/demos.md @@ -1,2 +1,2 @@ -:::{include} ../procrastinate_demos/README.md +:::{include} ../procrastinate/demos/README.md ::: diff --git a/docs/howto/production/logging.md b/docs/howto/production/logging.md index 7ed143c85..d6676b1dc 100644 --- a/docs/howto/production/logging.md +++ b/docs/howto/production/logging.md @@ -79,4 +79,4 @@ root.setLevel(log_level) [extra]: https://timber.io/blog/the-pythonic-guide-to-logging/#adding-context [`structlog`]: https://www.structlog.org/en/stable/ -[Django demo]: https://github.com/procrastinate-org/procrastinate/blob/main/procrastinate_demos/demo_django/project/settings.py#L151 +[Django demo]: https://github.com/procrastinate-org/procrastinate/blob/main/procrastinate/demos/demo_django/project/settings.py#L151 diff --git a/procrastinate_demos/README.md b/procrastinate/demos/README.md similarity index 69% rename from procrastinate_demos/README.md rename to procrastinate/demos/README.md index b7d7cb554..e0c5a6e2b 100644 --- a/procrastinate_demos/README.md +++ b/procrastinate/demos/README.md @@ -3,12 +3,12 @@ This modules contains 3 mini-applications that showcase using procrastinate in difference contexts: -- [demo_django]: a Django application, -- [demo_async]: an async application, it could be a - FastAPI application, but to make things simpler, it's just a plain - asyncio application. -- [demo_sync]: a synchronous application, similarily, it - could be representative of a Flask application. +- [demo_django]: a Django application, +- [demo_async]: an async application, it could be a + FastAPI application, but to make things simpler, it's just a plain + asyncio application. +- [demo_sync]: a synchronous application, similarily, it + could be representative of a Flask application. The demos are there both to showcase the code and as a way to easily recreate the issues that are reported in the issues. They are not @@ -17,8 +17,8 @@ up the Procrastinate development environment (see [contributing doc](contributing)) To run the demos, set PROCRASTINATE_APP to -`procrastinate_demos..app.app`, then run the -`procrastinate` CLI or `python -m procrastinate_demos.` +`procrastinate.demos..app.app`, then run the +`procrastinate` CLI or `python -m procrastinate.demos.` for the application main entrypoint. For all apps, you'll need to have a PostgreSQL database running, and set @@ -34,13 +34,13 @@ baclground processes). Launch the worker in the first terminal: ```console -$ PROCRASTINATE_APP=procrastinate_demos.demo_async.app.app procrastinate worker +$ PROCRASTINATE_APP=procrastinate.demos.demo_async.app.app procrastinate worker ``` In the second terminal, run the application: ```console -$ python -m procrastinate_demos.demo_async +$ python -m procrastinate.demos.demo_async ``` Defer a job by sending commands, as indicated by the application. @@ -50,11 +50,11 @@ Defer a job by sending commands, as indicated by the application. Same with `sync`: ```console -$ PROCRASTINATE_APP=procrastinate_demos.demo_sync.app.app procrastinate worker +$ PROCRASTINATE_APP=procrastinate.demos.demo_sync.app.app procrastinate worker ``` ```console -$ python -m procrastinate_demos.demo_sync +$ python -m procrastinate.demos.demo_sync ``` ## Django demo @@ -62,14 +62,14 @@ $ python -m procrastinate_demos.demo_sync In the first terminal, run the migrations, and then the Django server: ```console -$ procrastinate_demos/demo_django/manage.py migrate -$ procrastinate_demos/demo_django/manage.py runserver +$ procrastinate/demos/demo_django/manage.py migrate +$ procrastinate/demos/demo_django/manage.py runserver ``` In the second terminal, run the procrastinate worker: ```console -$ procrastinate_demos/demo_django/manage.py procrastinate worker +$ procrastinate/demos/demo_django/manage.py procrastinate worker ``` In your browser (`http://localhost:8000/`), you can now: - Create a @@ -86,7 +86,7 @@ deferring a job from another job.) You can visit the admin, too. You'll need to create a superuser first: ```console -$ procrastinate_demos/demo_django/manage.py createsuperuser +$ procrastinate/demos/demo_django/manage.py createsuperuser ``` Then lauch the server, head to `http://localhost:8000/admin/` and see the jobs, @@ -94,6 +94,6 @@ the events and the periodic defers. (…Yes I’m not a frontend dev :) ) -[demo_async]: https://github.com/procrastinate-org/procrastinate/tree/main/procrastinate_demos/demo_async/ -[demo_django]: https://github.com/procrastinate-org/procrastinate/tree/main/procrastinate_demos/demo_django/ -[demo_sync]: https://github.com/procrastinate-org/procrastinate/tree/main/procrastinate_demos/demo_sync/ +[demo_async]: https://github.com/procrastinate-org/procrastinate/tree/main/procrastinate/demos/demo_async/ +[demo_django]: https://github.com/procrastinate-org/procrastinate/tree/main/procrastinate/demos/demo_django/ +[demo_sync]: https://github.com/procrastinate-org/procrastinate/tree/main/procrastinate/demos/demo_sync/ diff --git a/procrastinate_demos/__init__.py b/procrastinate/demos/__init__.py similarity index 100% rename from procrastinate_demos/__init__.py rename to procrastinate/demos/__init__.py diff --git a/procrastinate_demos/demo_async/__init__.py b/procrastinate/demos/demo_async/__init__.py similarity index 100% rename from procrastinate_demos/demo_async/__init__.py rename to procrastinate/demos/demo_async/__init__.py diff --git a/procrastinate_demos/demo_async/__main__.py b/procrastinate/demos/demo_async/__main__.py similarity index 100% rename from procrastinate_demos/demo_async/__main__.py rename to procrastinate/demos/demo_async/__main__.py diff --git a/procrastinate_demos/demo_async/app.py b/procrastinate/demos/demo_async/app.py similarity index 69% rename from procrastinate_demos/demo_async/app.py rename to procrastinate/demos/demo_async/app.py index 1c5cd26a1..1d834bbaf 100644 --- a/procrastinate_demos/demo_async/app.py +++ b/procrastinate/demos/demo_async/app.py @@ -4,5 +4,5 @@ app = procrastinate.App( connector=procrastinate.PsycopgConnector(), - import_paths=["procrastinate_demos.demo_async.tasks"], + import_paths=["procrastinate.demos.demo_async.tasks"], ) diff --git a/procrastinate_demos/demo_async/tasks.py b/procrastinate/demos/demo_async/tasks.py similarity index 100% rename from procrastinate_demos/demo_async/tasks.py rename to procrastinate/demos/demo_async/tasks.py diff --git a/procrastinate_demos/demo_django/__init__.py b/procrastinate/demos/demo_django/__init__.py similarity index 100% rename from procrastinate_demos/demo_django/__init__.py rename to procrastinate/demos/demo_django/__init__.py diff --git a/procrastinate_demos/demo_django/__main__.py b/procrastinate/demos/demo_django/__main__.py similarity index 100% rename from procrastinate_demos/demo_django/__main__.py rename to procrastinate/demos/demo_django/__main__.py diff --git a/procrastinate_demos/demo_django/demo/__init__.py b/procrastinate/demos/demo_django/demo/__init__.py similarity index 100% rename from procrastinate_demos/demo_django/demo/__init__.py rename to procrastinate/demos/demo_django/demo/__init__.py diff --git a/procrastinate_demos/demo_django/demo/admin.py b/procrastinate/demos/demo_django/demo/admin.py similarity index 100% rename from procrastinate_demos/demo_django/demo/admin.py rename to procrastinate/demos/demo_django/demo/admin.py diff --git a/procrastinate_demos/demo_django/demo/apps.py b/procrastinate/demos/demo_django/demo/apps.py similarity index 66% rename from procrastinate_demos/demo_django/demo/apps.py rename to procrastinate/demos/demo_django/demo/apps.py index 17e8b805f..a1d9fc474 100644 --- a/procrastinate_demos/demo_django/demo/apps.py +++ b/procrastinate/demos/demo_django/demo/apps.py @@ -4,4 +4,4 @@ class DemoConfig(AppConfig): - name = "procrastinate_demos.demo_django.demo" + name = "procrastinate.demos.demo_django.demo" diff --git a/procrastinate_demos/demo_django/demo/migrations/0001_initial.py b/procrastinate/demos/demo_django/demo/migrations/0001_initial.py similarity index 100% rename from procrastinate_demos/demo_django/demo/migrations/0001_initial.py rename to procrastinate/demos/demo_django/demo/migrations/0001_initial.py diff --git a/procrastinate_demos/demo_django/demo/migrations/__init__.py b/procrastinate/demos/demo_django/demo/migrations/__init__.py similarity index 100% rename from procrastinate_demos/demo_django/demo/migrations/__init__.py rename to procrastinate/demos/demo_django/demo/migrations/__init__.py diff --git a/procrastinate_demos/demo_django/demo/models.py b/procrastinate/demos/demo_django/demo/models.py similarity index 100% rename from procrastinate_demos/demo_django/demo/models.py rename to procrastinate/demos/demo_django/demo/models.py diff --git a/procrastinate_demos/demo_django/demo/tasks.py b/procrastinate/demos/demo_django/demo/tasks.py similarity index 100% rename from procrastinate_demos/demo_django/demo/tasks.py rename to procrastinate/demos/demo_django/demo/tasks.py diff --git a/procrastinate_demos/demo_django/demo/templates/demo/book_form.html b/procrastinate/demos/demo_django/demo/templates/demo/book_form.html similarity index 100% rename from procrastinate_demos/demo_django/demo/templates/demo/book_form.html rename to procrastinate/demos/demo_django/demo/templates/demo/book_form.html diff --git a/procrastinate_demos/demo_django/demo/templates/demo/book_list.html b/procrastinate/demos/demo_django/demo/templates/demo/book_list.html similarity index 100% rename from procrastinate_demos/demo_django/demo/templates/demo/book_list.html rename to procrastinate/demos/demo_django/demo/templates/demo/book_list.html diff --git a/procrastinate_demos/demo_django/demo/views.py b/procrastinate/demos/demo_django/demo/views.py similarity index 100% rename from procrastinate_demos/demo_django/demo/views.py rename to procrastinate/demos/demo_django/demo/views.py diff --git a/procrastinate_demos/demo_django/manage.py b/procrastinate/demos/demo_django/manage.py similarity index 91% rename from procrastinate_demos/demo_django/manage.py rename to procrastinate/demos/demo_django/manage.py index 4b4b417f6..793f2c7ed 100755 --- a/procrastinate_demos/demo_django/manage.py +++ b/procrastinate/demos/demo_django/manage.py @@ -10,7 +10,7 @@ def main(): """Run administrative tasks.""" os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", "procrastinate_demos.demo_django.project.settings" + "DJANGO_SETTINGS_MODULE", "procrastinate.demos.demo_django.project.settings" ) try: from django.core.management import execute_from_command_line diff --git a/procrastinate_demos/demo_django/project/__init__.py b/procrastinate/demos/demo_django/project/__init__.py similarity index 100% rename from procrastinate_demos/demo_django/project/__init__.py rename to procrastinate/demos/demo_django/project/__init__.py diff --git a/procrastinate_demos/demo_django/project/asgi.py b/procrastinate/demos/demo_django/project/asgi.py similarity index 86% rename from procrastinate_demos/demo_django/project/asgi.py rename to procrastinate/demos/demo_django/project/asgi.py index 2eba4f33b..aec69591f 100644 --- a/procrastinate_demos/demo_django/project/asgi.py +++ b/procrastinate/demos/demo_django/project/asgi.py @@ -14,7 +14,7 @@ from django.core.asgi import get_asgi_application os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", "procrastinate_demos.demo_django.project.settings" + "DJANGO_SETTINGS_MODULE", "procrastinate.demos.demo_django.project.settings" ) application = get_asgi_application() diff --git a/procrastinate_demos/demo_django/project/settings.py b/procrastinate/demos/demo_django/project/settings.py similarity index 94% rename from procrastinate_demos/demo_django/project/settings.py rename to procrastinate/demos/demo_django/project/settings.py index 6c04cb8e8..0483bbbac 100644 --- a/procrastinate_demos/demo_django/project/settings.py +++ b/procrastinate/demos/demo_django/project/settings.py @@ -40,7 +40,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "procrastinate_demos.demo_django.demo", + "procrastinate.demos.demo_django.demo", "procrastinate.contrib.django", ] @@ -54,7 +54,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "procrastinate_demos.demo_django.project.urls" +ROOT_URLCONF = "procrastinate.demos.demo_django.project.urls" TEMPLATES = [ { @@ -72,7 +72,7 @@ }, ] -WSGI_APPLICATION = "procrastinate_demos.demo_django.project.wsgi.application" +WSGI_APPLICATION = "procrastinate.demos.demo_django.project.wsgi.application" # Database @@ -165,7 +165,7 @@ def filter(self, record: logging.LogRecord): }, "filters": { "procrastinate": { - "()": "procrastinate_demos.demo_django.project.settings.ProcrastinateFilter", + "()": "procrastinate.demos.demo_django.project.settings.ProcrastinateFilter", "name": "procrastinate", }, }, @@ -178,4 +178,4 @@ def filter(self, record: logging.LogRecord): }, } -PROCRASTINATE_ON_APP_READY = "procrastinate_demos.demo_django.demo.tasks.on_app_ready" +PROCRASTINATE_ON_APP_READY = "procrastinate.demos.demo_django.demo.tasks.on_app_ready" diff --git a/procrastinate_demos/demo_django/project/urls.py b/procrastinate/demos/demo_django/project/urls.py similarity index 94% rename from procrastinate_demos/demo_django/project/urls.py rename to procrastinate/demos/demo_django/project/urls.py index 6307b94d0..9c4d8d941 100644 --- a/procrastinate_demos/demo_django/project/urls.py +++ b/procrastinate/demos/demo_django/project/urls.py @@ -21,7 +21,7 @@ from django.contrib.staticfiles import views from django.urls import path, re_path -from procrastinate_demos.demo_django.demo.views import CreateBookView, ListBooksView +from procrastinate.demos.demo_django.demo.views import CreateBookView, ListBooksView urlpatterns = [ path("admin/", admin.site.urls), diff --git a/procrastinate_demos/demo_django/project/wsgi.py b/procrastinate/demos/demo_django/project/wsgi.py similarity index 86% rename from procrastinate_demos/demo_django/project/wsgi.py rename to procrastinate/demos/demo_django/project/wsgi.py index 5aefbb683..2c352cdc8 100644 --- a/procrastinate_demos/demo_django/project/wsgi.py +++ b/procrastinate/demos/demo_django/project/wsgi.py @@ -14,7 +14,7 @@ from django.core.wsgi import get_wsgi_application os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", "procrastinate_demos.demo_django.project.settings" + "DJANGO_SETTINGS_MODULE", "procrastinate.demos.demo_django.project.settings" ) application = get_wsgi_application() diff --git a/procrastinate_demos/demo_sync/__init__.py b/procrastinate/demos/demo_sync/__init__.py similarity index 100% rename from procrastinate_demos/demo_sync/__init__.py rename to procrastinate/demos/demo_sync/__init__.py diff --git a/procrastinate_demos/demo_sync/__main__.py b/procrastinate/demos/demo_sync/__main__.py similarity index 100% rename from procrastinate_demos/demo_sync/__main__.py rename to procrastinate/demos/demo_sync/__main__.py diff --git a/procrastinate_demos/demo_sync/app.py b/procrastinate/demos/demo_sync/app.py similarity index 69% rename from procrastinate_demos/demo_sync/app.py rename to procrastinate/demos/demo_sync/app.py index 23aec368a..6d78a7128 100644 --- a/procrastinate_demos/demo_sync/app.py +++ b/procrastinate/demos/demo_sync/app.py @@ -4,5 +4,5 @@ app = procrastinate.App( connector=procrastinate.PsycopgConnector(), - import_paths=["procrastinate_demos.demo_sync.tasks"], + import_paths=["procrastinate.demos.demo_sync.tasks"], ) diff --git a/procrastinate_demos/demo_sync/tasks.py b/procrastinate/demos/demo_sync/tasks.py similarity index 100% rename from procrastinate_demos/demo_sync/tasks.py rename to procrastinate/demos/demo_sync/tasks.py diff --git a/tests/integration/contrib/sphinx/test-root/index.rst b/tests/integration/contrib/sphinx/test-root/index.rst index 0f287a494..db38bb360 100644 --- a/tests/integration/contrib/sphinx/test-root/index.rst +++ b/tests/integration/contrib/sphinx/test-root/index.rst @@ -1,5 +1,5 @@ Tasks ===== -.. automodule:: procrastinate_demos.demo_async.tasks +.. automodule:: procrastinate.demos.demo_async.tasks :members: From cdc6c53ccd0698caf6ae7af3bffe78bc3ebaf5a9 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:33:15 +0100 Subject: [PATCH 115/375] Exclude demos from build --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b95ccd2b4..0974cc9f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,12 @@ build-backend = "hatchling.build" [tool.hatch.version] source = "uv-dynamic-versioning" +[tool.hatch.build.targets.sdist] +exclude = ["procrastinate/demos"] + +[tool.hatch.build.targets.wheel] +exclude = ["procrastinate/demos"] + [project] name = "procrastinate" dynamic = ["version"] From 62309cc18f83aa15e71a1ff2b2a5136c008b5550 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:33:29 +0100 Subject: [PATCH 116/375] Fix docker compose raising warning --- docker-compose.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ed1a5f4bb..0f779fc78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '2.1' services: procrastinate: build: @@ -23,5 +22,5 @@ services: image: postgres:17 ports: ["5432:5432"] environment: - POSTGRES_DB: procrastinate - POSTGRES_PASSWORD: password + POSTGRES_DB: procrastinate + POSTGRES_PASSWORD: password From 0492addd9ce929ddb8cad292beafaaccc0b6ca7d Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:46:38 +0100 Subject: [PATCH 117/375] The option is called --group, not --with-group --- .github/workflows/ci.yml | 2 +- .readthedocs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae26ad4e7..ffa0558ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,7 +117,7 @@ jobs: python-version: "3.9" - name: Install dependencies - run: uv sync --all-extras --with-group=types + run: uv sync --all-extras --group=types - name: Activate virtualenv run: echo ".venv/bin" >> $GITHUB_PATH diff --git a/.readthedocs.yml b/.readthedocs.yml index 3064bdd4e..648126d74 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,7 +13,7 @@ build: post_create_environment: - python -m pip install uv post_install: - - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m uv sync --with-group docs + - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m uv sync --group docs sphinx: configuration: docs/conf.py From c180f88298322c3f7e14904d116ed42120125f76 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:48:04 +0100 Subject: [PATCH 118/375] Readthedocs needs all extras --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 648126d74..3a8fc3a0d 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,7 +13,7 @@ build: post_create_environment: - python -m pip install uv post_install: - - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m uv sync --group docs + - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH python -m uv sync --all-extras --group docs sphinx: configuration: docs/conf.py From 24c6515dc4157b7d2f6ee8485382829eb42cc4cd Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 00:57:30 +0100 Subject: [PATCH 119/375] pyright broken because pyright python issued a .post0 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffa0558ce..b650ed56a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: run: > yq '.repos | filter(.repo == "https://github.com/RobertCraigie/pyright-python").0.rev - | "pyright-version="+sub("^v", "")' + | "pyright-version="+sub("^v", "") | sub(".post\d+$"; "")' .pre-commit-config.yaml >> $GITHUB_OUTPUT - uses: jakebailey/pyright-action@v2 From 29f578fa567819f70dd94e21d39de2acd78fac09 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Wed, 22 Jan 2025 22:01:19 +0100 Subject: [PATCH 120/375] Improve CI --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b650ed56a..5a8b62668 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - "current_version_without_post_migration" - "stable_version_without_post_migration" - name: "acceptance-test" + name: "e2e ${{ matrix.mode }}" runs-on: ubuntu-latest services: @@ -139,6 +139,7 @@ jobs: runs-on: ubuntu-latest needs: - build + - acceptance - static-typing steps: - name: Report success From 488207ad483e3811db2c8296e109600c1a8e2470 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Fri, 24 Jan 2025 20:36:33 +0100 Subject: [PATCH 121/375] Fix incorrect async pattern in test_shell --- tests/unit/test_shell.py | 271 +++++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 140 deletions(-) diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index 4ba1c4e34..b57d9f60a 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -1,10 +1,8 @@ from __future__ import annotations -import asyncio - import pytest -from procrastinate import manager +from procrastinate import manager, utils from procrastinate import shell as shell_module from .. import conftest @@ -23,31 +21,28 @@ def test_EOF(shell): assert shell.do_EOF("") is True -def test_list_jobs(shell, connector, capsys): - asyncio.run( - connector.defer_job_one( - "task1", - 0, - "lock1", - "queueing_lock1", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue1", - ) +async def test_list_jobs(shell, connector, capsys): + await connector.defer_job_one( + "task1", + 0, + "lock1", + "queueing_lock1", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue1", ) - asyncio.run( - connector.defer_job_one( - "task2", - 0, - "lock2", - "queueing_lock2", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue2", - ) + + await connector.defer_job_one( + "task2", + 0, + "lock2", + "queueing_lock2", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue2", ) - shell.do_list_jobs("") + await utils.sync_to_async(shell.do_list_jobs, "") captured = capsys.readouterr() assert captured.out.splitlines() == [ "#1 task1 on queue1 - [todo]", @@ -68,31 +63,29 @@ def test_list_jobs(shell, connector, capsys): ] -def test_list_jobs_filters(shell, connector, capsys): - asyncio.run( - connector.defer_job_one( - "task1", - 0, - "lock1", - "queueing_lock1", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue1", - ) +async def test_list_jobs_filters(shell, connector, capsys): + await connector.defer_job_one( + "task1", + 0, + "lock1", + "queueing_lock1", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue1", ) - asyncio.run( - connector.defer_job_one( - "task2", - 0, - "lock2", - "queueing_lock2", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue2", - ) + await connector.defer_job_one( + "task2", + 0, + "lock2", + "queueing_lock2", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue2", ) - shell.do_list_jobs("id=2 queue=queue2 task=task2 lock=lock2 status=todo") + await utils.sync_to_async( + shell.do_list_jobs, "id=2 queue=queue2 task=task2 lock=lock2 status=todo" + ) captured = capsys.readouterr() assert captured.out.splitlines() == [ "#2 task2 on queue2 - [todo]", @@ -112,31 +105,27 @@ def test_list_jobs_filters(shell, connector, capsys): ] -def test_list_jobs_details(shell, connector, capsys): - asyncio.run( - connector.defer_job_one( - "task1", - 5, - "lock1", - "queueing_lock1", - {"x": 11}, - conftest.aware_datetime(1000, 1, 1), - "queue1", - ) +async def test_list_jobs_details(shell, connector, capsys): + await connector.defer_job_one( + "task1", + 5, + "lock1", + "queueing_lock1", + {"x": 11}, + conftest.aware_datetime(1000, 1, 1), + "queue1", ) - asyncio.run( - connector.defer_job_one( - "task2", - 7, - "lock2", - "queueing_lock2", - {"y": 22}, - conftest.aware_datetime(2000, 1, 1), - "queue2", - ) + await connector.defer_job_one( + "task2", + 7, + "lock2", + "queueing_lock2", + {"y": 22}, + conftest.aware_datetime(2000, 1, 1), + "queue2", ) - shell.do_list_jobs("details") + await utils.sync_to_async(shell.do_list_jobs, "details") captured = capsys.readouterr() assert captured.out.splitlines() == [ "#1 task1 on queue1 - [todo] (attempts=0, priority=5, scheduled_at=1000-01-01 " @@ -146,21 +135,21 @@ def test_list_jobs_details(shell, connector, capsys): ] -def test_list_jobs_empty(shell, connector, capsys): - shell.do_list_jobs("") +async def test_list_jobs_empty(shell, connector, capsys): + await utils.sync_to_async(shell.do_list_jobs, "") captured = capsys.readouterr() assert captured.out == "" -def test_list_queues(shell, connector, capsys): - asyncio.run( - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") +async def test_list_queues(shell, connector, capsys): + await connector.defer_job_one( + "task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1" ) - asyncio.run( - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + await connector.defer_job_one( + "task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2" ) - shell.do_list_queues("") + await utils.sync_to_async(shell.do_list_queues, "") captured = capsys.readouterr() assert captured.out.splitlines() == [ "queue1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", @@ -174,15 +163,17 @@ def test_list_queues(shell, connector, capsys): ] -def test_list_queues_filters(shell, connector, capsys): - asyncio.run( - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") +async def test_list_queues_filters(shell, connector, capsys): + await connector.defer_job_one( + "task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1" ) - asyncio.run( - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + await connector.defer_job_one( + "task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2" ) - shell.do_list_queues("queue=queue2 task=task2 lock=lock2 status=todo") + await utils.sync_to_async( + shell.do_list_queues, "queue=queue2 task=task2 lock=lock2 status=todo" + ) captured = capsys.readouterr() assert captured.out.splitlines() == [ "queue2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", @@ -200,21 +191,21 @@ def test_list_queues_filters(shell, connector, capsys): ] -def test_list_queues_empty(shell, connector, capsys): - shell.do_list_queues("") +async def test_list_queues_empty(shell, connector, capsys): + await utils.sync_to_async(shell.do_list_queues, "") captured = capsys.readouterr() assert captured.out == "" -def test_list_tasks(shell, connector, capsys): - asyncio.run( - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") +async def test_list_tasks(shell, connector, capsys): + await connector.defer_job_one( + "task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1" ) - asyncio.run( - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + await connector.defer_job_one( + "task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2" ) - shell.do_list_tasks("") + await utils.sync_to_async(shell.do_list_tasks, "") captured = capsys.readouterr() assert captured.out.splitlines() == [ "task1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", @@ -228,15 +219,17 @@ def test_list_tasks(shell, connector, capsys): ] -def test_list_tasks_filters(shell, connector, capsys): - asyncio.run( - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") +async def test_list_tasks_filters(shell, connector, capsys): + await connector.defer_job_one( + "task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1" ) - asyncio.run( - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + await connector.defer_job_one( + "task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2" ) - shell.do_list_tasks("queue=queue2 task=task2 lock=lock2 status=todo") + await utils.sync_to_async( + shell.do_list_tasks, "queue=queue2 task=task2 lock=lock2 status=todo" + ) captured = capsys.readouterr() assert captured.out.splitlines() == [ "task2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", @@ -254,21 +247,21 @@ def test_list_tasks_filters(shell, connector, capsys): ] -def test_list_tasks_empty(shell, connector, capsys): - shell.do_list_tasks("") +async def test_list_tasks_empty(shell, connector, capsys): + await utils.sync_to_async(shell.do_list_tasks, "") captured = capsys.readouterr() assert captured.out == "" -def test_list_locks(shell, connector, capsys): - asyncio.run( - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") +async def test_list_locks(shell, connector, capsys): + await connector.defer_job_one( + "task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1" ) - asyncio.run( - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + await connector.defer_job_one( + "task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2" ) - shell.do_list_locks("") + await utils.sync_to_async(shell.do_list_locks, "") captured = capsys.readouterr() assert captured.out.splitlines() == [ "lock1: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", @@ -282,15 +275,17 @@ def test_list_locks(shell, connector, capsys): ] -def test_list_locks_filters(shell, connector, capsys): - asyncio.run( - connector.defer_job_one("task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1") +async def test_list_locks_filters(shell, connector, capsys): + await connector.defer_job_one( + "task1", 0, "lock1", "queueing_lock1", {}, 0, "queue1" ) - asyncio.run( - connector.defer_job_one("task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2") + await connector.defer_job_one( + "task2", 0, "lock2", "queueing_lock2", {}, 0, "queue2" ) - shell.do_list_locks("queue=queue2 task=task2 lock=lock2 status=todo") + await utils.sync_to_async( + shell.do_list_locks, "queue=queue2 task=task2 lock=lock2 status=todo" + ) captured = capsys.readouterr() assert captured.out.splitlines() == [ "lock2: 1 jobs (todo: 1, doing: 0, succeeded: 0, failed: 0, cancelled: 0, aborted: 0)", @@ -308,52 +303,48 @@ def test_list_locks_filters(shell, connector, capsys): ] -def test_list_locks_empty(shell, connector, capsys): - shell.do_list_locks("") +async def test_list_locks_empty(shell, connector, capsys): + await utils.sync_to_async(shell.do_list_locks, "") captured = capsys.readouterr() assert captured.out == "" -def test_retry(shell, connector, capsys): - asyncio.run( - connector.defer_job_one( - "task", - 0, - "lock", - "queueing_lock", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue", - ) +async def test_retry(shell, connector, capsys): + await connector.defer_job_one( + "task", + 0, + "lock", + "queueing_lock", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue", ) - asyncio.run(connector.set_job_status_run(1, "failed")) + await connector.set_job_status_run(1, "failed") - shell.do_list_jobs("id=1") + await utils.sync_to_async(shell.do_list_jobs, "id=1") captured = capsys.readouterr() assert captured.out.strip() == "#1 task on queue - [failed]" - shell.do_retry("1") + await utils.sync_to_async(shell.do_retry, "1") captured = capsys.readouterr() assert captured.out.strip() == "#1 task on queue - [todo]" -def test_cancel(shell, connector, capsys): - asyncio.run( - connector.defer_job_one( - "task", - 0, - "lock", - "queueing_lock", - {}, - conftest.aware_datetime(2000, 1, 1), - "queue", - ) +async def test_cancel(shell, connector, capsys): + await connector.defer_job_one( + "task", + 0, + "lock", + "queueing_lock", + {}, + conftest.aware_datetime(2000, 1, 1), + "queue", ) - shell.do_list_jobs("id=1") + await utils.sync_to_async(shell.do_list_jobs, "id=1") captured = capsys.readouterr() assert captured.out.strip() == "#1 task on queue - [todo]" - shell.do_cancel("1") + await utils.sync_to_async(shell.do_cancel, "1") captured = capsys.readouterr() assert captured.out.strip() == "#1 task on queue - [cancelled]" From c10fc9f0b1af4bdc3e861c67bd0f0552b4f6bd96 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Fri, 24 Jan 2025 20:36:44 +0100 Subject: [PATCH 122/375] Fix bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index 4ade2e871..cc0789964 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -22,4 +22,4 @@ if ! which uv || ! which pre-commit || ! which nox; then fi pre-commit install -uv sync --all-extras +uv sync --all-extras --all-groups From d0470c2adeda3cf0875d48b1289b8e22617d3ee6 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Fri, 24 Jan 2025 21:51:28 +0100 Subject: [PATCH 123/375] Instanciating the worker means you need an async context, because of the event --- tests/unit/test_worker_sync.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_worker_sync.py b/tests/unit/test_worker_sync.py index 7a916fb50..48715162f 100644 --- a/tests/unit/test_worker_sync.py +++ b/tests/unit/test_worker_sync.py @@ -7,16 +7,16 @@ @pytest.fixture -def test_worker(app: App) -> worker.Worker: +async def test_worker(app: App) -> worker.Worker: return worker.Worker(app=app, queues=["yay"]) -def test_worker_find_task_missing(test_worker): +async def test_worker_find_task_missing(test_worker): with pytest.raises(exceptions.TaskNotFound): test_worker.find_task("foobarbaz") -def test_worker_find_task(app: App): +async def test_worker_find_task(app: App): test_worker = worker.Worker(app=app, queues=["yay"]) @app.task(name="foo") @@ -26,7 +26,7 @@ def task_func(): assert test_worker.find_task("foo") == task_func -def test_stop(test_worker, caplog): +async def test_stop(test_worker, caplog): caplog.set_level("INFO") test_worker.stop() From aecb6cb7e9ac23d8c2a49263c2168eb6c486e086 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Fri, 24 Jan 2025 23:04:51 +0100 Subject: [PATCH 124/375] Fix acceptance test when releasing --- noxfile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index d22959369..fe0320088 100644 --- a/noxfile.py +++ b/noxfile.py @@ -77,8 +77,12 @@ def stable_version_without_post_migration(session: nox.Session): shutil.copy(base_path / "poetry.lock", temp_path / "poetry.lock") session.run("poetry", "install", "--with", "test", "--no-root", external=True) - # Install latest procrastinate from PyPI - session.install("procrastinate") + # Install latest procrastinate from GitHub + # During a tag release, we have not yet published the new version to PyPI + # so we need to install it from GitHub + session.install( + f"procrastinate @ git+https://github.com/procrastinate-org/procrastinate.git@{latest_tag}" + ) session.run( "pytest", From dc8552d23401227238eca6e885d30281c378c9d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 15:31:31 +0000 Subject: [PATCH 125/375] Lock file maintenance --- uv.lock | 60 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/uv.lock b/uv.lock index 3dacf9edf..f5890df8f 100644 --- a/uv.lock +++ b/uv.lock @@ -69,11 +69,11 @@ wheels = [ [[package]] name = "attrs" -version = "24.3.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, ] [[package]] @@ -496,14 +496,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, + { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, ] [[package]] @@ -1207,27 +1207,27 @@ sdist = { url = "https://files.pythonhosted.org/packages/48/9c/6d8035cafa2d2d314 [[package]] name = "ruff" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, - { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, - { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, - { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, - { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, - { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, - { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, - { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, - { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, - { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, - { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, - { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, - { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, - { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, - { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7f/60fda2eec81f23f8aa7cbbfdf6ec2ca11eb11c273827933fb2541c2ce9d8/ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a", size = 3586740 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/77/4fb790596d5d52c87fd55b7160c557c400e90f6116a56d82d76e95d9374a/ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624", size = 11656815 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/3338ecb97573eafe74505f28431df3842c1933c5f8eae615427c1de32858/ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c", size = 11594821 }, + { url = "https://files.pythonhosted.org/packages/8e/89/320223c3421962762531a6b2dd58579b858ca9916fb2674874df5e97d628/ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4", size = 11040475 }, + { url = "https://files.pythonhosted.org/packages/b2/bd/1d775eac5e51409535804a3a888a9623e87a8f4b53e2491580858a083692/ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439", size = 11856207 }, + { url = "https://files.pythonhosted.org/packages/7f/c6/3e14e09be29587393d188454064a4aa85174910d16644051a80444e4fd88/ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5", size = 11420460 }, + { url = "https://files.pythonhosted.org/packages/ef/42/b7ca38ffd568ae9b128a2fa76353e9a9a3c80ef19746408d4ce99217ecc1/ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4", size = 12605472 }, + { url = "https://files.pythonhosted.org/packages/a6/a1/3167023f23e3530fde899497ccfe239e4523854cb874458ac082992d206c/ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1", size = 13243123 }, + { url = "https://files.pythonhosted.org/packages/d0/b4/3c600758e320f5bf7de16858502e849f4216cb0151f819fa0d1154874802/ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5", size = 12744650 }, + { url = "https://files.pythonhosted.org/packages/be/38/266fbcbb3d0088862c9bafa8b1b99486691d2945a90b9a7316336a0d9a1b/ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4", size = 14458585 }, + { url = "https://files.pythonhosted.org/packages/63/a6/47fd0e96990ee9b7a4abda62de26d291bd3f7647218d05b7d6d38af47c30/ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6", size = 12419624 }, + { url = "https://files.pythonhosted.org/packages/84/5d/de0b7652e09f7dda49e1a3825a164a65f4998175b6486603c7601279baad/ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730", size = 11843238 }, + { url = "https://files.pythonhosted.org/packages/9e/be/3f341ceb1c62b565ec1fb6fd2139cc40b60ae6eff4b6fb8f94b1bb37c7a9/ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2", size = 11484012 }, + { url = "https://files.pythonhosted.org/packages/a3/c8/ff8acbd33addc7e797e702cf00bfde352ab469723720c5607b964491d5cf/ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519", size = 12038494 }, + { url = "https://files.pythonhosted.org/packages/73/b1/8d9a2c0efbbabe848b55f877bc10c5001a37ab10aca13c711431673414e5/ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b", size = 12473639 }, + { url = "https://files.pythonhosted.org/packages/cb/44/a673647105b1ba6da9824a928634fe23186ab19f9d526d7bdf278cd27bc3/ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c", size = 9834353 }, + { url = "https://files.pythonhosted.org/packages/c3/01/65cadb59bf8d4fbe33d1a750103e6883d9ef302f60c28b73b773092fbde5/ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4", size = 10821444 }, + { url = "https://files.pythonhosted.org/packages/69/cb/b3fe58a136a27d981911cba2f18e4b29f15010623b79f0f2510fd0d31fd3/ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b", size = 10038168 }, ] [[package]] @@ -1612,11 +1612,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2024.2" +version = "2025.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, + { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, ] [[package]] From 67717322e064a1be297812cf9dc335a12f4906e5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 15:31:41 +0000 Subject: [PATCH 126/375] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bd40b558..e736df092 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - alabaster==1.0.0 ; python_full_version >= '3.10' - asgiref==3.8.1 - async-timeout==4.0.3 - - attrs==24.3.0 + - attrs==25.1.0 - babel==2.16.0 - certifi==2024.12.14 - charset-normalizer==3.4.1 @@ -59,7 +59,7 @@ repos: and platform_machine == 'x86_64') - idna==3.10 - imagesize==1.4.1 - - importlib-metadata==8.5.0 ; python_full_version < '3.10' + - importlib-metadata==8.6.1 ; python_full_version < '3.10' - jinja2==3.1.5 - markupsafe==3.0.2 - packaging==24.2 @@ -85,7 +85,7 @@ repos: - tomli==2.2.1 ; python_full_version < '3.11' - types-pyyaml==6.0.12.20241230 - typing-extensions==4.12.2 - - tzdata==2024.2 ; sys_platform == 'win32' + - tzdata==2025.1 ; sys_platform == 'win32' - urllib3==2.3.0 - zipp==3.21.0 ; python_full_version < '3.10' - repo: https://github.com/astral-sh/ruff-pre-commit From aad09624223bc1af0800caab34d7fae76d3769c4 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sat, 25 Jan 2025 16:59:39 +0100 Subject: [PATCH 127/375] Django-upgrade linter --- .pre-commit-config.yaml | 6 ++++++ procrastinate/contrib/django/__init__.py | 1 - tests/acceptance/django_settings.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e736df092..94b2ccc02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -99,6 +99,12 @@ repos: hooks: - id: doc8 + - repo: https://github.com/adamchainz/django-upgrade + rev: "1.22.2" + hooks: + - id: django-upgrade + args: [--target-version, "4.2"] # Replace with Django version + - repo: local hooks: - id: sync-pre-commit diff --git a/procrastinate/contrib/django/__init__.py b/procrastinate/contrib/django/__init__.py index 8eacf0c47..148cb7d1b 100644 --- a/procrastinate/contrib/django/__init__.py +++ b/procrastinate/contrib/django/__init__.py @@ -7,4 +7,3 @@ "app", "connector_params", ] -default_app_config = "procrastinate.contrib.django.apps.ProcrastinateConfig" diff --git a/tests/acceptance/django_settings.py b/tests/acceptance/django_settings.py index 97a4d761c..304451421 100644 --- a/tests/acceptance/django_settings.py +++ b/tests/acceptance/django_settings.py @@ -5,7 +5,7 @@ SECRET_KEY = "test" DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", "NAME": os.environ.get("PGDATABASE", "procrastinate"), "TEST": {"NAME": "procrastinate_django_test"}, }, From ad8f1dbdff74328b093805c13679e2b4f1b48505 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sat, 25 Jan 2025 17:30:56 +0100 Subject: [PATCH 128/375] Ignore annoying bug in Django causing unwanted warnings, fixed in 5.2. https://adamj.eu/tech/2025/01/08/django-silence-exception-ignored-outputwrapper/ --- pyproject.toml | 3 +++ tests/conftest.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0974cc9f2..ce8240846 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,8 +114,11 @@ testpaths = [ "tests/acceptance", "tests/migration", ] +# https://adamj.eu/tech/2025/01/08/django-silence-exception-ignored-outputwrapper/ +# https://code.djangoproject.com/ticket/36056 filterwarnings = """ error + ignore:.+django.core.management.base.OutputWrapper:pytest.PytestUnraisableExceptionWarning ignore:unclosed.+:ResourceWarning """ asyncio_mode = "auto" diff --git a/tests/conftest.py b/tests/conftest.py index 09de9d254..f361ea282 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,14 +8,17 @@ import random import signal as stdlib_signal import string +import sys import uuid from pathlib import Path +import django import packaging.version import psycopg import psycopg.conninfo import psycopg.sql import pytest +from django.core.management.base import OutputWrapper from procrastinate import app as app_module from procrastinate import blueprints, builtin_tasks, jobs, schema, testing @@ -31,6 +34,27 @@ # that conflicts with our own "app" fixture pytest_plugins = ["sphinx.testing.fixtures"] +# Silence “Exception ignored in ... OutputWrapper”: +# ValueError: I/O operation on closed file. +# https://adamj.eu/tech/2025/01/08/django-silence-exception-ignored-outputwrapper/ +# https://code.djangoproject.com/ticket/36056 +if django.VERSION < (5, 2): + orig_unraisablehook = sys.unraisablehook + + def unraisablehook(unraisable): + print("A" * 30, unraisable) + if ( + unraisable.exc_type is ValueError + and unraisable.exc_value is not None + and unraisable.exc_value.args == ("I/O operation on closed file.",) + and isinstance(unraisable.object, OutputWrapper) + ): + print("B" * 30, "ignored") + return + orig_unraisablehook(unraisable) + + sys.unraisablehook = unraisablehook + def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( From 96bb1859a8b6b2e9e4fcc3c4b941fe23dea75feb Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sat, 25 Jan 2025 17:31:38 +0100 Subject: [PATCH 129/375] Add py 3.13 --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f08631f7d..60f6f0e2e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/python:3.12 +FROM mcr.microsoft.com/devcontainers/python:3.13 USER root diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a8b62668..c657d2388 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" name: "py${{ matrix.python-version }}" runs-on: ubuntu-latest @@ -89,7 +90,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5 with: - python-version: "3.12" + python-version: "3.13" - name: Get latest tag id: get-latest-tag From 9312c891f20b89d5208415bbf543ac3c5c834044 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sat, 25 Jan 2025 17:31:58 +0100 Subject: [PATCH 130/375] README fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 23caabf58..3177259a6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ **Procrastinate is looking for** [additional maintainers!](https://github.com/procrastinate-org/procrastinate/discussions/748) -Procrastinate is an open-source Python 3.8+ distributed task processing +Procrastinate is an open-source Python 3.9+ distributed task processing library, leveraging PostgreSQL to store task definitions, manage locks and dispatch tasks. It can be used within both sync and async code, has [Django](howto/django/configuration) integration, and is easy to use with ASGI frameworks. @@ -92,7 +92,7 @@ to the How-To sections for specific features. The Discussion section should hopefully answer your questions. Otherwise, feel free to open an [issue](https://github.com/procrastinate-org/procrastinate/issues). -*Note to my future self: add a quick note here on why this project is named* +_Note to my future self: add a quick note here on why this project is named_ "[Procrastinate]" ;) .