diff --git a/.env.dist.compose b/.env.dist.compose index f445f2b7..94961488 100644 --- a/.env.dist.compose +++ b/.env.dist.compose @@ -7,11 +7,11 @@ # odtp db instance in the mongo db: "odtp" ODTP_MONGO_DB= -# s3 bucket name: "odtp" +# s3 bucket name: "odtp" ODTP_BUCKET_NAME= # s3 access and secret key -ODTP_ACCESS_KEY= +ODTP_ACCESS_KEY= ODTP_SECRET_KEY= # your github token @@ -34,9 +34,11 @@ MONGODB_PATH= ODTP_DASHBOARD_PORT= ODTP_DASHBOARD_JSON_EDITOR= -# Log level -ODTP_LOG_LEVEL= -RUN_LOG_LEVEL= +# Log Level General +ODTP_LOG_LEVEL=ERROR + +# Log Level when running executions +RUN_LOG_LEVEL=INFO # App Path APP_PATH= diff --git a/.env.dist.compose.dev b/.env.dist.compose.dev new file mode 100644 index 00000000..1e132a27 --- /dev/null +++ b/.env.dist.compose.dev @@ -0,0 +1,48 @@ +# environment variables for installation with docker compose +# with the dev version: compose.dev.yml +# ----------------------------------------------------------- +# fill these variables in case you want to install odtp with +# docker compose + +# local setup and compose + +# odtp db instance in the mongo db: "odtp" +ODTP_MONGO_DB= +# s3 bucket name: "odtp" +ODTP_BUCKET_NAME= + +# s3 access and secret key +ODTP_ACCESS_KEY= +ODTP_SECRET_KEY= + +# your github token +GITHUB_TOKEN= + +# mongodb user and password +MONGO_DB_USER= +MONGO_DB_PASSWORD= + +# mongoexpress user and password +MONGO_EXPRESS_USER= +MONGO_EXPRESS_PASSWORD= + +# absolute path for docker volumes +ODTP_PATH= +MINIO_PATH= +MONGODB_PATH= + +# Dashboard parameters +ODTP_DASHBOARD_PORT= +ODTP_DASHBOARD_JSON_EDITOR= + +# Log Level General +ODTP_LOG_LEVEL=ERROR + +# Log Level when running executions +RUN_LOG_LEVEL=INFO + +# App Path +APP_PATH= + +# Install the package in editable mode. +PIP_INSTALL_ARGS="--editable" diff --git a/.env.dist.local b/.env.dist.local index c33b3c32..893b9da9 100644 --- a/.env.dist.local +++ b/.env.dist.local @@ -29,6 +29,8 @@ ODTP_DASHBOARD_JSON_EDITOR= # Working directory for user projects ODTP_PATH= -# Log level -ODTP_LOG_LEVEL= -RUN_LOG_LEVEL= +# Log Level General +ODTP_LOG_LEVEL=ERROR + +# Log Level when running executions +RUN_LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore index 4630bb9c..6bdec2cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__/ .DS_Store .env* -!.env*.dist +!.env.dist.* .nicegui .local +odtp/odtp.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 59db811b..2012ee76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ ## Changelog +- v0.4.0 + - gui: the general workflow for the user was improved (new Items are selected automatically for example) + - gui: validation has been implemented in the forms to make sure data that is entered will be valid + - gui: the run page for executions has been improved in the gui: logs are now also available from the gui when a workflow runs + - gui: on the dashboard homepage you can now directly see whether all services including github are connected + - gui: the workarea in the dashboard was removed and replace by an info section on top of the pages + - logging: improve logging at GUI/CLI: run commands are now logged to a file, while everything else is still logged to the command line: this is so run commands can be easier debugged by knowing what was run in docker and how that run was triggered + - logging: the logs of the execution runs are now exposed as volumes, so that will be directly available when the component starts and will remain in the project path even after a component exited prematurely + - execution: execution and step timestamps + - cli: printing table from mongodb colleciton + - cli: delete methods for execution and related items. + - components: avoid rebuilding image if component is available + - compose: a dev version for docker compose has been added to facilitate development + +- v0.3.1 + - hotfixes for bugs + - removal of unused dependencies and adition of pandas + - v0.3.0 dashboard refactoring - dashboard was refactored since code files were getting too long - homepage of the dashboard got an upgrade and also contains now connection checks diff --git a/compose.dev.yml b/compose.dev.yml index e3f49927..e49112ed 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -28,7 +28,7 @@ services: ODTP_DASHBOARD_RELOAD: ${ODTP_DASHBOARD_RELOAD} ODTP_DASHBOARD_JSON_EDITOR: ${ODTP_DASHBOARD_JSON_EDITOR} ODTP_LOG_LEVEL: ${ODTP_LOG_LEVEL} - RUN_LOG_LEVEL: ${ODTP_LOG_LEVEL} + RUN_LOG_LEVEL: ${RUN_LOG_LEVEL} stdin_open: true tty: true diff --git a/compose.yml b/compose.yml index 252053f5..1d1e520a 100644 --- a/compose.yml +++ b/compose.yml @@ -23,7 +23,7 @@ services: ODTP_DASHBOARD_PORT: ${ODTP_DASHBOARD_PORT} ODTP_DASHBOARD_JSON_EDITOR: ${ODTP_DASHBOARD_JSON_EDITOR} ODTP_LOG_LEVEL: ${ODTP_LOG_LEVEL} - RUN_LOG_LEVEL: ${ODTP_LOG_LEVEL} + RUN_LOG_LEVEL: ${RUN_LOG_LEVEL} stdin_open: true tty: true diff --git a/odtp/cli/component.py b/odtp/cli/component.py index 8ef2edfa..96eaeee2 100644 --- a/odtp/cli/component.py +++ b/odtp/cli/component.py @@ -3,6 +3,7 @@ """ import typer from typing_extensions import Annotated +import logging from odtp.run import DockerManager import odtp.helpers.git as odtp_git @@ -11,6 +12,9 @@ app = typer.Typer() +log = logging.getLogger(__name__) + + ## Adding listing so we can have multiple flags from typing import List @@ -34,10 +38,10 @@ def prepare( ) componentManager.prepare_component() except Exception as e: - print(f"ERROR: Prepare component failed: {e}") + log.error(f"ERROR: Prepare component failed: {e}") raise typer.Abort() else: - print("SUCCESS: image for the component has been build") + log.info("SUCCESS: image for the component has been build") @app.command() def run( @@ -78,17 +82,12 @@ def run( instance_name=instance_name ) except Exception as e: - print(f"ERROR: Run of component failed: {e}") + log.error(f"ERROR: Run of component failed: {e}") raise typer.Abort() else: - print("SUCCESS: container for the component has been started") + log.info("SUCCESS: container for the component has been started") -#### TODO: Stop Component -@app.command() -def stop(): - pass - @app.command() def delete_instance( @@ -98,7 +97,7 @@ def delete_instance( ): componentManager = DockerManager() componentManager.delete_component(instance_name=instance_name) - print("Container deleted") + log.info("Container deleted") @app.command() @@ -109,7 +108,7 @@ def delete_image( ): componentManager = DockerManager(image_name=image_name) componentManager.delete_image() - print("Image deleted") + log.info("Image deleted") if __name__ == "__main__": diff --git a/odtp/cli/db.py b/odtp/cli/db.py index de13ee57..dfa14ece 100644 --- a/odtp/cli/db.py +++ b/odtp/cli/db.py @@ -3,16 +3,18 @@ """ import typer from typing_extensions import Annotated +import logging import odtp.mongodb.db as db import odtp.helpers.utils as odtp_utils app = typer.Typer() +log = logging.getLogger(__name__) @app.command() def get( - collection: str = typer.Option(..., "--collection", help="Specify the collection"), + collection: str = typer.Argument(...,help="Specify the collection"), id: Annotated[str, typer.Option(help="Specify the id")] = None, ): if id: @@ -21,6 +23,11 @@ def get( db_output = db.get_collection(collection=collection) odtp_utils.print_output_as_json(db_output) +@app.command() +def ls(collection_name: str): + """List all documents in a collection""" + db_output = db.get_collection(collection=collection_name) + odtp_utils.output_as_pretty_table(db_output, collection_name) @app.command() def showAll(): @@ -76,7 +83,7 @@ def delete_document( id: str = typer.Option(help="Specify the id") ): db.delete_document_by_id(document_id=id, collection=collection) - print(f"Document with ID {id} was deleted") + log.info(f"Document with ID {id} was deleted") @app.command() @@ -84,13 +91,13 @@ def delete_collection( collection: str = typer.Option(..., "--collection", help="Specify the collection"), ): db.delete_collection(collection=collection) - print(f"Collection {collection} was deleted.") + log.info(f"Collection {collection} was deleted.") @app.command() def deleteAll(): db.delete_all() - print("All collection deleted.") + log.info("All collection deleted.") if __name__ == "__main__": diff --git a/odtp/cli/execution.py b/odtp/cli/execution.py index d55daf02..54c38c5b 100644 --- a/odtp/cli/execution.py +++ b/odtp/cli/execution.py @@ -1,19 +1,26 @@ """ This scripts contains odtp subcommands for 'execution' """ - +import sys import typer from typing_extensions import Annotated +import logging import odtp.mongodb.db as db import odtp.helpers.parse as odtp_parse from odtp.workflow import WorkflowManager from directory_tree import display_tree import odtp.helpers.environment as odtp_env +from odtp.storage import s3Manager from nicegui import ui +import os app = typer.Typer() +log = logging.getLogger(__name__) + +log = logging.getLogger(__name__) + ## Adding listing so we can have multiple flags from typing import List @@ -46,10 +53,14 @@ def prepare( flowManager = WorkflowManager(execution, project_path, secrets) flowManager.prepare_workflow() except Exception as e: - print(f"ERROR: Prepare execution failed: {e}") + msg = f"ERROR: Prepare execution failed: {e}" + log.exception(msg) + print(msg) raise typer.Abort() else: - print("SUCCESS: images for the execution have been build") + msg = "SUCCESS: images for the execution have been build" + log.info(msg) + print(msg) @app.command() @@ -84,27 +95,75 @@ def run( flowManager = WorkflowManager(execution, project_path, secrets) flowManager.run_workflow() except Exception as e: - print(f"ERROR: Run execution failed: {e}") + msg = f"ERROR: Prepare execution failed: {e}" + log.exception(msg) + print(msg) raise typer.Abort() else: - print("SUCCESS: containers for the execution have been run") + msg = "SUCCESS: containers for the execution have been run" + log.info(msg) + print(msg) + + +@app.command() +def streamlogs( + project_path: str = typer.Option( + ..., "--project-path", help="Specify the path for the execution" + ), + step_nr: str = typer.Option( + ..., "--step-nr", help="Specify the step for the execution" + ), +): + try: + log_file_path = f"{project_path}/{step_nr}_*/odtp-logs/*" + os.system(f"tail -f {log_file_path}") + except KeyboardInterrupt: + sys.exit() @app.command() -def output( +def delete( + execution_name: str = typer.Option( + None, "--execution-name", help="Specify the name of the execution" + ), execution_id: str = typer.Option( - ..., "--execution-id", help="Specify the ID of the execution" + None, "--execution-id", help="Specify the ID of the execution" ), project_path: str = typer.Option( - ..., "--project-path", help="Specify the path for the execution" + None, "--project-path", help="Specify the path for the execution" ), -): + keep_project_path: bool = typer.Option( + True, "--keep-project-path", help="Keep the project directory after deleting contents" + ), +): try: - display_tree(project_path) - except Exception as e: - print(f"ERROR: Output printing failed: {e}") - raise typer.Abort() + if execution_id is None and execution_name is None: + raise typer.Exit("Please provide either --execution-name or --execution-id") + + if execution_name: + execution_id = db.get_document_id_by_field_value("title", execution_name, db.collection_executions) + + # S3 + s3_keys = db.get_all_outputs_s3_keys(execution_id) + s3M = s3Manager() + s3M.deletePaths(s3_keys) + # DB + db.delete_execution(execution_id) + + # Folders + if project_path: + odtp_env.delete_folder(project_path, keep_project_path=keep_project_path) + + except Exception as e: + msg = f"ERROR: Delete execution failed: {e}" + log.exception(msg) + print(msg) + raise typer.Abort() + else: + msg = "SUCCESS: execution has been deleted" + log.info(msg) + print(msg) if __name__ == "__main__": app() diff --git a/odtp/cli/new.py b/odtp/cli/new.py index a6250d28..23d8ef61 100644 --- a/odtp/cli/new.py +++ b/odtp/cli/new.py @@ -3,6 +3,7 @@ """ import typer from typing_extensions import Annotated +import logging import odtp.mongodb.db as db import odtp.helpers.parse as odtp_parse @@ -15,6 +16,7 @@ app = typer.Typer() +log = logging.getLogger(__name__) @app.command() def user_entry( @@ -24,7 +26,7 @@ def user_entry( ): """Add new user in the MongoDB""" user_id = db.add_user(name=name, github=github, email=email) - print(f"A user has been added {user_id}") + log.info(f"A user has been added {user_id}") @app.command() @@ -57,11 +59,11 @@ def odtp_component_entry( ports=ports, ) except Exception as e: - print(f"ERROR: {e}") + log.error(f"ERROR: {e}") if hasattr(e, "__notes__"): - print(f"{','.join(e.__notes__)}") + log.error(f"{','.join(e.__notes__)}") raise typer.Abort() - print(f"""SUCCESS: component version has been added: see above for the details. + log.info(f"""SUCCESS: component version has been added: see above for the details. component_id: {component_id} version_id: {version_id}""") @@ -76,10 +78,10 @@ def digital_twin_entry( raise typer.Exit("Please provide either --user-id or --user-email") if user_email: - user_id = db.get_document_id_by_field_value("user_email", user_email, "users") + user_id = db.get_document_id_by_field_value("email", user_email, "users") dt_id = db.add_digital_twin(userRef=user_id, name=name) - print(f"Digital Twin added with ID {dt_id}") + log.info(f"Digital Twin added with ID {dt_id}") @app.command() @@ -130,11 +132,11 @@ def execution_entry( ports=ports, ) except Exception as e: - print(f"ERROR: {e}") + log.error(f"ERROR: {e}") if hasattr(e, "__notes__"): - print(f"{','.join(e.__notes__)}") + log.error(f"{','.join(e.__notes__)}") raise typer.Abort() - print(f"""SUCCESS: execution has been added: see above for the details. + log.info(f"""SUCCESS: execution has been added: see above for the details. execution id: {execution_id} step_ids: {step_ids}""") diff --git a/odtp/cli/s3.py b/odtp/cli/s3.py index 00bfa1f2..6815115f 100644 --- a/odtp/cli/s3.py +++ b/odtp/cli/s3.py @@ -31,11 +31,11 @@ def download( def check(): try: s3 = s3Manager() - bucket = s3.test_connection() - print("S3 is connected. Bucket is ready to use") + s3.test_connection() + log.info("S3 is connected. Bucket is ready to use") except Exception as e: log.exception(f"S3 connection could not be established: an Exception {e} occurred") - print(f"S3 connection could not be established: an Exception {e} occurred") + log.error(f"S3 connection could not be established: an Exception {e} occurred") if __name__ == "__main__": diff --git a/odtp/cli/setup.py b/odtp/cli/setup.py index a41cda4e..f1be6c77 100644 --- a/odtp/cli/setup.py +++ b/odtp/cli/setup.py @@ -2,19 +2,27 @@ This scripts contains odtp subcommands for 'setup' """ import typer - +import logging from odtp.storage import s3Manager import odtp.mongodb.db as db app = typer.Typer() +log = logging.getLogger(__name__) @app.command() def initiate(): db.init_collections() odtpS3 = s3Manager() - odtpS3.create_folders(["odtp"]) + try: + bucketAvailable = odtpS3.test_connection() + except Exception as e: + log.error("S3 bucket not found. Please create the bucket on minio folder or use the dashboard.") + log.exception(e) + + + odtpS3.createFolderStructure(["odtp"]) odtpS3.close() print("ODTP DB/S3 data generated") @@ -27,7 +35,7 @@ def delete(): odtpS3 = s3Manager() odtpS3.deleteAll() - print("All deleted") + log.info("All deleted") if __name__ == "__main__": diff --git a/odtp/dashboard/page_executions/table.py b/odtp/dashboard/page_executions/table.py index d994b786..39684eab 100644 --- a/odtp/dashboard/page_executions/table.py +++ b/odtp/dashboard/page_executions/table.py @@ -13,8 +13,10 @@ def ui_table_layout(executions): return df = pd.DataFrame(data=executions) df["_id"] = df["_id"].astype("string") - df["timestamp"] = df["start_timestamp"] + df["createdAt"] = df["createdAt"] + df["start_timestamp"] = df["start_timestamp"].fillna('') + df["end_timestamp"] = df["end_timestamp"].fillna('') df["steps"] = df["steps"].apply(helpers.pd_lists_to_counts).astype("string") - df = df[["timestamp", "title", "steps"]] - df = df.sort_values(by="timestamp", ascending=False) + df = df[["createdAt", "title", "steps", "start_timestamp", "end_timestamp"]] + df = df.sort_values(by="createdAt", ascending=False) ui.table.from_pandas(df) diff --git a/odtp/dashboard/page_run/folder.py b/odtp/dashboard/page_run/folder.py index d637423f..bf2e3219 100644 --- a/odtp/dashboard/page_run/folder.py +++ b/odtp/dashboard/page_run/folder.py @@ -1,6 +1,9 @@ import json import logging import os.path +import os +import shutil +from slugify import slugify from nicegui import app, ui import odtp.dashboard.utils.storage as storage @@ -9,100 +12,41 @@ import odtp.dashboard.page_run.helpers as rh from odtp.dashboard.utils.file_picker import local_file_picker -FOLDER_NOT_SET = 0 -FOLDER_DOES_NOT_MATCH = 1 -FOLDER_EMPTY = 2 -FOLDER_PREPARED = 3 -FOLDER_HAS_OUTPUT = 4 - -FOLDER_STATUS = { - "not_set" - "no_match", - "empty", - "prepared", - "output", -} - log = logging.getLogger(__name__) -def ui_prepare_folder(dialog, result, workdir, current_run, folder_status): +def ui_prepare_folder(workdir, current_run, folder_status, project_path): stepper = current_run.get("stepper") if stepper and rh.STEPPERS.index(stepper) != rh.STEPPER_SELECT_FOLDER: return - project_path = current_run.get("project_path") - execution = current_run.get("execution") if project_path: - with ui.row().classes("w-full"): - ui.markdown( - f""" - **project path**: {project_path} - """ - ) - with ui.row(): - if folder_status == FOLDER_EMPTY: - ui.icon("check").classes("text-teal text-lg") - ui.label("Project folder for the execution run has been selected").classes("text-teal") - elif folder_status == FOLDER_NOT_SET: - ui.icon("clear").classes("text-red text-lg") - ui.label("Project folder missing: please select one").classes("text-red") - elif folder_status == FOLDER_DOES_NOT_MATCH: - ui.icon("clear").classes("text-red text-lg") - ui.label("The project folder structure does not match the steps of the execution: choose an empty project folder or create a new project folder").classes("text-red") - elif folder_status == FOLDER_PREPARED: - ui.icon("check").classes("text-teal text-lg") - ui.label("The execution has been prepared").classes("text-teal") - elif folder_status == FOLDER_HAS_OUTPUT: - ui.icon("check").classes("text-teal text-lg") - ui.label("The execution has been run").classes("text-teal") - folder_matches = folder_status in [ - FOLDER_EMPTY, - FOLDER_HAS_OUTPUT, - FOLDER_PREPARED, - ] - if folder_matches: - from odtp.dashboard.page_run.run import build_command - cli_output_command = build_command( - cmd="output", - execution_id=execution["execution_id"], - project_path=project_path, - ) - else: - cli_output_command = None - with ui.row().classes("w-full flex items-center"): - ui.button( - "Choose existing project folder", - on_click=lambda: pick_folder(workdir, current_run), - icon="folder", - ).props("flat") - project_folder_input = ui.input( - label="Project folder name", - placeholder="execution", - validation={ - f"Please provide a folder name does not yet exist in the working directory": lambda value: validators.validate_folder_does_not_exist( - value, workdir + ui.label(project_path) + rh.ui_display_folder_status(folder_status) + if not project_path: + execution = current_run.get("execution") + preset_value = slugify(execution["title"]) + if not project_path: + with ui.row().classes("w-full flex items-center"): + project_folder_input = ui.input( + value=preset_value, + label="Project folder name", + placeholder="execution", + validation={ + f"Project folder already exists and is not empty": lambda value: validators.validate_folder_does_not_exist( + value, workdir + ) + }, ) - }, - ) - ui.button( - "Create new project folder", - on_click=lambda: create_folder(workdir, project_folder_input, current_run), - icon="add", - ).props("flat ") - ui.button( - f"Reset project folder", - on_click=lambda: remove_project_folder(current_run), - icon="clear", - ).props("flat") - with ui.row().classes("w-full"): - from odtp.dashboard.page_run.run import run_command - if cli_output_command: - ui.button( - "Show project folder", - on_click=lambda: run_command(cli_output_command, dialog, result), - icon="info", - ).props("no-caps") - rh.ui_next_back(current_run) + ui.button( + "Create new project folder", + on_click=lambda: create_folder(workdir, project_folder_input, current_run), + icon="add", + ).props("flat ") + if folder_status >= rh.FOLDER_EMPTY: + ready_for_next = True + else: + ready_for_next = False + rh.ui_next_back(current_run, ready_for_next) async def pick_folder(workdir, current_run) -> None: @@ -128,10 +72,19 @@ async def pick_folder(workdir, current_run) -> None: def create_folder(workdir, folder_name_input, current_run): + if ( + not folder_name_input.validate() + ): + ui.notify( + "Project folder already exists and is not empty", type="negative" + ) + return try: folder_name = folder_name_input.value project_path = os.path.join(workdir, folder_name) - os.mkdir(project_path) + if os.path.exists(project_path): + shutil.rmtree(project_path) + os.makedirs(project_path) current_run["project_path"] = project_path current_run["stepper"] = rh.STEPPERS[rh.STEPPER_SELECT_FOLDER] app.storage.user[storage.EXECUTION_RUN] = json.dumps(current_run) @@ -149,34 +102,3 @@ def create_folder(workdir, folder_name_input, current_run): from odtp.dashboard.page_run.main import ui_workarea, ui_stepper ui_workarea.refresh() ui_stepper.refresh() - - -def remove_project_folder(current_run) -> None: - try: - current_run["project_path"] = "" - app.storage.user[storage.EXECUTION_RUN] = json.dumps(current_run) - ui.notify("The project path been reset", type="positive") - except Exception as e: - log.exception(f"The project directory could not be reset: an exception occurred: {e}") - else: - from odtp.dashboard.page_run.main import ui_workarea, ui_stepper - ui_stepper.refresh() - ui_workarea.refresh() - - -def get_folder_status(execution_id, project_path): - folder_empty = odtp_env.project_folder_is_empty(project_folder=project_path) - folder_matches_execution = odtp_env.directory_folder_matches_execution( - project_folder=project_path, execution_id=execution_id - ) - folder_has_output = odtp_env.directory_has_output( - execution_id=execution_id, project_folder=project_path - ) - if folder_empty: - return FOLDER_EMPTY - elif folder_matches_execution and not folder_has_output: - return FOLDER_PREPARED - elif folder_matches_execution and folder_has_output: - return FOLDER_HAS_OUTPUT - else: - return FOLDER_DOES_NOT_MATCH diff --git a/odtp/dashboard/page_run/helpers.py b/odtp/dashboard/page_run/helpers.py index ad353e31..fa3f8524 100644 --- a/odtp/dashboard/page_run/helpers.py +++ b/odtp/dashboard/page_run/helpers.py @@ -2,6 +2,7 @@ import logging from nicegui import app, ui +import odtp.helpers.environment as odtp_env import odtp.dashboard.utils.storage as storage import odtp.dashboard.utils.ui_theme as ui_theme @@ -11,18 +12,32 @@ STEPPERS = ( "Display Execution", - "Add Secret Files", "Select Folder", + "Add Secret Files", "Prepare Execution (Build Images)", "Run Execution (Run Containers)", ) STEPPER_DISPLAY_EXECUTION = 0 -STEPPER_ADD_SECRETS = 1 -STEPPER_SELECT_FOLDER = 2 +STEPPER_SELECT_FOLDER = 1 +STEPPER_ADD_SECRETS = 2 STEPPER_PREPARE_EXECUTION = 3 STEPPER_RUN_EXECUTION = 4 +FOLDER_NOT_SET = 0 +FOLDER_DOES_NOT_MATCH = 1 +FOLDER_EMPTY = 2 +FOLDER_PREPARED = 3 +FOLDER_HAS_OUTPUT = 4 + +FOLDER_STATUS = { + "not_set" + "no_match", + "empty", + "prepared", + "output", +} + def execution_run_init(digital_twin, execution): step_count = len(execution["steps"]) @@ -45,19 +60,16 @@ def ui_execution_details(current_run): version_tags = execution.get("version_tags") current_ports = execution.get("ports") current_parameters = execution.get("parameters") - with ui.row(): - ui.icon("check").classes("text-teal text-lg") - ui.label("Execution to run is selected").classes("text-teal") ui_theme.ui_execution_display( execution_title=execution_title, version_tags=version_tags, ports=current_ports, parameters=current_parameters, ) - ui_next_back(current_run) + ui_next_back(current_run, ready_for_next=True) -def ui_next_back(current_run, ready_for_next=True): +def ui_next_back(current_run, ready_for_next=False): stepper = current_run.get("stepper") with ui.grid(columns=1).classes("w-full"): with ui.row().classes("w-full"): @@ -109,3 +121,77 @@ def next_step(current_run): else: from odtp.dashboard.page_run.main import ui_stepper ui_stepper.refresh() + +def get_folder_status(execution_id, project_path): + if not project_path: + return FOLDER_NOT_SET + folder_empty = odtp_env.project_folder_is_empty(project_folder=project_path) + folder_matches_execution = odtp_env.directory_folder_matches_execution( + project_folder=project_path, execution_id=execution_id + ) + folder_has_output = odtp_env.directory_has_output( + execution_id=execution_id, project_folder=project_path + ) + if folder_empty: + return FOLDER_EMPTY + elif folder_matches_execution and not folder_has_output: + return FOLDER_PREPARED + elif folder_matches_execution and folder_has_output: + return FOLDER_HAS_OUTPUT + else: + return FOLDER_DOES_NOT_MATCH + + +def ui_display_folder_status(folder_status): + with ui.row(): + if folder_status == FOLDER_EMPTY: + ui.icon("check").classes("text-teal text-lg") + ui.label("Project folder for the execution run has been selected").classes("text-teal") + elif folder_status == FOLDER_NOT_SET: + ui.icon("clear").classes("text-red text-lg") + ui.label("Project folder missing: please select one").classes("text-red") + elif folder_status == FOLDER_DOES_NOT_MATCH: + ui.icon("clear").classes("text-red text-lg") + ui.label("The project folder structure does not match the steps of the execution: choose an empty project folder or create a new project folder").classes("text-red") + elif folder_status == FOLDER_PREPARED: + ui.icon("check").classes("text-teal text-lg") + ui.label("Project folder for the execution run has been selected").classes("text-teal") + ui.icon("check").classes("text-teal text-lg") + ui.label("The execution has been prepared").classes("text-teal") + elif folder_status == FOLDER_HAS_OUTPUT: + ui.icon("check").classes("text-teal text-lg") + ui.label("Project folder for the execution run has been selected").classes("text-teal") + ui.icon("check").classes("text-teal text-lg") + ui.label("The execution has been prepared").classes("text-teal") + ui.icon("check").classes("text-teal text-lg") + ui.label("The execution has been run").classes("text-teal") + + +def ui_display_secrets(secrets): + with ui.row(): + if secrets: + ui.icon("check").classes("text-teal text-lg") + ui.label("Secrets have been set").classes("text-teal") + + + +def build_cli_command(cmd, project_path, execution_id=None, secret_files=None, step_nr=None): + cli_parameters = [ + f"--project-path {project_path}", + ] + if execution_id: + cli_parameters.append( + f"--execution-id {execution_id}", + ) + if step_nr: + cli_parameters.append( + f"--step-nr {step_nr}", + ) + if secret_files and [secret_file for secret_file in secret_files]: + secret_files_for_run = ",".join(secret_files) + if secret_files_for_run: + cli_parameters.append( + f"--secrets-files {secret_files_for_run}", + ) + cli_command = f"odtp execution {cmd} {' '.join(cli_parameters)}" + return cli_command diff --git a/odtp/dashboard/page_run/main.py b/odtp/dashboard/page_run/main.py index 07c21bdf..f8690e59 100644 --- a/odtp/dashboard/page_run/main.py +++ b/odtp/dashboard/page_run/main.py @@ -69,13 +69,10 @@ def ui_stepper( stepper = current_run.get("stepper") execution = current_run["execution"] project_path = current_run.get("project_path") - if project_path: - folder_status = folder.get_folder_status( - execution_id=execution["execution_id"], - project_path=project_path, - ) - else: - folder_status = folder.FOLDER_NOT_SET + folder_status = rh.get_folder_status( + execution_id=execution["execution_id"], + project_path=project_path, + ) if ODTP_DASHBOARD_JSON_EDITOR: with ui.expansion("Current Execution Run as JSON"): ui.json_editor( @@ -87,22 +84,21 @@ def ui_stepper( with ui.stepper(value=stepper).props("vertical").classes("w-full") as stepper: with ui.step(rh.STEPPERS[rh.STEPPER_DISPLAY_EXECUTION]): rh.ui_execution_details(current_run) - with ui.step(rh.STEPPERS[rh.STEPPER_ADD_SECRETS]): + with ui.step(rh.STEPPERS[rh.STEPPER_SELECT_FOLDER]): with ui.stepper_navigation(): with ui.row(): - secrets.ui_add_secrets_form( + folder.ui_prepare_folder( current_run=current_run, workdir=workdir, + project_path=project_path, + folder_status=folder_status, ) - with ui.step(rh.STEPPERS[rh.STEPPER_SELECT_FOLDER]): + with ui.step(rh.STEPPERS[rh.STEPPER_ADD_SECRETS]): with ui.stepper_navigation(): with ui.row(): - folder.ui_prepare_folder( - dialog=dialog, - result=result, + secrets.ui_add_secrets_form( current_run=current_run, workdir=workdir, - folder_status=folder_status, ) with ui.step(rh.STEPPERS[rh.STEPPER_PREPARE_EXECUTION]): with ui.stepper_navigation(): diff --git a/odtp/dashboard/page_run/run.py b/odtp/dashboard/page_run/run.py index fdb69991..2192e1be 100644 --- a/odtp/dashboard/page_run/run.py +++ b/odtp/dashboard/page_run/run.py @@ -5,10 +5,13 @@ import sys from nicegui import ui +import odtp.helpers.settings as config import odtp.dashboard.page_run.helpers as rh import odtp.dashboard.page_run.folder as folder log = logging.getLogger(__name__) +log.setLevel(logging.INFO) +log.addHandler(config.get_command_log_handler()) def ui_prepare_execution(dialog, result, current_run, folder_status): @@ -25,83 +28,29 @@ def ui_prepare_execution(dialog, result, current_run, folder_status): - create folder structure """ ) - with ui.row().classes("w-full"): - if folder_status == folder.FOLDER_EMPTY: - ui.icon("check").classes("text-teal text-lg") - ui.label("Project folder for the execution run has been selected").classes("text-teal") - elif folder_status == folder.FOLDER_NOT_SET: - ui.icon("clear").classes("text-red text-lg") - ui.label("Project folder missing: please select one").classes("text-red") - elif folder_status == folder.FOLDER_DOES_NOT_MATCH: - ui.icon("clear").classes("text-red text-lg") - ui.label("The project folder structure does not match the steps of the execution: choose an empty project folder or create a new project folder").classes("text-red") - elif folder_status == folder.FOLDER_PREPARED: - ui.icon("check").classes("text-teal text-lg") - ui.label("The execution has been prepared").classes("text-teal") - elif folder_status == folder.FOLDER_HAS_OUTPUT: - ui.icon("check").classes("text-teal text-lg") - ui.label("The execution has been run").classes("text-teal") - folder_matches = folder_status in [ - folder.FOLDER_EMPTY, - folder.FOLDER_HAS_OUTPUT, - folder.FOLDER_PREPARED, - ] - if folder_matches: + if folder_status == rh.FOLDER_PREPARED: + with ui.row().classes("w-full"): + rh.ui_display_folder_status(folder_status) + if folder_status == rh.FOLDER_EMPTY: execution = current_run["execution"] - project_path = current_run["project_path"] - cli_output_command = build_command( - cmd="output", - execution_id=execution["execution_id"], - project_path=project_path, - ) - if folder_status == folder.FOLDER_EMPTY: - cli_prepare_command = build_command( + project_path = current_run["project_path"] + cli_prepare_command = rh.build_cli_command( cmd="prepare", execution_id=execution["execution_id"], project_path=project_path, ) - with ui.grid(columns=1): - if folder_status == folder.FOLDER_EMPTY: - with ui.row().classes("w-full"): - ui.label(cli_prepare_command).classes("font-mono") - with ui.row().classes("w-full"): - ui.button( - "Prepare execution", - on_click=lambda: run_command(cli_prepare_command, dialog, result), - icon="folder", - ).props("no-caps") - if folder_matches: - with ui.row().classes("w-full"): - ui.button( - "Show project folder", - on_click=lambda: run_command(cli_output_command, dialog, result), - icon="info", - ).props("no-caps") - rh.ui_next_back(current_run) + with ui.grid(columns=1): + with ui.row().classes("w-full"): + ui.label(cli_prepare_command).classes("font-mono w-full") + with ui.row().classes("w-full"): + ui.button( + "Prepare execution", + on_click=lambda: run_command(cli_prepare_command, dialog, result), + icon="folder", + ).props("no-caps") + rh.ui_next_back(current_run, ready_for_next=True) -def build_command(cmd, project_path, execution_id, secret_files=None): - cli_parameters = [ - f"--project-path {project_path}", - f"--execution-id {execution_id}", - ] - if secret_files and [secret_file for secret_file in secret_files]: - secret_files_for_run = ",".join(secret_files) - if secret_files_for_run: - cli_parameters.append( - f"--secrets-files {secret_files_for_run}", - ) - cli_command = f"odtp execution {cmd} {' '.join(cli_parameters)}" - return cli_command - - -def get_docker_command(execution_id): - cli_parameters = [ - f"--execution-id {execution_id}", - ] - cli_command = f"odtp execution docker_container {' '.join(cli_parameters)}" - return cli_command - def ui_run_execution(dialog, result, current_run, folder_status): stepper = current_run.get("stepper") if stepper and rh.STEPPERS.index(stepper) != rh.STEPPER_RUN_EXECUTION: @@ -114,70 +63,65 @@ def ui_run_execution(dialog, result, current_run, folder_status): - Run docker images as containers - write output """ - ) - msg = "" - if folder_status == folder.FOLDER_DOES_NOT_MATCH: - msg = """The project folder structure does not match the steps of the execution: - choose an empty project folder and prepare the execution before you can run it.""" - text_color = "text-red" - elif folder_status == folder.FOLDER_NOT_SET: - msg = """The project folder has not been set: - Select an empty project folder and prepare the execution before you can run it.""" - text_color = "text-red" - elif folder_status == folder.FOLDER_EMPTY: - msg = """The project folder is empty: - Prepare the execution before you can run it.""" - text_color = "text-red" - if msg: - ui.label(msg).classes(text_color) - rh.ui_next_back(current_run, ready_for_next=False) - return - if folder_status == folder.FOLDER_HAS_OUTPUT: - msg = """The execution has already been run and the project folder has output.""" - text_color = "text-teal" - else: - msg = """The execution is ready to run.""" - text_color = "text-teal" - ui.label(msg).classes(text_color) + ) execution = current_run["execution"] project_path = current_run["project_path"] secret_files = current_run["secret_files"] - if folder_status != folder.FOLDER_HAS_OUTPUT: - cli_run_command = build_command( + if folder_status >= rh.FOLDER_PREPARED: + cli_run_command = rh.build_cli_command( cmd="run", secret_files=secret_files, execution_id=execution["execution_id"], project_path=project_path, ) + cli_log_commands = [] + for i, _ in enumerate(execution["versions"]): + cli_log_commands. append(rh.build_cli_command( + cmd="streamlogs", + project_path=project_path, + step_nr=str(i) + )) with ui.row().classes("w-full"): ui.label(cli_run_command).classes("font-mono") - with ui.row().classes("w-full"): - ui.icon("warning").classes("text-lg text-yellow") - ui.label( - """It can take a while until you see output in this step: - loading means just that the job is still running.""" - ) with ui.row().classes("w-full"): ui.button( "Run execution", - on_click=lambda: run_command(cli_run_command, dialog, result), + on_click=lambda: submit_command(cli_run_command), icon="rocket", ).props("no-caps") - else: - cli_output_command = build_command( - cmd="output", - execution_id=execution["execution_id"], - project_path=project_path, - ) with ui.row().classes("w-full"): - ui.button( - "Show folder with output", - on_click=lambda: run_command(cli_output_command, dialog, result), - icon="info", - ).props("no-caps") + ui.icon("warning").classes("text-lg text-yellow") + ui.label( + """The logs for a step only become available once it is running. + So if they are not available right away, they may be so when you + click the button again. + """ + ) + for i, cli_log_command in enumerate(cli_log_commands): + with ui.row().classes("w-full"): + ui.button( + f"show logs for step {i}", + on_click=lambda cli_log_command=cli_log_command: run_command(cli_log_command, dialog, result), + icon="info", + ).props("no-caps") rh.ui_next_back(current_run, ready_for_next=False) +async def submit_command(command): + try: + log.info(command) + log.info(shlex.split(command, posix="win" not in sys.platform.lower())) + process = await asyncio.create_subprocess_exec( + *shlex.split(command, posix="win" not in sys.platform.lower()), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=os.path.dirname(os.path.abspath(__file__)), + ) + ui.notify(f"{command} has been submitted as {process}. Click on 'Show logs to view the progress", type="positive") + except Exception as e: + log.exception(f"run command failed with Exception {e}") + + async def run_command(command: str, dialog, result) -> None: """Run a command in the background and display the output in the pre-created dialog. This function has been copied from nicegui examples. @@ -185,8 +129,8 @@ async def run_command(command: str, dialog, result) -> None: try: dialog.open() result.content = "... loading" - # NOTE replace with machine-independent Python path (#1240) - command = command.replace("python3", sys.executable) + log.info(command) + log.info(shlex.split(command, posix="win" not in sys.platform.lower())) process = await asyncio.create_subprocess_exec( *shlex.split(command, posix="win" not in sys.platform.lower()), stdout=asyncio.subprocess.PIPE, @@ -196,7 +140,7 @@ async def run_command(command: str, dialog, result) -> None: # NOTE we need to read the output in chunks, otherwise the process will block output = "" while True: - new = await process.stdout.read(4096) + new = await process.stdout.read(100000) if not new: break output += new.decode() diff --git a/odtp/dashboard/page_run/secrets.py b/odtp/dashboard/page_run/secrets.py index 45afdd19..6dcf8941 100644 --- a/odtp/dashboard/page_run/secrets.py +++ b/odtp/dashboard/page_run/secrets.py @@ -21,8 +21,7 @@ def ui_add_secrets_form(current_run, workdir): if not version_tags: return with ui.row(): - ui.icon("check").classes("text-teal text-lg") - ui.label("Adding Secrets files is an optional step").classes("text-teal") + ui.label("Adding Secrets files is an optional step") with ui.grid(columns=2).classes("flex items-center w-full"): for j, version_tag in enumerate(version_tags): secret_file = current_run["secret_files"][j] @@ -52,7 +51,7 @@ def ui_add_secrets_form(current_run, workdir): on_click=lambda: remove_secrets_files(current_run), icon="clear", ).props("flat") - rh.ui_next_back(current_run) + rh.ui_next_back(current_run, ready_for_next=True) async def pick_secrets_file(step_nr, workdir, current_run) -> None: diff --git a/odtp/dashboard/page_run/workarea.py b/odtp/dashboard/page_run/workarea.py index 139f0023..c189b61e 100644 --- a/odtp/dashboard/page_run/workarea.py +++ b/odtp/dashboard/page_run/workarea.py @@ -2,6 +2,7 @@ import odtp.dashboard.utils.storage as storage import odtp.dashboard.utils.ui_theme as ui_theme +import odtp.dashboard.page_run.helpers as rh def ui_workarea_layout(current_user, workdir, current_execution, current_digital_twin): @@ -45,8 +46,13 @@ def ui_workarea_layout(current_user, workdir, current_execution, current_digital secret_files = "" else: ",".join(secret_files) + folder_status = rh.get_folder_status( + execution_id=current_execution["execution_id"], + project_path=project_path, + ) + project_path_display = project_path if not project_path: - project_path = ui_theme.MISSING_VALUE + project_path_display = ui_theme.MISSING_VALUE with ui.grid(columns=2): with ui.column(): ui.markdown( @@ -57,9 +63,11 @@ def ui_workarea_layout(current_user, workdir, current_execution, current_digital - **current execution**: {current_execution.get("title")} - **secret files**: {secret_files} - **work directory**: {workdir} - - **project directory**: {project_path} + - **project directory**: {project_path_display} """ ) + rh.ui_display_folder_status(folder_status) + rh.ui_display_secrets(secret_files) with ui.column(): if current_execution: ui.markdown( diff --git a/odtp/dashboard/utils/helpers.py b/odtp/dashboard/utils/helpers.py index 0f29b99f..c2ec61dd 100644 --- a/odtp/dashboard/utils/helpers.py +++ b/odtp/dashboard/utils/helpers.py @@ -62,14 +62,14 @@ def get_execution_select_options(digital_twin_id): sub_collection=db.collection_executions, item_id=digital_twin_id, ref_name=db.collection_executions, - sort_by=[("start_timestamp", db.DESCENDING)], + sort_by=[("createdAt", db.DESCENDING)], ) if not executions: return {} execution_options = {} for execution in executions: execution_options[str(execution["_id"])] = ( - f"{execution['start_timestamp'].strftime('%d/%m/%y')} {execution.get('title')}" + f"{execution['createdAt'].strftime('%d/%m/%y')} {execution.get('title')}" ) return execution_options @@ -103,7 +103,7 @@ def build_execution_with_steps(execution_id): execution_with_steps = { "execution_id": execution_id, "title": execution.get("title"), - "timestamp": execution.get("start_timestamp").strftime("%m/%d/%Y, %H:%M:%S"), + "createdAt": execution.get("createdAt").strftime("%m/%d/%Y, %H:%M:%S"), "versions": execution["workflowSchema"]["component_versions"], "version_tags": version_tags, "steps": step_ids, diff --git a/odtp/helpers/environment.py b/odtp/helpers/environment.py index 5c7ab88c..b8317a84 100644 --- a/odtp/helpers/environment.py +++ b/odtp/helpers/environment.py @@ -1,9 +1,12 @@ import os +import shutil +import logging import odtp.helpers.utils as utils import odtp.helpers.utils as odtp_utils import odtp.mongodb.db as db +log = logging.getLogger(__name__) class OdtpLocalEnvironmentException(Exception): pass @@ -58,3 +61,12 @@ def directory_has_output(execution_id, project_folder): if len(os.listdir(output_dir)) != 0: return True return False + +def delete_folder(folder_path, keep_project_path=True): + if os.path.exists(folder_path): + shutil.rmtree(folder_path) + + if keep_project_path: + os.mkdir(folder_path) + + log.info("Folder deleted: %s", folder_path) \ No newline at end of file diff --git a/odtp/helpers/settings.py b/odtp/helpers/settings.py index 0dec8df0..386d433d 100644 --- a/odtp/helpers/settings.py +++ b/odtp/helpers/settings.py @@ -5,6 +5,7 @@ load_dotenv() logger = logging.getLogger(__name__) +code_file_dir = os.path.abspath(__file__) DEFAULT_LOG_LEVEL = "ERROR" DEFAULT_RUN_LOG_LEVEL = "INFO" @@ -49,3 +50,12 @@ class OdtpSettingsException(Exception): log_levels = logging.getLevelNamesMapping() if not RUN_LOG_LEVEL in log_levels.keys(): RUN_LOG_LEVEL = DEFAULT_RUN_LOG_LEVEL + +def get_command_log_handler(): + log_file_path = os.path.join(os.path.dirname(os.path.dirname(code_file_dir)), 'odtp.log') + command_log_handler = logging.FileHandler(log_file_path) + FORMATTER = logging.Formatter( + '%(asctime)s - [%(module)s:%(levelname)s] %(lineno)d %(filename)s %(funcName)s - %(message)s' + ) + command_log_handler.setFormatter(FORMATTER) + return command_log_handler diff --git a/odtp/helpers/utils.py b/odtp/helpers/utils.py index f6cbb974..0c33c273 100644 --- a/odtp/helpers/utils.py +++ b/odtp/helpers/utils.py @@ -1,4 +1,5 @@ import json +from prettytable import PrettyTable import odtp.mongodb.db as db from odtp import __version__ @@ -45,3 +46,57 @@ def get_version_names_for_execution(execution, naming_function=get_execution_ste version_dict = get_version_name_dict_for_version_ids(version_ids, naming_function=naming_function) version_names = [version_dict[version_id] for version_id in version_ids] return version_names + +def output_as_pretty_table(db_output, collection_name): + table = PrettyTable() + + if collection_name == "users": + keys = ["_id", "displayName", "email", "github"] + table.field_names = ["User ID", "Display Name", "Email", "Github User"] + + for item in db_output: + table.add_row([item[key] for key in keys] ) + + elif collection_name == "digitalTwins": + keys = ["_id", "name", "status", "public", "created_at", "updated_at", "userRef"] + table.field_names = keys + + for item in db_output: + table.add_row([item[key] for key in keys] ) + + elif collection_name == "executions": + keys = ["_id", "title", "description", "start_timestamp", "end_timestamp", "digitalTwinRef"] + table.field_names = keys + + for item in db_output: + table.add_row([item[key] for key in keys] ) + + elif collection_name == "components": + keys = ["_id", "author", "componentName", "repoLink", "status", "title", "type", "description", "created_at", "updated_at"] + table.field_names = keys + + for item in db_output: + table.add_row([item[key] for key in keys] ) + + elif collection_name == "versions": + keys = ["_id", "componentId", "odtp_version", "component_version", "commitHash", "title", "description", "ports", "created_at", "updated_at"] + table.field_names = keys + + for item in db_output: + table.add_row([item[key] for key in keys] ) + + elif collection_name == "outputs": + keys = ["_id", "output_type", "s3_bucket", "s3_key", "file_name", "file_size", "file_type", "created_at", "updated_at", "stepRef"] + table.field_names = keys + + for item in db_output: + table.add_row([item[key] for key in keys] ) + + elif collection_name == "results": + keys = ["_id", "executionRef", "digitalTwinRef", "title", "description", "created_at", "updated_at"] + table.field_names = keys + + for item in db_output: + table.add_row([item[key] for key in keys] ) + + print(table) \ No newline at end of file diff --git a/odtp/mongodb/db.py b/odtp/mongodb/db.py index bf933adf..e8635ca2 100644 --- a/odtp/mongodb/db.py +++ b/odtp/mongodb/db.py @@ -20,6 +20,8 @@ collection_executions = "executions" collection_steps = "steps" collection_results = "results" +collection_logs = "logs" +collection_outputs = "outputs" log = logging.getLogger(__name__) @@ -122,10 +124,11 @@ def check_document_id_in_collection(document_id, collection): def delete_document_by_id(document_id, collection): + log.debug(f"Deleting {collection} : {document_id}") with MongoClient(ODTP_MONGO_SERVER) as client: db = client[ODTP_MONGO_DB] document = db[collection].delete_one({"_id": ObjectId(document_id)}) - log.info(f"Document with ID {document_id} was deleted") + log.debug(f"Document with ID {document_id} was deleted") def get_sub_collection_items(collection, sub_collection, item_id, ref_name, sort_by=None): @@ -163,6 +166,26 @@ def get_component_version(component_name, version_tag): version_documents = mongodb_utils.get_list_from_cursor(cursor) return version_documents +def get_documents_id_by_field_value(field_path, field_value, collection): + with MongoClient(ODTP_MONGO_SERVER) as client: + db = client[ODTP_MONGO_DB] + documents_cursors = db[collection].find({field_path: field_value}, {"_id": 1}) + + documents = [str(doc["_id"]) for doc in documents_cursors] + if len(documents) > 0: + return documents + else: + return None + +def remove_value_from_list_in_field(collection, document_id, field_name, value): + with MongoClient(ODTP_MONGO_SERVER) as client: + db = client[ODTP_MONGO_DB] + db[collection].update_one( + {"_id": ObjectId(document_id)}, + {"$pull": {field_name: value}} + ) + + def add_user(name, github, email): """add new user and return id""" @@ -346,17 +369,19 @@ def add_execution( "component_versions": versions, "workflowExecutorSchema": workflow, }, - "start_timestamp": datetime.now(timezone.utc), - "end_timestamp": datetime.now(timezone.utc), + "start_timestamp": None, + "end_timestamp": None, # Array of ObjectIds referencing Steps collection. Change in a future by DAG graph "steps": [], + "createdAt": datetime.now(timezone.utc), + "updatedAt": datetime.now(timezone.utc) } steps = [] for i, version in enumerate(versions): step = { "timestamp": datetime.now(timezone.utc), - "start_timestamp": datetime.now(timezone.utc), - "end_timestamp": datetime.now(timezone.utc), + "start_timestamp": None, + "end_timestamp": None, "type": "ephemeral", "logs": [], "inputs": {}, @@ -364,6 +389,8 @@ def add_execution( "component_version": versions[i], "parameters": parameters[i] or {}, "ports": ports[i], + "createdAt": datetime.now(timezone.utc), + "updatedAt": datetime.now(timezone.utc) } steps.append(step) execution_id = append_execution_to_digital_twin(db, dt_id, execution) @@ -400,6 +427,51 @@ def append_step_to_execution(db, execution_id, step): db.executions.update_one({"_id": execution_id}, {"$push": {"steps": step_id}}) return step_id +def get_all_outputs_s3_keys(execution_id): + execution_doc = get_document_by_id(execution_id, collection_executions) + digital_twin_id = execution_doc["digitalTwinRef"] + steps_ids = execution_doc['steps'] + + s3_keys = [] + for step_id in steps_ids: + output_ids = get_documents_id_by_field_value("stepRef", str(step_id), collection_outputs) + if output_ids: + s3_keys += [get_document_by_id(output_id, collection_outputs)["s3_key"] for output_id in output_ids] + + return s3_keys + +def delete_execution(execution_id, debug=True): + # DB + # Delete execution, steps, output, logs, + # Update: remove id from results, remove execution from dt + execution_doc = get_document_by_id(execution_id, collection_executions) + digital_twin_id = execution_doc["digitalTwinRef"] + # TODO: Waiting for results to be implemented + #results_id = get_document_by_id(digital_twin_id, collection_digital_twins)["results"][0] + + steps_ids = execution_doc['steps'] + for step_id in steps_ids: + logs_ids = get_documents_id_by_field_value("stepRef", str(step_id), collection_logs) + if logs_ids: + _ = [delete_document_by_id(log_id, collection_logs) for log_id in logs_ids] + + output_ids = get_documents_id_by_field_value("stepRef", str(step_id), collection_outputs) + if output_ids: + # Update the results document without any outputs reference + for output_id in output_ids: + # TODO: Waiting for results to be implemented + #_ = remove_value_from_list_in_field(collection_results, results_id, "output", ObjectId(output_id)) + pass + # Delete the output document + _ = [delete_document_by_id(output_id, collection_outputs) for output_id in output_ids] + + _ = delete_document_by_id(step_id, collection_steps) + + _ = delete_document_by_id(execution_id, collection_executions) + + # Update the digital twin document without the execution reference + _ = remove_value_from_list_in_field(collection_digital_twins, digital_twin_id, "executions", ObjectId(execution_id)) + def delete_collection(collection): with MongoClient(ODTP_MONGO_SERVER) as client: @@ -432,8 +504,11 @@ def init_collections(): collection_users, collection_components, collection_digital_twins, + collection_executions, + collection_outputs, collection_results, collection_versions, + collection_logs, ]: if name not in collection_names: db.create_collection(name) diff --git a/odtp/run.py b/odtp/run.py index 5a330294..6a6f1dd2 100644 --- a/odtp/run.py +++ b/odtp/run.py @@ -2,6 +2,7 @@ import os import json import subprocess +import docker import odtp.helpers.settings as config import odtp.helpers.git as git_helpers import odtp.helpers.environment as env_helpers @@ -12,10 +13,12 @@ REPO_DIR = "repository" INPUT_DIR = "odtp-input" OUTPUT_DIR = "odtp-output" +LOG_DIR = "odtp-logs" log = logging.getLogger(__name__) -log.setLevel(logging.DEBUG) +log.setLevel(logging.INFO) +log.addHandler(config.get_command_log_handler()) class OdtpRunSetupException(Exception): @@ -33,13 +36,15 @@ def __init__(self, repo_url="", commit_hash="", image_name="", project_folder="" self.dockerfile_path = os.path.join(self.project_folder, REPO_DIR) self.docker_image_name = image_name self.input_volume = os.path.join(self.project_folder, INPUT_DIR) + self.log_volume = os.path.join(self.project_folder, LOG_DIR) self.output_volume = os.path.join(self.project_folder, OUTPUT_DIR) def prepare_component(self): self._checks_for_prepare() self._create_project_folder_structure() - self._download_repo() - self._build_image() + if not self._check_if_image_exists(): + self._download_repo() + self._build_image() def _create_project_folder_structure(self): """Create all the folder structure in project_folder""" @@ -47,7 +52,8 @@ def _create_project_folder_structure(self): os.makedirs(self.repository_path, exist_ok=True) os.makedirs(self.input_volume, exist_ok=True) os.makedirs(self.output_volume, exist_ok=True) - + os.makedirs(self.log_volume, exist_ok=True) + def _check_project_folder_prepared(self): log.debug(f"VALIDATION: check project folder structure: {self.project_folder}") """check whether the project folder is prepared with the expected @@ -57,7 +63,7 @@ def _check_project_folder_prepared(self): for entry in entries: if entry.is_dir(): subdirs.append(entry.name) - if set(subdirs) != set(REPO_DIR, INPUT_DIR, OUTPUT_DIR): + if set(subdirs) != set(REPO_DIR, INPUT_DIR, OUTPUT_DIR, LOG_DIR): raise OdtpRunSetupException( f"""project folder {self.project_folder} does not have expected directory structure with {REPO_DIR}, {INPUT_DIR}, {OUTPUT_DIR}""" @@ -72,7 +78,7 @@ def _checks_for_prepare(self): ) def _checks_for_run(self, parameters, ports, image_name): - log.info("VALIDATION: check for run") + log.info("VALIDATION: check for run") self._check_project_folder_prepared() self._check_image_exists() try: @@ -83,6 +89,20 @@ def _checks_for_run(self, parameters, ports, image_name): db_utils.check_port_mappings_for_component_runs(ports) self._check_image_exists() + def _check_if_image_exists(self): + """ + Check whether a docker image exists + """ + logging.info(f"VALIDATION: Checking if Docker image exists: {self.docker_image_name}") + client = docker.from_env() + images = client.images.list(name=self.docker_image_name) + logging.info(f"Images found: {images}") + + if len(images) > 0: + return True + else: + return False + def _download_repo(self): """ Download a GitHub repository to the specified destination. @@ -90,24 +110,34 @@ def _download_repo(self): Args: destination (str): The destination directory to download the repository. """ - log.info(f"PREPARE: Downloading repository from {self.repo_url} to {self.repository_path}") - subprocess.run( - ["git", + log.debug(f"PREPARE: Downloading repository from {self.repo_url} to {self.repository_path}") + git_clone_command = [ + "git", "clone", - "--recurse-submodules", self.repo_url, - os.path.join(self.project_folder, "repository") - ] - ) - subprocess.run( - ["git", + self.repository_path, + ] + log.info(" ".join(git_clone_command)) + subprocess.run(git_clone_command) + git_checkout_command = [ + "git", "-C", - os.path.join(self.project_folder, - "repository"), + self.repository_path, "checkout", - self.commit_hash - ] - ) + self.commit_hash, + ] + log.info(" ".join(git_checkout_command)) + subprocess.run(git_checkout_command) + git_submodule_command = [ + "git", + "-C", + self.repository_path, + "submodule", + "update", + "--init", + ] + log.info(" ".join(git_submodule_command)) + subprocess.run(git_submodule_command) def _build_image(self): """ @@ -139,14 +169,14 @@ def _create_volume(self, volume_name): log.info(f"RUN: Creating Docker volume {volume_name}") subprocess.run(["docker", "volume", "create", volume_name]) - def run_component(self, parameters, secrets, ports, instance_name, step_id=None, debug=False): + def run_component(self, parameters, secrets, ports, container_name, step_id=None): """ Run a Docker component with the specified parameters. Args: secrets (dict): The secrets variables to pass to the Docker component. parameters (dict): The environment variables to pass to the Docker component. - instance_name (str, optional): The name of the Docker container. Defaults to "odtp_component". + container_name (str, optional): The name of the Docker container. Defaults to "odtp_component". Returns: str: The ID of the Docker run. @@ -173,25 +203,31 @@ def run_component(self, parameters, secrets, ports, instance_name, step_id=None, else: secrets_args = [""] - docker_run_command = ["docker", "run", "--rm", "-it", "--name", instance_name, + docker_run_command = ["docker", "run", "--rm", "-it", "--name", container_name, "--network", "odtp_odtp-network", "--volume", f"{os.path.abspath(self.input_volume)}:/odtp/odtp-input", + "--volume", f"{os.path.abspath(self.log_volume)}:/odtp/odtp-logs", "--volume", f"{os.path.abspath(self.output_volume)}:/odtp/odtp-output"] + env_args + ports_args + secrets_args + [self.docker_image_name] command_string = ' '.join(docker_run_command) - if debug: - log.debug(f"Command to be executed: {command_string}") + command_string_log_safe = command_string + for value in [parameters["ODTP_SECRET_KEY"], parameters["ODTP_ACCESS_KEY"], parameters["ODTP_MONGO_SERVER"]]: + command_string_log_safe = command_string_log_safe.replace(value, "x") + if secrets: + for value in secrets.values(): + command_string_log_safe = command_string_log_safe.replace(value, "x") + log.info(command_string_log_safe) process = subprocess.Popen(command_string, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) output, error = process.communicate() - + if process.returncode != 0: - log.exception(f"Failed to run Docker component {instance_name}: {error.decode()}") + log.exception(f"Failed to run Docker component {container_name}: {error.decode()}") return None else: docker_run_id = output.decode().strip() - log.info(f"Docker run was started with success: {instance_name}") + log.info(f"Docker run was started with success: {container_name}") return docker_run_id def stop_component(self, name="odtpruntest"): @@ -215,7 +251,7 @@ def stop_component(self, name="odtpruntest"): else: return f"Docker component {name} has been stopped." - def delete_component(self, instance_name="odtpruntest"): + def delete_component(self, container_name="odtpruntest"): """ Delete a Docker component. @@ -225,16 +261,16 @@ def delete_component(self, instance_name="odtpruntest"): Returns: str: A message indicating the Docker component has been deleted. """ - log.info(f"Deleting Docker component {instance_name}") - docker_rm_command = ["docker", "rm", instance_name] + log.info(f"Deleting Docker component {container_name}") + docker_rm_command = ["docker", "rm", container_name] process = subprocess.Popen(docker_rm_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) _, error = process.communicate() if process.returncode != 0: - log.exception(f"Failed to delete Docker component {instance_name}: {error.decode()}") + log.exception(f"Failed to delete Docker component {container_name}: {error.decode()}") return None else: - return f"Docker component {instance_name} has been deleted." + return f"Docker component {container_name} has been deleted." def delete_image(self): """ diff --git a/odtp/storage.py b/odtp/storage.py index 2a17b4e0..4bbb8872 100644 --- a/odtp/storage.py +++ b/odtp/storage.py @@ -16,8 +16,7 @@ def __init__(self): self.bucketName = settings.ODTP_BUCKET_NAME def test_connection(self): - bucket = self.s3.head_bucket(Bucket=self.bucketName) - return bucket + self.s3.head_bucket(Bucket=self.bucketName) # Method to close the client connection def closeConnection(self): @@ -37,7 +36,7 @@ def createFolderStructure(self, structure=["odtp"]): # Add a trailing slash to make S3 recognize it as a folder self.s3.put_object(Bucket=self.bucketName, Key=path + '/') - print("Folder Structure Created") + log.info("Folder Structure Created") # Method to create a specific folder # The idea is to create paths such as Digital Twin > Execution > Step > Output @@ -106,7 +105,7 @@ def deleteAll(self): # This will delete all objects in the bucket. bucket.objects.all().delete() - print("Folder Structure Deleted") + log.info("Folder Structure Deleted") # Method to delete one file in s3 def deleteFile(self, s3_path): @@ -123,6 +122,23 @@ def deleteFile(self, s3_path): self.s3.delete_object(Bucket=self.bucketName, Key=s3_path) log.info(f"File '{s3_path}' deleted from S3 bucket") + # Method to delete multiple files in s3 + def deletePaths(self, s3_paths): + """ + Deletes multiple files from specific paths in the S3 bucket. + Args: + s3_paths (list): A list of S3 paths of the files to delete. + Returns: + None + """ + + for s3_path in s3_paths: + objects_to_delete = self.s3.list_objects(Bucket=self.bucketName, Prefix=s3_path) + if 'Contents' in objects_to_delete: + for key in objects_to_delete['Contents']: + self.s3.delete_object(Bucket=self.bucketName, Key=key['Key']) + log.info(f"Path '{s3_path}' deleted from S3 bucket") + def create_folders(self, structure): self.s3.createFolderStructure(structure) log.info("Folder structure generated") diff --git a/odtp/workflow.py b/odtp/workflow.py index 375557dd..23bef008 100644 --- a/odtp/workflow.py +++ b/odtp/workflow.py @@ -1,13 +1,15 @@ import os from odtp.run import DockerManager, OdtpRunSetupException import odtp.helpers.utils as odtp_utils +import odtp.helpers.settings as config import odtp.helpers.environment as env_helpers import odtp.mongodb.db as db import logging import zipfile log = logging.getLogger(__name__) -log.setLevel(logging.DEBUG) +log.setLevel(logging.INFO) +log.addHandler(config.get_command_log_handler()) class WorkflowManager: @@ -20,7 +22,7 @@ def __init__(self, execution_data, working_path, secrets): self.image_names = [] self.repo_urls = [] self.commits = [] - self.instance_names = [] + self.container_names = [] self.steps_folder_paths = [] self.secrets = secrets @@ -48,12 +50,15 @@ def __init__(self, execution_data, working_path, secrets): step_folder_path = os.path.join(self.working_path, step_name) self.steps_folder_paths.append(step_folder_path) - image_name = step_name + image_name = odtp_utils.get_execution_step_name( + component_name=component_name, + component_version=component_version + ) self.image_names.append(image_name) self.repo_urls.append(repo_link) self.commits.append(commit_hash) - self.instance_names.append(image_name) + self.container_names.append(step_name) except Exception as e: raise OdtpRunSetupException( f"Workflowmanager could not be intialized: Exception occured: {e}" @@ -92,20 +97,6 @@ def prepare_workflow(self): log.info("COMPONENTS DOWNLOADED AND BUILT") - def download_data_from_storage(self): - # Implement the logic to download data from S3 - pass - - def download_and_build_components(self): - # This method will clone the githubs and build the components. - pass - - def extract_parameters(self): - # Implement the logic to extract parameters from the Barfi schema - # These parameters will be send as environment variable. - self.paramenters = {} - pass - def run_workflow(self): # Implement the logic to send tasks following the DAG schema. # This can make use of barfi workflow execution function. Each @@ -113,12 +104,19 @@ def run_workflow(self): # Temporally the parameters are taken from the environment files and not # taken from the steps documents + + # Start execution timestamp + db.set_document_timestamp(self.execution["_id"], db.collection_executions, "start_timestamp") + for step_index in self.schema["workflowExecutorSchema"]: log.info(f"running step {step_index}") step_index = int(step_index) step_id = self.execution["steps"][step_index] + # Start step timestamp + db.set_document_timestamp(step_id, db.collection_steps, "start_timestamp") + secrets = self.secrets[step_index] step_doc = db.get_document_by_id( @@ -159,7 +157,7 @@ def run_workflow(self): # By now the image_name is just the name of the component and the version componentManager = DockerManager( repo_url=self.repo_urls[step_index], - image_name=self.image_names[step_index], + image_name=self.image_names[step_index], project_folder=self.steps_folder_paths[step_index] ) @@ -170,35 +168,12 @@ def run_workflow(self): parameters, secrets, ports=ports, - instance_name=self.instance_names[step_index], + container_name=self.container_names[step_index], step_id=self.execution["steps"][step_index] ) + + # End step timestamp + db.set_document_timestamp(step_id, db.collection_steps, "end_timestamp") - def run_task(self): - # Implement the logic of running one single task. - # Send the step ID as Environment variable so the component can log the progress - pass - - def stop_workflow(self): - # This will stop the execution of the workflow. - pass - - def stop_task(self): - # This will stop one single task - pass - - def delete_workflow(self): - # This method will delete all records in the DB and related docker components downloaded in the folder. - pass - - def delete_db_entry(self): - # Method to remove the workflow execution from the database - pass - - def delete_docker_components(self): - # Method to remove all docker containers used by this workflow. - pass - - def restart_workflow(self): - # This will restart the execution of the workflow. - pass + # End execution timestamp + db.set_document_timestamp(self.execution["_id"], db.collection_executions, "end_timestamp") \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 44f01029..7f90c7ef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "aiofiles" -version = "23.2.1" +version = "24.1.0" description = "File support for asyncio." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, - {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, ] [[package]] @@ -122,24 +122,24 @@ frozenlist = ">=1.1.0" [[package]] name = "annotated-types" -version = "0.6.0" +version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "anyio" -version = "4.3.0" +version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] [package.dependencies] @@ -227,17 +227,17 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.34.103" +version = "1.34.134" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.103-py3-none-any.whl", hash = "sha256:59b6499f1bb423dd99de6566a20d0a7cf1a5476824be3a792290fd86600e8365"}, - {file = "boto3-1.34.103.tar.gz", hash = "sha256:58d097241f3895c4a4c80c9e606689c6e06d77f55f9f53a4cc02dee7e03938b9"}, + {file = "boto3-1.34.134-py3-none-any.whl", hash = "sha256:342782c02ff077aae118c9c61179eed95c585831fba666baacc5588ff04aa6e1"}, + {file = "boto3-1.34.134.tar.gz", hash = "sha256:f6d6e5b0c9ab022a75373fa16c01f0cd54bc1bb64ef3b6ac64ac7cedd56cbe9c"}, ] [package.dependencies] -botocore = ">=1.34.103,<1.35.0" +botocore = ">=1.34.134,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -246,13 +246,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.103" +version = "1.34.134" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.103-py3-none-any.whl", hash = "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431"}, - {file = "botocore-1.34.103.tar.gz", hash = "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939"}, + {file = "botocore-1.34.134-py3-none-any.whl", hash = "sha256:45219e00639755f92569b29f8f279d5dde721494791412c1f7026a3779e8d9f4"}, + {file = "botocore-1.34.134.tar.gz", hash = "sha256:e29c299599426ed16dd2d4c1e20eef784f96b15e1850ebbc59a3250959285b95"}, ] [package.dependencies] @@ -261,17 +261,17 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.20.9)"] +crt = ["awscrt (==0.20.11)"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -1016,68 +1016,68 @@ files = [ [[package]] name = "orjson" -version = "3.10.3" +version = "3.10.5" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"}, - {file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"}, - {file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"}, - {file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"}, - {file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"}, - {file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"}, - {file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"}, - {file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"}, - {file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"}, - {file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"}, - {file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"}, - {file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"}, - {file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"}, - {file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"}, - {file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"}, - {file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"}, + {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"}, + {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"}, + {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"}, + {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"}, + {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"}, + {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"}, + {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"}, + {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"}, + {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"}, + {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"}, + {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"}, + {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"}, + {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"}, + {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"}, + {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"}, + {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, ] [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1165,13 +1165,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -1357,71 +1357,61 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymongo" -version = "4.7.2" +version = "4.8.0" description = "Python driver for MongoDB " optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pymongo-4.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:268d8578c0500012140c5460755ea405cbfe541ef47c81efa9d6744f0f99aeca"}, - {file = "pymongo-4.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:827611beb6c483260d520cfa6a49662d980dfa5368a04296f65fa39e78fccea7"}, - {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a754e366c404d19ff3f077ddeed64be31e0bb515e04f502bf11987f1baa55a16"}, - {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44efab10d9a3db920530f7bcb26af8f408b7273d2f0214081d3891979726328"}, - {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35b3f0c7d49724859d4df5f0445818d525824a6cd55074c42573d9b50764df67"}, - {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e37faf298a37ffb3e0809e77fbbb0a32b6a2d18a83c59cfc2a7b794ea1136b0"}, - {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1bcd58669e56c08f1e72c5758868b5df169fe267501c949ee83c418e9df9155"}, - {file = "pymongo-4.7.2-cp310-cp310-win32.whl", hash = "sha256:c72d16fede22efe7cdd1f422e8da15760e9498024040429362886f946c10fe95"}, - {file = "pymongo-4.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:12d1fef77d25640cb78893d07ff7d2fac4c4461d8eec45bd3b9ad491a1115d6e"}, - {file = "pymongo-4.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc5af24fcf5fc6f7f40d65446400d45dd12bea933d0299dc9e90c5b22197f1e9"}, - {file = "pymongo-4.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:730778b6f0964b164c187289f906bbc84cb0524df285b7a85aa355bbec43eb21"}, - {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47a1a4832ef2f4346dcd1a10a36ade7367ad6905929ddb476459abb4fd1b98cb"}, - {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6eab12c6385526d386543d6823b07187fefba028f0da216506e00f0e1855119"}, - {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37e9ea81fa59ee9274457ed7d59b6c27f6f2a5fe8e26f184ecf58ea52a019cb8"}, - {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e9d9d2c0aae73aa4369bd373ac2ac59f02c46d4e56c4b6d6e250cfe85f76802"}, - {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6e00a79dff22c9a72212ad82021b54bdb3b85f38a85f4fc466bde581d7d17a"}, - {file = "pymongo-4.7.2-cp311-cp311-win32.whl", hash = "sha256:02efd1bb3397e24ef2af45923888b41a378ce00cb3a4259c5f4fc3c70497a22f"}, - {file = "pymongo-4.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:87bb453ac3eb44db95cb6d5a616fbc906c1c00661eec7f55696253a6245beb8a"}, - {file = "pymongo-4.7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:12c466e02133b7f8f4ff1045c6b5916215c5f7923bc83fd6e28e290cba18f9f6"}, - {file = "pymongo-4.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f91073049c43d14e66696970dd708d319b86ee57ef9af359294eee072abaac79"}, - {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87032f818bf5052ab742812c715eff896621385c43f8f97cdd37d15b5d394e95"}, - {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a87eef394039765679f75c6a47455a4030870341cb76eafc349c5944408c882"}, - {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d275596f840018858757561840767b39272ac96436fcb54f5cac6d245393fd97"}, - {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82102e353be13f1a6769660dd88115b1da382447672ba1c2662a0fbe3df1d861"}, - {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194065c9d445017b3c82fb85f89aa2055464a080bde604010dc8eb932a6b3c95"}, - {file = "pymongo-4.7.2-cp312-cp312-win32.whl", hash = "sha256:db4380d1e69fdad1044a4b8f3bb105200542c49a0dde93452d938ff9db1d6d29"}, - {file = "pymongo-4.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:fadc6e8db7707c861ebe25b13ad6aca19ea4d2c56bf04a26691f46c23dadf6e4"}, - {file = "pymongo-4.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2cb77d09bd012cb4b30636e7e38d00b5f9be5eb521c364bde66490c45ee6c4b4"}, - {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56bf8b706946952acdea0fe478f8e44f1ed101c4b87f046859e6c3abe6c0a9f4"}, - {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcf337d1b252405779d9c79978d6ca15eab3cdaa2f44c100a79221bddad97c8a"}, - {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ffd1519edbe311df73c74ec338de7d294af535b2748191c866ea3a7c484cd15"}, - {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d59776f435564159196d971aa89422ead878174aff8fe18e06d9a0bc6d648c"}, - {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:347c49cf7f0ba49ea87c1a5a1984187ecc5516b7c753f31938bf7b37462824fd"}, - {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:84bc00200c3cbb6c98a2bb964c9e8284b641e4a33cf10c802390552575ee21de"}, - {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fcaf8c911cb29316a02356f89dbc0e0dfcc6a712ace217b6b543805690d2aefd"}, - {file = "pymongo-4.7.2-cp37-cp37m-win32.whl", hash = "sha256:b48a5650ee5320d59f6d570bd99a8d5c58ac6f297a4e9090535f6561469ac32e"}, - {file = "pymongo-4.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5239ef7e749f1326ea7564428bf861d5250aa39d7f26d612741b1b1273227062"}, - {file = "pymongo-4.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2dcf608d35644e8d276d61bf40a93339d8d66a0e5f3e3f75b2c155a421a1b71"}, - {file = "pymongo-4.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25eeb2c18ede63891cbd617943dd9e6b9cbccc54f276e0b2e693a0cc40f243c5"}, - {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9349f0bb17a31371d4cacb64b306e4ca90413a3ad1fffe73ac7cd495570d94b5"}, - {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffd4d7cb2e6c6e100e2b39606d38a9ffc934e18593dc9bb326196afc7d93ce3d"}, - {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a8bd37f5dabc86efceb8d8cbff5969256523d42d08088f098753dba15f3b37a"}, - {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c78f156edc59b905c80c9003e022e1a764c54fd40ac4fea05b0764f829790e2"}, - {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d892fb91e81cccb83f507cdb2ea0aa026ec3ced7f12a1d60f6a5bf0f20f9c1f"}, - {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87832d6076c2c82f42870157414fd876facbb6554d2faf271ffe7f8f30ce7bed"}, - {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ce1a374ea0e49808e0380ffc64284c0ce0f12bd21042b4bef1af3eb7bdf49054"}, - {file = "pymongo-4.7.2-cp38-cp38-win32.whl", hash = "sha256:eb0642e5f0dd7e86bb358749cc278e70b911e617f519989d346f742dc9520dfb"}, - {file = "pymongo-4.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:4bdb5ffe1cd3728c9479671a067ef44dacafc3743741d4dc700c377c4231356f"}, - {file = "pymongo-4.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:743552033c63f0afdb56b9189ab04b5c1dbffd7310cf7156ab98eebcecf24621"}, - {file = "pymongo-4.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5239776633f7578b81207e5646245415a5a95f6ae5ef5dff8e7c2357e6264bfc"}, - {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727ad07952c155cd20045f2ce91143c7dc4fb01a5b4e8012905a89a7da554b0c"}, - {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9385654f01a90f73827af4db90c290a1519f7d9102ba43286e187b373e9a78e9"}, - {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d833651f1ba938bb7501f13e326b96cfbb7d98867b2d545ca6d69c7664903e0"}, - {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf17ea9cea14d59b0527403dd7106362917ced7c4ec936c4ba22bd36c912c8e0"}, - {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cecd2df037249d1c74f0af86fb5b766104a5012becac6ff63d85d1de53ba8b98"}, - {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65b4c00dedbd333698b83cd2095a639a6f0d7c4e2a617988f6c65fb46711f028"}, - {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d9b6cbc037108ff1a0a867e7670d8513c37f9bcd9ee3d2464411bfabf70ca002"}, - {file = "pymongo-4.7.2-cp39-cp39-win32.whl", hash = "sha256:cf28430ec1924af1bffed37b69a812339084697fd3f3e781074a0148e6475803"}, - {file = "pymongo-4.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:e004527ea42a6b99a8b8d5b42b42762c3bdf80f88fbdb5c3a9d47f3808495b86"}, - {file = "pymongo-4.7.2.tar.gz", hash = "sha256:9024e1661c6e40acf468177bf90ce924d1bc681d2b244adda3ed7b2f4c4d17d7"}, + {file = "pymongo-4.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a"}, + {file = "pymongo-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1"}, + {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49"}, + {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8"}, + {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526"}, + {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33"}, + {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84"}, + {file = "pymongo-4.8.0-cp310-cp310-win32.whl", hash = "sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1"}, + {file = "pymongo-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5"}, + {file = "pymongo-4.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68"}, + {file = "pymongo-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7"}, + {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d"}, + {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f"}, + {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486"}, + {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758"}, + {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554"}, + {file = "pymongo-4.8.0-cp311-cp311-win32.whl", hash = "sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba"}, + {file = "pymongo-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88"}, + {file = "pymongo-4.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526"}, + {file = "pymongo-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab"}, + {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf"}, + {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5"}, + {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4"}, + {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8"}, + {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2"}, + {file = "pymongo-4.8.0-cp312-cp312-win32.whl", hash = "sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69"}, + {file = "pymongo-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8"}, + {file = "pymongo-4.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750"}, + {file = "pymongo-4.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f"}, + {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a"}, + {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36"}, + {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470"}, + {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0"}, + {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ecd71b9226bd1d49416dc9f999772038e56f415a713be51bf18d8676a0841c8"}, + {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216"}, + {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f"}, + {file = "pymongo-4.8.0-cp38-cp38-win32.whl", hash = "sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70"}, + {file = "pymongo-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b"}, + {file = "pymongo-4.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0"}, + {file = "pymongo-4.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704"}, + {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871"}, + {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb"}, + {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac"}, + {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c"}, + {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6564780cafd6abeea49759fe661792bd5a67e4f51bca62b88faab497ab5fe89"}, + {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53"}, + {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c"}, + {file = "pymongo-4.8.0-cp39-cp39-win32.whl", hash = "sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8"}, + {file = "pymongo-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f"}, + {file = "pymongo-4.8.0.tar.gz", hash = "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde"}, ] [package.dependencies] @@ -1429,6 +1419,7 @@ dnspython = ">=1.16.0,<3.0.0" [package.extras] aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] +docs = ["furo (==2023.9.10)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-rtd-theme (>=2,<3)", "sphinxcontrib-shellcheck (>=1,<2)"] encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.6.0,<2.0.0)"] gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] @@ -1486,13 +1477,13 @@ cli = ["click (>=5.0)"] [[package]] name = "python-engineio" -version = "4.9.0" +version = "4.9.1" description = "Engine.IO server and client for Python" optional = false python-versions = ">=3.6" files = [ - {file = "python-engineio-4.9.0.tar.gz", hash = "sha256:e87459c15638e567711fd156e6f9c4a402668871bed79523f0ecfec744729ec7"}, - {file = "python_engineio-4.9.0-py3-none-any.whl", hash = "sha256:979859bff770725b75e60353d7ae53b397e8b517d05ba76733b404a3dcca3e4c"}, + {file = "python_engineio-4.9.1-py3-none-any.whl", hash = "sha256:f995e702b21f6b9ebde4e2000cd2ad0112ba0e5116ec8d22fe3515e76ba9dddd"}, + {file = "python_engineio-4.9.1.tar.gz", hash = "sha256:7631cf5563086076611e494c643b3fa93dd3a854634b5488be0bba0ef9b99709"}, ] [package.dependencies] @@ -1517,15 +1508,32 @@ files = [ [package.extras] dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + [[package]] name = "python-socketio" -version = "5.11.2" +version = "5.11.3" description = "Socket.IO server and client for Python" optional = false python-versions = ">=3.8" files = [ - {file = "python-socketio-5.11.2.tar.gz", hash = "sha256:ae6a1de5c5209ca859dc574dccc8931c4be17ee003e74ce3b8d1306162bb4a37"}, - {file = "python_socketio-5.11.2-py3-none-any.whl", hash = "sha256:b9f22a8ff762d7a6e123d16a43ddb1a27d50f07c3c88ea999334f2f89b0ad52b"}, + {file = "python_socketio-5.11.3-py3-none-any.whl", hash = "sha256:2a923a831ff70664b7c502df093c423eb6aa93c1ce68b8319e840227a26d8b69"}, + {file = "python_socketio-5.11.3.tar.gz", hash = "sha256:194af8cdbb7b0768c2e807ba76c7abc288eb5bb85559b7cddee51a6bc7a65737"}, ] [package.dependencies] @@ -1632,13 +1640,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "s3transfer" -version = "0.10.1" +version = "0.10.2" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "s3transfer-0.10.1-py3-none-any.whl", hash = "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d"}, - {file = "s3transfer-0.10.1.tar.gz", hash = "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19"}, + {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"}, + {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"}, ] [package.dependencies] @@ -1703,6 +1711,17 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "typer" version = "0.9.0" @@ -1726,13 +1745,13 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6. [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1748,13 +1767,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -1765,13 +1784,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.29.0" +version = "0.30.1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, - {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, + {file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"}, + {file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"}, ] [package.dependencies] @@ -1848,86 +1867,86 @@ pscript = ">=0.7.0,<0.8.0" [[package]] name = "watchfiles" -version = "0.21.0" +version = "0.22.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.8" files = [ - {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, - {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, - {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, - {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, - {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, - {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, - {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, - {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, - {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, - {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, - {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, - {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, - {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, - {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, - {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, + {file = "watchfiles-0.22.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538"}, + {file = "watchfiles-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d9188979a58a096b6f8090e816ccc3f255f137a009dd4bbec628e27696d67c1"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2bdadf6b90c099ca079d468f976fd50062905d61fae183f769637cb0f68ba59a"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:067dea90c43bf837d41e72e546196e674f68c23702d3ef80e4e816937b0a3ffd"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf8a20266136507abf88b0df2328e6a9a7c7309e8daff124dda3803306a9fdb"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1235c11510ea557fe21be5d0e354bae2c655a8ee6519c94617fe63e05bca4171"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2444dc7cb9d8cc5ab88ebe792a8d75709d96eeef47f4c8fccb6df7c7bc5be71"}, + {file = "watchfiles-0.22.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c5af2347d17ab0bd59366db8752d9e037982e259cacb2ba06f2c41c08af02c39"}, + {file = "watchfiles-0.22.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9624a68b96c878c10437199d9a8b7d7e542feddda8d5ecff58fdc8e67b460848"}, + {file = "watchfiles-0.22.0-cp310-none-win32.whl", hash = "sha256:4b9f2a128a32a2c273d63eb1fdbf49ad64852fc38d15b34eaa3f7ca2f0d2b797"}, + {file = "watchfiles-0.22.0-cp310-none-win_amd64.whl", hash = "sha256:2627a91e8110b8de2406d8b2474427c86f5a62bf7d9ab3654f541f319ef22bcb"}, + {file = "watchfiles-0.22.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8c39987a1397a877217be1ac0fb1d8b9f662c6077b90ff3de2c05f235e6a8f96"}, + {file = "watchfiles-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a927b3034d0672f62fb2ef7ea3c9fc76d063c4b15ea852d1db2dc75fe2c09696"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052d668a167e9fc345c24203b104c313c86654dd6c0feb4b8a6dfc2462239249"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e45fb0d70dda1623a7045bd00c9e036e6f1f6a85e4ef2c8ae602b1dfadf7550"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c49b76a78c156979759d759339fb62eb0549515acfe4fd18bb151cc07366629c"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a65474fd2b4c63e2c18ac67a0c6c66b82f4e73e2e4d940f837ed3d2fd9d4da"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc0cba54f47c660d9fa3218158b8963c517ed23bd9f45fe463f08262a4adae1"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ebe84a035993bb7668f58a0ebf998174fb723a39e4ef9fce95baabb42b787f"}, + {file = "watchfiles-0.22.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0f0a874231e2839abbf473256efffe577d6ee2e3bfa5b540479e892e47c172d"}, + {file = "watchfiles-0.22.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:213792c2cd3150b903e6e7884d40660e0bcec4465e00563a5fc03f30ea9c166c"}, + {file = "watchfiles-0.22.0-cp311-none-win32.whl", hash = "sha256:b44b70850f0073b5fcc0b31ede8b4e736860d70e2dbf55701e05d3227a154a67"}, + {file = "watchfiles-0.22.0-cp311-none-win_amd64.whl", hash = "sha256:00f39592cdd124b4ec5ed0b1edfae091567c72c7da1487ae645426d1b0ffcad1"}, + {file = "watchfiles-0.22.0-cp311-none-win_arm64.whl", hash = "sha256:3218a6f908f6a276941422b035b511b6d0d8328edd89a53ae8c65be139073f84"}, + {file = "watchfiles-0.22.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c7b978c384e29d6c7372209cbf421d82286a807bbcdeb315427687f8371c340a"}, + {file = "watchfiles-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd4c06100bce70a20c4b81e599e5886cf504c9532951df65ad1133e508bf20be"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:425440e55cd735386ec7925f64d5dde392e69979d4c8459f6bb4e920210407f2"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68fe0c4d22332d7ce53ad094622b27e67440dacefbaedd29e0794d26e247280c"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8a31bfd98f846c3c284ba694c6365620b637debdd36e46e1859c897123aa232"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc2e8fe41f3cac0660197d95216c42910c2b7e9c70d48e6d84e22f577d106fc1"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b7cc10261c2786c41d9207193a85c1db1b725cf87936df40972aab466179b6"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28585744c931576e535860eaf3f2c0ec7deb68e3b9c5a85ca566d69d36d8dd27"}, + {file = "watchfiles-0.22.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00095dd368f73f8f1c3a7982a9801190cc88a2f3582dd395b289294f8975172b"}, + {file = "watchfiles-0.22.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:52fc9b0dbf54d43301a19b236b4a4614e610605f95e8c3f0f65c3a456ffd7d35"}, + {file = "watchfiles-0.22.0-cp312-none-win32.whl", hash = "sha256:581f0a051ba7bafd03e17127735d92f4d286af941dacf94bcf823b101366249e"}, + {file = "watchfiles-0.22.0-cp312-none-win_amd64.whl", hash = "sha256:aec83c3ba24c723eac14225194b862af176d52292d271c98820199110e31141e"}, + {file = "watchfiles-0.22.0-cp312-none-win_arm64.whl", hash = "sha256:c668228833c5619f6618699a2c12be057711b0ea6396aeaece4ded94184304ea"}, + {file = "watchfiles-0.22.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d47e9ef1a94cc7a536039e46738e17cce058ac1593b2eccdede8bf72e45f372a"}, + {file = "watchfiles-0.22.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28f393c1194b6eaadcdd8f941307fc9bbd7eb567995232c830f6aef38e8a6e88"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd64f3a4db121bc161644c9e10a9acdb836853155a108c2446db2f5ae1778c3d"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2abeb79209630da981f8ebca30a2c84b4c3516a214451bfc5f106723c5f45843"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cc382083afba7918e32d5ef12321421ef43d685b9a67cc452a6e6e18920890e"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d048ad5d25b363ba1d19f92dcf29023988524bee6f9d952130b316c5802069cb"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:103622865599f8082f03af4214eaff90e2426edff5e8522c8f9e93dc17caee13"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e1f3cf81f1f823e7874ae563457828e940d75573c8fbf0ee66818c8b6a9099"}, + {file = "watchfiles-0.22.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8597b6f9dc410bdafc8bb362dac1cbc9b4684a8310e16b1ff5eee8725d13dcd6"}, + {file = "watchfiles-0.22.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b04a2cbc30e110303baa6d3ddce8ca3664bc3403be0f0ad513d1843a41c97d1"}, + {file = "watchfiles-0.22.0-cp38-none-win32.whl", hash = "sha256:b610fb5e27825b570554d01cec427b6620ce9bd21ff8ab775fc3a32f28bba63e"}, + {file = "watchfiles-0.22.0-cp38-none-win_amd64.whl", hash = "sha256:fe82d13461418ca5e5a808a9e40f79c1879351fcaeddbede094028e74d836e86"}, + {file = "watchfiles-0.22.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3973145235a38f73c61474d56ad6199124e7488822f3a4fc97c72009751ae3b0"}, + {file = "watchfiles-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:280a4afbc607cdfc9571b9904b03a478fc9f08bbeec382d648181c695648202f"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a0d883351a34c01bd53cfa75cd0292e3f7e268bacf2f9e33af4ecede7e21d1d"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9165bcab15f2b6d90eedc5c20a7f8a03156b3773e5fb06a790b54ccecdb73385"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc1b9b56f051209be458b87edb6856a449ad3f803315d87b2da4c93b43a6fe72"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc1fc25a1dedf2dd952909c8e5cb210791e5f2d9bc5e0e8ebc28dd42fed7562"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc92d2d2706d2b862ce0568b24987eba51e17e14b79a1abcd2edc39e48e743c8"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97b94e14b88409c58cdf4a8eaf0e67dfd3ece7e9ce7140ea6ff48b0407a593ec"}, + {file = "watchfiles-0.22.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96eec15e5ea7c0b6eb5bfffe990fc7c6bd833acf7e26704eb18387fb2f5fd087"}, + {file = "watchfiles-0.22.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:28324d6b28bcb8d7c1041648d7b63be07a16db5510bea923fc80b91a2a6cbed6"}, + {file = "watchfiles-0.22.0-cp39-none-win32.whl", hash = "sha256:8c3e3675e6e39dc59b8fe5c914a19d30029e36e9f99468dddffd432d8a7b1c93"}, + {file = "watchfiles-0.22.0-cp39-none-win_amd64.whl", hash = "sha256:25c817ff2a86bc3de3ed2df1703e3d24ce03479b27bb4527c57e722f8554d971"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b810a2c7878cbdecca12feae2c2ae8af59bea016a78bc353c184fa1e09f76b68"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7e1f9c5d1160d03b93fc4b68a0aeb82fe25563e12fbcdc8507f8434ab6f823c"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030bc4e68d14bcad2294ff68c1ed87215fbd9a10d9dea74e7cfe8a17869785ab"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace7d060432acde5532e26863e897ee684780337afb775107c0a90ae8dbccfd2"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5834e1f8b71476a26df97d121c0c0ed3549d869124ed2433e02491553cb468c2"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0bc3b2f93a140df6806c8467c7f51ed5e55a931b031b5c2d7ff6132292e803d6"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fdebb655bb1ba0122402352b0a4254812717a017d2dc49372a1d47e24073795"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c8e0aa0e8cc2a43561e0184c0513e291ca891db13a269d8d47cb9841ced7c71"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2f350cbaa4bb812314af5dab0eb8d538481e2e2279472890864547f3fe2281ed"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7a74436c415843af2a769b36bf043b6ccbc0f8d784814ba3d42fc961cdb0a9dc"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00ad0bcd399503a84cc688590cdffbe7a991691314dde5b57b3ed50a41319a31"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72a44e9481afc7a5ee3291b09c419abab93b7e9c306c9ef9108cb76728ca58d2"}, + {file = "watchfiles-0.22.0.tar.gz", hash = "sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb"}, ] [package.dependencies] @@ -2134,4 +2153,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "33cd478f24b83c6ded17df55b12924f493beed5f72c0cf1c68c1cbf0bca7167a" +content-hash = "aae5db2a78674fa5f3c6e3b65137ed51bd3945dc28af20e853475e09ea0f4582" diff --git a/pyproject.toml b/pyproject.toml index 7e3ea099..f1f54423 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [tool.poetry] name = "odtp" -version = "0.3.1" +version = "0.4.0" description = "A tool to deploy and manage open digital twins" -authors = ["caviri ", "sabinem "] +authors = ["caviri ", "sabinem ", "sabrinaossey ", "rmfranken "] license = "AGPL 3.0" readme = "README.md" @@ -17,8 +17,10 @@ python-dotenv = "^1.0.0" boto3 = "^1.33.13" nicegui = "1.4.24" directory-tree = "^0.0.4" +docker = "7.1.0" +python-slugify = "^8.0.4" pandas = "^2.2.2" - +prettytable = "3.10.0" [tool.poetry.group.dev.dependencies] pytest = "^7.2.1"