diff --git a/mula/docs/architecture.md b/mula/docs/architecture.md index 81f7d9fd1ca..ba2e5f117f4 100644 --- a/mula/docs/architecture.md +++ b/mula/docs/architecture.md @@ -129,13 +129,20 @@ we will check the following: - check if the same task is already on the priority queue -Important to note is that when a `BoefjeTask` is created and pushed onto the -queue as a `PrioritizedItem` a new unique `TaskRun` is generated.[^1] This -ensures that each task has its own dedicated `TaskRun` throughout its entire -lifecycle. This approach maintains a distinct record for each task, providing -an accurate and independent history of task statuses. This means that each -execution of a `BoefjeTask`, regardless of whether it's the same task being -repeated in the future, is tracked independently with its own unique `TaskRun`. +> [!IMPORTANT] +> Important to note is that when a `PrioritizedItem` is created and pushed onto +> the queue. A `TaskRun` and `Schedule` is created for this item. Below we'll +> explain the explain the role of these two entities in more detail. + +##### `TaskRun` + +When a `BoefjeTask` is created and pushed onto the queue as a `PrioritizedItem` +a new unique `TaskRun` is generated.[^1] This ensures that each task has its +own dedicated `TaskRun` throughout its entire lifecycle. This approach +maintains a distinct record for each task, providing an accurate and +independent history of task statuses. This means that each execution of a +`BoefjeTask`, regardless of whether it's the same task being repeated in the +future, is tracked independently with its own unique `TaskRun`. This approach ensures that the historical record of each task's execution is distinct, providing a clear and isolated view of each instance of the task's @@ -169,6 +176,27 @@ keep track of the status of this task throughout the system we update its `TaskRun` status by either setting the status to `COMPLETED`, `FAILED` or `CANCELLED`. (5) +##### `Schedule` + +Since a task within the KAT implementation of the scheduler, can generate +findings at a specific moment in time. We want to account for additional +findings or changes for the same task at a later moment in time. Meaning we +want to be able to reschedule particular tasks. + +In order to support this, every task that is executed by the +`BoefjesScheduler` a `Schedule` is created. This `Schedule` contains +the necessary information and the specific task in order to reschedule a task +at a later moment in time. + +![diagram006](./img/diagram006.svg) + +A `Schedule` supports a cron-like expression as schedule, which makes it +possible to schedule tasks at certain intervals. When such an expression isn't +set, the task will be scheduled at a future calculated date (deadline ranker +calculation). + +To see how task will be rescheduled, refer to the 'Processes' section. + #### Processes In order to create a `BoefjeTask` and trigger the dataflow we described above @@ -180,12 +208,25 @@ tasks. Namely: 3. rescheduling of prior tasks 4. manual scan job -![diagram006](./img/diagram006.svg) +![diagram007](./img/diagram007.svg) -##### 1. Scan profile mutations +#### Processes + +In order to create a `BoefjeTask` and trigger the dataflow we described above +we have 4 different processes within a `BoefjeScheduler` that can create boefje +tasks. Namely: + +1. scan profile mutations +2. enabling of boefjes +3. rescheduling of prior tasks +4. manual scan job ![diagram007](./img/diagram007.svg) +##### 1. Scan profile mutations + +![diagram008](./img/diagram008.svg) + When a scan level is increased on an OOI (`schedulers.boefje.push_tasks_for_scan_profile_mutations`) a message is pushed on the RabbitMQ `{organization_id}__scan_profile_mutations` queue. The scheduler @@ -212,7 +253,7 @@ The dataflow is as follows: ##### 2. Enabling of boefjes -![diagram008](./img/diagram008.svg) +![diagram009](./img/diagram009.svg) When a plugin of type `boefje` is enabled or disabled in Rocky. The dataflow is triggered when the plugin cache of an organisation is flushed. @@ -236,7 +277,7 @@ The dataflow is as follows: ##### 3. Rescheduling of prior tasks -![diagram009](./img/diagram009.svg) +![diagram010](./img/diagram010.svg) In order to re-run tasks that have been executed in the past we try to create new tasks on ooi's. We continuously get a batch of random ooi's from octopoes @@ -266,7 +307,7 @@ The dataflow is as follows: ##### 4. Manual scan job -![diagram010](./img/diagram010.svg) +![diagram011](./img/diagram011.svg) Scan jobs created by the user in Rocky (`server.push_queue`), will get the highest priority of 1. Note, that this will circumvent all the checks @@ -477,25 +518,39 @@ classDiagram ```mermaid erDiagram - items { - uuid id PK - character_varying scheduler_id - character_varying hash - integer priority - jsonb data - timestamp_with_time_zone created_at - timestamp_with_time_zone modified_at - } - - tasks { - uuid id PK - character_varying scheduler_id - taskstatus status - timestamp_with_time_zone created_at - timestamp_with_time_zone modified_at - jsonb p_item - character_varying type - } +schedules { + uuid id PK + character_varying scheduler_id + boolean enabled + jsonb p_item + character_varying cron_expression + timestamp_with_time_zone deadline_at + timestamp_with_time_zone evaluated_at + timestamp_with_time_zone created_at + timestamp_with_time_zone modified_at +} +task_runs { + uuid id PK + uuid job_id FK + character_varying scheduler_id + taskstatus status + timestamp_with_time_zone created_at + timestamp_with_time_zone modified_at + jsonb p_item + character_varying type +} +items { + uuid id PK + character_varying scheduler_id + character_varying hash + integer priority + jsonb data + timestamp_with_time_zone created_at + timestamp_with_time_zone modified_at +} + + +tasks }o--|| jobs: "" ``` ## Project structure diff --git a/mula/docs/img/diagram006.svg b/mula/docs/img/diagram006.svg index acfb4572e4c..7fc32b5bf64 100644 --- a/mula/docs/img/diagram006.svg +++ b/mula/docs/img/diagram006.svg @@ -1,4 +1,4 @@ -diagram006Priority Queue BoefjeSchedulerp_itempushp_item Task Runnerpopp_itemp_itemp_itemBoefjeTaskscan profile mutationsenabled boefjesrescheduling123manual scan job4 +diagram006Priority Queue BoefjeSchedulerp_itempushp_item Task Runnerpopp_itemp_itemp_itemBoefjeTaskScheduleidscheduler_idscheduleenableddeadline_atp_item1boefje_org_10 12 * * 123post_push diff --git a/mula/docs/img/diagram010.svg b/mula/docs/img/diagram010.svg index 917e3ca172a..5e2b6da2fb7 100644 --- a/mula/docs/img/diagram010.svg +++ b/mula/docs/img/diagram010.svg @@ -1,4 +1,4 @@ -diagram010Priority Queuep_itempushp_item Task Runnerpopp_itemp_itemp_itemBoefjeTask BoefjeSchedulermanual scan jobPOST Rocky [external software system]4 +diagram010p_itempushp_item Task Runnerpopp_itemp_itemp_itemBoefjeTask BoefjeSchedulerreschedulingTaskScheduleidscheduler_idscheduleenableddeadline_atp_item1boefje_org_10 12 * * 123jobs where deadline >= now3 diff --git a/mula/docs/img/diagram011.svg b/mula/docs/img/diagram011.svg new file mode 100644 index 00000000000..7799d3a460f --- /dev/null +++ b/mula/docs/img/diagram011.svg @@ -0,0 +1,4 @@ + + + +diagram011Priority Queuep_itempushp_item Task Runnerpopp_itemp_itemp_itemBoefjeTask BoefjeSchedulermanual scan jobPOST Rocky [external software system]4 diff --git a/mula/docs/img/schematic-drawing.svg b/mula/docs/img/schematic-drawing.svg index 027054eba25..e346820fa95 100644 --- a/mula/docs/img/schematic-drawing.svg +++ b/mula/docs/img/schematic-drawing.svg @@ -1,4 +1,4 @@ - Scheduler App [software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky... Octopoes [graph database]Octopoes... RabbitMQ [message broker]RabbitMQ... Katalogus [software system]Katalogus... Bytes [software system]Bytes...PostgreSQLPostgreSQLdiagram001diagram001 diagram002diagram002 Schedulers Schedulers Server ServerPriority QueuePriority Queue Scheduler SchedulerScheduler App[software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky...TaskStore[table]TaskStore...PQStore[table]PQStore...diagram010diagram010 Priority QueuePriority Queuep_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulermanual scan jobmanual scan jobPOSTPOST  Rocky [external software system]Rocky...44diagram003diagram003 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itemPushPushp_itemp_item Task Runner Task RunnerPopPopp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskScheduler AppScheduler Appdiagram004diagram004 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itempushpushp_itemp_item Task Runner Task Runnerpoppopp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskBoefjeTaskBoefjeTask PrioritizedItem PrioritizedItemdiagram005diagram005 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppopp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskTaskRun (Task List)ididscheduler_idscheduler_idtypetypestatusstatuscreated_atcreated_atmodified_atmodified_atp_itemp_item11boefje_org_1boefje_org_1boefjeboefjeCOMPLETEDCOMPLETED2024-01-012024-01-01abcd-0123abcd-012322boefje_org_1boefje_org_1boefjeboefjeRUNNINGRUNNING2024-01-052024-01-05abcd-0123abcd-012333post_pushpost_push updateupdate11112222333344445555status: PENDINGstatus: PENDINGstatus: QUEUEDstatus: QUEUEDstatus: DISPATCHEDstatus: DISPATCHEDstatus: RUNNINGstatus: RUNNINGstatus: COMPLETED / FAILED / CANCELLEDstatus: COMPLETED / FAILED / CANCELLEDdiagram006diagram006 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskscan profile mutationsscan profile mutationsenabled boefjesenabled boefjesreschedulingrescheduling112233manual scan jobmanual scan job44diagram007diagram007 Priority QueuePriority Queuep_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerscan profile mutationsscan profile mutationssubscribesubscribe RabbitMQ [message broker]RabbitMQ...11diagram008diagram008 Priority QueuePriority Queuep_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerenabled boefjesenabled boefjescachedcached Katalogus [external software system]Katalogus...22diagram009diagram009 Priority QueuePriority Queuep_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerreschedulingreschedulingGETGET Octopoes [external software system]Octopoes...33Text is not SVG - cannot display + Scheduler App [software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky... Octopoes [graph database]Octopoes... RabbitMQ [message broker]RabbitMQ... Katalogus [software system]Katalogus... Bytes [software system]Bytes...PostgreSQLPostgreSQLdiagram001diagram001 diagram002diagram002 Schedulers Schedulers Server ServerPriority QueuePriority Queue Scheduler SchedulerScheduler App[software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky...TaskStore[table]TaskStore...PQStore[table]PQStore...diagram011diagram011 Priority QueuePriority Queuep_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulermanual scan jobmanual scan jobPOSTPOST  Rocky [external software system]Rocky...44diagram003diagram003 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itemPushPushp_itemp_item Task Runner Task RunnerPopPopp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskScheduler AppScheduler Appdiagram004diagram004 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itempushpushp_itemp_item Task Runner Task Runnerpoppopp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskBoefjeTaskBoefjeTask PrioritizedItem PrioritizedItemdiagram005diagram005 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppopp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskTaskRun (Task List)ididscheduler_idscheduler_idtypetypestatusstatuscreated_atcreated_atmodified_atmodified_atp_itemp_item11boefje_org_1boefje_org_1boefjeboefjeCOMPLETEDCOMPLETED2024-01-012024-01-01abcd-0123abcd-012322boefje_org_1boefje_org_1boefjeboefjeRUNNINGRUNNING2024-01-052024-01-05abcd-0123abcd-012333post_pushpost_push updateupdate11112222333344445555status: PENDINGstatus: PENDINGstatus: QUEUEDstatus: QUEUEDstatus: DISPATCHEDstatus: DISPATCHEDstatus: RUNNINGstatus: RUNNINGstatus: COMPLETED / FAILED / CANCELLEDstatus: COMPLETED / FAILED / CANCELLEDdiagram007diagram007 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskscan profile mutationsscan profile mutationsenabled boefjesenabled boefjesreschedulingrescheduling112233manual scan jobmanual scan job44diagram008diagram008 Priority QueuePriority Queuep_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerscan profile mutationsscan profile mutationssubscribesubscribe RabbitMQ [message broker]RabbitMQ...11diagram009diagram009 Priority QueuePriority Queuep_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerenabled boefjesenabled boefjescachedcached Katalogus [external software system]Katalogus...22diagram006diagram006 Priority QueuePriority Queue BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppopp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskScheduleididscheduler_idscheduler_idschedulescheduleenabledenableddeadline_atdeadline_atp_itemp_item11boefje_org_1boefje_org_10 12 * * 10 12 * * 12233post_pushpost_push diagram010diagram010 p_itemp_itempushpush p_itemp_item Task Runner Task Runnerpoppop p_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerreschedulingreschedulingTaskScheduleididscheduler_idscheduler_idschedulescheduleenabledenableddeadline_atdeadline_atp_itemp_item11boefje_org_1boefje_org_10 12 * * 10 12 * * 12233jobs where deadline >= nowjobs where deadline >= now33Text is not SVG - cannot display diff --git a/mula/docs/schematic.svg b/mula/docs/schematic.svg new file mode 100644 index 00000000000..8d8b6cc8375 --- /dev/null +++ b/mula/docs/schematic.svg @@ -0,0 +1,4 @@ + + + +Priority QueuePriority Queuepoppop BoefjeScheduler BoefjeSchedulerp_itemp_itempushpushp_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskPriority QueuePriority Queuepoppop  BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskscan profile mutationsscan profile mutationsenabled boefjesenabled boefjesreschedulingreschedulingPriority QueuePriority QueuePopPop BoefjeScheduler BoefjeSchedulerp_itemp_itemPushPushp_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskScheduler AppScheduler AppPriority QueuePriority QueuePopPop BoefjeScheduler BoefjeSchedulerp_itemp_itemPushPushp_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskPriority QueuePriority Queuepoppoppost_pushpost_push  BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_itemupdateupdate Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskTaskRun (Scan History)ididscheduler_idscheduler_idtypetypestatusstatuscreated_atcreated_atmodified_atmodified_atp_itemp_item11boefje_org_1boefje_org_1boefjeboefjeCOMPLETEDCOMPLETED2024-01-012024-01-01abcd-0123abcd-012322boefje_org_1boefje_org_1boefjeboefjeRUNNINGRUNNING2024-01-052024-01-05abcd-0123abcd-01233311112222333344445555status: PENDINGstatus: PENDINGstatus: QUEUEDstatus: QUEUEDstatus: DISPATCHEDstatus: DISPATCHEDstatus: RUNNINGstatus: RUNNINGstatus: COMPLETED / FAILED / CANCELLEDstatus: COMPLETED / FAILED / CANCELLEDBoefjeTaskBoefjeTask PrioritizedItem PrioritizedItem Schedulers Schedulers Server ServerPriority QueuePriority Queue BoefjeScheduler BoefjeSchedulerScheduler App[software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky... Scheduler App [software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky... Octopoes [graph database]Octopoes... RabbitMQ [message broker]RabbitMQ... Katalogus [software system]Katalogus... Bytes [software system]Bytes...Priority QueuePriority Queuepoppop p_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerscan profile mutationsscan profile mutationssubscribesubscribe RabbitMQ [message broker]RabbitMQ...Priority QueuePriority Queuepoppop p_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerenabled boefjesenabled boefjescachedcached Katalogus [external software system]Katalogus...Priority QueuePriority Queuepoppop p_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerreschedulingreschedulingGETGET Octopoes [external software system]Octopoes...112233112233poppop p_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerreschedulingreschedulingTaskStore[table]TaskStore...PQStore[table]PQStore...PostgreSQLPostgreSQLPriority QueuePriority Queuepoppoppost_pushpost_push  BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskTaskScheduleididscheduler_idscheduler_idschedulescheduleenabledenableddeadline_atdeadline_atp_itemp_item11boefje_org_1boefje_org_10 12 * * 10 12 * * 12233TaskScheduleididscheduler_idscheduler_idschedulescheduleenabledenableddeadline_atdeadline_atp_itemp_item11boefje_org_1boefje_org_10 12 * * 10 12 * * 12233jobs where deadline >= nowjobs where deadline >= nowCurrentCurrentFutureFuturePriority QueuePriority Queuepoppop BoefjeScheduler BoefjeSchedulerp_itemp_itempushpushp_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskPriority QueuePriority Queuepoppop  BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskscan profile mutationsscan profile mutationsenabled boefjesenabled boefjesreschedulingreschedulingPriority QueuePriority QueuePopPop BoefjeScheduler BoefjeSchedulerp_itemp_itemPushPushp_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskScheduler AppScheduler AppPriority QueuePriority QueuePopPop BoefjeScheduler BoefjeSchedulerp_itemp_itemPushPushp_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskPriority QueuePriority Queuepoppoppost_pushpost_push  BoefjeScheduler BoefjeSchedulerp_itemp_itempushpush p_itemp_itemupdateupdate Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTaskTaskRun (Scan History)ididscheduler_idscheduler_idtypetypestatusstatuscreated_atcreated_atmodified_atmodified_atp_itemp_item11boefje_org_1boefje_org_1boefjeboefjeCOMPLETEDCOMPLETED2024-01-012024-01-01abcd-0123abcd-012322boefje_org_1boefje_org_1boefjeboefjeRUNNINGRUNNING2024-01-052024-01-05abcd-0123abcd-01233311112222333344445555status: PENDINGstatus: PENDINGstatus: QUEUEDstatus: QUEUEDstatus: DISPATCHEDstatus: DISPATCHEDstatus: RUNNINGstatus: RUNNINGstatus: COMPLETED / FAILED / CANCELLEDstatus: COMPLETED / FAILED / CANCELLEDBoefjeTaskBoefjeTask PrioritizedItem PrioritizedItem Schedulers Schedulers Server ServerPriority QueuePriority Queue BoefjeScheduler BoefjeSchedulerScheduler App[software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky... Scheduler App [software system]Scheduler App... Task Runner [software system]Task Runner... Rocky [software system]Rocky... Octopoes [graph database]Octopoes... RabbitMQ [message broker]RabbitMQ... Katalogus [software system]Katalogus... Bytes [software system]Bytes...Priority QueuePriority Queuepoppop p_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerscan profile mutationsscan profile mutationssubscribesubscribe RabbitMQ [message broker]RabbitMQ...Priority QueuePriority Queuepoppop p_itemp_itempushpush p_itemp_item Task Runner Task Runnerp_itemp_itemp_itemp_itemp_itemp_itemBoefjeTaskBoefjeTask BoefjeScheduler BoefjeSchedulerenabled boefjesenabled boefjescachedcached Katalogus [external software system]Katalogus...1122331122TaskStore[table]TaskStore...PQStore[table]PQStore...PostgreSQLPostgreSQLText is not SVG - cannot display diff --git a/mula/poetry.lock b/mula/poetry.lock index 8ccfc6a51db..e6705c51a72 100644 --- a/mula/poetry.lock +++ b/mula/poetry.lock @@ -217,63 +217,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.2" +version = "7.4.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf54c3e089179d9d23900e3efc86d46e4431188d9a657f345410eecdd0151f50"}, - {file = "coverage-7.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe6e43c8b510719b48af7db9631b5fbac910ade4bd90e6378c85ac5ac706382c"}, - {file = "coverage-7.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b98c89db1b150d851a7840142d60d01d07677a18f0f46836e691c38134ed18b"}, - {file = "coverage-7.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5f9683be6a5b19cd776ee4e2f2ffb411424819c69afab6b2db3a0a364ec6642"}, - {file = "coverage-7.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cdcbf7b9cb83fe047ee09298e25b1cd1636824067166dc97ad0543b079d22f"}, - {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2599972b21911111114100d362aea9e70a88b258400672626efa2b9e2179609c"}, - {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ef00d31b7569ed3cb2036f26565f1984b9fc08541731ce01012b02a4c238bf03"}, - {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:20a875bfd8c282985c4720c32aa05056f77a68e6d8bbc5fe8632c5860ee0b49b"}, - {file = "coverage-7.4.2-cp310-cp310-win32.whl", hash = "sha256:b3f2b1eb229f23c82898eedfc3296137cf1f16bb145ceab3edfd17cbde273fb7"}, - {file = "coverage-7.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7df95fdd1432a5d2675ce630fef5f239939e2b3610fe2f2b5bf21fa505256fa3"}, - {file = "coverage-7.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8ddbd158e069dded57738ea69b9744525181e99974c899b39f75b2b29a624e2"}, - {file = "coverage-7.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81a5fb41b0d24447a47543b749adc34d45a2cf77b48ca74e5bf3de60a7bd9edc"}, - {file = "coverage-7.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2412e98e70f16243be41d20836abd5f3f32edef07cbf8f407f1b6e1ceae783ac"}, - {file = "coverage-7.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb79414c15c6f03f56cc68fa06994f047cf20207c31b5dad3f6bab54a0f66ef"}, - {file = "coverage-7.4.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf89ab85027427d351f1de918aff4b43f4eb5f33aff6835ed30322a86ac29c9e"}, - {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a178b7b1ac0f1530bb28d2e51f88c0bab3e5949835851a60dda80bff6052510c"}, - {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:06fe398145a2e91edaf1ab4eee66149c6776c6b25b136f4a86fcbbb09512fd10"}, - {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:18cac867950943fe93d6cd56a67eb7dcd2d4a781a40f4c1e25d6f1ed98721a55"}, - {file = "coverage-7.4.2-cp311-cp311-win32.whl", hash = "sha256:f72cdd2586f9a769570d4b5714a3837b3a59a53b096bb954f1811f6a0afad305"}, - {file = "coverage-7.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:d779a48fac416387dd5673fc5b2d6bd903ed903faaa3247dc1865c65eaa5a93e"}, - {file = "coverage-7.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:adbdfcda2469d188d79771d5696dc54fab98a16d2ef7e0875013b5f56a251047"}, - {file = "coverage-7.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac4bab32f396b03ebecfcf2971668da9275b3bb5f81b3b6ba96622f4ef3f6e17"}, - {file = "coverage-7.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:006d220ba2e1a45f1de083d5022d4955abb0aedd78904cd5a779b955b019ec73"}, - {file = "coverage-7.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3733545eb294e5ad274abe131d1e7e7de4ba17a144505c12feca48803fea5f64"}, - {file = "coverage-7.4.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42a9e754aa250fe61f0f99986399cec086d7e7a01dd82fd863a20af34cbce962"}, - {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2ed37e16cf35c8d6e0b430254574b8edd242a367a1b1531bd1adc99c6a5e00fe"}, - {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b953275d4edfab6cc0ed7139fa773dfb89e81fee1569a932f6020ce7c6da0e8f"}, - {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32b4ab7e6c924f945cbae5392832e93e4ceb81483fd6dc4aa8fb1a97b9d3e0e1"}, - {file = "coverage-7.4.2-cp312-cp312-win32.whl", hash = "sha256:f5df76c58977bc35a49515b2fbba84a1d952ff0ec784a4070334dfbec28a2def"}, - {file = "coverage-7.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:34423abbaad70fea9d0164add189eabaea679068ebdf693baa5c02d03e7db244"}, - {file = "coverage-7.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b11f9c6587668e495cc7365f85c93bed34c3a81f9f08b0920b87a89acc13469"}, - {file = "coverage-7.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:51593a1f05c39332f623d64d910445fdec3d2ac2d96b37ce7f331882d5678ddf"}, - {file = "coverage-7.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69f1665165ba2fe7614e2f0c1aed71e14d83510bf67e2ee13df467d1c08bf1e8"}, - {file = "coverage-7.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3c8bbb95a699c80a167478478efe5e09ad31680931ec280bf2087905e3b95ec"}, - {file = "coverage-7.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:175f56572f25e1e1201d2b3e07b71ca4d201bf0b9cb8fad3f1dfae6a4188de86"}, - {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8562ca91e8c40864942615b1d0b12289d3e745e6b2da901d133f52f2d510a1e3"}, - {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d9a1ef0f173e1a19738f154fb3644f90d0ada56fe6c9b422f992b04266c55d5a"}, - {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f40ac873045db4fd98a6f40387d242bde2708a3f8167bd967ccd43ad46394ba2"}, - {file = "coverage-7.4.2-cp38-cp38-win32.whl", hash = "sha256:d1b750a8409bec61caa7824bfd64a8074b6d2d420433f64c161a8335796c7c6b"}, - {file = "coverage-7.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b4ae777bebaed89e3a7e80c4a03fac434a98a8abb5251b2a957d38fe3fd30088"}, - {file = "coverage-7.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ff7f92ae5a456101ca8f48387fd3c56eb96353588e686286f50633a611afc95"}, - {file = "coverage-7.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:861d75402269ffda0b33af94694b8e0703563116b04c681b1832903fac8fd647"}, - {file = "coverage-7.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3507427d83fa961cbd73f11140f4a5ce84208d31756f7238d6257b2d3d868405"}, - {file = "coverage-7.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf711d517e21fb5bc429f5c4308fbc430a8585ff2a43e88540264ae87871e36a"}, - {file = "coverage-7.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c00e54f0bd258ab25e7f731ca1d5144b0bf7bec0051abccd2bdcff65fa3262c9"}, - {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f8e845d894e39fb53834da826078f6dc1a933b32b1478cf437007367efaf6f6a"}, - {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:840456cb1067dc350af9080298c7c2cfdddcedc1cb1e0b30dceecdaf7be1a2d3"}, - {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c11ca2df2206a4e3e4c4567f52594637392ed05d7c7fb73b4ea1c658ba560265"}, - {file = "coverage-7.4.2-cp39-cp39-win32.whl", hash = "sha256:3ff5bdb08d8938d336ce4088ca1a1e4b6c8cd3bef8bb3a4c0eb2f37406e49643"}, - {file = "coverage-7.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:ac9e95cefcf044c98d4e2c829cd0669918585755dd9a92e28a1a7012322d0a95"}, - {file = "coverage-7.4.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:f593a4a90118d99014517c2679e04a4ef5aee2d81aa05c26c734d271065efcb6"}, - {file = "coverage-7.4.2.tar.gz", hash = "sha256:1a5ee18e3a8d766075ce9314ed1cb695414bae67df6a4b0805f5137d93d6f1cb"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, + {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, + {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, + {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, + {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, + {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, + {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, + {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, + {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, + {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, + {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, + {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, + {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, ] [package.dependencies] @@ -282,6 +282,21 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "croniter" +version = "2.0.2" +description = "croniter provides iteration for datetime object with cron like format" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "croniter-2.0.2-py2.py3-none-any.whl", hash = "sha256:78bf110a2c7dbbfdd98b926318ae6c64a731a4c637c7befe3685755110834746"}, + {file = "croniter-2.0.2.tar.gz", hash = "sha256:8bff16c9af4ef1fb6f05416973b8f7cb54997c02f2f8365251f9bf1dded91866"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = ">2021.1" + [[package]] name = "decorator" version = "5.1.1" @@ -344,13 +359,13 @@ doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] [[package]] name = "faker" -version = "23.2.1" +version = "24.0.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-23.2.1-py3-none-any.whl", hash = "sha256:0520a6b97e07c658b2798d7140971c1d5bc4bcd5013e7937fe075fd054aa320c"}, - {file = "Faker-23.2.1.tar.gz", hash = "sha256:f07b64d27f67b62c7f0536a72f47813015b3b51cd4664918454011094321e464"}, + {file = "Faker-24.0.0-py3-none-any.whl", hash = "sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1"}, + {file = "Faker-24.0.0.tar.gz", hash = "sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc"}, ] [package.dependencies] @@ -1161,18 +1176,18 @@ files = [ [[package]] name = "pydantic" -version = "2.6.1" +version = "2.6.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, - {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.2" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -1180,90 +1195,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.2" +version = "2.16.3" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, - {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, - {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, - {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, - {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, - {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, - {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, - {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, - {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, - {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, - {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, - {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, - {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, - {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] @@ -1290,13 +1305,13 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pytest" -version = "8.0.1" +version = "8.0.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, - {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, ] [package.dependencies] @@ -1330,13 +1345,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "python-dateutil" -version = "2.8.2" +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" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {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] @@ -1356,6 +1371,17 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "requests" version = "2.31.0" @@ -1392,19 +1418,19 @@ decorator = ">=3.4.2" [[package]] name = "setuptools" -version = "69.1.0" +version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, - {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1419,71 +1445,71 @@ files = [ [[package]] name = "sniffio" -version = "1.3.0" +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.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "sqlalchemy" -version = "2.0.27" +version = "2.0.28" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-win32.whl", hash = "sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-win_amd64.whl", hash = "sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-win32.whl", hash = "sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-win_amd64.whl", hash = "sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-win32.whl", hash = "sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-win_amd64.whl", hash = "sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-win32.whl", hash = "sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-win_amd64.whl", hash = "sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-win32.whl", hash = "sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-win_amd64.whl", hash = "sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-win32.whl", hash = "sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-win_amd64.whl", hash = "sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620"}, - {file = "SQLAlchemy-2.0.27-py3-none-any.whl", hash = "sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac"}, - {file = "SQLAlchemy-2.0.27.tar.gz", hash = "sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-win32.whl", hash = "sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-win_amd64.whl", hash = "sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-win32.whl", hash = "sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-win_amd64.whl", hash = "sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-win32.whl", hash = "sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-win_amd64.whl", hash = "sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-win32.whl", hash = "sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-win32.whl", hash = "sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-win_amd64.whl", hash = "sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-win32.whl", hash = "sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-win_amd64.whl", hash = "sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b"}, + {file = "SQLAlchemy-2.0.28-py3-none-any.whl", hash = "sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986"}, + {file = "SQLAlchemy-2.0.28.tar.gz", hash = "sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6"}, ] [package.dependencies] @@ -1560,15 +1586,26 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-croniter" +version = "2.0.0.20240106" +description = "Typing stubs for croniter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-croniter-2.0.0.20240106.tar.gz", hash = "sha256:a5c92566d750e025ab31279029ab44b479e2e3509cd8db3784574bdab1012571"}, + {file = "types_croniter-2.0.0.20240106-py3-none-any.whl", hash = "sha256:266d9ecabbc06afab7cc0cfa7f2149eb36f613ed66ddd6c9bac4edcf727e9a58"}, +] + [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] @@ -1704,4 +1741,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "b789fb2f87e8bef62c488a8d453f68bfe453b7e0814acf8c6352802467920591" +content-hash = "45cc2079326fee4ad91563db781442d65eb7bdb6055f60baef6943b290ea8a41" diff --git a/mula/pyproject.toml b/mula/pyproject.toml index 828df8518ec..7276d5038bc 100644 --- a/mula/pyproject.toml +++ b/mula/pyproject.toml @@ -8,6 +8,7 @@ license = "EUPL" [tool.poetry.dependencies] python = "^3.10" alembic = "^1.12.1" +croniter = "^2.0.1" fastapi = "^0.109.1" mmh3 = "^4.0.1" pika = "^1.3.2" @@ -20,6 +21,8 @@ requests = "^2.31.0" retry2 = "^0.9.5" sqlalchemy = "^2.0.23" structlog = "^23.2.0" +types-croniter = "2.0.0.20240106" +typing-extensions = "^4.8.0" uvicorn = "^0.26.0" # OpenTelemetry diff --git a/mula/requirements-dev.txt b/mula/requirements-dev.txt index 086752c7d54..beda2eff648 100644 --- a/mula/requirements-dev.txt +++ b/mula/requirements-dev.txt @@ -113,59 +113,62 @@ click==8.1.7 ; python_version >= "3.10" and python_version < "4.0" \ colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (platform_system == "Windows" or sys_platform == "win32") \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 -coverage[toml]==7.4.2 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:006d220ba2e1a45f1de083d5022d4955abb0aedd78904cd5a779b955b019ec73 \ - --hash=sha256:06fe398145a2e91edaf1ab4eee66149c6776c6b25b136f4a86fcbbb09512fd10 \ - --hash=sha256:175f56572f25e1e1201d2b3e07b71ca4d201bf0b9cb8fad3f1dfae6a4188de86 \ - --hash=sha256:18cac867950943fe93d6cd56a67eb7dcd2d4a781a40f4c1e25d6f1ed98721a55 \ - --hash=sha256:1a5ee18e3a8d766075ce9314ed1cb695414bae67df6a4b0805f5137d93d6f1cb \ - --hash=sha256:20a875bfd8c282985c4720c32aa05056f77a68e6d8bbc5fe8632c5860ee0b49b \ - --hash=sha256:2412e98e70f16243be41d20836abd5f3f32edef07cbf8f407f1b6e1ceae783ac \ - --hash=sha256:2599972b21911111114100d362aea9e70a88b258400672626efa2b9e2179609c \ - --hash=sha256:2ed37e16cf35c8d6e0b430254574b8edd242a367a1b1531bd1adc99c6a5e00fe \ - --hash=sha256:32b4ab7e6c924f945cbae5392832e93e4ceb81483fd6dc4aa8fb1a97b9d3e0e1 \ - --hash=sha256:34423abbaad70fea9d0164add189eabaea679068ebdf693baa5c02d03e7db244 \ - --hash=sha256:3507427d83fa961cbd73f11140f4a5ce84208d31756f7238d6257b2d3d868405 \ - --hash=sha256:3733545eb294e5ad274abe131d1e7e7de4ba17a144505c12feca48803fea5f64 \ - --hash=sha256:3ff5bdb08d8938d336ce4088ca1a1e4b6c8cd3bef8bb3a4c0eb2f37406e49643 \ - --hash=sha256:3ff7f92ae5a456101ca8f48387fd3c56eb96353588e686286f50633a611afc95 \ - --hash=sha256:42a9e754aa250fe61f0f99986399cec086d7e7a01dd82fd863a20af34cbce962 \ - --hash=sha256:51593a1f05c39332f623d64d910445fdec3d2ac2d96b37ce7f331882d5678ddf \ - --hash=sha256:5b11f9c6587668e495cc7365f85c93bed34c3a81f9f08b0920b87a89acc13469 \ - --hash=sha256:69f1665165ba2fe7614e2f0c1aed71e14d83510bf67e2ee13df467d1c08bf1e8 \ - --hash=sha256:78cdcbf7b9cb83fe047ee09298e25b1cd1636824067166dc97ad0543b079d22f \ - --hash=sha256:7df95fdd1432a5d2675ce630fef5f239939e2b3610fe2f2b5bf21fa505256fa3 \ - --hash=sha256:81a5fb41b0d24447a47543b749adc34d45a2cf77b48ca74e5bf3de60a7bd9edc \ - --hash=sha256:840456cb1067dc350af9080298c7c2cfdddcedc1cb1e0b30dceecdaf7be1a2d3 \ - --hash=sha256:8562ca91e8c40864942615b1d0b12289d3e745e6b2da901d133f52f2d510a1e3 \ - --hash=sha256:861d75402269ffda0b33af94694b8e0703563116b04c681b1832903fac8fd647 \ - --hash=sha256:8b98c89db1b150d851a7840142d60d01d07677a18f0f46836e691c38134ed18b \ - --hash=sha256:a178b7b1ac0f1530bb28d2e51f88c0bab3e5949835851a60dda80bff6052510c \ - --hash=sha256:a8ddbd158e069dded57738ea69b9744525181e99974c899b39f75b2b29a624e2 \ - --hash=sha256:ac4bab32f396b03ebecfcf2971668da9275b3bb5f81b3b6ba96622f4ef3f6e17 \ - --hash=sha256:ac9e95cefcf044c98d4e2c829cd0669918585755dd9a92e28a1a7012322d0a95 \ - --hash=sha256:adbdfcda2469d188d79771d5696dc54fab98a16d2ef7e0875013b5f56a251047 \ - --hash=sha256:b3c8bbb95a699c80a167478478efe5e09ad31680931ec280bf2087905e3b95ec \ - --hash=sha256:b3f2b1eb229f23c82898eedfc3296137cf1f16bb145ceab3edfd17cbde273fb7 \ - --hash=sha256:b4ae777bebaed89e3a7e80c4a03fac434a98a8abb5251b2a957d38fe3fd30088 \ - --hash=sha256:b953275d4edfab6cc0ed7139fa773dfb89e81fee1569a932f6020ce7c6da0e8f \ - --hash=sha256:bf54c3e089179d9d23900e3efc86d46e4431188d9a657f345410eecdd0151f50 \ - --hash=sha256:bf711d517e21fb5bc429f5c4308fbc430a8585ff2a43e88540264ae87871e36a \ - --hash=sha256:c00e54f0bd258ab25e7f731ca1d5144b0bf7bec0051abccd2bdcff65fa3262c9 \ - --hash=sha256:c11ca2df2206a4e3e4c4567f52594637392ed05d7c7fb73b4ea1c658ba560265 \ - --hash=sha256:c5f9683be6a5b19cd776ee4e2f2ffb411424819c69afab6b2db3a0a364ec6642 \ - --hash=sha256:cf89ab85027427d351f1de918aff4b43f4eb5f33aff6835ed30322a86ac29c9e \ - --hash=sha256:d1b750a8409bec61caa7824bfd64a8074b6d2d420433f64c161a8335796c7c6b \ - --hash=sha256:d779a48fac416387dd5673fc5b2d6bd903ed903faaa3247dc1865c65eaa5a93e \ - --hash=sha256:d9a1ef0f173e1a19738f154fb3644f90d0ada56fe6c9b422f992b04266c55d5a \ - --hash=sha256:ddb79414c15c6f03f56cc68fa06994f047cf20207c31b5dad3f6bab54a0f66ef \ - --hash=sha256:ef00d31b7569ed3cb2036f26565f1984b9fc08541731ce01012b02a4c238bf03 \ - --hash=sha256:f40ac873045db4fd98a6f40387d242bde2708a3f8167bd967ccd43ad46394ba2 \ - --hash=sha256:f593a4a90118d99014517c2679e04a4ef5aee2d81aa05c26c734d271065efcb6 \ - --hash=sha256:f5df76c58977bc35a49515b2fbba84a1d952ff0ec784a4070334dfbec28a2def \ - --hash=sha256:f72cdd2586f9a769570d4b5714a3837b3a59a53b096bb954f1811f6a0afad305 \ - --hash=sha256:f8e845d894e39fb53834da826078f6dc1a933b32b1478cf437007367efaf6f6a \ - --hash=sha256:fe6e43c8b510719b48af7db9631b5fbac910ade4bd90e6378c85ac5ac706382c +coverage[toml]==7.4.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa \ + --hash=sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003 \ + --hash=sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f \ + --hash=sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c \ + --hash=sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e \ + --hash=sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0 \ + --hash=sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9 \ + --hash=sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52 \ + --hash=sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e \ + --hash=sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454 \ + --hash=sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0 \ + --hash=sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079 \ + --hash=sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352 \ + --hash=sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f \ + --hash=sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30 \ + --hash=sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe \ + --hash=sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113 \ + --hash=sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765 \ + --hash=sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc \ + --hash=sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e \ + --hash=sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501 \ + --hash=sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7 \ + --hash=sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2 \ + --hash=sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f \ + --hash=sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4 \ + --hash=sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524 \ + --hash=sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c \ + --hash=sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51 \ + --hash=sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840 \ + --hash=sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6 \ + --hash=sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee \ + --hash=sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e \ + --hash=sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45 \ + --hash=sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba \ + --hash=sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d \ + --hash=sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3 \ + --hash=sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10 \ + --hash=sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e \ + --hash=sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb \ + --hash=sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9 \ + --hash=sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a \ + --hash=sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47 \ + --hash=sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1 \ + --hash=sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3 \ + --hash=sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914 \ + --hash=sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328 \ + --hash=sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6 \ + --hash=sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d \ + --hash=sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0 \ + --hash=sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94 \ + --hash=sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc \ + --hash=sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2 +croniter==2.0.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:78bf110a2c7dbbfdd98b926318ae6c64a731a4c637c7befe3685755110834746 \ + --hash=sha256:8bff16c9af4ef1fb6f05416973b8f7cb54997c02f2f8365251f9bf1dded91866 decorator==5.1.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 @@ -178,9 +181,9 @@ exceptiongroup==1.2.0 ; python_version >= "3.10" and python_version < "3.11" \ factory-boy==3.3.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c \ --hash=sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1 -faker==23.2.1 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:0520a6b97e07c658b2798d7140971c1d5bc4bcd5013e7937fe075fd054aa320c \ - --hash=sha256:f07b64d27f67b62c7f0536a72f47813015b3b51cd4664918454011094321e464 +faker==24.0.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1 \ + --hash=sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc fastapi==0.109.2 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d \ --hash=sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73 @@ -558,168 +561,171 @@ psycopg2==2.9.9 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024 \ --hash=sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913 \ --hash=sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c -pydantic-core==2.16.2 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379 \ - --hash=sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06 \ - --hash=sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05 \ - --hash=sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7 \ - --hash=sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753 \ - --hash=sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a \ - --hash=sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731 \ - --hash=sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc \ - --hash=sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380 \ - --hash=sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3 \ - --hash=sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c \ - --hash=sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11 \ - --hash=sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990 \ - --hash=sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a \ - --hash=sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2 \ - --hash=sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8 \ - --hash=sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97 \ - --hash=sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a \ - --hash=sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8 \ - --hash=sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef \ - --hash=sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77 \ - --hash=sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33 \ - --hash=sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82 \ - --hash=sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5 \ - --hash=sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b \ - --hash=sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55 \ - --hash=sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e \ - --hash=sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b \ - --hash=sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7 \ - --hash=sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec \ - --hash=sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc \ - --hash=sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469 \ - --hash=sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b \ - --hash=sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20 \ - --hash=sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e \ - --hash=sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d \ - --hash=sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f \ - --hash=sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b \ - --hash=sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039 \ - --hash=sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e \ - --hash=sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2 \ - --hash=sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f \ - --hash=sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b \ - --hash=sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc \ - --hash=sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8 \ - --hash=sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522 \ - --hash=sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e \ - --hash=sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784 \ - --hash=sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a \ - --hash=sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890 \ - --hash=sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485 \ - --hash=sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545 \ - --hash=sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f \ - --hash=sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943 \ - --hash=sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878 \ - --hash=sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f \ - --hash=sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17 \ - --hash=sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7 \ - --hash=sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286 \ - --hash=sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c \ - --hash=sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb \ - --hash=sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646 \ - --hash=sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978 \ - --hash=sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8 \ - --hash=sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15 \ - --hash=sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272 \ - --hash=sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2 \ - --hash=sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55 \ - --hash=sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf \ - --hash=sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545 \ - --hash=sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4 \ - --hash=sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a \ - --hash=sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804 \ - --hash=sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4 \ - --hash=sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0 \ - --hash=sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a \ - --hash=sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113 \ - --hash=sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d \ - --hash=sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25 +pydantic-core==2.16.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a \ + --hash=sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed \ + --hash=sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979 \ + --hash=sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff \ + --hash=sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5 \ + --hash=sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45 \ + --hash=sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340 \ + --hash=sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad \ + --hash=sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23 \ + --hash=sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6 \ + --hash=sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7 \ + --hash=sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241 \ + --hash=sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda \ + --hash=sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187 \ + --hash=sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba \ + --hash=sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c \ + --hash=sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2 \ + --hash=sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c \ + --hash=sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132 \ + --hash=sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf \ + --hash=sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972 \ + --hash=sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db \ + --hash=sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade \ + --hash=sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4 \ + --hash=sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8 \ + --hash=sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f \ + --hash=sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9 \ + --hash=sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48 \ + --hash=sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec \ + --hash=sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d \ + --hash=sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9 \ + --hash=sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb \ + --hash=sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4 \ + --hash=sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89 \ + --hash=sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c \ + --hash=sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9 \ + --hash=sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da \ + --hash=sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac \ + --hash=sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b \ + --hash=sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf \ + --hash=sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e \ + --hash=sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137 \ + --hash=sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1 \ + --hash=sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b \ + --hash=sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8 \ + --hash=sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e \ + --hash=sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053 \ + --hash=sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01 \ + --hash=sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe \ + --hash=sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd \ + --hash=sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805 \ + --hash=sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183 \ + --hash=sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8 \ + --hash=sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99 \ + --hash=sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820 \ + --hash=sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074 \ + --hash=sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256 \ + --hash=sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8 \ + --hash=sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975 \ + --hash=sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad \ + --hash=sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e \ + --hash=sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca \ + --hash=sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df \ + --hash=sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b \ + --hash=sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a \ + --hash=sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a \ + --hash=sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721 \ + --hash=sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a \ + --hash=sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f \ + --hash=sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2 \ + --hash=sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97 \ + --hash=sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6 \ + --hash=sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed \ + --hash=sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc \ + --hash=sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1 \ + --hash=sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe \ + --hash=sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120 \ + --hash=sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f \ + --hash=sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a pydantic-settings==2.2.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed \ --hash=sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091 -pydantic==2.6.1 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f \ - --hash=sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9 +pydantic==2.6.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a \ + --hash=sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f pytest-cov==4.1.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \ --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a -pytest==8.0.1 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae \ - --hash=sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca -python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 +pytest==8.0.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd \ + --hash=sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096 +python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a +pytz==2024.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812 \ + --hash=sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319 requests==2.31.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 retry2==0.9.5 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:f7fee13b1e15d0611c462910a6aa72a8919823988dd0412152bc3719c89a4e55 -setuptools==69.1.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401 \ - --hash=sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6 +setuptools==69.1.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56 \ + --hash=sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8 six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 -sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \ - --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384 -sqlalchemy==2.0.27 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07 \ - --hash=sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71 \ - --hash=sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5 \ - --hash=sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d \ - --hash=sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131 \ - --hash=sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4 \ - --hash=sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac \ - --hash=sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51 \ - --hash=sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230 \ - --hash=sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5 \ - --hash=sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c \ - --hash=sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876 \ - --hash=sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb \ - --hash=sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d \ - --hash=sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5 \ - --hash=sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6 \ - --hash=sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910 \ - --hash=sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45 \ - --hash=sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6 \ - --hash=sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d \ - --hash=sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254 \ - --hash=sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8 \ - --hash=sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18 \ - --hash=sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951 \ - --hash=sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf \ - --hash=sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a \ - --hash=sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396 \ - --hash=sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f \ - --hash=sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8 \ - --hash=sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979 \ - --hash=sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc \ - --hash=sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c \ - --hash=sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a \ - --hash=sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32 \ - --hash=sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a \ - --hash=sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac \ - --hash=sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c \ - --hash=sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1 \ - --hash=sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd \ - --hash=sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4 \ - --hash=sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd \ - --hash=sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f \ - --hash=sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a \ - --hash=sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f \ - --hash=sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc \ - --hash=sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd \ - --hash=sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8 \ - --hash=sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01 \ - --hash=sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620 +sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc +sqlalchemy==2.0.28 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2 \ + --hash=sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa \ + --hash=sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462 \ + --hash=sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d \ + --hash=sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b \ + --hash=sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526 \ + --hash=sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b \ + --hash=sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53 \ + --hash=sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d \ + --hash=sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4 \ + --hash=sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750 \ + --hash=sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db \ + --hash=sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc \ + --hash=sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da \ + --hash=sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2 \ + --hash=sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368 \ + --hash=sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f \ + --hash=sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5 \ + --hash=sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d \ + --hash=sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986 \ + --hash=sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5 \ + --hash=sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197 \ + --hash=sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf \ + --hash=sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7 \ + --hash=sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7 \ + --hash=sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc \ + --hash=sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075 \ + --hash=sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5 \ + --hash=sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b \ + --hash=sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c \ + --hash=sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b \ + --hash=sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6 \ + --hash=sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9 \ + --hash=sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385 \ + --hash=sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c \ + --hash=sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9 \ + --hash=sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67 \ + --hash=sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02 \ + --hash=sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a \ + --hash=sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097 \ + --hash=sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133 \ + --hash=sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6 \ + --hash=sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8 \ + --hash=sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75 \ + --hash=sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252 \ + --hash=sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9 \ + --hash=sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05 \ + --hash=sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71 \ + --hash=sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6 starlette==0.36.3 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044 \ --hash=sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080 @@ -729,9 +735,12 @@ structlog==23.3.0 ; python_version >= "3.10" and python_version < "4.0" \ tomli==2.0.1 ; python_version >= "3.10" and python_full_version <= "3.11.0a6" \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f -typing-extensions==4.9.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ - --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd +types-croniter==2.0.0.20240106 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:266d9ecabbc06afab7cc0cfa7f2149eb36f613ed66ddd6c9bac4edcf727e9a58 \ + --hash=sha256:a5c92566d750e025ab31279029ab44b479e2e3509cd8db3784574bdab1012571 +typing-extensions==4.10.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb urllib3==2.2.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 diff --git a/mula/requirements.txt b/mula/requirements.txt index 177cf570896..390db0b4487 100644 --- a/mula/requirements.txt +++ b/mula/requirements.txt @@ -113,6 +113,9 @@ click==8.1.7 ; python_version >= "3.10" and python_version < "4.0" \ colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +croniter==2.0.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:78bf110a2c7dbbfdd98b926318ae6c64a731a4c637c7befe3685755110834746 \ + --hash=sha256:8bff16c9af4ef1fb6f05416973b8f7cb54997c02f2f8365251f9bf1dded91866 decorator==5.1.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 @@ -467,165 +470,177 @@ psycopg2==2.9.9 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024 \ --hash=sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913 \ --hash=sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c -pydantic-core==2.16.2 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379 \ - --hash=sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06 \ - --hash=sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05 \ - --hash=sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7 \ - --hash=sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753 \ - --hash=sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a \ - --hash=sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731 \ - --hash=sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc \ - --hash=sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380 \ - --hash=sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3 \ - --hash=sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c \ - --hash=sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11 \ - --hash=sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990 \ - --hash=sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a \ - --hash=sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2 \ - --hash=sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8 \ - --hash=sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97 \ - --hash=sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a \ - --hash=sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8 \ - --hash=sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef \ - --hash=sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77 \ - --hash=sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33 \ - --hash=sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82 \ - --hash=sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5 \ - --hash=sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b \ - --hash=sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55 \ - --hash=sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e \ - --hash=sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b \ - --hash=sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7 \ - --hash=sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec \ - --hash=sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc \ - --hash=sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469 \ - --hash=sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b \ - --hash=sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20 \ - --hash=sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e \ - --hash=sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d \ - --hash=sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f \ - --hash=sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b \ - --hash=sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039 \ - --hash=sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e \ - --hash=sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2 \ - --hash=sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f \ - --hash=sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b \ - --hash=sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc \ - --hash=sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8 \ - --hash=sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522 \ - --hash=sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e \ - --hash=sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784 \ - --hash=sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a \ - --hash=sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890 \ - --hash=sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485 \ - --hash=sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545 \ - --hash=sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f \ - --hash=sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943 \ - --hash=sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878 \ - --hash=sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f \ - --hash=sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17 \ - --hash=sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7 \ - --hash=sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286 \ - --hash=sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c \ - --hash=sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb \ - --hash=sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646 \ - --hash=sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978 \ - --hash=sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8 \ - --hash=sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15 \ - --hash=sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272 \ - --hash=sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2 \ - --hash=sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55 \ - --hash=sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf \ - --hash=sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545 \ - --hash=sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4 \ - --hash=sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a \ - --hash=sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804 \ - --hash=sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4 \ - --hash=sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0 \ - --hash=sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a \ - --hash=sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113 \ - --hash=sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d \ - --hash=sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25 +pydantic-core==2.16.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a \ + --hash=sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed \ + --hash=sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979 \ + --hash=sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff \ + --hash=sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5 \ + --hash=sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45 \ + --hash=sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340 \ + --hash=sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad \ + --hash=sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23 \ + --hash=sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6 \ + --hash=sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7 \ + --hash=sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241 \ + --hash=sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda \ + --hash=sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187 \ + --hash=sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba \ + --hash=sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c \ + --hash=sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2 \ + --hash=sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c \ + --hash=sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132 \ + --hash=sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf \ + --hash=sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972 \ + --hash=sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db \ + --hash=sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade \ + --hash=sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4 \ + --hash=sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8 \ + --hash=sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f \ + --hash=sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9 \ + --hash=sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48 \ + --hash=sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec \ + --hash=sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d \ + --hash=sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9 \ + --hash=sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb \ + --hash=sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4 \ + --hash=sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89 \ + --hash=sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c \ + --hash=sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9 \ + --hash=sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da \ + --hash=sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac \ + --hash=sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b \ + --hash=sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf \ + --hash=sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e \ + --hash=sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137 \ + --hash=sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1 \ + --hash=sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b \ + --hash=sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8 \ + --hash=sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e \ + --hash=sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053 \ + --hash=sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01 \ + --hash=sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe \ + --hash=sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd \ + --hash=sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805 \ + --hash=sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183 \ + --hash=sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8 \ + --hash=sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99 \ + --hash=sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820 \ + --hash=sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074 \ + --hash=sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256 \ + --hash=sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8 \ + --hash=sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975 \ + --hash=sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad \ + --hash=sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e \ + --hash=sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca \ + --hash=sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df \ + --hash=sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b \ + --hash=sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a \ + --hash=sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a \ + --hash=sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721 \ + --hash=sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a \ + --hash=sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f \ + --hash=sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2 \ + --hash=sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97 \ + --hash=sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6 \ + --hash=sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed \ + --hash=sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc \ + --hash=sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1 \ + --hash=sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe \ + --hash=sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120 \ + --hash=sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f \ + --hash=sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a pydantic-settings==2.2.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed \ --hash=sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091 -pydantic==2.6.1 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f \ - --hash=sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9 +pydantic==2.6.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a \ + --hash=sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f +python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a +pytz==2024.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812 \ + --hash=sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319 requests==2.31.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 retry2==0.9.5 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:f7fee13b1e15d0611c462910a6aa72a8919823988dd0412152bc3719c89a4e55 -setuptools==69.1.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401 \ - --hash=sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6 -sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \ - --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384 -sqlalchemy==2.0.27 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07 \ - --hash=sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71 \ - --hash=sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5 \ - --hash=sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d \ - --hash=sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131 \ - --hash=sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4 \ - --hash=sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac \ - --hash=sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51 \ - --hash=sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230 \ - --hash=sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5 \ - --hash=sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c \ - --hash=sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876 \ - --hash=sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb \ - --hash=sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d \ - --hash=sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5 \ - --hash=sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6 \ - --hash=sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910 \ - --hash=sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45 \ - --hash=sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6 \ - --hash=sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d \ - --hash=sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254 \ - --hash=sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8 \ - --hash=sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18 \ - --hash=sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951 \ - --hash=sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf \ - --hash=sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a \ - --hash=sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396 \ - --hash=sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f \ - --hash=sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8 \ - --hash=sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979 \ - --hash=sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc \ - --hash=sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c \ - --hash=sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a \ - --hash=sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32 \ - --hash=sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a \ - --hash=sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac \ - --hash=sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c \ - --hash=sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1 \ - --hash=sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd \ - --hash=sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4 \ - --hash=sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd \ - --hash=sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f \ - --hash=sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a \ - --hash=sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f \ - --hash=sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc \ - --hash=sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd \ - --hash=sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8 \ - --hash=sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01 \ - --hash=sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620 +setuptools==69.1.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56 \ + --hash=sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8 +six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc +sqlalchemy==2.0.28 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2 \ + --hash=sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa \ + --hash=sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462 \ + --hash=sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d \ + --hash=sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b \ + --hash=sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526 \ + --hash=sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b \ + --hash=sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53 \ + --hash=sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d \ + --hash=sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4 \ + --hash=sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750 \ + --hash=sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db \ + --hash=sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc \ + --hash=sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da \ + --hash=sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2 \ + --hash=sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368 \ + --hash=sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f \ + --hash=sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5 \ + --hash=sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d \ + --hash=sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986 \ + --hash=sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5 \ + --hash=sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197 \ + --hash=sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf \ + --hash=sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7 \ + --hash=sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7 \ + --hash=sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc \ + --hash=sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075 \ + --hash=sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5 \ + --hash=sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b \ + --hash=sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c \ + --hash=sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b \ + --hash=sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6 \ + --hash=sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9 \ + --hash=sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385 \ + --hash=sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c \ + --hash=sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9 \ + --hash=sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67 \ + --hash=sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02 \ + --hash=sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a \ + --hash=sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097 \ + --hash=sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133 \ + --hash=sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6 \ + --hash=sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8 \ + --hash=sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75 \ + --hash=sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252 \ + --hash=sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9 \ + --hash=sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05 \ + --hash=sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71 \ + --hash=sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6 starlette==0.36.3 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044 \ --hash=sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080 structlog==23.3.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:24b42b914ac6bc4a4e6f716e82ac70d7fb1e8c3b1035a765591953bfc37101a5 \ --hash=sha256:d6922a88ceabef5b13b9eda9c4043624924f60edbb00397f4d193bd754cde60a -typing-extensions==4.9.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ - --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd +types-croniter==2.0.0.20240106 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:266d9ecabbc06afab7cc0cfa7f2149eb36f613ed66ddd6c9bac4edcf727e9a58 \ + --hash=sha256:a5c92566d750e025ab31279029ab44b479e2e3509cd8db3784574bdab1012571 +typing-extensions==4.10.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb urllib3==2.2.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 diff --git a/mula/scheduler/alembic/versions/0008_add_schedules_table.py b/mula/scheduler/alembic/versions/0008_add_schedules_table.py new file mode 100644 index 00000000000..41fd83a8f2b --- /dev/null +++ b/mula/scheduler/alembic/versions/0008_add_schedules_table.py @@ -0,0 +1,104 @@ +"""Add schedules table + +Revision ID: 0008 +Revises: 0007 +Create Date: 2024-01-30 15:08:44.084080 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +import scheduler + +# revision identifiers, used by Alembic. +revision = "0008" +down_revision = "0007" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "schedules", + sa.Column("id", scheduler.utils.datastore.GUID(), nullable=False), + sa.Column("scheduler_id", sa.String(), nullable=True), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.Column("p_item", postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column("cron_expression", sa.String(), nullable=True), + sa.Column("deadline_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("evaluated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("modified_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "task_runs", + sa.Column("id", scheduler.utils.datastore.GUID(), nullable=False), + sa.Column("scheduler_id", sa.String(), nullable=True), + sa.Column("type", sa.String(), nullable=True), + sa.Column("p_item", postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column("schedule_id", scheduler.utils.datastore.GUID(), nullable=True), + sa.Column( + "status", + sa.Enum( + "PENDING", "QUEUED", "DISPATCHED", "RUNNING", "COMPLETED", "FAILED", "CANCELLED", name="taskstatus" + ), + nullable=False, + ), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("modified_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.ForeignKeyConstraint(["schedule_id"], ["schedules.id"], ondelete="SET NULL"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + "ix_p_item_hash", "task_runs", [sa.text("(p_item->>'hash')"), sa.text("created_at DESC")], unique=False + ) + op.drop_index("ix_tasks_p_item_hash", table_name="tasks") + op.drop_table("tasks") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "tasks", + sa.Column("id", sa.UUID(), autoincrement=False, nullable=False), + sa.Column("scheduler_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column( + "status", + postgresql.ENUM( + "PENDING", "QUEUED", "DISPATCHED", "RUNNING", "COMPLETED", "FAILED", "CANCELLED", name="taskstatus" + ), + autoincrement=False, + nullable=False, + ), + sa.Column( + "created_at", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + autoincrement=False, + nullable=False, + ), + sa.Column( + "modified_at", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + autoincrement=False, + nullable=False, + ), + sa.Column("p_item", postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.Column("type", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint("id", name="tasks_pkey"), + ) + op.create_index( + "ix_tasks_p_item_hash", + "tasks", + [sa.text("(p_item ->> 'hash'::text)"), sa.text("created_at DESC")], + unique=False, + ) + op.drop_index("ix_p_item_hash", table_name="task_runs") + op.drop_table("task_runs") + op.drop_table("schedules") + # ### end Alembic commands ### diff --git a/mula/scheduler/context/context.py b/mula/scheduler/context/context.py index 4369132a51a..c6c25a7bd2b 100644 --- a/mula/scheduler/context/context.py +++ b/mula/scheduler/context/context.py @@ -155,6 +155,7 @@ def __init__(self) -> None: **{ storage.TaskStore.name: storage.TaskStore(dbconn), storage.PriorityQueueStore.name: storage.PriorityQueueStore(dbconn), + storage.ScheduleStore.name: storage.ScheduleStore(dbconn), } ) diff --git a/mula/scheduler/models/__init__.py b/mula/scheduler/models/__init__.py index 894fb0de555..c5699b4246f 100644 --- a/mula/scheduler/models/__init__.py +++ b/mula/scheduler/models/__init__.py @@ -7,6 +7,7 @@ from .organisation import Organisation from .plugin import Plugin from .queue import PrioritizedItem, PrioritizedItemDB, Queue -from .request import PrioritizedItemRequest +from .request import PrioritizedItemRequest, ScheduleRequest +from .schedule import Schedule, ScheduleDB from .scheduler import Scheduler -from .tasks import BoefjeTask, NormalizerTask, Task, TaskDB, TaskStatus +from .tasks import BoefjeTask, NormalizerTask, TaskRun, TaskRunDB, TaskStatus diff --git a/mula/scheduler/models/errors.py b/mula/scheduler/models/errors.py new file mode 100644 index 00000000000..15e676c5dfe --- /dev/null +++ b/mula/scheduler/models/errors.py @@ -0,0 +1,2 @@ +class ValidationError(Exception): + pass diff --git a/mula/scheduler/models/request.py b/mula/scheduler/models/request.py index 327439daa21..88ce326c25e 100644 --- a/mula/scheduler/models/request.py +++ b/mula/scheduler/models/request.py @@ -1,3 +1,5 @@ +from datetime import datetime + from pydantic import BaseModel, Field @@ -6,3 +8,15 @@ class PrioritizedItemRequest(BaseModel): priority: int data: dict = Field(default_factory=dict) + + +class ScheduleRequest(BaseModel): + scheduler_id: str + + enabled: bool = True + + p_item: dict = Field(default_factory=dict) + + cron_expression: str | None = None + + deadline_at: datetime | None = None diff --git a/mula/scheduler/models/schedule.py b/mula/scheduler/models/schedule.py new file mode 100644 index 00000000000..079c9eb56d2 --- /dev/null +++ b/mula/scheduler/models/schedule.py @@ -0,0 +1,83 @@ +import uuid +from datetime import datetime, timezone + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import Boolean, Column, DateTime, String +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func + +from scheduler.utils import GUID, cron + +from .base import Base +from .errors import ValidationError +from .queue import PrioritizedItem +from .tasks import TaskRun + + +class Schedule(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: uuid.UUID = Field(default_factory=uuid.uuid4) + + scheduler_id: str + + enabled: bool = True + + # Priority item specification + p_item: PrioritizedItem + + tasks: list[TaskRun] = [] + + cron_expression: str | None = None + + deadline_at: datetime | None = None + evaluated_at: datetime | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + modified_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + # TODO: index on the p_item.hash + + def validate(self): + """Validate the schedule model""" + if self.cron_expression is not None: + try: + cron.next_run(self.cron_expression) + except Exception as exc: + raise ValidationError(f"Invalid cron expression: {self.cron_expression}") from exc + + +class ScheduleDB(Base): + __tablename__ = "schedules" + + id = Column(GUID, primary_key=True) + scheduler_id = Column(String) + enabled = Column(Boolean, nullable=False, default=True) + p_item = Column(JSONB, nullable=False) + tasks = relationship( + "TaskRunDB", back_populates="schedule", order_by="TaskRunDB.created_at", cascade="all,delete-orphan" + ) + cron_expression = Column(String, nullable=True) + + deadline_at = Column( + DateTime(timezone=True), + nullable=True, + ) + + evaluated_at = Column( + DateTime(timezone=True), + nullable=True, + ) + + created_at = Column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + + modified_at = Column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + ) diff --git a/mula/scheduler/models/tasks.py b/mula/scheduler/models/tasks.py index 52c229ecd39..42e71aadaf5 100644 --- a/mula/scheduler/models/tasks.py +++ b/mula/scheduler/models/tasks.py @@ -5,8 +5,9 @@ import mmh3 from pydantic import BaseModel, ConfigDict, Field -from sqlalchemy import Column, DateTime, Enum, String +from sqlalchemy import Column, DateTime, Enum, ForeignKey, String from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import relationship from sqlalchemy.schema import Index from sqlalchemy.sql import func from sqlalchemy.sql.expression import text @@ -44,7 +45,7 @@ class TaskStatus(str, enum.Enum): CANCELLED = "cancelled" -class Task(BaseModel): +class TaskRun(BaseModel): model_config = ConfigDict(from_attributes=True) id: uuid.UUID @@ -53,10 +54,13 @@ class Task(BaseModel): type: str + # Item that was pushed onto the queue p_item: PrioritizedItem status: TaskStatus + schedule_id: uuid.UUID | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) modified_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) @@ -65,8 +69,8 @@ def __repr__(self): return f"Task(id={self.id}, scheduler_id={self.scheduler_id}, type={self.type}, status={self.status})" -class TaskDB(Base): - __tablename__ = "tasks" +class TaskRunDB(Base): + __tablename__ = "task_runs" id = Column(GUID, primary_key=True) @@ -76,6 +80,9 @@ class TaskDB(Base): p_item = Column(JSONB, nullable=False) + schedule_id = Column(GUID, ForeignKey("schedules.id", ondelete="SET NULL"), nullable=True) + schedule = relationship("ScheduleDB", back_populates="tasks") + status = Column( Enum(TaskStatus), nullable=False, @@ -130,7 +137,7 @@ class BoefjeTask(BaseModel): id: uuid.UUID | None = Field(default_factory=uuid.uuid4) boefje: Boefje - input_ooi: str | None + input_ooi: str | None = None organization: str dispatches: list[Normalizer] = Field(default_factory=list) diff --git a/mula/scheduler/queues/boefje.py b/mula/scheduler/queues/boefje.py index 9177d97e9e8..4ec9112085f 100644 --- a/mula/scheduler/queues/boefje.py +++ b/mula/scheduler/queues/boefje.py @@ -21,4 +21,7 @@ def create_hash(self, p_item: models.PrioritizedItem) -> str: input_ooi = dict_utils.deep_get(p_item.model_dump(), ["data", "input_ooi"]) organization = dict_utils.deep_get(p_item.model_dump(), ["data", "organization"]) - return mmh3.hash_bytes(f"{input_ooi}-{boefje_id}-{organization}").hex() + if input_ooi: + return mmh3.hash_bytes(f"{input_ooi}-{boefje_id}-{organization}").hex() + + return mmh3.hash_bytes(f"{boefje_id}-{organization}").hex() diff --git a/mula/scheduler/rankers/__init__.py b/mula/scheduler/rankers/__init__.py index 49a565268ec..ade9d64af19 100644 --- a/mula/scheduler/rankers/__init__.py +++ b/mula/scheduler/rankers/__init__.py @@ -1,3 +1,4 @@ from .boefje import BoefjeRanker +from .job import DefaultDeadlineRanker from .normalizer import NormalizerRanker from .ranker import Ranker diff --git a/mula/scheduler/rankers/job.py b/mula/scheduler/rankers/job.py new file mode 100644 index 00000000000..02322a2ee76 --- /dev/null +++ b/mula/scheduler/rankers/job.py @@ -0,0 +1,41 @@ +import random +from datetime import datetime, timedelta, timezone +from typing import Any + +from scheduler.utils import cron + +from .ranker import Ranker + + +class DefaultDeadlineRanker(Ranker): + def rank(self, obj: Any) -> int: + """Create a deadline for a job + + Args: + obj (Any): The object to rank + + Returns: + int: The timestamp of the deadline + """ + # Do we have a cron expression? + if obj is not None and obj.cron_expression: + try: + next_run = cron.next_run(obj.cron_expression) + except Exception as exc: + raise ValueError(f"Invalid cron expression: {obj.cron_expression}") from exc + return int(next_run.timestamp()) + + # We at least delay a job by the grace period + minimum = self.ctx.config.pq_grace_period + deadline = datetime.now(timezone.utc) + timedelta(seconds=minimum) + + # We want to delay the job by a random amount of time, in a range of 5 hours + jitter_range_seconds = 5 * 60 * 60 + jitter_offset = timedelta(seconds=random.uniform(-jitter_range_seconds, jitter_range_seconds)) # noqa + + # Check if the adjusted time is earlier than the minimum, and + # ensure that the adjusted time is not earlier than the deadline + adjusted_time = deadline + jitter_offset + adjusted_time = max(adjusted_time, deadline) + + return int((datetime.now(timezone.utc) + timedelta(seconds=minimum)).timestamp()) diff --git a/mula/scheduler/schedulers/boefje.py b/mula/scheduler/schedulers/boefje.py index c03543777be..af4f4f515b4 100644 --- a/mula/scheduler/schedulers/boefje.py +++ b/mula/scheduler/schedulers/boefje.py @@ -62,9 +62,11 @@ def __init__( callback=callback, ) - self.ranker = rankers.BoefjeRanker( - ctx=self.ctx, - ) + # Priority ranker + self.priority_ranker = rankers.BoefjeRanker(self.ctx) + + # Deadline ranker + self.deadline_ranker = rankers.DefaultDeadlineRanker(self.ctx) def run(self) -> None: """The run method is called when the scheduler is started. It will @@ -106,10 +108,10 @@ def run(self) -> None: interval=60.0, ) - # Random OOI's from Octopoes + # Rescheduling self.run_in_thread( - name=f"BoefjeScheduler-{self.scheduler_id}-random", - target=self.push_tasks_for_random_objects, + name=f"scheduler-{self.scheduler_id}-reschedule", + target=self.push_tasks_for_rescheduling, interval=60.0, ) @@ -269,6 +271,134 @@ def push_tasks_for_new_boefjes(self) -> None: self.push_tasks_for_new_boefjes.__name__, ) + @tracer.start_as_current_span("boefje_push_tasks_for_rescheduling") + def push_tasks_for_rescheduling(self): + if self.queue.full(): + self.logger.warning( + "Boefjes queue is full, not populating with new tasks", + queue_qsize=self.queue.qsize(), + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + ) + return + + try: + schedules, _ = self.ctx.datastores.schedule_store.get_schedules( + scheduler_id=self.scheduler_id, + enabled=True, + max_deadline=datetime.now(timezone.utc), + ) + except Exception as exc_db: + self.logger.error( + "Could not get schedules for scheduler: %s", + self.scheduler_id, + scheduler_id=self.scheduler_id, + organisation_id=self.organisation.id, + exc_info=exc_db, + ) + raise exc_db + + if not schedules: + self.logger.debug( + "No schedules found for scheduler: %s", + self.scheduler_id, + scheduler_id=self.scheduler_id, + organisation_id=self.organisation.id, + ) + return + + with futures.ThreadPoolExecutor() as executor: + for schedule in schedules: + # Create a new task from the p_item spec of a schedule + try: + task = BoefjeTask.parse_obj(schedule.p_item.data) + except Exception as exc: + self.logger.error( + "Could not parse task from schedule: %s", + schedule.id, + schedule_id=schedule.id, + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + exc_info=exc, + ) + + # If we're not able to parse it because a faulty type of + # task was pushed by for instance the schedules endpoint we need + # to disable the schedule. + self.ctx.datastores.schedule_store.update_schedule_enabled(schedule.id, False) + continue + + # Boefje still exists? + boefje = self.ctx.services.katalogus.get_plugin_by_id_and_org_id(task.boefje.id, self.organisation.id) + if not boefje: + self.logger.debug( + "Boefje does not exist anymore, skipping", + boefje_id=task.boefje.id, + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + ) + self.ctx.datastores.schedule_store.update_schedule_enabled(schedule.id, False) + continue + + # Boefje still enabled? + if not boefje.enabled: + self.logger.debug( + "Boefje is disabled, skipping", + boefje_id=task.boefje.id, + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + ) + self.ctx.datastores.schedule_store.update_schedule_enabled(schedule.id, False) + continue + + # We check if the task has an input_ooi, since it is possible + # that task can have no ooi's + ooi = None + if task.input_ooi: + # OOI still exists? + ooi = self.ctx.services.octopoes.get_object(task.organization, task.input_ooi) + if not ooi: + self.logger.debug( + "OOI does not exist anymore, skipping", + ooi_primary_key=task.input_ooi, + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + ) + self.ctx.datastores.schedule_store.update_schedule_enabled(schedule.id, False) + continue + + # Boefje still consuming ooi? + if ooi.object_type not in boefje.consumes: + self.logger.debug( + "Boefje does not consume ooi anymore, skipping", + boefje_id=task.boefje.id, + ooi_primary_key=ooi.primary_key, + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + ) + self.ctx.datastores.schedule_store.update_schedule_enabled(schedule.id, False) + continue + + # Boefje allowed to scan ooi? + if not self.is_task_allowed_to_run(boefje, ooi): + self.logger.debug( + "Boefje not allowed to scan ooi, skipping", + boefje_id=task.boefje.id, + ooi_primary_key=ooi.primary_key, + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + ) + self.ctx.datastores.schedule_store.update_schedule_enabled(schedule.id, False) + continue + + executor.submit( + self.push_task, + boefje, + ooi, + self.push_tasks_for_rescheduling.__name__, + ) + + # FIXME: deprecated @tracer.start_as_current_span("boefje_push_tasks_for_random_objects") def push_tasks_for_random_objects(self) -> None: """Push tasks for random ooi's from octopoes to the queue.""" @@ -357,6 +487,21 @@ def is_task_allowed_to_run(self, boefje: Plugin, ooi: OOI) -> bool: ) return False + boefje_scan_level = boefje.scan_level + if boefje_scan_level is None: + self.logger.warning( + "No scan level found for boefje: %s", + boefje.id, + boefje_id=boefje.id, + organisation_id=self.organisation.id, + scheduler_id=self.scheduler_id, + ) + return False + + # We allow boefjes without an ooi to run. + if not ooi: + return True + if ooi.scan_profile is None: self.logger.debug( "No scan_profile found for ooi: %s", @@ -378,17 +523,6 @@ def is_task_allowed_to_run(self, boefje: Plugin, ooi: OOI) -> bool: ) return False - boefje_scan_level = boefje.scan_level - if boefje_scan_level is None: - self.logger.warning( - "No scan level found for boefje: %s", - boefje.id, - boefje_id=boefje.id, - organisation_id=self.organisation.id, - scheduler_id=self.scheduler_id, - ) - return False - # Boefje intensity score ooi clearance level, range # from 0 to 4. 4 being the highest intensity, and 0 being # the lowest. OOI clearance level defines what boefje @@ -534,7 +668,7 @@ def is_task_stalled(self, task: BoefjeTask) -> bool: return False @tracer.start_as_current_span("boefje_push_task") - def push_task(self, boefje: Plugin, ooi: OOI, caller: str = "") -> None: + def push_task(self, boefje: Plugin, ooi: OOI | None, caller: str = "") -> None: """Given a Boefje and OOI create a BoefjeTask and push it onto the queue. @@ -546,10 +680,12 @@ def push_task(self, boefje: Plugin, ooi: OOI, caller: str = "") -> None: """ task = BoefjeTask( boefje=Boefje.parse_obj(boefje.dict()), - input_ooi=ooi.primary_key, organization=self.organisation.id, ) + if ooi: + task.input_ooi = ooi.primary_key + if not self.is_task_allowed_to_run(boefje, ooi): self.logger.debug( "Task is not allowed to run: %s", @@ -649,7 +785,7 @@ def push_task(self, boefje: Plugin, ooi: OOI, caller: str = "") -> None: return prior_tasks = self.ctx.datastores.task_store.get_tasks_by_hash(task.hash) - score = self.ranker.rank( + score = self.priority_ranker.rank( SimpleNamespace( prior_tasks=prior_tasks, task=task, @@ -682,12 +818,10 @@ def push_task(self, boefje: Plugin, ooi: OOI, caller: str = "") -> None: return self.logger.info( - "Created boefje task: %s for ooi: %s", + "Created boefje task: %s", task.id, - ooi.primary_key, task_id=task.id, boefje_id=boefje.id, - ooi_primary_key=ooi.primary_key, organisation_id=self.organisation.id, scheduler_id=self.scheduler_id, caller=caller, diff --git a/mula/scheduler/schedulers/scheduler.py b/mula/scheduler/schedulers/scheduler.py index 446f02c8605..0ff7da5ead2 100644 --- a/mula/scheduler/schedulers/scheduler.py +++ b/mula/scheduler/schedulers/scheduler.py @@ -2,13 +2,14 @@ import threading import time from collections.abc import Callable -from datetime import datetime, timezone +from concurrent import futures +from datetime import datetime, timedelta, timezone from typing import Any import structlog from opentelemetry import trace -from scheduler import connectors, context, models, queues, storage, utils +from scheduler import connectors, context, models, queues, rankers, storage, utils from scheduler.utils import thread tracer = trace.get_tracer(__name__) @@ -58,8 +59,6 @@ def __init__( The id of the scheduler. queue: A queues.PriorityQueue instance - ranker: - A rankers.Ranker instance. max_tries: The maximum number of retries for a task to be pushed to the queue. @@ -67,11 +66,14 @@ def __init__( self.logger: structlog.BoundLogger = structlog.getLogger(__name__) self.ctx: context.AppContext = ctx - self.enabled: bool = True - self.scheduler_id: str = scheduler_id self.queue: queues.PriorityQueue = queue - self.max_tries: int = max_tries + self.deadline_ranker = rankers.DefaultDeadlineRanker(ctx=self.ctx) self.callback: Callable[[], Any] | None = callback + + # Properties + self.scheduler_id: str = scheduler_id + self.max_tries: int = max_tries + self.enabled: bool = True self._last_activity: datetime | None = None # Listeners @@ -81,110 +83,110 @@ def __init__( self.lock: threading.Lock = threading.Lock() self.stop_event_threads: threading.Event = threading.Event() self.threads: list[thread.ThreadRunner] = [] + self.executor: futures.ThreadPoolExecutor = futures.ThreadPoolExecutor(max_workers=10) @abc.abstractmethod def run(self) -> None: raise NotImplementedError - def post_push(self, p_item: models.PrioritizedItem) -> None: - """When a boefje task is being added to the queue. We - persist a task to the datastore with the status QUEUED. + def run_in_thread( + self, + name: str, + target: Callable[[], Any], + interval: float = 0.01, + daemon: bool = False, + loop: bool = True, + ) -> None: + """Make a function run in a thread, and add it to the dict of threads. Args: - p_item: The prioritized item from the priority queue. + name: The name of the thread. + target: The function to run in the thread. + interval: The interval to run the function. + daemon: Whether the thread should be a daemon. + loop: Whether the thread should loop. """ - # NOTE: we set the id of the task the same as the p_item, for easier - # lookup. - task = models.Task( - id=p_item.id, - scheduler_id=self.scheduler_id, - type=self.queue.item_type.type, - p_item=p_item, - status=models.TaskStatus.QUEUED, - created_at=datetime.now(timezone.utc), - modified_at=datetime.now(timezone.utc), + t = utils.ThreadRunner( + name=name, + target=target, + stop_event=self.stop_event_threads, + interval=interval, + daemon=daemon, + loop=loop, ) + t.start() - task_db = self.ctx.datastores.task_store.get_task_by_id(str(p_item.id)) - if task_db is not None: - self.ctx.datastores.task_store.update_task(task) - return - - self.ctx.datastores.task_store.create_task(task) - - self.last_activity = datetime.now(timezone.utc) + self.threads.append(t) - def post_pop(self, p_item: models.PrioritizedItem) -> None: - """When a boefje task is being removed from the queue. We - persist a task to the datastore with the status RUNNING + def push_items_to_queue(self, p_items: list[models.PrioritizedItem]) -> None: + """Push multiple PrioritizedItems to the queue. Args: - p_item: The prioritized item from the priority queue. + p_items: The list PrioritzedItem to add to the queue. """ - # NOTE: we set the id of the task the same as the p_item, for easier - # lookup. - task = self.ctx.datastores.task_store.get_task_by_id(str(p_item.id)) - if task is None: - self.logger.warning( - "PrioritizedItem %s popped from %s, task %s not found in datastore, could not update task status", - p_item.id, - self.queue.pq_id, - p_item.data.get("id"), - p_item_id=p_item.id, - task_id=p_item.data.get("id"), - queue_id=self.queue.pq_id, - scheduler_id=self.scheduler_id, - ) - return - - task.status = models.TaskStatus.DISPATCHED - self.ctx.datastores.task_store.update_task(task) + count = 0 + for p_item in p_items: + try: + self.push_item_to_queue(p_item) + except ( + queues.errors.NotAllowedError, + queues.errors.QueueFullError, + queues.errors.InvalidPrioritizedItemError, + ): + self.logger.debug( + "Unable to push item %s to queue %s", + p_item.id, + self.queue.pq_id, + p_item_id=p_item.id, + queue_id=self.queue.pq_id, + scheduler_id=self.scheduler_id, + ) + continue + except Exception as exc: + self.logger.error( + "Unable to push item %s to queue %s", + p_item.id, + self.queue.pq_id, + p_item_id=p_item.id, + queue_id=self.queue.pq_id, + scheduler_id=self.scheduler_id, + ) + raise exc - self.last_activity = datetime.now(timezone.utc) + count += 1 - @tracer.start_as_current_span("scheduler_pop_item_from_queue") - def pop_item_from_queue( - self, filters: storage.filters.FilterRequest | None = None - ) -> models.PrioritizedItem | None: - """Pop an item from the queue. + def push_item_to_queue_with_timeout( + self, + p_item: models.PrioritizedItem, + max_tries: int = 5, + timeout: int = 1, + ) -> None: + """Push an item to the queue, with a timeout. Args: - filters: A FilterRequest instance to filter the - prioritized items from the queue. + p_item: The item to push to the queue. + timeout: The timeout in seconds. + max_tries: The maximum number of tries. Set to -1 for infinite tries. - Returns: - A PrioritizedItem instance. + Raises: + QueueFullError: When the queue is full. """ - if not self.is_enabled(): - self.logger.warning( - "Scheduler is disabled, not popping item from queue", - queue_id=self.queue.pq_id, - queue_qsize=self.queue.qsize(), - scheduler_id=self.scheduler_id, - ) - raise queues.errors.NotAllowedError("Scheduler is disabled") - - try: - p_item = self.queue.pop(filters) - except queues.QueueEmptyError as exc: - raise exc - - if p_item is not None: + tries = 0 + while not self.is_space_on_queue() and (tries < max_tries or max_tries == -1): self.logger.debug( - "Popped item %s from queue %s with priority %s", - p_item.id, - self.queue.pq_id, - p_item.priority, - p_item_id=p_item.id, + "Queue %s is full, waiting for space", queue_id=self.queue.pq_id, + queue_qsize=self.queue.qsize(), scheduler_id=self.scheduler_id, ) + time.sleep(timeout) + tries += 1 - self.post_pop(p_item) + if tries >= max_tries and max_tries != -1: + raise queues.errors.QueueFullError() - return p_item + self.push_item_to_queue(p_item) - @tracer.start_as_current_span("scheduler_push_item_to_queue") def push_item_to_queue(self, p_item: models.PrioritizedItem) -> None: """Push a PrioritizedItem to the queue. @@ -245,119 +247,163 @@ def push_item_to_queue(self, p_item: models.PrioritizedItem) -> None: self.post_push(p_item) - def push_items_to_queue(self, p_items: list[models.PrioritizedItem]) -> None: - """Push multiple PrioritizedItems to the queue. + def post_push(self, p_item: models.PrioritizedItem) -> None: + """When a boefje task is being added to the queue. We + persist a task to the datastore with the status QUEUED. Args: - p_items: The list PrioritzedItem to add to the queue. + p_item: The prioritized item from the priority queue. """ - count = 0 - for p_item in p_items: - try: - self.push_item_to_queue(p_item) - except ( - queues.errors.NotAllowedError, - queues.errors.QueueFullError, - queues.errors.InvalidPrioritizedItemError, - ): - self.logger.debug( - "Unable to push item %s to queue %s", - p_item.id, - self.queue.pq_id, - p_item_id=p_item.id, - queue_id=self.queue.pq_id, - scheduler_id=self.scheduler_id, - ) - continue - except Exception as exc: - self.logger.error( - "Unable to push item %s to queue %s", - p_item.id, - self.queue.pq_id, - p_item_id=p_item.id, - queue_id=self.queue.pq_id, + # Set last activity of scheduler + self.last_activity = datetime.now(timezone.utc) + + # Create TaskSchedule + # + # Do we have a schedule for this task? + schedule_db = self.ctx.datastores.schedule_store.get_schedule_by_hash(p_item.hash) + if schedule_db is None: + schedule_db = self.ctx.datastores.schedule_store.create_schedule( + models.Schedule( scheduler_id=self.scheduler_id, + p_item=p_item, + deadline_at=datetime.now(timezone.utc) + timedelta(seconds=self.ctx.config.pq_grace_period), + created_at=datetime.now(timezone.utc), + modified_at=datetime.now(timezone.utc), ) - raise exc + ) - count += 1 + # Create Task + # + # NOTE: we set the id of the task the same as the p_item, for easier + # lookup. + task = models.TaskRun( + id=p_item.id, + scheduler_id=self.scheduler_id, + type=self.queue.item_type.type, + p_item=p_item, + status=models.TaskStatus.QUEUED, + schedule_id=schedule_db.id, + created_at=datetime.now(timezone.utc), + modified_at=datetime.now(timezone.utc), + ) - def push_item_to_queue_with_timeout( - self, - p_item: models.PrioritizedItem, - max_tries: int = 5, - timeout: int = 1, - ) -> None: - """Push an item to the queue, with a timeout. + task_db = self.ctx.datastores.task_store.get_task_by_id(str(p_item.id)) + if task_db is not None: + self.ctx.datastores.task_store.update_task(task) + return + + self.ctx.datastores.task_store.create_task(task) + + def pop_item_from_queue( + self, filters: storage.filters.FilterRequest | None = None + ) -> models.PrioritizedItem | None: + """Pop an item from the queue. Args: - p_item: The item to push to the queue. - timeout: The timeout in seconds. - max_tries: The maximum number of tries. Set to -1 for infinite tries. + filters: A FilterRequest instance to filter the + prioritized items from the queue. - Raises: - QueueFullError: When the queue is full. + Returns: + A PrioritizedItem instance. """ - tries = 0 - while not self.is_space_on_queue() and (tries < max_tries or max_tries == -1): - self.logger.debug( - "Queue %s is full, waiting for space", + if not self.is_enabled(): + self.logger.warning( + "Scheduler is disabled, not popping item from queue", queue_id=self.queue.pq_id, queue_qsize=self.queue.qsize(), scheduler_id=self.scheduler_id, ) - time.sleep(timeout) - tries += 1 + raise queues.errors.NotAllowedError("Scheduler is disabled") - if tries >= max_tries and max_tries != -1: - raise queues.errors.QueueFullError() + try: + p_item = self.queue.pop(filters) + except queues.QueueEmptyError as exc: + raise exc - self.push_item_to_queue(p_item) + if p_item is not None: + self.logger.debug( + "Popped item %s from queue %s with priority %s", + p_item.id, + self.queue.pq_id, + p_item.priority, + p_item_id=p_item.id, + queue_id=self.queue.pq_id, + scheduler_id=self.scheduler_id, + ) - def run_in_thread( - self, - name: str, - target: Callable[[], Any], - interval: float = 0.01, - daemon: bool = False, - loop: bool = True, - ) -> None: - """Make a function run in a thread, and add it to the dict of threads. + self.post_pop(p_item) + + return p_item + + def post_pop(self, p_item: models.PrioritizedItem) -> None: + """When a boefje task is being removed from the queue. We + persist a task to the datastore with the status RUNNING Args: - name: The name of the thread. - target: The function to run in the thread. - interval: The interval to run the function. - daemon: Whether the thread should be a daemon. - loop: Whether the thread should loop. + p_item: The prioritized item from the priority queue. """ - t = utils.ThreadRunner( - name=name, - target=target, - stop_event=self.stop_event_threads, - interval=interval, - daemon=daemon, - loop=loop, - ) - t.start() + # Update task + task = self.ctx.datastores.task_store.get_task_by_id(str(p_item.id)) + if task is None: + self.logger.warning( + "PrioritizedItem %s popped from %s, task %s not found in datastore, could not update task status", + p_item.id, + self.queue.pq_id, + p_item.data.get("id"), + p_item_id=p_item.id, + task_id=p_item.data.get("id"), + queue_id=self.queue.pq_id, + scheduler_id=self.scheduler_id, + ) + return - self.threads.append(t) + task.status = models.TaskStatus.DISPATCHED + self.ctx.datastores.task_store.update_task(task) - def is_space_on_queue(self) -> bool: - """Check if there is space on the queue. + self.last_activity = datetime.now(timezone.utc) - NOTE: maxsize 0 means unlimited + def signal_handler_task(self, task: models.TaskRun) -> None: + """Handle a task that has been completed or failed.""" + if task.status not in [models.TaskStatus.COMPLETED, models.TaskStatus.FAILED]: + return - Returns: - True if there is space on the queue, False otherwise. + def _calculate_deadline(task: models.TaskRun): + schedule = self.ctx.datastores.schedule_store.get_schedule_by_hash(task.p_item.hash) + + try: + schedule.deadline_at = datetime.fromtimestamp(self.deadline_ranker.rank(schedule)) + except Exception: + self.logger.error( + "Unable to calculate deadline for schedule %s. Disabling schedule", + schedule.id, + schedule_id=schedule.id, + task_hash=task.p_item.hash, + scheduler_id=self.scheduler_id, + ) + schedule.enabled = False + + schedule.evaluated_at = datetime.now(timezone.utc) + self.ctx.datastores.schedule_store.update_schedule(schedule) + + self.executor.submit(_calculate_deadline, task) + + def enable(self) -> None: + """Enable the scheduler. + + This will start the scheduler, and start all listeners and threads. """ - if (self.queue.maxsize - self.queue.qsize()) <= 0 and self.queue.maxsize != 0: - return False + if self.is_enabled(): + self.logger.debug("Scheduler is already enabled") + return - return True + self.logger.info("Enabling scheduler: %s", self.scheduler_id, scheduler_id=self.scheduler_id) + self.enabled = True - def is_item_on_queue_by_hash(self, item_hash: str) -> bool: - return self.queue.is_item_on_queue_by_hash(item_hash) + self.stop_event_threads.clear() + + self.run() + + self.logger.info("Enabled scheduler: %s", self.scheduler_id, scheduler_id=self.scheduler_id) def disable(self) -> None: """Disable the scheduler. @@ -387,32 +433,6 @@ def disable(self) -> None: self.logger.info("Disabled scheduler: %s", self.scheduler_id, scheduler_id=self.scheduler_id) - def enable(self) -> None: - """Enable the scheduler. - - This will start the scheduler, and start all listeners and threads. - """ - if self.is_enabled(): - self.logger.debug("Scheduler is already enabled") - return - - self.logger.info("Enabling scheduler: %s", self.scheduler_id, scheduler_id=self.scheduler_id) - self.enabled = True - - self.stop_event_threads.clear() - - self.run() - - self.logger.info("Enabled scheduler: %s", self.scheduler_id, scheduler_id=self.scheduler_id) - - def is_enabled(self) -> bool: - """Check if the scheduler is enabled. - - Returns: - True if the scheduler is enabled, False otherwise. - """ - return self.enabled - def stop(self, callback: bool = True) -> None: """Stop the scheduler. @@ -446,6 +466,30 @@ def stop_threads(self) -> None: self.threads = [] + def is_enabled(self) -> bool: + """Check if the scheduler is enabled. + + Returns: + True if the scheduler is enabled, False otherwise. + """ + return self.enabled + + def is_space_on_queue(self) -> bool: + """Check if there is space on the queue. + + NOTE: maxsize 0 means unlimited + + Returns: + True if there is space on the queue, False otherwise. + """ + if (self.queue.maxsize - self.queue.qsize()) <= 0 and self.queue.maxsize != 0: + return False + + return True + + def is_item_on_queue_by_hash(self, item_hash: str) -> bool: + return self.queue.is_item_on_queue_by_hash(item_hash) + @property def last_activity(self) -> datetime | None: """Get the last activity of the scheduler.""" diff --git a/mula/scheduler/server/server.py b/mula/scheduler/server/server.py index 112f29f3f1e..0ab92ee4dfa 100644 --- a/mula/scheduler/server/server.py +++ b/mula/scheduler/server/server.py @@ -5,7 +5,7 @@ import prometheus_client import structlog import uvicorn -from fastapi import status +from fastapi import BackgroundTasks, status from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor @@ -17,6 +17,7 @@ from scheduler import context, models, queues, schedulers, storage, version from scheduler.config import settings +from scheduler.models.errors import ValidationError from .pagination import PaginatedResponse, paginate @@ -128,6 +129,41 @@ def __init__( description="List all tasks for a scheduler", ) + self.api.add_api_route( + path="/schedules", + endpoint=self.list_schedules, + methods=["GET", "POST"], + response_model=PaginatedResponse | models.Schedule, + status_code=status.HTTP_200_OK, + description="List all schedules", + ) + + self.api.add_api_route( + path="/schedules/{schedule_id}", + endpoint=self.get_schedule, + methods=["GET"], + response_model=models.Schedule, + status_code=status.HTTP_200_OK, + description="Get a schedule", + ) + + self.api.add_api_route( + path="/schedules/{schedule_id}", + endpoint=self.patch_schedule, + methods=["PATCH"], + response_model=models.Schedule, + status_code=status.HTTP_200_OK, + description="Update a schedule", + ) + + self.api.add_api_route( + path="/schedules/{schedule_id}", + endpoint=self.delete_schedule, + methods=["DELETE"], + status_code=status.HTTP_204_NO_CONTENT, + description="Delete a schedule", + ) + self.api.add_api_route( path="/tasks", endpoint=self.list_tasks, @@ -157,7 +193,7 @@ def __init__( path="/tasks/{task_id}", endpoint=self.get_task, methods=["GET"], - response_model=models.Task, + response_model=models.TaskRun, status_code=status.HTTP_200_OK, description="Get a task", ) @@ -166,7 +202,7 @@ def __init__( path="/tasks/{task_id}", endpoint=self.patch_task, methods=["PATCH"], - response_model=models.Task, + response_model=models.TaskRun, status_code=status.HTTP_200_OK, description="Update a task", ) @@ -279,6 +315,172 @@ def patch_scheduler(self, scheduler_id: str, item: models.Scheduler) -> Any: return updated_scheduler + def list_schedules( + self, + request: fastapi.Request, + scheduler_id: str | None = None, + enabled: bool | None = None, + min_deadline: datetime.datetime | None = None, + max_deadline: datetime.datetime | None = None, + offset: int = 0, + limit: int = 10, + filters: storage.filters.FilterRequest | None = None, + schedule: models.ScheduleRequest | None = None, + ) -> Any: + if schedule is not None and request.method == "POST": + created_schedule = fastapi.encoders.jsonable_encoder(self.create_schedule(schedule)) + + return fastapi.responses.JSONResponse( + status_code=status.HTTP_201_CREATED, + content=created_schedule, + ) + + if (min_deadline is not None and max_deadline is not None) and min_deadline > max_deadline: + raise fastapi.HTTPException( + status_code=fastapi.status.HTTP_400_BAD_REQUEST, + detail="min_deadline must be less than max_deadline", + ) + + try: + results, count = self.ctx.datastores.schedule_store.get_schedules( + scheduler_id=scheduler_id, + enabled=enabled, + min_deadline=min_deadline, + max_deadline=max_deadline, + offset=offset, + limit=limit, + filters=filters, + ) + except storage.filters.errors.FilterError as exc: + raise fastapi.HTTPException( + status_code=fastapi.status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + except ValueError as exc: + raise fastapi.HTTPException( + status_code=fastapi.status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + except Exception as exc: + self.logger.exception(exc) + raise fastapi.HTTPException( + status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="failed to get tasks", + ) from exc + + return paginate(request, results, count=count, offset=offset, limit=limit) + + def create_schedule(self, schedule: models.ScheduleRequest) -> Any: + try: + schedule = models.Schedule(**schedule.dict()) + except Exception as exc: + raise fastapi.HTTPException( + status_code=fastapi.status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + + # Validate schedule + try: + schedule.validate() + except ValidationError as exc: + raise fastapi.HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) + + try: + created_schedule = self.ctx.datastores.schedule_store.create_schedule(schedule) + except Exception as exc: + self.logger.exception(exc) + raise fastapi.HTTPException( + status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="failed to create event", + ) from exc + + return created_schedule + + def get_schedule(self, schedule_id: str) -> Any: + try: + schedule = self.ctx.datastores.schedule_store.get_schedule_by_id(schedule_id) + except ValueError as exc: + raise fastapi.HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + except Exception as exc: + self.logger.exception(exc) + raise fastapi.HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="failed to get schedule", + ) from exc + + if schedule is None: + raise fastapi.HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="schedule not found", + ) + + return models.Schedule(**schedule.model_dump()) + + def patch_schedule(self, schedule_id: str, item: dict) -> Any: + if len(item) == 0: + raise fastapi.HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="no data to patch", + ) + + try: + schedule_db = self.ctx.datastores.schedule_store.get_schedule_by_id(schedule_id) + except Exception as exc: + raise fastapi.HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"failed to get schedule [exception: {exc}]", + ) from exc + + if schedule_db is None: + raise fastapi.HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="schedule not found", + ) + + updated_schedule = schedule_db.model_copy(update=item) + + # Validate schedule + try: + updated_schedule.validate() + except ValidationError as exc: + raise fastapi.HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) + + # Update schedule in database + try: + self.ctx.datastores.schedule_store.update_schedule(updated_schedule) + except Exception as exc: + self.logger.error(exc) + raise fastapi.HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="failed to update schedule", + ) from exc + + return updated_schedule + + def delete_schedule(self, schedule_id: str): + try: + self.ctx.datastores.schedule_store.delete_schedule(schedule_id) + except ValueError as exc: + raise fastapi.HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + except Exception as exc: + self.logger.exception(exc) + raise fastapi.HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="failed to delete schedule", + ) from exc + def list_tasks( self, request: fastapi.Request, @@ -435,9 +637,9 @@ def get_task(self, task_id: str) -> Any: detail="task not found", ) - return models.Task(**task.model_dump()) + return models.TaskRun(**task.model_dump()) - def patch_task(self, task_id: str, item: dict) -> Any: + def patch_task(self, task_id: str, item: dict, background_tasks: BackgroundTasks) -> Any: if len(item) == 0: raise fastapi.HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -470,6 +672,16 @@ def patch_task(self, task_id: str, item: dict) -> Any: detail="failed to update task", ) from exc + # Send signal event + s = self.schedulers.get(updated_task.scheduler_id) + if s is None: + raise fastapi.HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="scheduler not found", + ) + + background_tasks.add_task(s.signal_handler_task, updated_task) + return updated_task def get_task_stats(self, scheduler_id: str | None = None) -> dict[str, dict[str, int]] | None: diff --git a/mula/scheduler/storage/__init__.py b/mula/scheduler/storage/__init__.py index 4f6d7155872..a0edbdd7b65 100644 --- a/mula/scheduler/storage/__init__.py +++ b/mula/scheduler/storage/__init__.py @@ -1,4 +1,5 @@ from .filters import apply_filter from .pq_store import PriorityQueueStore +from .schedule_store import ScheduleStore from .storage import DBConn, retry from .task_store import TaskStore diff --git a/mula/scheduler/storage/schedule_store.py b/mula/scheduler/storage/schedule_store.py new file mode 100644 index 00000000000..327d7b254df --- /dev/null +++ b/mula/scheduler/storage/schedule_store.py @@ -0,0 +1,110 @@ +from datetime import datetime + +from sqlalchemy import exc + +from scheduler import models + +from .filters import FilterRequest, apply_filter +from .storage import DBConn, retry + + +class ScheduleStore: + name: str = "schedule_store" + + def __init__(self, dbconn: DBConn) -> None: + self.dbconn = dbconn + + @retry() + def get_schedules( + self, + scheduler_id: str, + enabled: bool | None = None, + min_deadline: datetime | None = None, + max_deadline: datetime | None = None, + filters: FilterRequest | None = None, + offset: int | None = 0, + limit: int | None = 100, + ) -> tuple[list[models.Schedule], int]: + with self.dbconn.session.begin() as session: + query = session.query(models.ScheduleDB) + + if scheduler_id is not None: + query = query.filter(models.ScheduleDB.scheduler_id == scheduler_id) + + if enabled is not None: + query = query.filter(models.ScheduleDB.enabled == enabled) + + if min_deadline is not None: + query = query.filter(models.ScheduleDB.deadline_at >= min_deadline) + + if max_deadline is not None: + query = query.filter(models.ScheduleDB.deadline_at <= max_deadline) + + if filters is not None: + query = apply_filter(models.ScheduleDB, query, filters) + + try: + count = query.count() + schedules_orm = query.order_by(models.ScheduleDB.created_at.desc()).offset(offset).limit(limit).all() + except exc.ProgrammingError as e: + raise ValueError(f"Invalid filter: {e}") from e + + schedules = [models.Schedule.model_validate(schedule_orm) for schedule_orm in schedules_orm] + + return schedules, count + + @retry() + def get_schedule_by_id(self, schedule_id: str) -> models.Schedule | None: + with self.dbconn.session.begin() as session: + schedule_orm = session.query(models.ScheduleDB).filter(models.ScheduleDB.id == schedule_id).first() + if schedule_orm is None: + return None + + schedule = models.Schedule.model_validate(schedule_orm) + + return schedule + + @retry() + def get_schedule_by_hash(self, schedule_hash: str) -> models.Schedule | None: + with self.dbconn.session.begin() as session: + schedule_orm = ( + session.query(models.ScheduleDB) + .filter(models.ScheduleDB.p_item["hash"].as_string() == schedule_hash) + .first() + ) + + if schedule_orm is None: + return None + + schedule = models.Schedule.model_validate(schedule_orm) + + return schedule + + @retry() + def create_schedule(self, schedule: models.Schedule) -> models.Schedule | None: + with self.dbconn.session.begin() as session: + schedule_orm = models.ScheduleDB(**schedule.model_dump(exclude={"tasks"})) + session.add(schedule_orm) + + created_schedule = models.Schedule.model_validate(schedule_orm) + + return created_schedule + + @retry() + def update_schedule(self, schedule: models.Schedule) -> None: + with self.dbconn.session.begin() as session: + ( + session.query(models.ScheduleDB) + .filter(models.ScheduleDB.id == schedule.id) + .update(schedule.model_dump(exclude={"tasks"})) + ) + + @retry() + def update_schedule_enabled(self, schedule_id: str, enabled: bool) -> None: + with self.dbconn.session.begin() as session: + (session.query(models.ScheduleDB).filter(models.ScheduleDB.id == schedule_id).update({"enabled": enabled})) + + @retry() + def delete_schedule(self, schedule_id: str) -> None: + with self.dbconn.session.begin() as session: + session.query(models.ScheduleDB).filter(models.ScheduleDB.id == schedule_id).delete() diff --git a/mula/scheduler/storage/task_store.py b/mula/scheduler/storage/task_store.py index c70842652c3..9bbd3e8ea65 100644 --- a/mula/scheduler/storage/task_store.py +++ b/mula/scheduler/storage/task_store.py @@ -25,103 +25,103 @@ def get_tasks( filters: FilterRequest | None = None, offset: int = 0, limit: int = 100, - ) -> tuple[list[models.Task], int]: + ) -> tuple[list[models.TaskRun], int]: with self.dbconn.session.begin() as session: - query = session.query(models.TaskDB) + query = session.query(models.TaskRunDB) if scheduler_id is not None: - query = query.filter(models.TaskDB.scheduler_id == scheduler_id) + query = query.filter(models.TaskRunDB.scheduler_id == scheduler_id) if task_type is not None: - query = query.filter(models.TaskDB.type == task_type) + query = query.filter(models.TaskRunDB.type == task_type) if status is not None: - query = query.filter(models.TaskDB.status == models.TaskStatus(status).name) + query = query.filter(models.TaskRunDB.status == models.TaskStatus(status).name) if min_created_at is not None: - query = query.filter(models.TaskDB.created_at >= min_created_at) + query = query.filter(models.TaskRunDB.created_at >= min_created_at) if max_created_at is not None: - query = query.filter(models.TaskDB.created_at <= max_created_at) + query = query.filter(models.TaskRunDB.created_at <= max_created_at) if filters is not None: - query = apply_filter(models.TaskDB, query, filters) + query = apply_filter(models.TaskRunDB, query, filters) try: count = query.count() - tasks_orm = query.order_by(models.TaskDB.created_at.desc()).offset(offset).limit(limit).all() + tasks_orm = query.order_by(models.TaskRunDB.created_at.desc()).offset(offset).limit(limit).all() except exc.ProgrammingError as e: raise ValueError(f"Invalid filter: {e}") from e - tasks = [models.Task.model_validate(task_orm) for task_orm in tasks_orm] + tasks = [models.TaskRun.model_validate(task_orm) for task_orm in tasks_orm] return tasks, count @retry() - def get_task_by_id(self, task_id: str) -> models.Task | None: + def get_task_by_id(self, task_id: str) -> models.TaskRun | None: with self.dbconn.session.begin() as session: - task_orm = session.query(models.TaskDB).filter(models.TaskDB.id == task_id).first() + task_orm = session.query(models.TaskRunDB).filter(models.TaskRunDB.id == task_id).first() if task_orm is None: return None - task = models.Task.model_validate(task_orm) + task = models.TaskRun.model_validate(task_orm) return task @retry() - def get_tasks_by_hash(self, task_hash: str) -> list[models.Task] | None: + def get_tasks_by_hash(self, task_hash: str) -> list[models.TaskRun] | None: with self.dbconn.session.begin() as session: tasks_orm = ( - session.query(models.TaskDB) - .filter(models.TaskDB.p_item["hash"].as_string() == task_hash) - .order_by(models.TaskDB.created_at.desc()) + session.query(models.TaskRunDB) + .filter(models.TaskRunDB.p_item["hash"].as_string() == task_hash) + .order_by(models.TaskRunDB.created_at.desc()) .all() ) if tasks_orm is None: return None - tasks = [models.Task.model_validate(task_orm) for task_orm in tasks_orm] + tasks = [models.TaskRun.model_validate(task_orm) for task_orm in tasks_orm] return tasks @retry() - def get_latest_task_by_hash(self, task_hash: str) -> models.Task | None: + def get_latest_task_by_hash(self, task_hash: str) -> models.TaskRun | None: with self.dbconn.session.begin() as session: task_orm = ( - session.query(models.TaskDB) - .filter(models.TaskDB.p_item["hash"].as_string() == task_hash) - .order_by(models.TaskDB.created_at.desc()) + session.query(models.TaskRunDB) + .filter(models.TaskRunDB.p_item["hash"].as_string() == task_hash) + .order_by(models.TaskRunDB.created_at.desc()) .first() ) if task_orm is None: return None - task = models.Task.model_validate(task_orm) + task = models.TaskRun.model_validate(task_orm) return task @retry() - def create_task(self, task: models.Task) -> models.Task | None: + def create_task(self, task: models.TaskRun) -> models.TaskRun | None: with self.dbconn.session.begin() as session: - task_orm = models.TaskDB(**task.model_dump()) + task_orm = models.TaskRunDB(**task.model_dump()) session.add(task_orm) - created_task = models.Task.model_validate(task_orm) + created_task = models.TaskRun.model_validate(task_orm) return created_task @retry() - def update_task(self, task: models.Task) -> None: + def update_task(self, task: models.TaskRun) -> None: with self.dbconn.session.begin() as session: - (session.query(models.TaskDB).filter(models.TaskDB.id == task.id).update(task.model_dump())) + (session.query(models.TaskRunDB).filter(models.TaskRunDB.id == task.id).update(task.model_dump())) @retry() def cancel_tasks(self, scheduler_id: str, task_ids: list[str]) -> None: with self.dbconn.session.begin() as session: - session.query(models.TaskDB).filter( - models.TaskDB.scheduler_id == scheduler_id, models.TaskDB.id.in_(task_ids) + session.query(models.TaskRunDB).filter( + models.TaskRunDB.scheduler_id == scheduler_id, models.TaskRunDB.id.in_(task_ids) ).update({"status": models.TaskStatus.CANCELLED.name}) @retry() @@ -132,19 +132,19 @@ def get_status_count_per_hour( with self.dbconn.session.begin() as session: query = ( session.query( - func.DATE_TRUNC("hour", models.TaskDB.modified_at).label("hour"), - models.TaskDB.status, - func.count(models.TaskDB.id).label("count"), + func.DATE_TRUNC("hour", models.TaskRunDB.modified_at).label("hour"), + models.TaskRunDB.status, + func.count(models.TaskRunDB.id).label("count"), ) .filter( - models.TaskDB.modified_at >= datetime.now(timezone.utc) - timedelta(hours=24), + models.TaskRunDB.modified_at >= datetime.now(timezone.utc) - timedelta(hours=24), ) - .group_by("hour", models.TaskDB.status) - .order_by("hour", models.TaskDB.status) + .group_by("hour", models.TaskRunDB.status) + .order_by("hour", models.TaskRunDB.status) ) if scheduler_id is not None: - query = query.filter(models.TaskDB.scheduler_id == scheduler_id) + query = query.filter(models.TaskRunDB.scheduler_id == scheduler_id) results = query.all() @@ -162,13 +162,13 @@ def get_status_count_per_hour( def get_status_counts(self, scheduler_id: str | None = None) -> dict[str, int] | None: with self.dbconn.session.begin() as session: query = ( - session.query(models.TaskDB.status, func.count(models.TaskDB.id).label("count")) - .group_by(models.TaskDB.status) - .order_by(models.TaskDB.status) + session.query(models.TaskRunDB.status, func.count(models.TaskRunDB.id).label("count")) + .group_by(models.TaskRunDB.status) + .order_by(models.TaskRunDB.status) ) if scheduler_id is not None: - query = query.filter(models.TaskDB.scheduler_id == scheduler_id) + query = query.filter(models.TaskRunDB.scheduler_id == scheduler_id) results = query.all() diff --git a/mula/scheduler/utils/cron.py b/mula/scheduler/utils/cron.py new file mode 100644 index 00000000000..7dcf7cea0d0 --- /dev/null +++ b/mula/scheduler/utils/cron.py @@ -0,0 +1,8 @@ +from datetime import datetime, timezone + +from croniter import croniter + + +def next_run(expression: str, start_time: datetime = datetime.now(timezone.utc)): + cron = croniter(expression, start_time) + return cron.get_next(datetime) diff --git a/mula/tests/integration/test_api.py b/mula/tests/integration/test_api.py index 6b7c29b0e68..ab833893659 100644 --- a/mula/tests/integration/test_api.py +++ b/mula/tests/integration/test_api.py @@ -1,4 +1,5 @@ import copy +import json import unittest import uuid from datetime import datetime, timedelta, timezone @@ -11,7 +12,7 @@ from tests.factories import OrganisationFactory from tests.mocks import queue as mock_queue from tests.mocks import scheduler as mock_scheduler -from tests.utils import functions +from tests.utils import UUIDEncoder, functions from tests.utils.functions import create_p_item_request @@ -30,6 +31,7 @@ def setUp(self): **{ storage.TaskStore.name: storage.TaskStore(self.dbconn), storage.PriorityQueueStore.name: storage.PriorityQueueStore(self.dbconn), + storage.ScheduleStore.name: storage.ScheduleStore(self.dbconn), } ) @@ -134,10 +136,22 @@ def test_push_queue(self): item = create_p_item_request(1) - response = self.client.post(f"/queues/{self.scheduler.scheduler_id}/push", data=item.model_dump_json()) - self.assertEqual(response.status_code, 201) + response_post = self.client.post(f"/queues/{self.scheduler.scheduler_id}/push", data=item.model_dump_json()) + self.assertEqual(201, response_post.status_code) self.assertEqual(1, self.scheduler.queue.qsize()) - self.assertIsNotNone(response.json().get("id")) + self.assertIsNotNone(response_post.json().get("id")) + + # Task should be created + response_get_task = self.client.get(f"/tasks/{response_post.json().get('id')}") + self.assertEqual(200, response_get_task.status_code) + self.assertEqual(response_post.json().get("id"), response_get_task.json().get("id")) + + # Schedule should be created + response_get_schedule = self.client.get(f"/schedules?hash{response_post.json().get('hash')}") + self.assertEqual(200, response_get_schedule.status_code) + self.assertEqual( + response_post.json().get("hash"), response_get_schedule.json().get("results")[0].get("p_item").get("hash") + ) def test_push_incorrect_item_type(self): response = self.client.post( @@ -483,6 +497,18 @@ def test_create_task(self): response_get = self.client.get(f"/tasks/{initial_item_id}") self.assertEqual(200, response_get.status_code) + # Task should be created + response_get_task = self.client.get(f"/tasks/{initial_item_id}") + self.assertEqual(200, response_get_task.status_code) + self.assertEqual(initial_item_id, response_get_task.json().get("id")) + + # Schedule should be created + response_get_schedule = self.client.get(f"/schedules?hash{response_post.json().get('hash')}") + self.assertEqual(200, response_get_schedule.status_code) + self.assertEqual( + response_post.json().get("hash"), response_get_schedule.json().get("results")[0].get("p_item").get("hash") + ) + def test_get_tasks(self): response = self.client.get("/tasks") self.assertEqual(200, response.status_code) @@ -623,3 +649,187 @@ def test_get_tasks_stats(self): response = self.client.get(f"/tasks/stats/{self.first_item_api.get('scheduler_id')}") self.assertEqual(200, response.status_code) + + +class APISchedulesEndpointTestCase(APITemplateTestCase): + def setUp(self): + super().setUp() + + first_schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + p_item=functions.create_p_item("test_scheduler_id", 1), + deadline_at=datetime.now(timezone.utc) + timedelta(days=1), + ) + + response = self.client.post( + "/schedules", data=json.dumps({"schedule": first_schedule.model_dump()}, cls=UUIDEncoder, default=str) + ) + self.assertEqual(201, response.status_code) + first_schedule_id = response.json().get("id") + + self.first_schedule_api = self.client.get(f"/schedules/{first_schedule_id}").json() + + second_schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + p_item=functions.create_p_item("test_scheduler_id", 1), + deadline_at=datetime.now(timezone.utc) + timedelta(days=2), + ) + + response = self.client.post( + "/schedules", data=json.dumps({"schedule": second_schedule.model_dump()}, cls=UUIDEncoder, default=str) + ) + self.assertEqual(201, response.status_code) + second_schedule_id = response.json().get("id") + + self.second_schedule_api = self.client.get(f"/schedules/{second_schedule_id}").json() + + def test_create_schedule(self): + # Arrange + schedule = { + "schedule": models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + p_item=functions.create_p_item("test_scheduler_id", 1), + ).model_dump() + } + + # Act + response_post = self.client.post("/schedules", data=json.dumps(schedule, cls=UUIDEncoder, default=str)) + response_get = self.client.get(f"/schedules/{response_post.json().get('id')}") + + # Assert + self.assertEqual(201, response_post.status_code) + self.assertEqual(200, response_get.status_code) + + def test_create_schedule_validate_malformed_cron_expresssion(self): + # Arrange + cron_expression = "malformed" + + schedule = { + "schedule": models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + p_item=functions.create_p_item("test_scheduler_id", 1), + cron_expression=cron_expression, + ).model_dump() + } + + # Act + response_post = self.client.post("/schedules", data=json.dumps(schedule, cls=UUIDEncoder, default=str)) + + # Assert + self.assertEqual(400, response_post.status_code) + self.assertTrue(response_post.json().get("detail").startswith("Invalid cron expression")) + + def test_list_schedules(self): + response = self.client.get("/schedules") + self.assertEqual(200, response.status_code) + self.assertEqual(2, response.json()["count"]) + self.assertEqual(2, len(response.json()["results"])) + + def test_list_schedules_scheduler_id(self): + response = self.client.get(f"/schedules?scheduler_id={self.scheduler.scheduler_id}") + self.assertEqual(200, response.status_code) + self.assertEqual(2, response.json()["count"]) + self.assertEqual(2, len(response.json()["results"])) + self.assertEqual(self.first_schedule_api.get("scheduler_id"), response.json()["results"][0]["scheduler_id"]) + + def test_list_schedules_enabled(self): + response = self.client.get("/schedules?enabled=true") + self.assertEqual(200, response.status_code) + self.assertEqual(2, response.json()["count"]) + self.assertEqual(2, len(response.json()["results"])) + + response = self.client.get("/schedules?enabled=false") + self.assertEqual(200, response.status_code) + self.assertEqual(0, response.json()["count"]) + self.assertEqual(0, len(response.json()["results"])) + + def test_list_schedules_min_deadline(self): + response = self.client.get(f"/schedules?min_deadline={self.first_schedule_api.get('deadline_at')}") + self.assertEqual(200, response.status_code) + self.assertEqual(2, response.json()["count"]) + self.assertEqual(2, len(response.json()["results"])) + + response = self.client.get(f"/schedules?min_deadline={self.second_schedule_api.get('deadline_at')}") + self.assertEqual(200, response.status_code) + self.assertEqual(1, response.json()["count"]) + self.assertEqual(1, len(response.json()["results"])) + self.assertEqual(self.second_schedule_api.get("id"), response.json()["results"][0]["id"]) + + def test_list_schedules_max_deadline(self): + response = self.client.get(f"/schedules?max_deadline={self.second_schedule_api.get('deadline_at')}") + self.assertEqual(200, response.status_code) + self.assertEqual(2, response.json()["count"]) + self.assertEqual(2, len(response.json()["results"])) + + response = self.client.get(f"/schedules?max_deadline={self.first_schedule_api.get('deadline_at')}") + self.assertEqual(200, response.status_code) + self.assertEqual(1, response.json()["count"]) + self.assertEqual(1, len(response.json()["results"])) + self.assertEqual(self.first_schedule_api.get("id"), response.json()["results"][0]["id"]) + + def test_list_schedules_min_and_max_deadline(self): + response = self.client.get( + f"/schedules?min_deadline={self.first_schedule_api.get('deadline_at')}&max_deadline={self.second_schedule_api.get('deadline_at')}" + ) + self.assertEqual(200, response.status_code) + self.assertEqual(2, response.json()["count"]) + self.assertEqual(2, len(response.json()["results"])) + + response = self.client.get( + f"/schedules?min_deadline={self.first_schedule_api.get('deadline_at')}&max_deadline={self.first_schedule_api.get('deadline_at')}" + ) + self.assertEqual(200, response.status_code) + self.assertEqual(1, response.json()["count"]) + self.assertEqual(1, len(response.json()["results"])) + self.assertEqual(self.first_schedule_api.get("id"), response.json()["results"][0]["id"]) + + def test_list_schedules_min_greater_than_max_deadline(self): + response = self.client.get( + f"/schedules?min_deadline={self.second_schedule_api.get('deadline_at')}&max_deadline={self.first_schedule_api.get('deadline_at')}" + ) + self.assertEqual(400, response.status_code) + self.assertEqual("min_deadline must be less than max_deadline", response.json().get("detail")) + + def test_get_schedules_filter(self): + response = self.client.post( + "/schedules", + json={ + "filters": { + "filters": [ + { + "column": "p_item", + "field": "id", + "operator": "eq", + "value": self.first_schedule_api.get("p_item").get("id"), + } + ] + } + }, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(1, len(response.json()["results"])) + + def test_get_schedule(self): + response = self.client.get(f"/schedules/{self.first_schedule_api.get('id')}") + self.assertEqual(200, response.status_code) + self.assertEqual(self.first_schedule_api.get("id"), response.json().get("id")) + + def test_patch_schedule(self): + response = self.client.patch(f"/schedules/{self.first_schedule_api.get('id')}", json={"enabled": False}) + self.assertEqual(200, response.status_code) + self.assertEqual(False, response.json().get("enabled")) + + def test_patch_schedule_validate_malformed_cron_expression(self): + response = self.client.patch( + f"/schedules/{self.first_schedule_api.get('id')}", json={"cron_expression": "malformed"} + ) + self.assertEqual(400, response.status_code) + self.assertTrue(response.json().get("detail").startswith("Invalid cron expression")) + + def test_delete_schedule(self): + response = self.client.delete(f"/schedules/{self.first_schedule_api.get('id')}") + self.assertEqual(204, response.status_code) + + response = self.client.get(f"/schedules/{self.first_schedule_api.get('id')}") + self.assertEqual(404, response.status_code) + self.assertEqual("schedule not found", response.json().get("detail")) diff --git a/mula/tests/integration/test_boefje_scheduler.py b/mula/tests/integration/test_boefje_scheduler.py index 22daf43c713..343882f0893 100644 --- a/mula/tests/integration/test_boefje_scheduler.py +++ b/mula/tests/integration/test_boefje_scheduler.py @@ -61,6 +61,7 @@ def setUp(self): **{ storage.TaskStore.name: storage.TaskStore(self.dbconn), storage.PriorityQueueStore.name: storage.PriorityQueueStore(self.dbconn), + storage.ScheduleStore.name: storage.ScheduleStore(self.dbconn), } ) @@ -90,6 +91,9 @@ def setUp(self): "scheduler.context.AppContext.services.bytes.get_last_run_boefje" ).start() + def tearDown(self): + mock.patch.stopall() + def test_is_allowed_to_run(self): # Arrange scan_profile = ScanProfileFactory(level=0) @@ -102,6 +106,18 @@ def test_is_allowed_to_run(self): # Assert self.assertTrue(allowed_to_run) + def test_is_allowed_to_run_no_ooi(self): + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + boefje = PluginFactory(scan_level=0, consumes=[ooi.object_type]) + + # Act + allowed_to_run = self.scheduler.is_task_allowed_to_run(ooi=None, boefje=boefje) + + # Assert + self.assertTrue(allowed_to_run) + def test_is_not_allowed_to_run(self): # Arrange scan_profile = ScanProfileFactory(level=0) @@ -162,7 +178,7 @@ def test_is_task_running_datastore_running(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -204,7 +220,7 @@ def test_is_task_running_datastore_not_running(self): hash=task.hash, ) - task_db_first = models.Task( + task_db_first = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -214,7 +230,7 @@ def test_is_task_running_datastore_not_running(self): modified_at=datetime.now(timezone.utc), ) - task_db_second = models.Task( + task_db_second = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -357,7 +373,7 @@ def test_is_task_running_stalled_before_grace_period(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -392,7 +408,7 @@ def test_is_task_running_stalled_after_grace_period(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -430,7 +446,7 @@ def test_is_task_running_mismatch_before_grace_period(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -471,7 +487,7 @@ def test_is_task_running_mismatch_after_grace_period(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -507,7 +523,7 @@ def test_has_grace_period_passed_datastore_passed(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -546,7 +562,7 @@ def test_has_grace_period_passed_datastore_not_passed(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -585,7 +601,7 @@ def test_has_grace_period_passed_bytes_passed(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -630,7 +646,7 @@ def test_has_grace_period_passed_bytes_not_passed(self): hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -806,6 +822,10 @@ def test_post_push(self): self.assertEqual(task_db.id, p_item.id) self.assertEqual(task_db.status, models.TaskStatus.QUEUED) + # Schedule should be in datastore + schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_hash(task.hash) + self.assertEqual(schedule_db.p_item.hash, task.hash) + def test_post_pop(self): """When a task is removed from the queue, its status should be updated""" # Arrange @@ -997,7 +1017,7 @@ def test_is_task_allowed_to_run_boefje_scan_level_is_none(self): self.assertFalse(is_allowed) -class ScanProfileTestCase(BoefjeSchedulerBaseTestCase): +class ScanProfileMutationTestCase(BoefjeSchedulerBaseTestCase): def setUp(self): super().setUp() @@ -1020,6 +1040,9 @@ def setUp(self): "scheduler.schedulers.BoefjeScheduler.get_boefjes_for_ooi", ).start() + def tearDown(self): + mock.patch.stopall() + def test_push_tasks_for_scan_profile_mutations(self): """Scan level change""" # Arrange @@ -1256,6 +1279,9 @@ def setUp(self): "scheduler.context.AppContext.services.octopoes.get_objects_by_object_types" ).start() + def tearDown(self): + mock.patch.stopall() + def test_push_tasks_for_new_boefjes(self): # Arrange scan_profile = ScanProfileFactory(level=0) @@ -1442,6 +1468,9 @@ def setUp(self): "scheduler.context.AppContext.services.octopoes.get_random_objects" ).start() + def tearDown(self): + mock.patch.stopall() + def test_push_tasks_for_random_objects(self): # Arrange scan_profile = ScanProfileFactory(level=0) @@ -1538,7 +1567,7 @@ def test_push_tasks_for_random_objects_prior_tasks(self, mock_get_tasks_by_hash) hash=task.hash, ) - task_db = models.Task( + task_db = models.TaskRun( id=p_item.id, scheduler_id=self.scheduler.scheduler_id, type="boefje", @@ -1640,3 +1669,353 @@ def test_push_tasks_for_random_objects_item_on_queue(self): task_db = self.mock_ctx.datastores.task_store.get_task_by_id(task_pq.id) self.assertEqual(task_db.id, task_pq.id) self.assertEqual(task_db.status, models.TaskStatus.QUEUED) + + +class RescheduleTestCase(BoefjeSchedulerBaseTestCase): + def setUp(self): + super().setUp() + + self.mock_is_task_running = mock.patch( + "scheduler.schedulers.BoefjeScheduler.is_task_running", + return_value=False, + ).start() + + self.mock_has_grace_period_passed = mock.patch( + "scheduler.schedulers.BoefjeScheduler.has_grace_period_passed", + return_value=True, + ).start() + + self.mock_get_schedules = mock.patch( + "scheduler.context.AppContext.datastores.schedule_store.get_schedules", + ).start() + + self.mock_get_object = mock.patch( + "scheduler.context.AppContext.services.octopoes.get_object", + ).start() + + self.mock_get_plugin = mock.patch( + "scheduler.context.AppContext.services.katalogus.get_plugin_by_id_and_org_id", + ).start() + + def tearDown(self): + mock.patch.stopall() + + def test_push_tasks_for_rescheduling(self): + """When the dealine of schedules have passed, the resulting task should be added to the queue""" + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + plugin = PluginFactory(scan_level=0, consumes=[ooi.object_type]) + + task = models.BoefjeTask( + boefje=models.Boefje.parse_obj(plugin.model_dump()), + input_ooi=ooi.primary_key, + organization=self.organisation.id, + ) + + p_item = models.PrioritizedItem( + id=task.id, + scheduler_id=self.scheduler.scheduler_id, + priority=1, + data=task.model_dump(), + hash=task.hash, + ) + + schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + hash=task.hash, + p_item=p_item, + ) + + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Mocks + self.mock_get_schedules.return_value = ([schedule], 1) + self.mock_get_object.return_value = ooi + self.mock_get_plugin.return_value = plugin + + # Act + self.scheduler.push_tasks_for_rescheduling() + + # Assert: new item should be on queue + self.assertEqual(1, self.scheduler.queue.qsize()) + + # Assert: new item is created with a similar task + peek = self.scheduler.queue.peek(0) + self.assertEqual(schedule_db.p_item.hash, peek.hash) + + # Assert: task should be created, and should be the one that is queued + task_db = self.mock_ctx.datastores.task_store.get_task_by_id(peek.id) + self.assertIsNotNone(task_db) + self.assertEqual(peek.id, task_db.id) + + def test_push_tasks_for_rescheduling_no_ooi(self): + """When the deadline has passed, and when the resulting tasks doesn't + have an OOI, it should create a task. + """ + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + plugin = PluginFactory(scan_level=0, consumes=[ooi.object_type]) + + task = models.BoefjeTask( + boefje=models.Boefje.parse_obj(plugin.model_dump()), + input_ooi=None, + organization=self.organisation.id, + ) + + p_item = models.PrioritizedItem( + id=task.id, + scheduler_id=self.scheduler.scheduler_id, + priority=1, + data=task.model_dump(), + hash=task.hash, + ) + + schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + hash=task.hash, + p_item=p_item, + ) + + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Mocks + self.mock_get_schedules.return_value = ([schedule], 1) + self.mock_get_object.return_value = ooi + self.mock_get_plugin.return_value = plugin + + # Act + self.scheduler.push_tasks_for_rescheduling() + + # Assert: new item should be on queue + self.assertEqual(1, self.scheduler.queue.qsize()) + + # Assert: new item is created with a similar task + peek = self.scheduler.queue.peek(0) + self.assertEqual(schedule_db.p_item.hash, peek.hash) + + # Assert: task should be created, and should be the one that is queued + task_db = self.mock_ctx.datastores.task_store.get_task_by_id(peek.id) + self.assertIsNotNone(task_db) + self.assertEqual(peek.id, task_db.id) + + def test_push_tasks_for_rescheduling_ooi_not_found(self): + """When ooi isn't found anymore for the schedule, we disable the schedule""" + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + plugin = PluginFactory(scan_level=0, consumes=[ooi.object_type]) + + task = models.BoefjeTask( + boefje=models.Boefje.parse_obj(plugin.model_dump()), + input_ooi=ooi.primary_key, + organization=self.organisation.id, + ) + + p_item = models.PrioritizedItem( + id=task.id, + scheduler_id=self.scheduler.scheduler_id, + priority=1, + data=task.model_dump(), + hash=task.hash, + ) + + schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + hash=task.hash, + p_item=p_item, + ) + + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Mocks + self.mock_get_schedules.return_value = ([schedule], 1) + self.mock_get_object.return_value = None + self.mock_get_plugin.return_value = plugin + + # Act + self.scheduler.push_tasks_for_rescheduling() + + # Assert: item should not be on queue + self.assertEqual(0, self.scheduler.queue.qsize()) + + # Assert: schedule should be disabled + schedule_db_disabled = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertFalse(schedule_db_disabled.enabled) + + def test_push_tasks_for_rescheduling_boefje_not_found(self): + """When boefje isn't found anymore for the schedule, we disable the schedule""" + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + plugin = PluginFactory(scan_level=0, consumes=[ooi.object_type]) + + task = models.BoefjeTask( + boefje=models.Boefje.parse_obj(plugin.model_dump()), + input_ooi=ooi.primary_key, + organization=self.organisation.id, + ) + + p_item = models.PrioritizedItem( + id=task.id, + scheduler_id=self.scheduler.scheduler_id, + priority=1, + data=task.model_dump(), + hash=task.hash, + ) + + schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + hash=task.hash, + p_item=p_item, + ) + + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Mocks + self.mock_get_schedules.return_value = ([schedule], 1) + self.mock_get_object.return_value = ooi + self.mock_get_plugin.return_value = None + + # Act + self.scheduler.push_tasks_for_rescheduling() + + # Assert: item should not be on queue + self.assertEqual(0, self.scheduler.queue.qsize()) + + # Assert: schedule should be disabled + schedule_db_disabled = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertFalse(schedule_db_disabled.enabled) + + def test_push_tasks_for_rescheduling_boefje_disabled(self): + """When boefje disabled for the schedule, we disable the schedule""" + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + plugin = PluginFactory(scan_level=0, consumes=[ooi.object_type], enabled=False) + + task = models.BoefjeTask( + boefje=models.Boefje.parse_obj(plugin.model_dump()), + input_ooi=ooi.primary_key, + organization=self.organisation.id, + ) + + p_item = models.PrioritizedItem( + id=task.id, + scheduler_id=self.scheduler.scheduler_id, + priority=1, + data=task.model_dump(), + hash=task.hash, + ) + + schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + hash=task.hash, + p_item=p_item, + ) + + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Mocks + self.mock_get_schedules.return_value = ([schedule], 1) + self.mock_get_object.return_value = ooi + self.mock_get_plugin.return_value = plugin + + # Act + self.scheduler.push_tasks_for_rescheduling() + + # Assert: item should not be on queue + self.assertEqual(0, self.scheduler.queue.qsize()) + + # Assert: schedule should be disabled + schedule_db_disabled = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertFalse(schedule_db_disabled.enabled) + + def test_push_tasks_for_rescheduling_boefje_doesnt_consume_ooi(self): + """When boefje doesn't consume the ooi, we disable the schedule""" + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + plugin = PluginFactory(scan_level=0, consumes=[]) + + task = models.BoefjeTask( + boefje=models.Boefje.parse_obj(plugin.model_dump()), + input_ooi=ooi.primary_key, + organization=self.organisation.id, + ) + + p_item = models.PrioritizedItem( + id=task.id, + scheduler_id=self.scheduler.scheduler_id, + priority=1, + data=task.model_dump(), + hash=task.hash, + ) + + schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + hash=task.hash, + p_item=p_item, + ) + + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Mocks + self.mock_get_schedules.return_value = ([schedule], 1) + self.mock_get_object.return_value = ooi + self.mock_get_plugin.return_value = plugin + + # Act + self.scheduler.push_tasks_for_rescheduling() + + # Assert: item should not be on queue + self.assertEqual(0, self.scheduler.queue.qsize()) + + # Assert: schedule should be disabled + schedule_db_disabled = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertFalse(schedule_db_disabled.enabled) + + def test_push_tasks_for_rescheduling_boefje_cannot_scan_ooi(self): + """When boefje cannot scan the ooi, we disable the schedule""" + # Arrange + scan_profile = ScanProfileFactory(level=0) + ooi = OOIFactory(scan_profile=scan_profile) + plugin = PluginFactory(scan_level=1, consumes=[ooi.object_type]) + + task = models.BoefjeTask( + boefje=models.Boefje.parse_obj(plugin.model_dump()), + input_ooi=ooi.primary_key, + organization=self.organisation.id, + ) + + p_item = models.PrioritizedItem( + id=task.id, + scheduler_id=self.scheduler.scheduler_id, + priority=1, + data=task.model_dump(), + hash=task.hash, + ) + + schedule = models.Schedule( + scheduler_id=self.scheduler.scheduler_id, + hash=task.hash, + p_item=p_item, + ) + + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Mocks + self.mock_get_schedules.return_value = ([schedule], 1) + self.mock_get_object.return_value = ooi + self.mock_get_plugin.return_value = plugin + + # Act + self.scheduler.push_tasks_for_rescheduling() + + # Assert: item should not be on queue + self.assertEqual(0, self.scheduler.queue.qsize()) + + # Assert: schedule should be disabled + schedule_db_disabled = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertFalse(schedule_db_disabled.enabled) diff --git a/mula/tests/integration/test_normalizer_scheduler.py b/mula/tests/integration/test_normalizer_scheduler.py index 42dcac8eab7..b0e06c6fd8a 100644 --- a/mula/tests/integration/test_normalizer_scheduler.py +++ b/mula/tests/integration/test_normalizer_scheduler.py @@ -35,6 +35,7 @@ def setUp(self): **{ storage.TaskStore.name: storage.TaskStore(self.dbconn), storage.PriorityQueueStore.name: storage.PriorityQueueStore(self.dbconn), + storage.ScheduleStore.name: storage.ScheduleStore(self.dbconn), } ) @@ -202,6 +203,7 @@ def test_push_tasks_for_received_raw_file(self): organization=self.organisation.id, ) + # Arrange: create the BoefjeTask p_item = functions.create_p_item(scheduler_id=self.scheduler.scheduler_id, priority=1, data=boefje_task) task = functions.create_task(p_item) self.mock_ctx.datastores.task_store.create_task(task) @@ -212,6 +214,7 @@ def test_push_tasks_for_received_raw_file(self): input_ooi=ooi.primary_key, ) + # Arrange: create the RawDataReceivedEvent raw_data_event = models.RawDataReceivedEvent( raw_data=RawDataFactory( boefje_meta=boefje_meta, diff --git a/mula/tests/integration/test_ranker.py b/mula/tests/integration/test_ranker.py new file mode 100644 index 00000000000..19cdd040631 --- /dev/null +++ b/mula/tests/integration/test_ranker.py @@ -0,0 +1,55 @@ +import unittest +from types import SimpleNamespace +from unittest import mock + +from scheduler import config, models, rankers, storage + + +class DefaultDeadlineRanker(unittest.TestCase): + def setUp(self): + # Application Context + self.mock_ctx = mock.patch("scheduler.context.AppContext").start() + self.mock_ctx.config = config.settings.Settings() + + # Database + self.dbconn = storage.DBConn(str(self.mock_ctx.config.db_uri)) + models.Base.metadata.drop_all(self.dbconn.engine) + models.Base.metadata.create_all(self.dbconn.engine) + + self.mock_ctx.datastores = SimpleNamespace( + **{ + storage.TaskStore.name: storage.TaskStore(self.dbconn), + storage.PriorityQueueStore.name: storage.PriorityQueueStore(self.dbconn), + storage.ScheduleStore.name: storage.ScheduleStore(self.dbconn), + } + ) + + self.ranker = rankers.DefaultDeadlineRanker(self.mock_ctx) + + def tearDown(self): + models.Base.metadata.drop_all(self.dbconn.engine) + self.dbconn.engine.dispose() + + def test_calculate_deadline(self): + deadline = self.ranker.rank(None) + self.assertIsNotNone(deadline) + + def test_calculate_deadline_cron(self): + schedule = models.Schedule( + scheduler_id="test", + p_item=models.PrioritizedItem(hash="test", priority=1), + cron_expression="0 12 * * 1", # every Monday at noon + ) + + deadline = self.ranker.rank(schedule) + self.assertIsNotNone(deadline) + + def test_calculate_deadline_malformed(self): + schedule = models.Schedule( + scheduler_id="test", + p_item=models.PrioritizedItem(hash="test", priority=1), + cron_expression=".&^%$#", + ) + + with self.assertRaises(ValueError): + self.ranker.rank(schedule) diff --git a/mula/tests/integration/test_schedule_store.py b/mula/tests/integration/test_schedule_store.py new file mode 100644 index 00000000000..1cd8881c69d --- /dev/null +++ b/mula/tests/integration/test_schedule_store.py @@ -0,0 +1,225 @@ +import unittest +from types import SimpleNamespace +from unittest import mock + +from scheduler import config, models, storage + +from tests.utils import functions + + +class ScheduleStore(unittest.TestCase): + def setUp(self): + # Application Context + self.mock_ctx = mock.patch("scheduler.context.AppContext").start() + self.mock_ctx.config = config.settings.Settings() + + # Database + self.dbconn = storage.DBConn(str(self.mock_ctx.config.db_uri)) + models.Base.metadata.drop_all(self.dbconn.engine) + models.Base.metadata.create_all(self.dbconn.engine) + + self.mock_ctx.datastores = SimpleNamespace( + **{ + storage.TaskStore.name: storage.TaskStore(self.dbconn), + storage.ScheduleStore.name: storage.ScheduleStore(self.dbconn), + } + ) + + def tearDown(self): + models.Base.metadata.drop_all(self.dbconn.engine) + self.dbconn.engine.dispose() + + def test_create_schedule(self): + # Arrange + scheduler_id = "test_scheduler_id" + schedule = models.Schedule( + scheduler_id=scheduler_id, + p_item=functions.create_p_item(scheduler_id=scheduler_id, priority=1), + ) + + # Act + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Assert + self.assertEqual(schedule, self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id)) + + def test_get_schedules(self): + # Arrange + scheduler_one = "test_scheduler_one" + for i in range(5): + schedule = models.Schedule( + scheduler_id=scheduler_one, + p_item=functions.create_p_item(scheduler_id=scheduler_one, priority=i), + ) + self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + scheduler_two = "test_scheduler_two" + for i in range(5): + schedule = models.Schedule( + scheduler_id=scheduler_two, + p_item=functions.create_p_item(scheduler_id=scheduler_two, priority=i), + ) + self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Act + schedules_scheduler_one, schedules_scheduler_one_count = self.mock_ctx.datastores.schedule_store.get_schedules( + scheduler_id=scheduler_one, + ) + schedules_scheduler_two, schedules_scheduler_two_count = self.mock_ctx.datastores.schedule_store.get_schedules( + scheduler_id=scheduler_two, + ) + + # Assert + self.assertEqual(5, len(schedules_scheduler_one)) + self.assertEqual(5, schedules_scheduler_one_count) + self.assertEqual(5, len(schedules_scheduler_two)) + self.assertEqual(5, schedules_scheduler_two_count) + + def test_get_schedule_by_id(self): + # Arrange + scheduler_id = "test_scheduler_id" + schedule = models.Schedule( + scheduler_id=scheduler_id, + p_item=functions.create_p_item(scheduler_id=scheduler_id, priority=1), + ) + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Act + schedule_by_id = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + + # Assert + self.assertEqual(schedule_by_id.id, schedule_db.id) + + def test_get_schedule_by_hash(self): + # Arrange + scheduler_id = "test_scheduler_id" + schedule = models.Schedule( + scheduler_id=scheduler_id, + p_item=functions.create_p_item(scheduler_id=scheduler_id, priority=1), + ) + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Act + schedule_by_hash = self.mock_ctx.datastores.schedule_store.get_schedule_by_hash(schedule_db.p_item.hash) + + # Assert + self.assertEqual(schedule_by_hash.id, schedule_db.id) + self.assertEqual(schedule_by_hash.p_item, schedule_db.p_item) + self.assertEqual(schedule_by_hash.p_item.hash, schedule_db.p_item.hash) + + def test_update_schedule(self): + # Arrange + scheduler_id = "test_scheduler_id" + schedule = models.Schedule( + scheduler_id=scheduler_id, + p_item=functions.create_p_item(scheduler_id=scheduler_id, priority=1), + ) + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Assert + self.assertEqual(schedule_db.enabled, True) + + # Act + schedule_db.enabled = False + self.mock_ctx.datastores.schedule_store.update_schedule(schedule_db) + + # Assert + schedule_db_updated = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertEqual(schedule_db_updated.enabled, False) + + def test_update_schedule_enabled(self): + # Arrange + scheduler_id = "test_scheduler_id" + schedule = models.Schedule( + scheduler_id=scheduler_id, + p_item=functions.create_p_item(scheduler_id=scheduler_id, priority=1), + ) + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Assert + self.assertEqual(schedule_db.enabled, True) + + # Act + self.mock_ctx.datastores.schedule_store.update_schedule_enabled(schedule_db.id, False) + + # Assert + schedule_db_updated = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertEqual(schedule_db_updated.enabled, False) + + def test_delete_schedule(self): + # Arrange + p_item = functions.create_p_item("test_scheduler_id", 1) + + schedule = models.Schedule( + scheduler_id=p_item.scheduler_id, + p_item=p_item, + ) + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + # Act + self.mock_ctx.datastores.schedule_store.delete_schedule(schedule_db.id) + + # Assert + is_schedule_deleted = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertEqual(is_schedule_deleted, None) + + def test_delete_schedule_cascade(self): + """When a schedule is deleted, its tasks should NOT be deleted.""" + # Arrange + p_item = functions.create_p_item("test_scheduler_id", 1) + + schedule = models.Schedule( + scheduler_id=p_item.scheduler_id, + p_item=p_item, + ) + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + task = models.TaskRun( + id=p_item.id, + hash=p_item.hash, + type=functions.TestModel.type, + status=models.TaskStatus.QUEUED, + scheduler_id=p_item.scheduler_id, + p_item=p_item, + schedule_id=schedule_db.id, + ) + task_db = self.mock_ctx.datastores.task_store.create_task(task) + + # Act + self.mock_ctx.datastores.schedule_store.delete_schedule(schedule_db.id) + + # Assert + is_schedule_deleted = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id) + self.assertEqual(is_schedule_deleted, None) + + is_task_deleted = self.mock_ctx.datastores.task_store.get_task_by_id(task_db.id) + self.assertIsNotNone(is_task_deleted) + self.assertIsNone(is_task_deleted.schedule_id) + + def test_relationship_schedule_tasks(self): + # Arrange + p_item = functions.create_p_item("test_scheduler_id", 1) + + schedule = models.Schedule( + scheduler_id=p_item.scheduler_id, + p_item=p_item, + ) + schedule_db = self.mock_ctx.datastores.schedule_store.create_schedule(schedule) + + task = models.TaskRun( + id=p_item.id, + hash=p_item.hash, + type=functions.TestModel.type, + status=models.TaskStatus.QUEUED, + scheduler_id=p_item.scheduler_id, + p_item=p_item, + schedule_id=schedule_db.id, + ) + task_db = self.mock_ctx.datastores.task_store.create_task(task) + + # Act + schedule_tasks = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(schedule_db.id).tasks + + # Assert + self.assertEqual(len(schedule_tasks), 1) + self.assertEqual(schedule_tasks[0].id, task_db.id) diff --git a/mula/tests/integration/test_scheduler.py b/mula/tests/integration/test_scheduler.py index 9727414bc16..d39817d49b3 100644 --- a/mula/tests/integration/test_scheduler.py +++ b/mula/tests/integration/test_scheduler.py @@ -15,10 +15,10 @@ class SchedulerTestCase(unittest.TestCase): def setUp(self): # Application Context self.mock_ctx = mock.patch("scheduler.context.AppContext").start() - cfg = config.settings.Settings() + self.mock_ctx.config = config.settings.Settings() # Database - self.dbconn = storage.DBConn(str(cfg.db_uri)) + self.dbconn = storage.DBConn(str(self.mock_ctx.config.db_uri)) models.Base.metadata.drop_all(self.dbconn.engine) models.Base.metadata.create_all(self.dbconn.engine) @@ -26,6 +26,7 @@ def setUp(self): **{ storage.TaskStore.name: storage.TaskStore(self.dbconn), storage.PriorityQueueStore.name: storage.PriorityQueueStore(self.dbconn), + storage.ScheduleStore.name: storage.ScheduleStore(self.dbconn), } ) @@ -33,7 +34,7 @@ def setUp(self): queue = mock_queue.MockPriorityQueue( pq_id=identifier, - maxsize=cfg.pq_maxsize, + maxsize=self.mock_ctx.config.pq_maxsize, item_type=mock_task.MockTask, allow_priority_updates=True, pq_store=self.mock_ctx.datastores.pq_store, @@ -71,6 +72,10 @@ def test_post_push(self): self.assertEqual(task_db.id, p_item.id) self.assertEqual(task_db.status, models.TaskStatus.QUEUED) + # Schedule should be in datastore + schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(task_db.schedule_id) + self.assertEqual(schedule_db.id, task_db.schedule_id) + def test_post_pop(self): """When a task is popped from the queue, it should be removed from the database""" # Arrange @@ -204,3 +209,83 @@ def test_enable_scheduler(self): # Stop the scheduler self.scheduler.stop() + + def test_signal_handler_task(self): + # Arrange + p_item = functions.create_p_item( + scheduler_id=self.scheduler.scheduler_id, + priority=1, + ) + + self.scheduler.push_item_to_queue(p_item) + self.scheduler.pop_item_from_queue() + + task_db = self.mock_ctx.datastores.task_store.get_task_by_id(p_item.id) + + # Get schedule + initial_schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(task_db.schedule_id) + initial_timestamp = initial_schedule_db.deadline_at + + # Set task to complete + task_db.status = models.TaskStatus.COMPLETED + self.mock_ctx.datastores.task_store.update_task(task_db) + + # Act + self.scheduler.signal_handler_task(task_db) + + # Assert: schedule have a new deadline + updated_schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(task_db.schedule_id) + updated_timestamp = updated_schedule_db.deadline_at + self.assertNotEqual(initial_timestamp, updated_timestamp) + + def test_signal_handler_task_not_finished(self): + # Arrange + p_item = functions.create_p_item( + scheduler_id=self.scheduler.scheduler_id, + priority=1, + ) + + self.scheduler.push_item_to_queue(p_item) + + task_db = self.mock_ctx.datastores.task_store.get_task_by_id(p_item.id) + + # Get schedule + initial_schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(task_db.schedule_id) + initial_timestamp = initial_schedule_db.deadline_at + + self.scheduler.signal_handler_task(task_db) + + # Assert: schedule should have same deadline + updated_schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(task_db.schedule_id) + updated_timestamp = updated_schedule_db.deadline_at + self.assertEqual(initial_timestamp, updated_timestamp) + + def test_signal_handler_malformed_cron_expression(self): + # Arrange + p_item = functions.create_p_item( + scheduler_id=self.scheduler.scheduler_id, + priority=1, + ) + + self.scheduler.push_item_to_queue(p_item) + self.scheduler.pop_item_from_queue() + + task_db = self.mock_ctx.datastores.task_store.get_task_by_id(p_item.id) + + # Get schedule + initial_schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(task_db.schedule_id) + + # Set cron expression to malformed + initial_schedule_db.cron_expression = ".&^%$#" + self.mock_ctx.datastores.schedule_store.update_schedule(initial_schedule_db) + + # Set task to complete + task_db.status = models.TaskStatus.COMPLETED + self.mock_ctx.datastores.task_store.update_task(task_db) + + # Act + self.scheduler.signal_handler_task(task_db) + + # Assert: schedule should be disabled + updated_schedule_db = self.mock_ctx.datastores.schedule_store.get_schedule_by_id(task_db.schedule_id) + self.assertFalse(updated_schedule_db.enabled) diff --git a/mula/tests/integration/test_task_store.py b/mula/tests/integration/test_task_store.py index af193d82b60..1ef98df10ab 100644 --- a/mula/tests/integration/test_task_store.py +++ b/mula/tests/integration/test_task_store.py @@ -69,7 +69,7 @@ def test_get_status_counts(self): ): for _ in r: p_item = functions.create_p_item(self.organisation.id, 1) - task = models.Task( + task = models.TaskRun( id=p_item.id, hash=p_item.hash, type=functions.TestModel.type, @@ -121,7 +121,7 @@ def test_get_status_count_per_hour(self): ): for _ in r: p_item = functions.create_p_item(self.organisation.id, 1) - task = models.Task( + task = models.TaskRun( id=p_item.id, hash=p_item.hash, type=functions.TestModel.type, diff --git a/mula/tests/utils/__init__.py b/mula/tests/utils/__init__.py index 43d8652922c..5c404729fae 100644 --- a/mula/tests/utils/__init__.py +++ b/mula/tests/utils/__init__.py @@ -1 +1,2 @@ +from .json import UUIDEncoder from .memory import profile_memory diff --git a/mula/tests/utils/functions.py b/mula/tests/utils/functions.py index af08da29598..cb8750b5434 100644 --- a/mula/tests/utils/functions.py +++ b/mula/tests/utils/functions.py @@ -49,8 +49,8 @@ def create_p_item(scheduler_id: str, priority: int, data: TestModel | None = Non ) -def create_task(p_item: models.PrioritizedItem) -> models.Task: - return models.Task( +def create_task(p_item: models.PrioritizedItem) -> models.TaskRun: + return models.TaskRun( id=p_item.id, hash=p_item.hash, type=TestModel.type, diff --git a/mula/tests/utils/json.py b/mula/tests/utils/json.py new file mode 100644 index 00000000000..bec91f1a326 --- /dev/null +++ b/mula/tests/utils/json.py @@ -0,0 +1,9 @@ +import json +from uuid import UUID + + +class UUIDEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, UUID): + return obj.hex + return json.JSONEncoder.default(self, obj)