From 8261190082d9723072d5390e0cd1ee22eb9b0779 Mon Sep 17 00:00:00 2001 From: Angelos Angelopoulos Date: Sun, 15 Sep 2024 16:27:28 -0400 Subject: [PATCH] Public release --- .github/workflows/docs.yml | 40 + .gitignore | 173 + LICENSE | 11 + README.md | 122 + config.example.yml | 27 + docker/.env.example | 28 + docker/docker-compose.yml | 66 + docs/Makefile | 20 + docs/_static/custom.css | 3 + docs/_static/img/dmta-loop.png | Bin 0 -> 43013 bytes docs/_static/img/eos-computers.png | Bin 0 -> 40373 bytes docs/_static/img/eos-logo.png | Bin 0 -> 280426 bytes docs/_static/img/example-package-tree.png | Bin 0 -> 40735 bytes docs/_static/img/experiment-graph.png | Bin 0 -> 133166 bytes docs/_static/img/laboratory.png | Bin 0 -> 33952 bytes docs/_static/img/optimize-experiment-loop.png | Bin 0 -> 20405 bytes docs/_static/img/package.png | Bin 0 -> 30411 bytes docs/_static/img/task-inputs-outputs.png | Bin 0 -> 47650 bytes docs/_static/img/tasks-devices.png | Bin 0 -> 52132 bytes docs/conf.py | 49 + docs/index.rst | 25 + docs/make.bat | 35 + docs/user-guide/campaigns.rst | 126 + docs/user-guide/configuration.rst | 28 + docs/user-guide/devices.rst | 107 + docs/user-guide/experiments.rst | 286 ++ docs/user-guide/index.rst | 25 + docs/user-guide/installation.rst | 59 + docs/user-guide/jinja2_templating.rst | 24 + docs/user-guide/laboratories.rst | 267 ++ docs/user-guide/optimizers.rst | 171 + docs/user-guide/packages.rst | 31 + docs/user-guide/running.rst | 26 + docs/user-guide/tasks.rst | 254 ++ eos/__init__.py | 0 eos/campaigns/__init__.py | 0 eos/campaigns/campaign_executor.py | 332 ++ eos/campaigns/campaign_executor_factory.py | 45 + eos/campaigns/campaign_manager.py | 178 + eos/campaigns/campaign_optimizer_manager.py | 123 + eos/campaigns/entities/__init__.py | 0 eos/campaigns/entities/campaign.py | 71 + eos/campaigns/exceptions.py | 10 + eos/campaigns/repositories/__init__.py | 0 .../repositories/campaign_repository.py | 30 + eos/cli/__init__.py | 0 eos/cli/orchestrator_cli.py | 185 + eos/cli/pkg_cli.py | 28 + eos/cli/web_api_cli.py | 30 + eos/configuration/__init__.py | 0 eos/configuration/configuration_manager.py | 230 + eos/configuration/constants.py | 18 + eos/configuration/entities/__init__.py | 0 .../entities/device_specification.py | 9 + eos/configuration/entities/experiment.py | 21 + eos/configuration/entities/lab.py | 42 + eos/configuration/entities/parameters.py | 209 + eos/configuration/entities/task.py | 21 + .../entities/task_specification.py | 110 + eos/configuration/exceptions.py | 38 + .../experiment_graph/__init__.py | 0 .../experiment_graph/experiment_graph.py | 88 + .../experiment_graph_builder.py | 108 + eos/configuration/package.py | 18 + eos/configuration/package_manager.py | 332 ++ eos/configuration/package_validator.py | 173 + .../plugin_registries/__init__.py | 0 .../campaign_optimizer_plugin_registry.py | 116 + .../device_plugin_registry.py | 23 + .../plugin_registries/plugin_registry.py | 118 + .../plugin_registries/task_plugin_registry.py | 23 + eos/configuration/spec_registries/__init__.py | 0 .../device_specification_registry.py | 9 + .../spec_registries/specification_registry.py | 38 + .../task_specification_registry.py | 24 + eos/configuration/validation/__init__.py | 0 .../validation/container_registry.py | 28 + .../validation/container_validator.py | 36 + .../validation/experiment_validator.py | 39 + eos/configuration/validation/lab_validator.py | 164 + .../validation/multi_lab_validator.py | 51 + .../validation/task_sequence/__init__.py | 0 .../base_task_sequence_validator.py | 28 + .../task_input_container_validator.py | 113 + .../task_input_parameter_validator.py | 139 + ...task_sequence_input_container_validator.py | 92 + ...task_sequence_input_parameter_validator.py | 95 + .../validation/task_sequence_validator.py | 111 + .../validation/validation_utils.py | 33 + eos/containers/__init__.py | 0 eos/containers/container_manager.py | 159 + eos/containers/entities/__init__.py | 0 eos/containers/entities/container.py | 16 + eos/containers/exceptions.py | 6 + eos/containers/repositories/__init__.py | 0 .../repositories/container_repository.py | 5 + eos/devices/__init__.py | 0 eos/devices/base_device.py | 167 + eos/devices/device_actor_references.py | 57 + eos/devices/device_manager.py | 198 + eos/devices/entities/__init__.py | 0 eos/devices/entities/device.py | 32 + eos/devices/exceptions.py | 18 + eos/eos.py | 15 + eos/experiments/__init__.py | 0 eos/experiments/entities/__init__.py | 0 eos/experiments/entities/experiment.py | 48 + eos/experiments/exceptions.py | 18 + eos/experiments/experiment_executor.py | 304 ++ .../experiment_executor_factory.py | 49 + eos/experiments/experiment_manager.py | 174 + eos/experiments/repositories/__init__.py | 0 .../repositories/experiment_repository.py | 45 + eos/logging/__init__.py | 0 eos/logging/batch_error_logger.py | 30 + eos/logging/logger.py | 48 + eos/logging/rich_console_handler.py | 32 + eos/monitoring/__init__.py | 0 .../graceful_termination_monitor.py | 34 + eos/optimization/__init__.py | 0 .../abstract_sequential_optimizer.py | 54 + eos/optimization/exceptions.py | 2 + .../sequential_bayesian_optimizer.py | 164 + .../sequential_optimizer_actor.py | 27 + eos/orchestration/__init__.py | 0 eos/orchestration/exceptions.py | 18 + eos/orchestration/orchestrator.py | 721 ++++ eos/persistence/__init__.py | 0 eos/persistence/abstract_repository.py | 35 + eos/persistence/db_manager.py | 55 + eos/persistence/exceptions.py | 2 + eos/persistence/file_db_manager.py | 91 + eos/persistence/mongo_repository.py | 91 + eos/persistence/service_credentials.py | 9 + eos/resource_allocation/__init__.py | 0 .../container_allocation_manager.py | 121 + .../device_allocation_manager.py | 118 + eos/resource_allocation/entities/__init__.py | 0 .../entities/container_allocation.py | 7 + .../entities/device_allocation.py | 8 + .../entities/resource_allocation.py | 14 + .../entities/resource_request.py | 61 + eos/resource_allocation/exceptions.py | 18 + .../repositories/__init__.py | 0 .../resource_request_repository.py | 36 + .../resource_allocation_manager.py | 261 ++ eos/scheduling/__init__.py | 0 eos/scheduling/abstract_scheduler.py | 42 + eos/scheduling/basic_scheduler.py | 280 ++ eos/scheduling/entities/__init__.py | 0 eos/scheduling/entities/scheduled_task.py | 11 + eos/scheduling/exceptions.py | 10 + eos/tasks/__init__.py | 0 eos/tasks/base_task.py | 47 + eos/tasks/entities/__init__.py | 0 eos/tasks/entities/task.py | 86 + .../entities/task_execution_parameters.py | 10 + eos/tasks/exceptions.py | 26 + eos/tasks/on_demand_task_executor.py | 102 + eos/tasks/repositories/__init__.py | 0 eos/tasks/repositories/task_repository.py | 5 + eos/tasks/task_executor.py | 278 ++ eos/tasks/task_input_parameter_caster.py | 33 + eos/tasks/task_input_parameter_validator.py | 156 + eos/tasks/task_input_resolver.py | 130 + eos/tasks/task_manager.py | 215 + eos/tasks/task_validator.py | 18 + eos/utils/__init__.py | 0 eos/utils/dict_utils.py | 53 + eos/utils/file_utils.py | 134 + eos/utils/ray_utils.py | 40 + eos/utils/singleton.py | 10 + eos/web_api/__init__.py | 0 eos/web_api/common/__init__.py | 0 eos/web_api/common/entities.py | 55 + eos/web_api/orchestrator/__init__.py | 0 .../orchestrator/controllers/__init__.py | 0 .../controllers/campaign_controller.py | 33 + .../controllers/experiment_controller.py | 75 + .../controllers/file_controller.py | 74 + .../controllers/lab_controller.py | 43 + .../controllers/task_controller.py | 45 + .../orchestrator/exception_handling.py | 40 + eos/web_api/public/__init__.py | 0 eos/web_api/public/controllers/__init__.py | 0 .../public/controllers/campaign_controller.py | 47 + .../controllers/experiment_controller.py | 116 + .../public/controllers/file_controller.py | 49 + .../public/controllers/lab_controller.py | 73 + .../public/controllers/task_controller.py | 71 + eos/web_api/public/exception_handling.py | 40 + eos/web_api/public/server.py | 50 + pdm.lock | 3701 +++++++++++++++++ pyproject.toml | 151 + tests/__init__.py | 0 tests/fixtures.py | 292 ++ tests/test_basic_scheduler.py | 73 + tests/test_bayesian_sequential_optimizer.py | 78 + tests/test_campaign_executor.py | 45 + tests/test_config.yaml | 16 + tests/test_configuration_manager.py | 131 + tests/test_container_allocator.py | 135 + tests/test_container_manager.py | 46 + tests/test_device_allocator.py | 135 + tests/test_device_manager.py | 37 + tests/test_experiment_executor.py | 103 + tests/test_experiment_graph.py | 41 + tests/test_experiment_manager.py | 93 + tests/test_lab_validation.py | 68 + tests/test_multi_lab_validation.py | 17 + tests/test_resource_allocation_manager.py | 216 + tests/test_task_executor.py | 100 + tests/test_task_input_parameter_validator.py | 129 + tests/test_task_manager.py | 112 + tests/test_task_specification_validation.py | 262 ++ tests/user/testing/common/__init__.py | 0 .../devices/abstract_lab/DT1/device.py | 14 + .../devices/abstract_lab/DT1/device.yml | 2 + .../devices/abstract_lab/DT2/device.py | 14 + .../devices/abstract_lab/DT2/device.yml | 2 + .../devices/abstract_lab/DT3/device.py | 14 + .../devices/abstract_lab/DT3/device.yml | 2 + .../devices/abstract_lab/DT4/device.py | 14 + .../devices/abstract_lab/DT4/device.yml | 2 + .../devices/abstract_lab/DT5/device.py | 14 + .../devices/abstract_lab/DT5/device.yml | 2 + .../devices/abstract_lab/DT6/device.py | 14 + .../devices/abstract_lab/DT6/device.yml | 2 + .../multiplication_lab/analyzer/device.py | 17 + .../multiplication_lab/analyzer/device.yml | 2 + .../multiplication_lab/multiplier/device.py | 17 + .../multiplication_lab/multiplier/device.yml | 2 + .../devices/small_lab/computer/device.py | 14 + .../devices/small_lab/computer/device.yml | 2 + .../devices/small_lab/evaporator/device.py | 14 + .../devices/small_lab/evaporator/device.yml | 2 + .../devices/small_lab/fridge/device.py | 14 + .../devices/small_lab/fridge/device.yml | 2 + .../small_lab/magnetic_mixer/device.py | 14 + .../small_lab/magnetic_mixer/device.yml | 2 + .../abstract_experiment/experiment.yml | 61 + .../optimize_multiplication/experiment.yml | 33 + .../optimize_multiplication/optimizer.py | 27 + .../water_purification/experiment.yml | 42 + .../labs/abstract_lab/abstract_lab.map | 0 tests/user/testing/labs/abstract_lab/lab.yml | 22 + .../testing/labs/multiplication_lab/lab.yml | 10 + .../multiplication_lab/multiplication.map | 0 tests/user/testing/labs/small_lab/lab.yml | 131 + .../user/testing/labs/small_lab/small_lab.map | 0 .../tasks/fridge_temperature_control/task.py | 11 + .../tasks/fridge_temperature_control/task.yml | 13 + tests/user/testing/tasks/gc_analysis/task.py | 11 + tests/user/testing/tasks/gc_analysis/task.yml | 50 + tests/user/testing/tasks/gc_injection/task.py | 11 + .../user/testing/tasks/gc_injection/task.yml | 10 + .../user/testing/tasks/hplc_analysis/task.py | 11 + .../user/testing/tasks/hplc_analysis/task.yml | 68 + .../testing/tasks/magnetic_mixing/task.py | 13 + .../testing/tasks/magnetic_mixing/task.yml | 31 + .../compute_multiplication_objective/task.py | 21 + .../compute_multiplication_objective/task.yml | 21 + .../multiplication_lab/multiplication/task.py | 19 + .../multiplication/task.yml | 21 + tests/user/testing/tasks/noop/task.py | 11 + tests/user/testing/tasks/noop/task.yml | 2 + tests/user/testing/tasks/purification/task.py | 13 + .../user/testing/tasks/purification/task.yml | 72 + .../robot_arm_container_transfer/task.py | 11 + .../robot_arm_container_transfer/task.yml | 22 + tests/user/testing/tasks/sleep/task.py | 26 + tests/user/testing/tasks/sleep/task.yml | 10 + .../user/testing/tasks/wafer_sampling/task.py | 11 + .../testing/tasks/wafer_sampling/task.yml | 15 + .../testing/tasks/weigh_container/task.py | 11 + .../testing/tasks/weigh_container/task.yml | 19 + user/.gitkeep | 0 277 files changed, 18636 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.example.yml create mode 100644 docker/.env.example create mode 100644 docker/docker-compose.yml create mode 100644 docs/Makefile create mode 100644 docs/_static/custom.css create mode 100644 docs/_static/img/dmta-loop.png create mode 100644 docs/_static/img/eos-computers.png create mode 100644 docs/_static/img/eos-logo.png create mode 100644 docs/_static/img/example-package-tree.png create mode 100644 docs/_static/img/experiment-graph.png create mode 100644 docs/_static/img/laboratory.png create mode 100644 docs/_static/img/optimize-experiment-loop.png create mode 100644 docs/_static/img/package.png create mode 100644 docs/_static/img/task-inputs-outputs.png create mode 100644 docs/_static/img/tasks-devices.png create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/user-guide/campaigns.rst create mode 100644 docs/user-guide/configuration.rst create mode 100644 docs/user-guide/devices.rst create mode 100644 docs/user-guide/experiments.rst create mode 100644 docs/user-guide/index.rst create mode 100644 docs/user-guide/installation.rst create mode 100644 docs/user-guide/jinja2_templating.rst create mode 100644 docs/user-guide/laboratories.rst create mode 100644 docs/user-guide/optimizers.rst create mode 100644 docs/user-guide/packages.rst create mode 100644 docs/user-guide/running.rst create mode 100644 docs/user-guide/tasks.rst create mode 100644 eos/__init__.py create mode 100644 eos/campaigns/__init__.py create mode 100644 eos/campaigns/campaign_executor.py create mode 100644 eos/campaigns/campaign_executor_factory.py create mode 100644 eos/campaigns/campaign_manager.py create mode 100644 eos/campaigns/campaign_optimizer_manager.py create mode 100644 eos/campaigns/entities/__init__.py create mode 100644 eos/campaigns/entities/campaign.py create mode 100644 eos/campaigns/exceptions.py create mode 100644 eos/campaigns/repositories/__init__.py create mode 100644 eos/campaigns/repositories/campaign_repository.py create mode 100644 eos/cli/__init__.py create mode 100644 eos/cli/orchestrator_cli.py create mode 100644 eos/cli/pkg_cli.py create mode 100644 eos/cli/web_api_cli.py create mode 100644 eos/configuration/__init__.py create mode 100644 eos/configuration/configuration_manager.py create mode 100644 eos/configuration/constants.py create mode 100644 eos/configuration/entities/__init__.py create mode 100644 eos/configuration/entities/device_specification.py create mode 100644 eos/configuration/entities/experiment.py create mode 100644 eos/configuration/entities/lab.py create mode 100644 eos/configuration/entities/parameters.py create mode 100644 eos/configuration/entities/task.py create mode 100644 eos/configuration/entities/task_specification.py create mode 100644 eos/configuration/exceptions.py create mode 100644 eos/configuration/experiment_graph/__init__.py create mode 100644 eos/configuration/experiment_graph/experiment_graph.py create mode 100644 eos/configuration/experiment_graph/experiment_graph_builder.py create mode 100644 eos/configuration/package.py create mode 100644 eos/configuration/package_manager.py create mode 100644 eos/configuration/package_validator.py create mode 100644 eos/configuration/plugin_registries/__init__.py create mode 100644 eos/configuration/plugin_registries/campaign_optimizer_plugin_registry.py create mode 100644 eos/configuration/plugin_registries/device_plugin_registry.py create mode 100644 eos/configuration/plugin_registries/plugin_registry.py create mode 100644 eos/configuration/plugin_registries/task_plugin_registry.py create mode 100644 eos/configuration/spec_registries/__init__.py create mode 100644 eos/configuration/spec_registries/device_specification_registry.py create mode 100644 eos/configuration/spec_registries/specification_registry.py create mode 100644 eos/configuration/spec_registries/task_specification_registry.py create mode 100644 eos/configuration/validation/__init__.py create mode 100644 eos/configuration/validation/container_registry.py create mode 100644 eos/configuration/validation/container_validator.py create mode 100644 eos/configuration/validation/experiment_validator.py create mode 100644 eos/configuration/validation/lab_validator.py create mode 100644 eos/configuration/validation/multi_lab_validator.py create mode 100644 eos/configuration/validation/task_sequence/__init__.py create mode 100644 eos/configuration/validation/task_sequence/base_task_sequence_validator.py create mode 100644 eos/configuration/validation/task_sequence/task_input_container_validator.py create mode 100644 eos/configuration/validation/task_sequence/task_input_parameter_validator.py create mode 100644 eos/configuration/validation/task_sequence/task_sequence_input_container_validator.py create mode 100644 eos/configuration/validation/task_sequence/task_sequence_input_parameter_validator.py create mode 100644 eos/configuration/validation/task_sequence_validator.py create mode 100644 eos/configuration/validation/validation_utils.py create mode 100644 eos/containers/__init__.py create mode 100644 eos/containers/container_manager.py create mode 100644 eos/containers/entities/__init__.py create mode 100644 eos/containers/entities/container.py create mode 100644 eos/containers/exceptions.py create mode 100644 eos/containers/repositories/__init__.py create mode 100644 eos/containers/repositories/container_repository.py create mode 100644 eos/devices/__init__.py create mode 100644 eos/devices/base_device.py create mode 100644 eos/devices/device_actor_references.py create mode 100644 eos/devices/device_manager.py create mode 100644 eos/devices/entities/__init__.py create mode 100644 eos/devices/entities/device.py create mode 100644 eos/devices/exceptions.py create mode 100755 eos/eos.py create mode 100644 eos/experiments/__init__.py create mode 100644 eos/experiments/entities/__init__.py create mode 100644 eos/experiments/entities/experiment.py create mode 100644 eos/experiments/exceptions.py create mode 100644 eos/experiments/experiment_executor.py create mode 100644 eos/experiments/experiment_executor_factory.py create mode 100644 eos/experiments/experiment_manager.py create mode 100644 eos/experiments/repositories/__init__.py create mode 100644 eos/experiments/repositories/experiment_repository.py create mode 100644 eos/logging/__init__.py create mode 100644 eos/logging/batch_error_logger.py create mode 100644 eos/logging/logger.py create mode 100644 eos/logging/rich_console_handler.py create mode 100644 eos/monitoring/__init__.py create mode 100644 eos/monitoring/graceful_termination_monitor.py create mode 100644 eos/optimization/__init__.py create mode 100644 eos/optimization/abstract_sequential_optimizer.py create mode 100644 eos/optimization/exceptions.py create mode 100644 eos/optimization/sequential_bayesian_optimizer.py create mode 100644 eos/optimization/sequential_optimizer_actor.py create mode 100644 eos/orchestration/__init__.py create mode 100644 eos/orchestration/exceptions.py create mode 100644 eos/orchestration/orchestrator.py create mode 100644 eos/persistence/__init__.py create mode 100644 eos/persistence/abstract_repository.py create mode 100644 eos/persistence/db_manager.py create mode 100644 eos/persistence/exceptions.py create mode 100644 eos/persistence/file_db_manager.py create mode 100644 eos/persistence/mongo_repository.py create mode 100644 eos/persistence/service_credentials.py create mode 100644 eos/resource_allocation/__init__.py create mode 100644 eos/resource_allocation/container_allocation_manager.py create mode 100644 eos/resource_allocation/device_allocation_manager.py create mode 100644 eos/resource_allocation/entities/__init__.py create mode 100644 eos/resource_allocation/entities/container_allocation.py create mode 100644 eos/resource_allocation/entities/device_allocation.py create mode 100644 eos/resource_allocation/entities/resource_allocation.py create mode 100644 eos/resource_allocation/entities/resource_request.py create mode 100644 eos/resource_allocation/exceptions.py create mode 100644 eos/resource_allocation/repositories/__init__.py create mode 100644 eos/resource_allocation/repositories/resource_request_repository.py create mode 100644 eos/resource_allocation/resource_allocation_manager.py create mode 100644 eos/scheduling/__init__.py create mode 100644 eos/scheduling/abstract_scheduler.py create mode 100644 eos/scheduling/basic_scheduler.py create mode 100644 eos/scheduling/entities/__init__.py create mode 100644 eos/scheduling/entities/scheduled_task.py create mode 100644 eos/scheduling/exceptions.py create mode 100644 eos/tasks/__init__.py create mode 100644 eos/tasks/base_task.py create mode 100644 eos/tasks/entities/__init__.py create mode 100644 eos/tasks/entities/task.py create mode 100644 eos/tasks/entities/task_execution_parameters.py create mode 100644 eos/tasks/exceptions.py create mode 100644 eos/tasks/on_demand_task_executor.py create mode 100644 eos/tasks/repositories/__init__.py create mode 100644 eos/tasks/repositories/task_repository.py create mode 100644 eos/tasks/task_executor.py create mode 100644 eos/tasks/task_input_parameter_caster.py create mode 100644 eos/tasks/task_input_parameter_validator.py create mode 100644 eos/tasks/task_input_resolver.py create mode 100644 eos/tasks/task_manager.py create mode 100644 eos/tasks/task_validator.py create mode 100644 eos/utils/__init__.py create mode 100644 eos/utils/dict_utils.py create mode 100644 eos/utils/file_utils.py create mode 100644 eos/utils/ray_utils.py create mode 100644 eos/utils/singleton.py create mode 100644 eos/web_api/__init__.py create mode 100644 eos/web_api/common/__init__.py create mode 100644 eos/web_api/common/entities.py create mode 100644 eos/web_api/orchestrator/__init__.py create mode 100644 eos/web_api/orchestrator/controllers/__init__.py create mode 100644 eos/web_api/orchestrator/controllers/campaign_controller.py create mode 100644 eos/web_api/orchestrator/controllers/experiment_controller.py create mode 100644 eos/web_api/orchestrator/controllers/file_controller.py create mode 100644 eos/web_api/orchestrator/controllers/lab_controller.py create mode 100644 eos/web_api/orchestrator/controllers/task_controller.py create mode 100644 eos/web_api/orchestrator/exception_handling.py create mode 100644 eos/web_api/public/__init__.py create mode 100644 eos/web_api/public/controllers/__init__.py create mode 100644 eos/web_api/public/controllers/campaign_controller.py create mode 100644 eos/web_api/public/controllers/experiment_controller.py create mode 100644 eos/web_api/public/controllers/file_controller.py create mode 100644 eos/web_api/public/controllers/lab_controller.py create mode 100644 eos/web_api/public/controllers/task_controller.py create mode 100644 eos/web_api/public/exception_handling.py create mode 100644 eos/web_api/public/server.py create mode 100644 pdm.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/fixtures.py create mode 100644 tests/test_basic_scheduler.py create mode 100644 tests/test_bayesian_sequential_optimizer.py create mode 100644 tests/test_campaign_executor.py create mode 100644 tests/test_config.yaml create mode 100644 tests/test_configuration_manager.py create mode 100644 tests/test_container_allocator.py create mode 100644 tests/test_container_manager.py create mode 100644 tests/test_device_allocator.py create mode 100644 tests/test_device_manager.py create mode 100644 tests/test_experiment_executor.py create mode 100644 tests/test_experiment_graph.py create mode 100644 tests/test_experiment_manager.py create mode 100644 tests/test_lab_validation.py create mode 100644 tests/test_multi_lab_validation.py create mode 100644 tests/test_resource_allocation_manager.py create mode 100644 tests/test_task_executor.py create mode 100644 tests/test_task_input_parameter_validator.py create mode 100644 tests/test_task_manager.py create mode 100644 tests/test_task_specification_validation.py create mode 100644 tests/user/testing/common/__init__.py create mode 100644 tests/user/testing/devices/abstract_lab/DT1/device.py create mode 100644 tests/user/testing/devices/abstract_lab/DT1/device.yml create mode 100644 tests/user/testing/devices/abstract_lab/DT2/device.py create mode 100644 tests/user/testing/devices/abstract_lab/DT2/device.yml create mode 100644 tests/user/testing/devices/abstract_lab/DT3/device.py create mode 100644 tests/user/testing/devices/abstract_lab/DT3/device.yml create mode 100644 tests/user/testing/devices/abstract_lab/DT4/device.py create mode 100644 tests/user/testing/devices/abstract_lab/DT4/device.yml create mode 100644 tests/user/testing/devices/abstract_lab/DT5/device.py create mode 100644 tests/user/testing/devices/abstract_lab/DT5/device.yml create mode 100644 tests/user/testing/devices/abstract_lab/DT6/device.py create mode 100644 tests/user/testing/devices/abstract_lab/DT6/device.yml create mode 100644 tests/user/testing/devices/multiplication_lab/analyzer/device.py create mode 100644 tests/user/testing/devices/multiplication_lab/analyzer/device.yml create mode 100644 tests/user/testing/devices/multiplication_lab/multiplier/device.py create mode 100644 tests/user/testing/devices/multiplication_lab/multiplier/device.yml create mode 100644 tests/user/testing/devices/small_lab/computer/device.py create mode 100644 tests/user/testing/devices/small_lab/computer/device.yml create mode 100644 tests/user/testing/devices/small_lab/evaporator/device.py create mode 100644 tests/user/testing/devices/small_lab/evaporator/device.yml create mode 100644 tests/user/testing/devices/small_lab/fridge/device.py create mode 100644 tests/user/testing/devices/small_lab/fridge/device.yml create mode 100644 tests/user/testing/devices/small_lab/magnetic_mixer/device.py create mode 100644 tests/user/testing/devices/small_lab/magnetic_mixer/device.yml create mode 100644 tests/user/testing/experiments/abstract_experiment/experiment.yml create mode 100644 tests/user/testing/experiments/optimize_multiplication/experiment.yml create mode 100644 tests/user/testing/experiments/optimize_multiplication/optimizer.py create mode 100644 tests/user/testing/experiments/water_purification/experiment.yml create mode 100644 tests/user/testing/labs/abstract_lab/abstract_lab.map create mode 100644 tests/user/testing/labs/abstract_lab/lab.yml create mode 100644 tests/user/testing/labs/multiplication_lab/lab.yml create mode 100644 tests/user/testing/labs/multiplication_lab/multiplication.map create mode 100644 tests/user/testing/labs/small_lab/lab.yml create mode 100644 tests/user/testing/labs/small_lab/small_lab.map create mode 100644 tests/user/testing/tasks/fridge_temperature_control/task.py create mode 100644 tests/user/testing/tasks/fridge_temperature_control/task.yml create mode 100644 tests/user/testing/tasks/gc_analysis/task.py create mode 100644 tests/user/testing/tasks/gc_analysis/task.yml create mode 100644 tests/user/testing/tasks/gc_injection/task.py create mode 100644 tests/user/testing/tasks/gc_injection/task.yml create mode 100644 tests/user/testing/tasks/hplc_analysis/task.py create mode 100644 tests/user/testing/tasks/hplc_analysis/task.yml create mode 100644 tests/user/testing/tasks/magnetic_mixing/task.py create mode 100644 tests/user/testing/tasks/magnetic_mixing/task.yml create mode 100644 tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.py create mode 100644 tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.yml create mode 100644 tests/user/testing/tasks/multiplication_lab/multiplication/task.py create mode 100644 tests/user/testing/tasks/multiplication_lab/multiplication/task.yml create mode 100644 tests/user/testing/tasks/noop/task.py create mode 100644 tests/user/testing/tasks/noop/task.yml create mode 100644 tests/user/testing/tasks/purification/task.py create mode 100644 tests/user/testing/tasks/purification/task.yml create mode 100644 tests/user/testing/tasks/robot_arm_container_transfer/task.py create mode 100644 tests/user/testing/tasks/robot_arm_container_transfer/task.yml create mode 100644 tests/user/testing/tasks/sleep/task.py create mode 100644 tests/user/testing/tasks/sleep/task.yml create mode 100644 tests/user/testing/tasks/wafer_sampling/task.py create mode 100644 tests/user/testing/tasks/wafer_sampling/task.yml create mode 100644 tests/user/testing/tasks/weigh_container/task.py create mode 100644 tests/user/testing/tasks/weigh_container/task.yml create mode 100644 user/.gitkeep diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..5d1ab1a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,40 @@ +name: Documentation building and deployment + +on: + release: + types: [published] + push: + branches: + - master + +jobs: + docs: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - uses: pdm-project/setup-pdm@v4 + name: Set up PDM + with: + python-version: "3.10" + allow-python-prereleases: false + cache: true + cache-dependency-path: | + ./pdm.lock + + - name: Install dependencies + run: pdm install --group docs --no-default + + - name: Build docs + run: pdm run docs-build-gh + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: docs/_build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a46e4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,173 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +docker/.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +*.drawio.bkp +*.drawio.dtmp +*.pdf + +.idea/ +.vscode/ + +# EOS config file +/config.yml + +/user/* +!/user/.gitkeep diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf69be5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright 2024 The University of North Carolina at Chapel Hill + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f45da9 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +

+ Alt Text +

+ +

The Experiment Orchestration System (EOS)

+ +The Experiment Orchestration System (EOS) is a comprehensive software framework and runtime for laboratory automation, designed +to serve as the foundation for one or more automated or self-driving labs (SDLs). + +EOS provides: + +* A common framework to implement laboratory automation +* A plugin system for defining labs, devices, experiments, tasks, and optimizers +* A package system for sharing and reusing code and resources across the community +* Extensive static and dynamic validation of experiments, task parameters, and more +* A runtime for executing tasks, experiments, and experiment campaigns +* A central authoritative orchestrator that can communicate with and control multiple devices +* Distributed task execution and optimization using the Ray framework +* Built-in Bayesian experiment parameter optimization +* Optimized task scheduling +* Device and sample container allocation system to prevent conflicts +* Result aggregation such as automatic output file storage + +![os](https://img.shields.io/badge/OS-win%7Cmac%7Clinux-9cf) + +## Installation + +### 1. Install PDM + +PDM is used as the project manager for EOS, making it easier to install dependencies and build it. + +#### Linux/Mac + +```shell +curl -sSL https://pdm-project.org/install-pdm.py | python3 - +``` + +#### Windows + +```shell +(Invoke-WebRequest -Uri https://pdm-project.org/install-pdm.py -UseBasicParsing).Content | py - +``` + +### 2. Clone the EOS Repository + +```shell +git clone https://github.com/aangelos28/eos +``` + +### 3. Install Dependencies + +Navigate to the cloned repository and run: + +```shell +pdm install +``` + +(Optional) If you wish to contribute to EOS development: + +```shell +pdm install -G dev +``` + +(Optional) If you also wish to contribute to the EOS documentation: + +```shell +pdm install -G docs +``` + +## Configuration + +After installation, you need to configure external services such as MongoDB and MinIO as well as EOS itself. + +### 1. Configure External Services + +We provide a Docker Compose file that can run all external services for you. + +Copy the example environment file: + +```shell +cp docker/.env.example docker/.env +``` + +Edit `docker/.env` and provide values for all fields. + +### 2. Configure EOS + +EOS reads parameters from a YAML configuration file. + +Copy the example configuration file: + +```shell +cp config.example.yml config.yml +``` + +Edit `config.yml`. Ensure that credentials are provided for the MongoDB and MinIO services. + +## Running +### 1. Start External Services + +```shell +cd docker +docker compose up -d +``` + +### 2. Source the Virtual Environment + +```shell +source env/bin/activate +``` + +### 3. Start the EOS Orchestrator + +```shell +eos orchestrator +``` + +### 4. Start the EOS REST API + +```shell +eos api +``` diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..108d0c4 --- /dev/null +++ b/config.example.yml @@ -0,0 +1,27 @@ +user_dir: ./user +labs: + - lab1 + - lab2 +experiments: + - experiment1 + - experiment2 +log_level: INFO + +# EOS orchestrator's internal web API server configuration +web_api: + host: localhost + port: 8070 + +# EOS database configuration +db: + host: localhost + port: 27017 + username: "" + password: "" + +# EOS file database configuration +file_db: + host: localhost + port: 9004 + username: "" + password: "" diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..3d6b778 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,28 @@ +# EOS ##################################### +COMPOSE_PROJECT_NAME=eos + +# MongoDB root username +EOS_MONGO_INITDB_ROOT_USERNAME= + +# MongoDB root user password +EOS_MONGO_INITDB_ROOT_PASSWORD= + +# MinIO root username +EOS_MINIO_ROOT_USER= + +# MinIO root user password +EOS_MINIO_ROOT_PASSWORD= + +# Budibase ################################ +# You can set the below to random values +BB_JWT_SECRET= +BB_MINIO_ACCESS_KEY= +BB_MINIO_SECRET_KEY= +BB_REDIS_PASSWORD= +BB_COUCHDB_USER= +BB_COUCHDB_PASSWORD= +BB_INTERNAL_API_KEY= + +# Admin user credentials to login to Budibase +BB_ADMIN_USER_EMAIL= +BB_ADMIN_USER_PASSWORD= diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..11ad4bd --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,66 @@ +services: + eos-mongodb: + image: mongo:jammy + container_name: eos-mongodb + restart: unless-stopped + environment: + MONGO_INITDB_ROOT_USERNAME: ${EOS_MONGO_INITDB_ROOT_USERNAME} + MONGO_INITDB_ROOT_PASSWORD: ${EOS_MONGO_INITDB_ROOT_PASSWORD} + ports: + - "27017:27017" + networks: + - eos_network + volumes: + - mongodb_data:/data/db + + eos-minio: + image: minio/minio:latest + container_name: eos-minio + restart: unless-stopped + environment: + MINIO_ROOT_USER: ${EOS_MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${EOS_MINIO_ROOT_PASSWORD} + ports: + - "9004:9000" + - "9005:9001" + networks: + - eos_network + volumes: + - minio_data:/data + command: server --console-address ":9001" /data + + eos-budibase: + image: budibase/budibase:latest + container_name: eos-budibase + restart: unless-stopped + ports: + - "8080:80" + environment: + JWT_SECRET: ${BB_JWT_SECRET} + MINIO_ACCESS_KEY: ${BB_MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${BB_MINIO_SECRET_KEY} + REDIS_PASSWORD: ${BB_REDIS_PASSWORD} + COUCHDB_USER: ${BB_COUCHDB_USER} + COUCHDB_PASSWORD: ${BB_COUCHDB_PASSWORD} + INTERNAL_API_KEY: ${BB_INTERNAL_API_KEY} + BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL} + BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD} + networks: + - eos_network + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - budibase_data:/data + +networks: + eos_network: + name: eos_network + driver: bridge + +volumes: + mongodb_data: + driver: local + minio_data: + driver: local + budibase_data: + driver: local diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..e3d63a5 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,3 @@ +.bd-page-width { + max-width: 100rem; /* default is 88rem */ +} \ No newline at end of file diff --git a/docs/_static/img/dmta-loop.png b/docs/_static/img/dmta-loop.png new file mode 100644 index 0000000000000000000000000000000000000000..9874f0ae19685b8e27eced967097ba53805f6732 GIT binary patch literal 43013 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⪙x^$330@$T^@={WATt5_5?&yc`O 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 literal 0 HcmV?d00001 diff --git a/docs/_static/img/eos-logo.png b/docs/_static/img/eos-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..34f45c187bdcb91f42f730d6fb541b82ddac5689 GIT binary patch literal 280426 zcmYg%1z6Kx*!Jjd1nHEJE@?(dN-8DY-5?;{CEcBZl1fMq5b07{DJ#iq0|2OxZ;>Z4(H}4Oo(+Ei0Hg?e zIXO*bIXQYa4_6y|Cu;zJEhR8TQmIdxy5DqK<#iSQ2STD@%>vFC75AB!q$RRnp1t?x zZM($D6=Y`TP7HJG?M>>h3XgBB!>vJ}JpBkvKbNDs= zi%4gh-E_#e#$$D(CZoE>PXqo_Q{B{HM{L}j`CH5jCBl;8IO36SM4xq7`(@*P#84Wb25Zi;`)EqAy&9BYGh&wf8gSS)_3&#yp7^eAuWyp$g;fpd^|~cVuW$C`Z?J z{Y+?%#*`x$mj~*lD+>8Mm)Zgfw@*kftK2JVz4K|(bHBBRPat+2++}1*M-$iazi zP+iqw%ZqbOgWA)pPE0QyhT7V*_EHN~S%#QhJBa_q(!0MEg`BPscBf&6XlF?-jf%~j zOtr<*K8BnoX2%ldv{bItFr`qA)_DC`n($tyE*%}4RnOI=v4gt0!u@;&9dEs>WuA=z zubY`vzYFf&e1Ec(t=9vDF===R)5sFHhHDh7H6H=Xbljkzhr^X*(=Swr?zWPE1!Bop;1pU$`tYHLD0D!0(NS5F>?dWL3q-<5qSuym91UZlPhgc zQI@{0;N=HNsGs43gwQ7#Y*}-&>|~|ll@;PJVmxa&)%M^z`u%aSI--%Cobrd!I@k## z$;+=W04;b1J+Yu}ji5t%zRPs(_}eS`p7s*5;`b;P=MA> zwI&FbaWmc{xZnv&?RvMX=lSb`0EsW(5)6a&Z*Ea;fS2&T>qM{?m6AfO`OL_TT+;4% zLd)4?R&54rlyMOWV~P?paqN+_mrgQ7Z%;4CgRPx0_6n6y00T3OqsvKN)qHvxGh*cF zOY)3M%7Os-r#l+L^+hT6{9^PWO?rCjf!^ERF5AqVUPJEIdMaR(XiU&1rj1Pz3k zVg@D>|J_0f_-`mDj?&f_Wqo2sSqxDB=tTt%76u5T8k03|~;w=fY!bK2%|Y=S*R zz8z}J)16kfRDc0$fn8=B#>Hqz0f&oU5i-DWEaum-vfrcS?1)^`Hu;b?ZLxXtg=sMSOSf zI~r}kDShzOP~#XKN~)*bu~=Fs+i!_AqFgC7dJd0HPw_IufFR_=@#m zhD%-6`7Jy!4HQ�yEgUq)hsQLjSZ#DKrWho855`o)sun0y^po0vygsy7i-nP+|C* zTIYqNkH^J$R!P6sPG_nLGE25}FE!Y2?|ISfk?C1Sw?9u2e5}R(SPG|TOg1HXTNIGM z{K77VhVJQyha#A~tpW2dQdVsULbMD+TDq`%Z0dd1A_9&kvfi5PxbFG`W~|TZ59zvx zSp`qK^Jn#t^D@Q zgN_30w=09r)lGcQT;jgrW=>x!47KZP4cx^0v^IsN8Jzz%<8wT%ENU+@z5NW_0m1ME zIu2#U+9X_2deno1%-62x`~bM3!4;1a`}XEz4tM2rVD}YRc$Gi2c?q0sjT*f&iN337 z9Gq}>8=1@hF|F{M&dh*BkayLt>G2)^*0!(vE_``F!_E;8(4dUcql>fGaO}O(@;9>G z2908l-h$*hfc4>aUgP&TC19DQ==T~rN1lv52rObNE2!^0V@Z$qEJ+YK*=R8 zQzb!w6X$)Lrp<-0_UJGfzWW568*$PH*@eIh<~=6r`k3zozj~cDxo4&@D=;GMCI9GA zj#7%2&$9cAtVh=x)6#j2$2rM!nou7UE$F-+5MINkL}*2F@v#!v-5cb_R0wRg?isj; zPX{I9bx1&w_2?5H+T0mi2H9ek{wapWlEc`y(w4qUEThYgw&GJ zN9p(q2mdtoL>Au>8=Dv<)YCJ4J-6(|zL)mF;zcU%EL~qAAP2k(cQceiRB}`RXN>BS z7?Q+kF{}(iBbYi*!#70p$#m!(hn8)7%8Ow0`Pa01TicCaJssh%kN2C7|^r*_Iy!R z$eQ(%-yIzwJadH~h`hxz%CzGp0KkO>(4-W+VNzyqch=tbI01b^+))9?xr8CF#WWcE z-8-`!QVp9)4Hx8PYQ!cXSm)TN5x9f=CLSj4uu&1Q?q6LuLD36s5Bes6u-JE&uC@l* zuMWRA&0dTu1OWe_m~~EC{w4YCUPS297qh(h;CTRRQI}a+xKJICqGk1$r1)_E`j+R( zD=VKdx-4C^IiVfbs5O4pKV$2LhSF`eQ&C2ol%X07ObYSU(~E8(D?}yN+Yd=wHv8pN z?gbPS-7Y}3E3)$Yr>10Po}ubxlH(GOq9{Be8Go!iiu6BJ6vxxb{aU%?m)HUJ#;FhP z`)ubX*G7`*7Z;|~h`&sep4E+WNvR8JaO{spl)LZ01`>q0cW8qd`*j{tGlK*q< zU&NyTg&(>sH15QG*{DaCKMQOaX!%ATz7$q3&44vBY7-4sX8!X1F6}E6-L6EO84E5? z$2c+}bF0ayz5r_`X9R@S3>+Vbh5ua4^YJZ8_Sy0K`5Q9;2>`^Gw{6 zmpXle9yIT4!s8W9p89pFDeH}tk-oh)Mg)>mjyBXaupb2zrd!OfXq=F!d%=S?{lkV5;TftPi)YOevB}Y662}eoYCU_~5t!f|AlhIS zQOukW!A!Jxq1Yp~hB6^BM>O%*hg;CrKz|~oe0gg|y%Uje_c6L4SXnJG%3?^&+vO)7 zgJA)anX!5ZgZR)aC!N^0;Jm~iiBR2L)aO@s6b{T&?GNqT(E}A%m7x&BtPj>{4~FyN z&DPS+4LiHvHXUa~S$_CGJpVCadHK&Oh9=75TzxkFY`?%aa_575gb=v*hsN7~rU2jV zq}{JW9yH+Rt1zY9#uye6l6TZUP*nbkV%G(q^*0T%%k#Cz*i#^xv66|5*Ld9WM0RVN zi^L)U+-x{ZP+p?#7Ih~v^9hnNC(OOeGS(Hy_teRVc-?S>2?@A4?!0RjJ`E!`k==U< zZoQF_g1Sl#G+r2|dXY6h*P(cF&0*I6q)Wr*DSE6xyXs);g;2{`QP2bB+Hh5Gt#FS& z*pCT#?Q?y`ia2a+Ma9H!)xIsz-X(W$@ZkUM3ZeBd{3#pNYB5Wvd>6F$cBaVi2 zNE=cyKSg?N6)ms8v}~stdaUaAWQ}8}JL4Cb0|8>uU-j@s#BhO+(GZ;M%Z^CSdZMAr z_Qbq#U()?4&-v-*XzLDVyF$%-M#plgG1oP#;N;=4(6Rq2+n1h1Yh97^&FzRi=!kW-CvP?21(y#do<{M4fc~y~W+JdB zBWv>X0&&2aZEq!V$W!us`u_D3PChPPB~sVXV%bOo7o+3iU)G4BT(TnYD;~|a*AnRU zH(qoaORfN3laGg^4yHcy;;X+L_B#q$B;m*IFDwx;?~~8d7pR+Tv?u-w#XCfm(W`xF zfRi6)z~LUjdG=E`^`uaR@M3E^mI;DD#1aQJdqIDh(T4>o_zrAUeorF&J^g4&PZ~q{ z^W4dc^Cj-xQq>PS4@fWLjzcyddLMkMZ#I=X5cXi6fv)Z%mrW+(SoDy8T9+jzL@xZh zBLuWK4?5cBVVLX?-vG@sze+7kHOaW0Zivmmt5vTOddK>Jw3h)mdMFHi#%f2QA4SBA zll{!ffU5C)3z+?;EvBSgdEmIkbZ}La!E1N4+$1xdjLee_sVM#SSpd_w4atqtbCM@3 z`(JESJsU^YkhqpVJ_JF8z@>lQ@N)DR<^avGHNM?xjE^JNX*tt^ zGKb8;L~9TT?^^xawrLZSHvDOJkI<~i@G-#`<1?zpIGM-^&(+;Wivb(IeH*IuXpULKd-p#(470rnn zFe$YCTVGs9*#~dxE8pkA%U9nf6&-021MW?)%qLx|VZqg#xWfLxzPbInzWE6=;khg8 zslI$$oA-~iAAFp>o>2hOV4@VNdJk$Pel2^G@yyKn%6U745p`LG5p)}gCU3L?2qefc9Klbg=1 z#f{$l8Q&d!N%THb>ym9^q$iHKVEcP?(Hb=6Q%=`sAL;tAU#*9B^9l9l^F?=O9C>%H zG}$~a`(lgwK*2Mx@xO;b1gqVRDXWuT>P>$UESPR)fLF5dKw?{WRxYQvPUr9$hXIXo zw5SBIsUExHfCQpR0l6-ZHhj}oz<=6mtbSbznEQq4(Y>BfsgB|V$4qCKtoCES#2kn^{}M9;D^T8b8qepyQC z-A{_S=UW-7+^drYw#~d+=b+Rm$=)dn5&P2w7%hYI(O3DZ><8czy{4)C7X(^GpSZ z@V8Cs;(Z;cI=y4O(?ZVt83-9tcl1=xmH7JN*~^&dxv`^>*uo7>q$^or4(LzY?$;X= zY_F?=!e#&IEfOW-j(^4~o>IthKi>_fS?k0zg+R^dz<;U0h@~*u zyqdzomB{%l;3h?mUX8c6-1Iidmh+zoLJt`t6L@|BY;qEO?#XX|Bfi~iJGjz4ZBpnk zWeH~@d1a#cq3C;q9BktoDcZ!F#z#+zS^+cGYD%7c1tkLsT%E zB=Q`m2+EC<5IVpBRv6j;Dmi`l3^Sc93QOmFe!>e7mt0~a;wot9Hrq@ty{hn=KdZEn zKd!V2CD|YigHCLyS1!K1_MxeHT)`Z$st&moX)VMbAWRZd_^@;X;R8>a1y?1!V(p<% zBkSulV41?P|1_!S;Q0-&Gnn4Cr#;`?+?L0z(oj9buJ9>HA4>c+h8a1vzY}o4Av_C$ z4;Y$!%C=B^`+tVh973w<&Q8+3=zUZ!+VZqgXS=Bt{r9nm3owv za*}c!Jz*w|+QcNkPDF%__cje>e?=U=zw1LTiBuxLNianBI5VwKPnZA7}97BjD);n zRQeO`(+>&6vjuS&A(11Pnl`^6+&;a;f^4xDX)7`h3IGlcpjQW)#aj8ej-GK$WDU0U zmt_8=FlefjEHljpU$w9nTV&Np+V(|R=%nY_!? zH#9IW-1$jxG5b=hBT{|Im+BKAWlP>sQ~$kd%hKtW!MDd>i*Vbd-uSh(n;(j}{uRpL z=+0&mnq&e8QMY>c-8AJ6E;P3E2I#uIygsXynq4`%p#gsAq-N4)KkU70n9)OACY1>B z6|1n*?J4^R|Jod55D_cr`S~5+2ZKJ)vs%6ehMCAe_U)ybqjHG-+w)IWYf5tmA*K*X~?;10VRjxEmet>M4E@FF6(!R zE4=2p?v}S_p~W%g?l>}<5ce_tdPER~z4pkIxo!~CpbTA4;RFR7yf}S7hgptf!cF)S zz!T#Eo?c^}+PFEhd%B7cD#woX9dLj^LImnpZ<+j7Sd9_CoOS1(&Gv9-uDRrPkxpdr zEWKNpjk#q%n9Z1Twpaz4wJpj>3nA%zQ*_NiYPaWcyS*zQa7btj>8Ic5CH@^zAGp6n zdN9HjV=%LThTEV%+ z$)m%4^dDI_cVr?;HQNn(Vt?;@DHFGF?|5mmXL!G!Nw&x_bOEL6rZzThz)i10QFe)c zE*$+|L%KL)pQII?2qfq%zy7Re43j0>$*MRZ^GA-&*_e(LSI%|GN0Dh7W7X`cSmMIOp7@;2{+g{qRRpQgjg#_QZvt_TENdfG&(P#gQ(8YPHJwW8~LgKGF3b0O1qT4v{rbMjN|$ zPl$rA~~3$eTD^_irWt$NQ7;-yDL;$ zsD>_(i$uy_k_+L*0$>S%-uBM5a~%67Y6jPO|Hv2=aPe;n_`QH>?;Wl=??7i{wCXhvh+~R0WvD zP^PLHr4aWPe#W6{&Hp0=Ik9by)AW9fJXF{PT8G#?);PNWTAIZTdp$>%U(O}*A+d|@ z(Qb)9=Ej^GFle^b;e&i8GAlc&Zu4R1z=_e=`{02s^hP3c8%p%4o=d0V^>7D{(apwa zPJ!be1JpNXA&uuQVd!BRqF$(=yJK5rL;qyLQ$|#Q0Mm6Z$sH%??0w27b0dyxIm3#Yh&f0Y}i>)SC(R?c!oFGQ4Y z2YjczHSqG^uCJZI;(ve4`m`e{Sk{^Vo`x{FNovv0Hzw}epQi&ac81TTE37ww(bp6; z&&pXwgD0iGX?dc{$V7Z#9h~`HtTWWx6iXhb;aS%O_mCaZjjUH0CZt!>PHh_QOQPA# z+4Ue+miGF5@Sj9_%Rtx9{giAO+V43!!aeR>O9J{iKLQ8nI`VDSwofwW+qlx6vE&TK zD_!dvpKYwF#VSe&ei`C*mL73fkejvNG;}!%H;;CHTx>R;H_m1?Ue8{fH0r$v2xVWNzPw z?mF-Dyvqii zCv$^&9`ua8!JNwHF|U0$xB@IQ2Cn?N+WnQJ$Ljdrs$o}S`TLXFXxPj^tVTJgL+^wk z^?|#E#*-d9Ik2G%YNC=^2X*iYIUzsOvuJyy#kZ^etWeE7470L|DDqxpuM3OwG~I$P zNwT5}l(CyVIFtQ9k-rLuMa`1upinFG5BA#bi2Ck5kMxT>xC~yD`?nJwABp}Ehevw@ zOQAnT6#{wDC%8^~D~Z~$`1iu%P50>m2U}do*R??Yvl}X_X7)r00xxmV)sE&#!@HMA zLl;h(x&XJZuT%CKmY5->uY2A+`a6&RwzrvTtJQ3p`RAmu^;FT5hh^b?oA-H04_4ro z37p69`Efh`2l1|5a1!ZVws#9;7nL59dYq>zVDZN5t1ROWL=)2SVG?;#Xu>?7I0>@J z$7Ct$>q;g`Leg5qQSQ7^nY;%eRV3Y<2zDV7Krtjt8mNhdnYco>nULo`s6LL9t> zu~__DZ#-`r9p|N{4orL&_k(%(N`*gkhVnknL0vDkU94&|(J<#yTjqTpvwiDfi`s|! zLg|f!pRQ>UzfJ$T;C3si5|vB&AOmx$4hM=RXCtoHsIE4g`RI zUx$z6zo_KzZ26H0W1ld88(Q8=4!PwAUSBr~2kai2A}E6{=7Y(Lnl$&XQtKXrJ~ArB zpDdMms&6yfSEitR(?8ost1B0)Y52$R)rsiK@d_nGlB(WGmg)p2d+X9AX2cq= z`0YIDxpt@eH+X{f{8Xe04NKXc6B!dpnM%TsZ8zT3k9rpM*rvP-VJbX#i8_#abuRn- z;-2C@0H`@@_OR;+zBue4plH252)a)`*Z8}0RmQCmIpPMP6Ap(#zL03+pCH1|v!~bV zdyofzEGX4yK4Zy}(e(j$NgIut;9MFa*1# zt~)Bpd8an?0$0$?k@(j6lp3ZHqCttS3FH&eiRX=c*jG7hkiKhY zzY=&;&HM(`JB^X$d$-0Y;OQwy8KgJ>KV-)o;9EC;N*oBgKTCR?(DbJU5fh!oHzT^O z+WUHcrY^o}pWe6E%F4twsD-+h0p&U_5?xl!Z%&M<#V)BG?2i?VJZ>X7xw#1AK)$C~ zcnSKBouS2#@g*S!W&7buo5qU?M7c5mN#NIu?2nmQ@gS)j+CQaiVuGpphOQkT>?Aiw zd1iOn)CxgLF3|lp%)o);wRuyPSRSH*AJs3(|)bQn|JzGCvbL-i{Uajj8={ZY69E*N-kuL;CD5zC~E$k7yz{gA&k?Ur(_v*(?XZ-u4`v_eZ)?EbKC+EwpL2#d=}vk6*% zWeV68PhxUG_f~%9dWgO#I1w9?b$-w#GyS3|AnaW%;DQjdN(7>pfv`UVh^=h(yU*j% zeQ^Xj+ni`zmp+; zZu^F}vWS90tf5OQRmZMtWop{@P#kbx*vy zerfdJZ_umdQseAYHfKWj#Vp9NGWSHZTj8)>VmY1IgGjdY=69I2+Pzl|$*%{ap zntC6=A2D04A7ewx!M44hp1yYrWtf4=Cu3?LCnp<=dlL1)5-SAIW~R;5&`zc#(c<}RVq&TK^evoh9nZ6Tw7^&w{D zpHlpl3))#;HNj(*fcRX?wHtCMRM7r6>sZ^gdrIcso2@#I<{zznEpQGiySL_?H zbm^yl*7WN^VB9KUXiV;fvd#zdQM%X@nV9RC^G5zGKA?5~*6UnV5w|tQ+zuPxjRiC* z*n;cX85k4|LmV(n?TBVK_ppsh3^+VRaSZ~vfD^%Z$GTeN?q2kHe+X|gXLLg2*ABQA za9lawpYB5^{+u68Ak9K9#}{?tlAn+_^cTJ*{$N|xJ@obW)=3rU^Jj3R z+nUQ7JA2%PuFkC;RriKZX|GJgWjXim!24g8`X9+6nMskf zv+9B{-gNR652393xe;m#o(0?$@?0$D2?v2WT!Sf!ZfCOW+H?42a=&v`BD}r-%qLMWLNnMzlW`vOqNLMs7RQ zStfq8GUAt+;@*WH^m3G`l=w#fY!1wWuAe6ioPg30YDO^Sx!$v9YF@0%s@l6md>(Hx zoqwJ;Qv0r(z~{g)nblKC9vjFBt}8|CHd|;lx65f&p34R+;#&3g7vlaU;=mL^lqBJi zFDOaHMLm(^BjMU|ui29OjDMBt`H$tjFWWiZeILKU&^O!rtSE*Rc)%}&4s-B@;9kGZ zcWodoCl8w$>P3T4s?H!PZ7K3soxyq>-~;(2Fk8Xk#G|GZCo)u1pCf5{TlD)dp|6uD9}mtb1-8&{xePy-yk884i{Ez@1xTtF z^(sfV={hcqH7 z&ehAks|B_|@f!tFqyI|kz6COFEc>+tmvI50V4P|272PvvPp33Nlt#xE?ew(=IXr08dVov`eH<@8Ix1EuwR3Ge2vce6FllC}*fVGvQT+yfl zcUgUt;Som%eYPItQnZ99^M&b%lkoe^;7dw4TmzI@4x_fZonb|CtiYO-xO;bexatK9 zEKeAiSt;^K-2M<{Al=M^L|@|22L$e_Fv+dr)u<5*?~{74L9SyYwUg!p$6m{oFdL|LZ zVVs@tmJZT$nsZvXQ0#VOk58NUqH`2(^}JTSA4EVcm)(v{7I83Q{r=^X{;TTovyYh8 zljgTQd(qRuJ#XTT4@6u5Wz#C{gdXd6pL{6D_}StbuG>g4(YV#nD@n`Z zj|!`7c$CC!ix_?oaBD&L6=(^o03*kqmg$dQQ%B4*GzF7pZn8x}F~MnRxy+O!tck_0 z9w4mk#%uP^X#ITPz>b17XtZ}f;M zx~+t))kuzt>=V8t67HI0f{+E=-vHaz9pS^>@9^m)+pm*IvcW%>96ZshWVJ9E^uc$v z#a+4*2pg3l&vzwV>EW;3c1}5*&-;raL_q#MJgLfcpDr5vd|>46MrO6?)%C8+-rKCT zKo(;V^>vVj&x3R04RC*x!9n^7x5EvTt(Yimi}KE^^i0(j_fmhLM%M|-5LQjAy-jYQ zrcWQC)m;lv@ZWcL=`_!52}F2v^3YVwI&5@tsP?F>M@hB7aN&Ew_dG#o;2JRgtoAA* zm}XP1`y65~NEIQZ^K6;asU)!`Pj5KvuFl(v7NAR^^{F&D6rO#`dA6gJ*C+x1B>@Qr z^IYs}00%ZT{1ds3ZA(Pr%T)b+_uRK3-3Szk+rNBoBF&<%IvrRfdhL76bx=;d7R2+; zNFe@9wt|U&+y)67LT}gRm$Q^6pDVSYZ&7^v6j=!^6kh2u5}-7H%r#n3###fOu8NzQ zS`s5CqMNCsH`Bwk49%}%%L$jO8hF}spRIm@oA{xKXsTu}q3BBV_`~l;Zwr9F>2eoQ z)#^U7*4x*w*14xj*^<3IbL~|uU4-eNp9wBli#nq`CnWRA;&GM{wHW&P`sikeNOJ8j z#1&jy3s}b=qBYqvBN|?Q+H<%i5{{z=U=@5Q9zveVMK4qOOfTLqZ-2bnR}_4$EOqhK zbiD>=V*8VTjom96KjF>T%B1PzpM@*7X^yU+P%_`Cc=JpI@&9?L+J+o!ec!BSI&uTr z2N2V;?D0=IrKA6>3W%sC7O(rO)XCVZQ$mYR$*%rv9N&F(Jd3Yo)U)QT?i_FY?f)Qz zI!gMw3gEHFV@RJbS|#UX9Hp_5D?odVMkxrH&e^baz;L68;Xo(Rkd|k)4|B$|Vqly3q*A#i z>1Z)tYEwTb5HGhDeeTCpm!YvB@?$*k*eLf7Hk`dF|L$O`WBuTp#@&Zk2d!0F!1~%m z99mN8gAEre;}XrKEm(9{cWrIC7*rw(1ZP+=)3_0V(|hAI`NMI1pCLHjo!O?QMj|U~ z8@WN5BN;9Gx4}-$v0;%qDALx1N^jgzjTo3uo&!(k9HF&{yFssWmhr7mQ?K8v2O8x> zt`F!LlU=8Eb>I1?GJCZfOW+eFY2*pQQXk0u*j9G8nZe>#WKgll;n_JpDL+KIs-X&_ zK=q;0BxZ3R#BB$am7S6O_|0%g&Vm2Os}xp0#O1+nO_MTKhU`CfzCGdTQJi>DBr(7E7zSI+;y0Aie zYnPbSRZHbtpf(_s*}kNJ+MC9RKL`@-em$u)pm}^#-xb1^6N#UU{yX>=nH_cJEXjMVp}MiYb!Cuw|))J&*iUXP-t!}535hoUq;b+0rrHVs*=LLp~4P^O@{~8>p zvL4NkUYfOCbiQlSs98(kXN{|Al&guV;Rd?}BVhG*Et`E$b=b!V&hXoNYx%r4o@{T= z6Yom=O0w|nHH&}eTmm@Z8K{>$i?8#Y_d-U*+_LEX+#o!iIw|*EWpCgx{7AmG66#o# zO86M4)BVk8@`H)H*bLD1*<u0^$^aA;GhMV_b4TYrVI-{7-h$F46k!h9qL3A!&>i02iW z0T_O4G59~1YzcV<0yb1oq)VWD>2`u#0mVJ4 z3XS3REpfX%J?++p%E)47nEHIsZR={52G7F&?fj(R`wP?-g7}Q`r%K@eEMTunC}yoO z44MG$jJ8c;A)Pauo%K1r?Djy=coEFa7B${l`ho`*HBQ^bO?sO3Abksr#n!13u`1vX z+BS9=0y0*(=pipDnGatk6Rt?Hte0@$_m2|$fKIgWjj&4mTgRZ?{FDBkFy>XM2$C%n z`)_}p&u)1nCLZ(nsN)L{cK^CRhTf&lbhzMkl(^Q_-CYpb30vr&O|y(Os##dns^RoJ zo|kA97}H{vEfUll-Mee!Yxb^j+~_$}07q3;p}#wH=9R|Q=E)Xng}^rhE`7Rx!Qgi< zwhw}!+Rq_fkwRDa=bs|NR>rM^)?lU+E%1I;a{%s%C*THh78_?jQ-cn|!Y>(<@?(98iehoy?Dd{RNPLT5J&8UR$ zMQzT`uC8AgN?(!gme9>{m=A5&*QJIOlm{=5%x7V@>JkX*<1?6LPUjnLbphe?n8&V+ zKa5POm9AXCB*i0qQfzvc9w;INBC>_ke2V-&l8EhYxY)%?`hox|f8NoR_fO;G)Lf)@ zxa`5WdC8udK@DmHc(b*1i3~X7^TUI3b1C&{bk1q$lQIvse|c-PsEmYRorjl1)eGQ* zI+KTiA|9d17MFXxylMrzqIk4VGiHRI^VPP)>;P}kszv;K(E z!_fkcLyPzQnDLo;WmoJQbwKZc+eS@#D_?%?9lz1M`i%`wPwiu~*PM7iRLPxA zTN`ZH&XbwLo88s>j^MktJS2MI{L1bJK=P+^3n*li$9dxzQ=Na#_oB7bNjqPpsnV8V z#MACoFDr9GV8FU+^z9Zao!JfO!p_#97Mb=b%Q^g?iEcIhMyIJL)D*N6x<0$@_6-5v zPP>*4x0#KrEb0A-TcCRa%o0n7 zhA#r%fc1iGE0_9TfUa{HnGSBGK6eWP!=-gm^La$d{37Ur=2hK)G*xY>{+X}3DG-DO zFCZ)E9~84FS@O`q*I4aaCg~yXDQu*1&;Gw8tUUX}gG(60^QuC|i>CLx=S9FvibSp= z85j0U-KxlI4%8A{0!*YPp952&V?VSItu-eiQ;CVCllH`mNn(jnotm==y;jx~`eNkQ z_$AMiN^P*oB3v>dLk(4L%~AIRrkdZY$&)`eUvs&1S@W2X8KiAmss$w_s05>b3UL*p zw>KYN2)F{--u`0k#%{)CjEGA)fx*-jLzdHUB62}zNS7vq0WaMrTFWhgkvx+77%apfBySosv<^KIiM}f)ue+ZDdy^C)9)|&f@fr zg3)bHJhdoWeUPbyTr z-!4AtYih5M(OR|3myYq3H>!Z_b6_mTofWgC(h{Dqn6-S>eE)2~J+v}N!4tNbpY*$+Tn?qHqv-3yGYdMO;?Qwj7FwfwbZZmILb8@Ih7P9>6A3uK9$ zrwZqPy(;`+*4&ttGB$^xU*^nfAI=Wx?!a3gboZbAR>j|Oa<<^r`c~@H)olPc&#-x) z48=F0+jRCZj`tZPikoUQiyMg3T=Pyj`Ho?KuxZrl8tTX`?Bc&*QrGqKd!=8D#cc=k z+`_jp^KK8(L?fh!qAvJ_Dw8Vjo=x1k0&TJ_a&JTpW(0lqRa;3Ry;Q*FAx#;QG@h$* z+_vrS>$2Z|gy35aqnjY38*`&G)U9Zxy|s@~^7zz)JRr0=o5cS9h%SFNxRTOJ*fvC(6BPf&DC)Ao{f^?v_ICHBM_j+H8ymMB*dADo z-MxQ+#rWD3NEx+WmDc}`;OyjPc_a(bOBZ7F$pFPYQJJr?>RnZP?J>b9dE1`EdRFii zoyVZF?UYJls(0bQSH=?6#}o5 zvUyYQ4$aDw9v?@R6@`htPfFnnb&l!~4igXhGF*cnGDjsbz|k8iL70laa89!9vFUd| zHVIdA>9`8W-RCp<9zr^5cW)ibKo;Kq;f&@FBAM=(JaLEcW}g| zQ_ov7C=pxc+eQ!dgk9}N0ps)m#WFVkx;NZt+Qgz%tzLAj}9wKx2R&zvYB zKIW~5(|ujEI!aS77XPk@1qc@;jnAfGHT+1LNoP^-9~KOKTfdeSydCZKF~UafRqORJ zU57dJJ*%T-52`p6H#B97N1u;X&-69tnG{{|{#E!}70G>f5xD>T(VCrIw6Mm<`{TceP6Y zXo`8;UyFGo9_&Zz9>Xk11rw1fI{%&&6q=lmk}BJmfeh5i59WC*Qmqn6Bwc%C+vJqV z>oJ&l%^U=ZQ!JFxCzs%a*EkbS8D!9HcnVJm@d+eW_2Gyv2O57`pnIqJ;T-D8r`YHFPV5hVbipOZd0kC(9Yq4OwV!>;d>1^U z6B>2cdRZE8U}Ca^c-ThttXtwV?dXt3}%mPGJ_5$)+f*^SJ2i*_YNkD&Uu-$j<`1^M| zgUNjG<|3~C16?+>pAz7UA4^Kj39Zy%!hAi>=c}TEq}%x!A=xRUG&VaDKAk)L%QsgV z8&vFTSlWKsY>pY|;@|hpnIeOrLNkq>>ESP0e;@kSvJ8GJg798tW3hDY66p?hVb)C& zGNMd6zQdpUAln#4+^}GFM)kkF0G5xi(Z39DP*sjo%yxe7$Df#4c!DO4ZPF_?cI1%K z_^TlJ7(EEm0#qdYKSX_Fc%IP~Y|O^C+1R$N#&#MfjcwbuZGEwwG`5qbvHj(y=iGDe zpZ)${&&I5oS@W6{3FTi*1*mvlLl<)mS=;Rl+qO;bJs4%5{Z4}@a5`O?ApWr>i&AM~ zXK?HU(})Te-vS=9g>_S*$Ttj*#F(_PCRH~(^n}w&NtJ!_B9M0o@xi!1CMulZ(g7US zxQli3AxhKp#reG1@eO9&yu(n7$;iNz5=nOepMRXRFN*1?%M$FI<_29JzY-nsM#@1m z9OBLz`QRf{HUH17**F{6eT7!aT&9^v2It0iqL)9=9cMFXU~}ckVP7M^F@TTht1tY9 z)M&kIw>@M$aLqx15)gbb<(MPhPw zJ`8w4fPQ}1agWZoAZ4gPg|%x76F<5zs_i7Ct`^cIw&jvb8Iq+q9D@xeFguQ>To;(A z(sxGq1P1knX~s%BzuEABudF7mG%92)ZOKVugD7>zSqP5@v1mG}e#^Esz(i2&zx~|= zIJo3IxWL}VoSe(c1)W?rXbNShRjz-EhKj{+k`_2sU+g5u-@7CNWNgJykr)WcIigmk ze-_)@@zm4K=fUi1eDfz(qUO5p8Fz9;{Cpwvi#)X>t^gEH%ppPyZ9ai% z*LASu`qi?_dKpE0@i2HEcR7p{Ah3~U67wOGvsniwy&pvB(ngNJXPz|06{<_fHQ33r zs%VPH1mwiK=kA@ZII^(G+*2A{_2Cz5jGegS66*%_>BztK+@(J*9N_@qos#ZPUG|4l zTaru{`Q#-$4HuWqKcu7{?DzT-p%e+c5g$TPnUZ823F%Ja4ONaK>Lo^E7`uYu87xy| zP!Tc2tK3g7sJ&L~k(GY%OybOxe2=^=M@EU{qneFm=LTuDU0*msAjYw{2+`Pa^;cKD zK`3yL+53KA$fz976%UaTOpGXvn%WtlKrM@JcXHeka@EV^i9mSL#(1F0%Bn(o!NYB=Kuz;1VzmK?tq=dDA8b zW_{*;yR)l=Q+9TJx9>!}8JQVid9#Fb_;HX*@@sHP=kbv>H-kh1!AQG-WQNBK&T2smLYLY$ znn+jzMtD;CtvSiOrK25UDnD-Ers>cDh^zxg6oi5>Gc+>@n0G`LjePXslz9%o@pC>O zd>8si8@0Lv)t)8ePUqkjsOY1Bu%~&svLjlJ#KC16c+B-^@;8rh*;%TFw+LG_GMwKU zNAseqYZRAs9{l8tI))-I7D0(~6OzwwalTOzzvu%jUvr4mkhLJC{e5v7`xb}DHG1wn z-v9b}V5m+DT?c5JGO4J65M%_)=6YiC&TJ}NR^EQXVbJoGqJfx)BV_oS`Qn-C5j4MW zc@o8(6$qcOGyk@b9d0C2ES^>8fkSP4kvncF^7A_;+UaMiL-5)zG}qS&58APo+%o#R zh5%m)iS~ecZaF|JMl$efOr=AS%=5q2phritx z*Dw#&027gs3PTqw=pA~)(o1l4Di}^eH6R_B6wIetXo*JjIdviTOmL_IT_knQpQ!0P z)!GdUiq*$$NkG{Uh6CGn-Bjnnw@>KL6d^9XQA@s&xdAo0kx&1GYS}zfv(uXtFNvIZ zo3oU07p6Q!-OvzUHYDe8x-c^!D}(D4NQ-MKf%F;uG&*Zf;PM!Y(mV_G$&Y%TC#SanML#x=Cfw2S#(J^#W9oZ>}$m$G)$0k95Y%&Qi0eEu(z)sEJ#wALV%}yL-Ci z#|S^bO^A@9io0Hvb!{w7U9gi!Wl<~^DF*8osmE@?h6plNasWL^zVdzID33|SCB5Sl zG*jlV5pa>yQG#TbN^j+JF+1rV%^b{bbrfXm*;kYJYnyFt0v67RTv+(tyHU1-_kKdG z#rdp(rj&FL9ZyNsa7Hql2K+ETA5)LV#g0OWN|^C<0po5LUC#0!T^GS+on>*g*%|qK z?z>7f%dg!{K(T3q6k#_+$Zq^V|4nBQdcXYp{RBoUk-*FE>d$ML>;ogzti7gpy zz)dmuvh?Mw+0WyalGl^LgS?lWI2_=_UYiAShCYxpP-HZ7O_b_;JLcCmy!Jx1$TNu?dbE8e}Dp;mW zcL(1A&KT9cyb_Y@8<4}jx_)}GD#<-zyS-IrD8OifpOTPw<=qid>D~aGMlBqOZK+cXZ@n4|hp3bpHAE+a{3hk=cR*uEjGPf|VP2EE#SbCH<4%q;)0XqL(s_wr$g!Gr)bgyG)v2 zc&Xs8l>*-wYaa8A)&|vhCXxxS*}IRCRr(e|iV9?YHJlRKSc$?$?uf;w2Nrbf&`^w{ z2$E45O;V*RhV(8Digpc$F?vJClB)frBQbA;BXU~7adZoYH`qvco`^Qi}y7|!9&#g&+#g9zp zoGO?a*hr$1kNzQdT%C=onvOsj@wF=D9hSH zm3e%fDWa?T;hltl_ml5Iat7Ke=sEGDH|)s>3c9gBy4L73K-yK%J-(&r49+DeH!{TJ zINj>gqXNAU0aVKIIljR$=(;kG3)*%^OfSyxH0@P&`t9iRmv&E6RyjNls%}Ep0`nu` z=13S2C}GnWz_+4BAOXI>8(c*UI)^Qto|(Omwet>@3p}SvWG6`kW>l!cs?-hZ`b~;% zdLB%(jo>Yz>J*5C(1I#`t#IF&$1ZGPP?-&b$HysqUzT@@5G+k$m-hj#UcP#3JKTi* zVFF*S`sJx)5dL{-kjXEjeL-FIidA%l{ocAY(eaz$YS`03dE4-;mjUvmviAjQ6r=%^ zG5=8zgj1R^YW!Hi3t{eEbd0XIZ?ye|S+TMx8~*3bd^ZxZID(T!yYq_OP;B8HT|#%U zTOoIgCb!oK%`-rLTHCl`^aCYp3|TA64H=+yOuS`CPB{%$P?l$w$uxr=5m1CLw2_PN z4~wkhaEda+<%4FutR)s%Ntl)rNoXu%(g`Ob(!YgFYS(o^qOD43k_g#iM6g6Iv?eoe zg=A@g6Z-AI&3Uh*DBH`GV5KuoN<}1B<@|3~ZXGy6LgfOcx7+)7l#bdxS%Y^HBtG4; z`Bkyh8pOmdZkJLa$lyX;#&|=bYp47BwnjkMYbAn4%q(y zT37S1n7PX26wv!?PHjOoQ*W|%Rp?zO*bPPgKpk<&#Wi)9`!g#m4wNT)O#%BQa|`0Un$h)937?2V6<~zTX)%SaA2IC<)mTf6ySnSDP*Kv}8m{(1~87UxoAs}H-$Wt&lUnhRFH-dmW zX53uCRKe!Sfgv}ZI#TID(`3B+;67*LB}mYVv!;31ePW?-m{fsIdTT#WatKv&pzd>@ zxBUgWEol+8y5>DA<+8W7Irq9`r_MLFTKw%q;?*|f<*e$$qvqMYCNuC0e${*OM>EN) z^r4%7^Sg)k8}Idg*+5_V6KITVGC9tSoDn#CIe{gZWZ~IT0I__XQsw46TI(-S6b`ybwr%1a8ysLL0!3{vC(J`h!Wf9k8gcXF@Y2-g zxIFVUGmWhdmE>9mCbERGf_L6&%{c9O7f2Q?L`Vww%G~k3(6{UOAYNr(EaGWM__0TfNU0n66ppfZ5*@Sn+|>Ij z+G?X01(9>iO;eTr#jVgwmvips*uR1Iyr$H#3UEl|nk}tsf89rDn{QSB?xi14_Fz3_ zoSL3NI^PuFes_)h!QH-3R#Uv*|Avt?KQMj7i>I+sFf z)7JOKT7}yGtIvcE0>|iS8!GRuj29UvNE3uU5Z)!9BB?zXxZ6wBo_P7>6wjefsl}P; zIGm!`=inch;?0!Kt8WV$&}ywhRO9&J;&w9^geH{+Q=9y77DdL7i*FpI@8uE;&$vE5 zPE|(&5oZ9}fpFy%4dVg#P2QZ2qmjj(T~6UBN5%kB2vhaMK4n%3D;)ve2=H`vuSmE>~`8P&48u%B`dhk2S`0xwmO{g z$MS^?@b?j%G3#0r(Oo5{{e~F9xZ|r<-nVEwM+N-Iw@^}xL~{2Hf1u$6{2P0@+6j$? zAn`|dbb(Baoj;+oMxKmHVAGw-JPjQ)O$Oc9%ovw`jT3aw)+G)HbpxEO|)v!DI)*OzB$AuBtpsr6AS>oy*Y;dtn*HQh)bEHMd<=ufP=8kukWYN&l zdrB^RQ)q5$i=Y$zo}5e%j8VlV{0I)G1^Qg;x}1E0Mg)XZ zKuQrw^@PVxM>g5paQKo1@2XroDPu-(6W!impm-; zfGXL$iAXdvd*xZMzfADtE`{RYpj8vDX0w4qGt z;7JuvQtGUu)V~oNq-wheL(<*@GQlwT%4mi$UR`=mvCC(8hLWM+2!zbi>fJUW|6U0a z4XxWo-lK!dsPfFM97ulP{np9Loj6h~P^yndunoo}C+XD(RcBOBW#C0`$;n(`j65#2 zAND*y+1Bu(7A1>h%>JG2uNT!fw7#wZHb|9xOEMV#t_@-0;NZo&6EdQ%7*W--_?E&z z9kR$VS%Q0yI;&wyH<}UCajW0V&O?z_RCsdR(YJ7m6r*u z-u{i9`yCvECQLN(=iM6!@1FWyzgiDJPau+LbkR?EncHa-DqPGnKgPD1cc&+R#uPL| zi7C#p1*gr1=4YqXJ8c_4`{vC-{ZrMX25%U>CrIZu$KXgH;3Dj`xp;|6WqaFR{ z`*7X$M(%?i-)!(qG8NTXZ+LScCA`PUk`yR zEWaP8-jRKYgwF+y_w7N~sZk_B8PRESXh5l5QvN4b3cS<4}$>)W*4 z;bWIrDHT$1fmzt2N}jx#=lKm~PE8cPz#)^(Uklxb{rl+kP=R@uC=7L4Vx23N4#Drl zieBp-;ibEqx0mjMaKE@f)@kSjtvdRvN|qii*KP~=$4k~+$HEeJ`8}~lkiW^bQlca| z-;`Jo17So2kn3lD;Vx+gvTE|qJlVHH?=u5ftba^2)c%1no{)iOV(AZciMs}z*oAl_ zNyudM=5M`}egdGhouNLfQ34P)ofT)640>|3eV{FU)|vJAw+-DCVE=MkZBPn4`|T*& zlnBxk5vxcIaEwTFy!A-BPvU(V1cq%wH_}0q$-=b$r_hiu{!8@#?#)WcjQLA5gD;fi zVi;U8PDw_F0K5nH8~%Ksj6p|BL?xLsK^p3_4zk16&~2GBZj03#I@9unRk!h|!|* z%8~dPAae6|+mi<~ID1;N>#?OsB$>8$U@X^YsRslvG-W(K#SOl(;+q?l+K8Fs_)s74v zB0^ij=kY@(jyWL%l4TTqzpC;<1Vwhg0Z;GT9G>$*V_r`nV|_z@&ky8JgwiAtCQx5} z^!XhWCvhe|BEM9i*L=nQR;-|{)6Z7by?Xq1g;SpAh>spAd6n&h)+iqS#hYj~3 zBj5p+YrIlpQIWT;hHqel!22@Kp3DTif3Xal24R60$g>5mpUh@$P)`@E;subexn%0h zs;cD+aw&RK;*5y7h`#-8cJ*X1);?EQS(F25*K;I5t|gm__1n-V;DWBd+;U>I-Dxjm z19r_c17trv_cJJ;)EGe>dDRaZEtp`$*yMX3%^EWGb@TPjN3= z?}>&poIQs#a)Fm|y~PoZ1jrS^)=9DBzYb2|Uj&ig$%^~sks_saso03Ztm02N-){|E zNOgWkjCZ@;NN7uF)eY&ZU)pq!n(2R@CR$L3YOJ9jwP+pt&{K(Cz_j(I`_+;8WfX!D zl=@6A@MsuZbSyq$I2ECN2>HK&)3ojJl^z#7I%Zsm;k|L@%52{WiS5sToe;Vb zOs#b*8K&WO4(w?313l=G_j&wJSrVEvnV1v^%*x0HD7?7Dj2Q2?x2_kUZJ3l6e5-!) zFjA}{z0PPCzY~gOaB%sYvPO_8ac#)MTgM^TUL)1g^%`N}*sr&|AX+=2a-e3iUQ`3Q zV<3iXRmuCP-)3~0W1+iED$yl>$JePT2x+IBb$>i)m`8W4fdPtM!^?otX+HU)+|^Vj zmdyQRaD(*k_ZmzP;;s zu~hs%0b*kdL59F&{XFCHG-m^^zJI#~%gRL#<<-tR-Y99^giz7{E@up0@Ib8CVTz{c zIpA4XpEuTd>jNL28qu;_hUTOfp`alH#F&-vky-zl_=c%%Fk2@0CQ7*7i&UYySnlRv=|H%6588-Dt;?<^Q*z3o;dmz<+Ic07vY{+by>Lb2~1QeCB z_X$Qo4$);+vAV1!ujDkjrZF9)RWdE$k4M@~?9M9&B@&^r4FCr9!;utQkE6_kcn;C{ zdU4zZJnwzz3HIi+`M0q~1I#KfXvWneIaUe?i#)m7C9Spz6NDU0x@JpOHz0Quz3e)c@k9k%$p&q9BEXuLyxZ2VfM#%F82{B zlp!t$tJ%c59Dj3~6Vo}vn_34`(c`H61QU5;_AMww%e&&TYD;%2*+aMOH+8v+$hizfv0&;*W#&}~;Ioh4 zAeF#T&;ILq$M-{Ca2iA7o%)SvZG}P_2~`R9`BK3$Jw@$YOp`R4lcMO6%b>@TsBKF* z%pKe#6@&M~Z0)m}cTu|)JRYO80EP#na4zuOHTNc4{(O~Nrpl+i_kEQp?}%p72|(+} zH*nD=(AVvD1!*0Xc%8^*!+>ds5(P1PFvFX{O4J6K)&?4W5a@8OnFJOIQyDl*GOdd= zc<1EfDz*&ueZIQU5?3UTV5{J-W|KPC=fHwY2;#!11+!Ot^$=MRKG%8s(Fin?3dDzS zi6KfZMZ(^Xu2#PBsJR?Yb1z?y&M^4dP>W9~0&7InXi>9(P161#M*3uWI+nx$N3?YgA+aP`W@UaaA5jeqW=IfO8Jq_O>mI=h9>Y7=Vm-b8H@{YifZEmJCYtW z+$$+x@U3OOJ}>61wEZ@+N!mR)oYYX=%{vgOP8`y|5>?!#be)+~{}UuQn9q3(@SCON z7eg%j17@1QDVw)I2t7E0SGEivc>J3Rq#^et;nJ?CTrO4w(o!z{iAl5bW=9z=y6nU; z!o-?I4TZhpH~WluW2Me-mpyODPg{Q2DaGE2kLewfJ`u%)FZ3iztg)f^$B`%Yhs80_ z^(+zd`mjSr&=x!`#Kh{BN9>t)RCyooTT%c>EYf}L81e}99Oo>KtoP-vFVWi`m+Ft) ztpb+eh2=n7kH`1kr`RW>1MDTCWAGj)y8jAI$6BHknJcN4WIWXtWK3NQ2O=)3X|FGm zZ%-E6SEK52i#K0k>5gn^Mi+oq%dNG5GaR-iu`K#sJ!$@|QCtpeF@yw0IcM>3Waj3^ zY<3-vyNWpPP{GeC#XU$H(}a9Y(C^3qQU3EymI?d!5j;x*9w}7V?Jg~?Mwfh2!*qpv z5md}OfnJr=8j}L0hTJ7;m$D#?z?8z2e)xX>{sKz#U+*?O@58)ny=@ZYcyJFO7Z9Ij zAPZ~U7byhrM-T)&jUN8gsC@g6*O6Fx0~L6WM(@I;2*FBH+4MicJRC;1pfgrL3b`c2 z9JY~vfou5PKx0ct6{4t_#roTN(!`9Fn|Zb0jxrE85^AVRx&NR;FBK1`DKXU_?L16y zP7ki9zU%q(HcPXv!;=kCNx|!TqBUjK1UX$&LEfx41cX(Ju+Yo0i2e+S1o^>Phmez3 zq|(Xk{(D?B+wYM_VH;V9)ux+yad`ZP^2V;tPG=5T5||3#+uC-CmfHvPc3p^e+MxgK zR>^MnoKmw5cAaKN)7kF-ya0J*rz9M8@*d+UC!`l%;Vs?Ir3f^3LE1{zj*StY?>)B< zS5Sah60fH&wZ^m~gp~nKc2p za_I9j;L7~_GtiXJ!z&-S4Ajc#2NPnc;g1!?-?XWeo%w#C7Lt4-eEMXpguZCVK3v7M z`f;j_b~nYREhiuult(|y++AtljxIC#sn`2(=oPfiaj>usDO5k3joc9dh`=c+M$fW2 z=yJ=Qh`&y!JDmwsgN9;0TOr;!ejUoL%OsJ}+XyRH6=u;3Y#4Yn^hejoFb@1D#_flV;3@I!=RAAx zFC_Ok46bd?IkbX#YEE50BrQS`)S{-xwx`D>tRWi?ImhJ;Je(R~2OMLFtIhilE==CT zpOG?tCO2?3iv{Y(;pardd&BW>f$Rj|64F6&I*YFOAhF<3IYX#b7`W{54j!`kZ1*$Q z-cRC$Z2}{H>OST0GMghT51AQXYwGr$%b=p4az$IA-F9{CN9JTZHIBBqlC_I(?#!h@ zz=)Y@MaOa8-6(EN1&qf)5YUICdVS|q*zNh$PBg3@4imam$$g!Y4WHnkSd4FVPNW`r z6DmR}0u%gHwcj7xqe;|mEYDw3Tm1L3a0Mv;Us5A75Jv>89#Dds`jb5x&P9F zhQaxn=M0}Suf+u)0-6)u@1}zcw~O)<5E*IFhdDblpZ+^1Vq0ikb=^;!QQgYO{(5U? zv#wBViiUyVG=W$?d*c`qnf*O7-pDn#^5KV_?x^A(LX*P415iN=pcQwmB=y8|*Ifvw z*|6~}A|S)~Ae55P<7KVghVfy(*d2=oo!zvDfCbj9Bvc`&lAvhjSYEt%vXN zy+B!y7B-DaR7ATz@*{BZ$H4&6PW?=!IEEt56WScqbx;V3VqBLh4ntOEa+Qc*s)BQ% zO9oX-@!#WJ*@C*k7A|a*(}6dC@bKe20Ng2(HPArBwT!xV5D-UBs)n_cu6|7jIFVbRMZf{W-c z$~A;5L3#a#RxjuD(lp9-<(QjkyRWzHY>(yYhoOq=E-`lhOW<`LkLy=)>|dCGr4$gU zrs!RM(!dyN-H$qhgB~J*sm%Fcz9K0wr9%mU=YFKSHNR)DB9o%$-Ep7)RByn93-ns^ zirK)5Ct>u>gYL(ZPwAt_uu**>r9Xwh&JRDoK$=$(y}lu4&i7qJ{kuv7;}st-CiOWL zn)8LcuFxHylQ$d!f_^jGA%@v*{AKX2&CVPCPNQ-SIhgRLEq~rwCtd742WM6y;Ly|9 z!GoL`XtW{ODO3s+W+@XE2okeA>f>!v9UwgPoFXyXe4#-97~$FC@%d?^@M=~lPES`> ziuK5#4k%!yK9~lY^b5S2Wi8-}r76mCUNHJ#Tu}ggIyS#R?|uhQIEDh1B-3S-?;EpT z@&hGkX(>x^I0M#t(N;gA_${RjDxm3bt@*pMckQoU2J(KEJjkmPHhS4lP;ak5R@dCA z8P+yKa;jEyOs%f|T`tme=GyK@S471_DfB%h{GM+^78dVUdcA}XZ!2lUu!g$I+i`N3C@8^nu1V-SbpK%iDRlAqL#0GHE%j2_4K&o4R z@a@}=xRxc)w*8O0qUo1rH9xoEVWjU-XojoreuY~*XaveG@c&^nVl^dS2_DupgRXq8 zD04m5WIN6qqqwZ@C@%pAVY*KrSrwck@q~=Ot8!OmKq&9yLC}>=P<0?kw7PQaeNHxX z{T3?tUfz?W7x;&YS7#nGXENpuc|@5$eklFL-@mocA3B)DtnGFo8i}69y(W_7A}WuG zcAo7HOSi-wK@;sW5oxHa4CQwF9TH8Y=Z)FYF8)^=TXpaW!oP5s@P3|vtbiOXu=)bt zF6yW=Yf(tZ-8RNBLF7 z>vYj2uzwc|oU?o!xJ|QgO1b{1IIKa}qlCa~h2MVY#ud4KSFBpGsUTwe5-GT)OoSjl zl%)JbqM43GWEOArHJLs?-@*=E+P$T`fWPdDcQacsS|z(vn-cZG3#1w(qM$8 z6*Rlo4;|~UqUSYf@pmF*0aJKC`rm`*W4bIOTE80f>}aXmBHul3ruZ%iLO7F12L&An%ztPR&+H1S*7IGQ%~M7 z-;{}LxI82P8zAabIy9j>1d1#ZW?kS@Nbu2s*62SN z`ad0_Qou|6pYemkc@0d$KzH`MSmBz5SZhuZrBQq;S?ps$&ZD-`_s zz}%5}5_UF-Z?vK1sk@CI=bI?*uVyBiy>XhvKgk#J;_{9lGY`D)ID~&TyWo93cWkup zm5Q#qqX`&%r8=fu?nY9?{^a_x@0p`*0-@o=t7(#|CP7$P81U8pWcO)0!3$w$LJ}UE z;%|vtt}-Mu+>!Szsx($I`kHMaF}qrVunvJ|d_FIc>Cc1Olb3o+h1hjc1R`I^@3d0&al1=Jf1w9Ti1VVuKlNF2E5uMoVjI-wUgeZEyG8C>OieQVDG%Y9d z#+hAL{%O42oF*pt1-b$kUcI-|8T)wmBlNLVR@7E*6R+#bAYr;oZ zh7&SHD6*%g;7|(a^E+>qtdCR2Y3GkcqTS2HO zDrs(lB{>#b9+o7PbnvjZX{t49nB3c&@tCnjqjypFz0e=buvI$njt+c`>f;vIz6di{ zgCl9em`6x-pYUweF%xvBPiB46+V2na^uS~i={X~@!NL8}5}AEO0;105=g!Li!Wj>$ zIVM>{jQVaC{hytnpC)D&)Rr;jtwB|v559IiCGv)3mOjdKN35iUeBv8OHxp^Olk4-M znznn&y3#W0$#ZElRdn4pZtT2Fuv*-Dq zJz-?VSUK#gnR!r=H=9+LX;fEYi+)B;iN8dv?SI)J(6sk4zf4G9>gulU?KzuPZu#q;P!Gf-3C^nf1F7xa@=m#L7`U-!}|;?o>>{ zsa$OS(>4Ex@!;Z6Q3_4Ycd%|wXE&ucuEZ)7uYj&H(Qt*VPA1o-2km%s5Q<=#_zx6Q>7?UIbv1jj8<$5j8$=R+4=j3oNy=eev;7AwG9)af-moaeYw6F_UD;NBOL8KIa?Oi=hI-~kB&g76G2MpYGK2z zOg;wrPS(ntjvtvtT-)6_q3~Pv@%XarWFyg0{S(=^R;#~k-P_S7cmYt@oF8~mw%|+! zJO3W`|5u2mPywxKGd}gX6jgy2OE=S+h(4R$q|a-nIQ(s?8L5UAt1vdICvwnN-x;`I znq^K`=ixgA3Jz$Y$W}1=K|-|1I#0FB!6^cLl&ID4O{&f3pf}+WWs`Zn(cr>c(Q9H4 zWhXmt6jQi%*C^_U)n-RY}EL&)i4g7=RxhPtPE}@PY5ey7 zXEcEOF7Sm|SIp{uQCNHnFFDPxb=aDR`@z(!6z15Pr)3wm5a!R0X-zltf&;1=Chy0v zkbk#c@9)RNbmm)k;x>GLma7OY14<#8$b7kD@-lUDH5_e22>}=Qo=$l28hidOB!uS4 z0vgJ~WF_*(pnnZe<-Yp`Z>K2yjy1fsJM)v;O>(dh>{U{A;6iD=19jBI`zC57p#w_& zFIQQ(wip6!fWIV#5V<>e&E_8RE%J5$-UoS`uSm`IpUco+uv9^h$s5pYbihyqJU{(N=ja&W zEfR(f7=}(_;!`^t8*@r4sC3})eHIi9Apm@-r5NZfaN8g`X2vg!dw*u1>9i>wcY4mT zf`9OKp$UDsK#v}W;^&g_QPqF&(5m@EGh)`y>PGu0Pveh4|9>9+fxy6{Bfiv1)Qv*7 zbq~pFUvb@oYQ)d%6(Y-%x!*P3x2&AbhECC~_Cob@zR6ir`$DntUn0nJU{m;ma3a^J znXgdocl@QdU1bqzmh#H*3g-=5uXHZ_012@K^uu7!2uG#3D7 zaoORAq|AQ`l~$SCH}A2XV?n9kAl0_tcLLd9(su5RGneZm6#P>WthPCR}Qi3Jwk}h6kYtOkHfkiofqVhNKp5Jf~X}D zQ4$ke79}`U@whYAR}m9m5uIjsRr7lC+JLWA4vnc}5?e#V#)}qEvaeXL_BoCi%`m_w zIz}{{$(D0G%2+lOW3?3O#$nATXJ}JNGLn3@_D3D0yRfSd3pSpdmaG}GUl%m1mgpvU z5IRN3e5j4)aIOdnoYxojn^CLGq^eni7=JGs1$nL%!zu)2ksN>>*6n{sXU!G~`z$9! z?nJxYm~Fv@BhZX03BHnF-}osPB~ULI*^sz}Q)vWm<${C6u=hbtT= zUBz$z*UsXPxR*8z8hd{Y5jncb<7iSn2Tnyh@uuEBl=!nR-56Hdxdg@bAK(td((}ML zIHzb$3h^|a10wu5jnOW4t_SwQYf`Q?x^ij=_<9S@fbKWQAM1Xj{z;sb#5mP5V6Y^#S?Oyr0%LIv#c0oKCiI^f_2~S_51s(QvMh zgoE%uZq3M4l7qh=a+fr3-PZIB9doV^9bVxJ?K)G*>UQn zqnYW+{rpe#f$pufvp1?<4Jd}wg-J*;>?lf9PergacVU7i@~K46CFO%$@aGEg}o{Q~bL3J0Pt zxM_dq7!x=RD**7vTz4Gb&d6|GeMYm~jN`$rMm`poKHgK|dWRMS*FQqxPw)KHt}Jop zLp_e03d44YrQA4Wa*7CPrYrb$-S;X0KnsHz=`7CchdP7V9pDz0Qd2~618Ck6*H=e@ zS^T;O8}@R{56cR+wI*0j?s9e-bUE{ZTEBlYPrb6`>Obq_A}Hu`R1y4`J7A>|Pb~h> zV>zyeoOYsq`TpQM3W|J2e$W`D0rK5njGz%jHT4+j07P>Wi*)>NE(_CB1%PzaE0O4!$O|FO+LGl zNl4w{i-H#>J#Qm~weP)N8MR}I8eXg`%b7BlS{`VAMSgf7dOa!ZznkX=IMXDbxt{>f zruMi-<)7j_ePH00vB!{{CzktU>(&(<+)y^=l90135E3(B*?%Y=OIlGW9@YUbCn35* zg(UauS05Nhaxw%Wa#e!3XWe2`VR)Zycl;WCYm+#QMow^0ko(tR${;(65O5@@{rmlj{CDkiIXFJAmmlU|Kv2;JX=|4Iw1I_?i z#wo*0pTdCxf1l3gr#_>6fTADptbRjA?>F+dNUv9{#SX)WUP7YpRVBlsgSk8P&(;-u zT|uJ;HcRVs&(2@@Mg}V0z9Jg`HKR?0(D3>HUTfh}Qyr1~=JyP2yP447Pr4}<&un#{ z|B-gvs3DlQ(tF8|&*uaoS`!Vb5D$?6k>8)JGf5@jg2(7SBW@$NocJdJCrZsiS?!e0 z-#MkuVrX7?M&JNF*WnWT$>0v8v;k7F$WRiCE7fI+7OGG=GlM1Nr$_7owl$LkaZl z@AqK`80H>20nOEqGZ)ezaNfokd*8u3qEFb^2X|&k3T*9Ih#9sw3lzBw5E%tidgG01 zqDhY(qTU!Rflq0+L=m?8Z7-zjnVom|9;b(g)pZGx3LBD1%`KIGpeGI#spa4CrNwWN zUY0fz7$KS_r{)nL^<*DmA1Vm(m+%Intt0M%U zNJ!4?ZhU(9Ti0!b5p2aK{_-{4Elr*}SCJ@U@v0p>;g_GSO4Zp9`G&UZJ6=YG`ot6%6nXGjjOOEdEs=|wm#b0dt7 zVWJ#(n;B`;YCRXNT4JD;5_=sx;?V_Zf}EQC4zGFt$88CkXSSakfP_sZ)<6+SFHK*n zUS~umioT&@mQOBqpHuhi6JNwJ`o+d-M#CR4?!NDSI_b)?+>TU)CjKM#d9S}3Q7sx| zh@EnS%=>o1t;uT#S8VwK=>(M5x8YN0wnsvIK%BppKcF;>%OmGR_VZEDeK=~ya7sWb zdrLHNd$NN!7|HK&#r?vXv44VG;#aC$wV(HY0QUbL?9qWqdRO5|5ETc&8-MRWlz}_5 z?@!JILAk9%@@2A1nA5#2z_rK8UFV8{f>=~S!+KyK;GjF0I+PgFE#Qv%=kj%f;`Ug< zyWXY6sFpEj`5Y5VnaUD4gGTBNt1fCl@EH8KJ&j0wEqOM|PHaJGVS z)m|?Lf*&p%cGV`)n7W{GC^%=s6Y1*zZB70-qC*jcnmoK=C|qMn6weZ#{o(qZlf#yV z(Ut3nkAfpxlPV{>Jv_{yh`JJPD{{^1VufWry8ZGS67MS}q!JGRAt7m^|Ik>5*7B&_ zlpF75$!mkz2Y(O~Zuyh3_r>Wn+l5Hss7cf;lFMgx2$PST#BCEwI1W@a9Sh$U2!?EaWe-X@2nR1Gp@k={N5} zkx$r#MU7;oZt0_-M*XGy(G&D!tTgJ)k@~TA{2}b~x_#IOU{*S@;Ng(17rgQ=cFtPe z=UZSK%nT+AGpx)2gSt8%>;1!|`ls7KfH;8!Lhue?&Mg>qzvg`+xZ#}r9ofLr@Y1JG zqSdQMxOC-L(EUfp)79YeNB_xA!`gNWpR+qwc?Ji&>z!&{cl;`mP!oQjWDv1YfOD48 zR1q@-PROA*aNo~ha$6KC#u_3;9P%?N<2#7=oQJKYlT825KQj~!5tj1jM=o`}*;qZh z_{KT;cKz0&@SxGe4Z=+{bg^kYuaI*10%E4T5z9>cz92uu{e@dr>*{%aG?FV*zpMY_EZ$>|qGs8OSYHoMXn-OO z<#TiY;eCmM5>4Y-L}tkU-Z7EgA&FXp1Z~{>8Yu1w$`;dPyC>1>S1moZyDQA$O{mFH z;)Dlm*S04ca}G3l9Z5qy*Fxv)E|1@xcW*6fB;S3j-Q$(65$OVTr-zVPq(<5t59kM0 zfP&b1UbG`aROy_5)3aA6A+fS2bbJq%+K=t%w0623F3k}{E#&sZm2tx+bEHqM^p{&mgd zk@?(&I03ae@xRdKa@m)9MoW1l%R zA>GdRH$iRzG^1s^ulDn;B@`Jx*qdmh6;`9?kFG9|xSuyU*daTuJ~q`$W8p)Yu21p_ zj>qN<6aBV&)iz~}K^D!uX^GC&G$>*@M_9Y>w!FlLcVNPpQT&c#eGOMr0Y&uj=i;1SiHZXc`9u*}0bmS7W04rmdI#ox(6TR(-d;2|mRYi@ z(71ft508h9i0A-zp564wJ2x&3Le6!BRqb>yv5e=IaBF2ASX`mJ(|#;AEmF*OVm zx&R?`aW)-W2Ea2|?rpOy_ggk=1*gPo_KRot@(-qvatI|Fw{enCqB;cV^6Q~H=n*AsjWfP zPLG3*=66RAJZO=W=p6=w0bg$)`K&>u^u30254TXPMP(myC`k{sTw64zipLZI zw|Yc7iu26el^t@(A*ZJW*`dl>LVPZN_wgH; zY>&})V-N(9qAH_z&7R5QG5>nM*aXhZ*JAz)X=hof$S}pFPu$A?&)%EISyol&-`}^GB)PaHL+#h-w9`*vYzR*F?EKEx&w0Gm+~S7=9>6@p%Yl02 z5B5S2S_SLhIG^~rHz4%$yFq(>{%nHB1q87B`|tNPq0s&Q2io!XoFBc`zn(iHy#k*3 z&WAZp;QeHcaSp>Et3@{oZ8R5N^i0mb=$WkFxUQLQm|Baqwxh+CZxcJKluI0c(s7)A z=4pt6d+yxaY$c}VJSTef>7MWS?R|hC`TVoubD~pWf_`rJ$&IYpu!2(_do08?6t+1L zGQ!CtIGh*`J?c2V_=RsUT#fO%lwAn%bC?wi`vC0z+ze!cmwcyL^5gRvS+Hwllx52T zp8WVF=;WcczUnf!>FqUm!IUiJ!PR`@TMyz`h4-elODw)cFd#WMlDDU8ew@sBd1bnF zMP^=4R32z)0ns`?Btg)5yYrj?@TTo3+~f$@&wT&QX1H7q7V=TZf&7Xb8pK8o%bM6VP^cCmKF2!Hd&vsg25 z4*|o7?Bz;t4A!saxA(2)t#AJwd&XDeWO+^+`N@P`kLTB1pU3lkeg`_l=oFjm;BD`E zIY%89P+}ZW4#A_uBjgz@gjhYuNNs@2FMS=i+&+v6mw*der_t!{ut<;1#gDgI_lpJU z^$1fAs7DdjInH|W#FFBhIs^6~&*b!gRSu^eLW+1t7>1PlR`Q0|U&k+Q*?|iySVdcNZU>U$R0>jO z{rZ55FMA^O@nKLIka0q`8>$W*c3YgvcH7&)nWsV!9>SMCe-k_Qlu7IWS<+elOL^mf zX}vLsUV|7=F==fJXFh%nXP&kOiFczNtquhH^zqhE9M8OQ(5cIId2On)o#?zPfwBf= zP4jQPqp1Ij(m+VG!s^ET(s^$#{{5qxD)bFy`XC6X*B|8B7aNW|au1?L z=NH|HqL%japZ~d)yY2!yMTiuj$_K@63#jH?B2s!PdStI8`p8&`#;Wo_in{| zw_jTPEy{RF8!!@xqJ&bZM9dKX&&%G%cfR>f1|`8$M~ThBvud`n(t*K* zHrZmu-ri;{6{FbUt;Pv+YM0o$zPX?via1{U{T=5U*)a|!S(%U$hx5C^PEZPFTep|^m(TtNRo)2nb(F*4RWrNlKr}IiD2iFWd}%gLzLUDkZViL? zB~pfX>9ud-d*8nUGq4KhLmZB&iuja2n9BfKR;$%H<&4L0)wM5VV4#mZdxiO_G9;yyhLOO+kaVs7EoTRLSe&*+<))u8q=BC{jos z9V(JE#*{)58=`{^yypLXAK&}ltpt6`uxMhJ zV7lgUyJTcL!L?9iz+Be<*;w~9474J#DMo3kW8*ya{Ij|In#-|i%0#tBxm?a$$_9Av zI!-LR7M%-~2?K1JVzj|Ji}#Mvkx|Zm#*^55&%NAz`(0G}%cN;eh;?O+9B4$mnx`f} zVTBdezWyHI8UV~!13>NHn)QRN=D*kH9qbwalL37X5tc=+(LdNU0G6y+$_rj}Io5fU zR$1+;2wLeJ@JsW$HoP_V3au1IYm^A0RmU6<5zctRY3$s-gS&3Ohf1ZC)tFC(3uhUy zF0=-KpTqoZ2}t_3^irPQ|+36^okT_gPQAN?J- z-L(@52XIy}K2It4I%_@GGtGBl1Eij#X?;g0KkGb|O2GKY80*#!aQZ3z7_}4c(puV#^Lh#9hTyV|+r=2ngb~hrk=>m|Cp>%?Oq{6m`2l?oyAI7SUIPcmu%W`2$ z+Eo1a=5BMD2pm~GxKRnw`2D=#s>k!$e}65{JogYbuJ)9I9RzAOu#5gu%$ijt9)H$R zTzdKW4D=81(;xnva%B*dX;wWnQu2lQ)r{W9d3#yA*|PYjx=IYz>)Vy_(zeeWBwYB_16phQ}xP{&#=O zDW@FIrXvmm6`)K%N)J)9L%jTD|G*bMcLOe1Nh)EkuOg^CTdMo{?&laM94PUN5`{;Y z7@y#Ri_hb#KX?HX6IF}}D1|{&XU8|%sQRvBQJt?TRNna^SJV)N$D>5CcHJ6&dDE|O zF3S*^DpREAeAvet0E_!{TZFi2zVoZl064HO=s~UlP!IqG0Z=|#@8iJ-A0kj1Pqy=~ zx&ns`J073}fVifrym!)Z#EPu0)p+RZALJJ|{g%7$c#y-7K8clw9>LvPNBQU{uICl6 zcrTkD8Y3K9hIoxjQvwXeWuPoejGj9IFdyNwUZdH4zv(dJ1mf7RYui0saKRdu4pkAG zF7(x|8FN@qG2W zZ{nP@R}z>9Asxl}8u%*WVnkVx6x;+Q6LH}MXL84_cW~?NJ3tL|%$uf=f=pe;&>`|S zqljLnaq3*2`krj#X-_V5=HpgilO1SDX172|=k@hYA-az(TT6WMUw7exjW}IoCP`jscLBz%xijZFqnb9g+( zem$S~*Kxl5pAQjM){~?PG1;g)Xge@AIxBhDI>*Jv-P0B90^ZKS!76Y)6-po~Hgq9|tdnpNC$*WK*cz6)baK>!p4 zKtTXJ$_Ri$1E8?N3Y&iU&)fGk>@kmh3^uWp$|XbzTDpM#6ajojF4xE9qcB0&Zc z^gIF>38=;nyT}oqbDpu0QG&p-YE{6S-*qhkTX6ni3<*lgn2I55h$KLYF^%SwR5`W zLaBcPi3@?W6BIq7D)bcITb_UArCj{{XEQuHiqeX*DUmuiYqCMKfbArDl?WJv_YNIs z%BqCYic?N|4ENr%CGQAOlsgty*rUmMJ{?5VjNYuzoW`}tj6QEdMrQVzM5br+XcQ$K zzRxd^NQWP6)}9JJl^^KGi&r!IyeIHH-@i>IdrDuBG0AF3moHz&nzgG*ZHhJqr8Hh? zP#P}^FAB6EM&VVU zs&ab)i^8HvETmo&^sQn~EnvrJg_sqTht}eygd#){k~&De5PM6Vgv4h)c|D$LiC1}W zDS1Xj+X|SsW}Mc$&(C%GRG`1%0N!e%iImiaRQfmYu}|E^h&`5S5@rHbQ|)+mc9Ltm zSAaCl=XFZxhPmw7hjaMq8s1e&wZUnP7mZgzF8-zvm$kz4&GLzMcOBlX?%(YikGBa% zgONI+-oe}7{Zclrw$#SA6P8M(J|J|=BDYuA?@c1?A|#F-XId}9r({R#T>#!J2cT=(Gtj9{Q-jh0q6R6hftXa1j5g`nNMTw`hpV<$% zc<%s<-{*UL^Z$F(;d4b$-RHlr@ytTwk8V8J%lG^F^;o}(Hh_f{R#?wP&wEc$3JFVL zGuUtLEVL~~N@-NS+i$eOXf=fvfS|NN=Vb#*XLX9oXhdb-H~!|efBR|}`$#F{F*`E1Le^&&asCjJn75HFE_vQ#F?xc&ifWO14RUVE z({hMJh>Ua1AD+kh4I$1YXdO->2NyO4R1w#+7JhpjeCezUfX6UVOQ$C_7ij#0(11#T z2`;(pDTF%8yDUsMK^mW(trcNtz>l+To$!iRU4f$Bye|7fB|eEPP=)kfu6*HfY*^zm z{RJD({@Qjn79f>DhT~0q@-w?g)CwG7bG$QBPPLH$2e#RKjT5IJ6|57i)1?%ZQ zNRlejl%`N!C!jvKp97^ZMClCtKmYeG<#|^=m)gVx{r&xj(kR`!hB|+~Ho%2eD*Ikk z_Ih_Fo}LVHRM)e(Z=esQG)a<@#wmprR@kG*3Id?8!U~(20LTS``ui(QrGF9C1n`aD zX=gnfz`wobzXAS@39{fNe_!uBF17eP&9VLWhn5ZTe_!_smakqx6vz84aDOD5_pKfA z#kGuO7E**1?<>Tzf=_(>=frwBQmV|Wcej!%O9mg{ndh&;$QZb!395v}VYeG4G2VLO zxXL*foQC(KU=xf|O=ex_%qW=BbH zQ@qW=!fg1WrWQdCL=9LxUImPd)@Cu++>GAKqFO={N331j&xwyY6h)eY|HhWMn>^>H zII-A{^SCFUl<6?+!|=*!gLUIUa(5TXSzWl}ex#wOGmZ6nu zwS~)f?Wg0P+V6c|D5&pui<5+?9`V8#UCt%XyNL0z32a;DjMAoq1TacXA_25+5dqyK zK=yh|H$ar?AOZUN`YOU0-&(M3M;HVaI~IMxzr5WX9_g*6u`edpqWeR zRlOduX2V+E{NBH1*{Wrv?7Q8&!shvK@gQv=*kK6!H}bfr=6W~T$ zoiBRHUrhQPLuC8dH8#L!Kl>|E*5XZnbAF_0y69zf2X@BM!DmrD;)T~-&hxH(E@_&g zji#@!59?gs#lXO$x{q`a5SB^=CPYu;#)KFhV03^onFo~; zwK!tc+Eu*u{coXvXn-h95Iwnmw~?ByluYT(uzZG^I% zzo0%Do6cP0-(4fz{rl6sf1~7~o(SVs^WhKOg42&7wtbVq&a5>79pb927)*H1g(sk* z`%yN@wIDj%OKLKoZw;X8_*+kXd<^gE;A6xkxw1p%(f~*~;K!z!I|t4oM3mGRhizDXur@nyd-Ydc+5W)F&XW=QZE69ryOv>tMZL%m8D<5qQaWoEJzF zrOR}n!9|>Q`X;&7 zlq;_|fs(%qqeeS*E+$Kk)zjbSxrWC=OT_GJ zLcluT;oa+Vul2gudcW8fXoItkFbJrQSGnw}OS$~_pU+rz0(4dv-Kf{srLgVVn!Hp_ zI!OT0h|+-S)CQQ21Zd0J3ris*!=pH7@!k{1y}N?!>l*%jeSHpejjH1O3M&YJ!U`*F z-w^=LqXW&z$S7%&V6?$#-3&;&!GAlD6j9B3|GjzOxI7&)NmEv=S;<@9|7QA!28h!H zlu+zJ9k}K?a7?Zl5g5Yq8h&&GeD4Q)2r4V{>)eb`X44z9`JT!p_-F?gp1+=T%M!F2 z%O~x=sev9J9Ybrdt`6F_b@0o|0Ps`hVT1RGOYm-l_3H*0>{Dp5ourn`A6$9wnq%Qb zQIElUJ$tb3yFjYS+jlz6MAa<{=x=?r%C#I~jyZNUup5Xv$F!~IeJ7&LUNfR`9*imx zM-FRg9k2?5WuWe(9r{Y~quje1M?bk5>JF+9$wh zZwb%BIxTgMQW!EmUggSbuHfpIT-nqW@z&$D>Huo(Wp<71xpoquvE$!(t*H&rqa2{2 z0aB}0v56(nMMJQ{3M&YJ!U`*_u-vrLc<(5cLWcK@ux0aBDwT3BUU@D?hSGS>SHj1-sYz%_Ar)dFeWCK$7fJrfl^^XXrcx&bgc zBLJqFBQnKT>HGy_#yD!z2p2!QAFYNn=qK$v(lgcK&+qa1yIbx?yg`$-o|9aSUK*d< zzTMhr9U(1k1YE@W4a-@(ZYj>iw5PPUGz8{B!sJAZGy{G}Jz1v?zgMS>R)!>n@relE z%&h4I;O#<{^8!q%MU^xsoU{>`Kz!0h0J!PSxDz;Ai%{a;J0HR(nKHg$P--DT&t$?$ z%Q(vhYy80EFV&epyY3W|bRPH-<7utT7`XRpc%{~0;R}ktIh)v<3Tm23Wal%D= zo{BIsJi^s~@ItQs!z-!RBcdoKO;VK6InZx?Ra)&G36Pfqbc^&#uD;$7+P(AkyAUl! z{eNMF6$C(Gg%wuV6gW(TG)ej0Ew|G*(2vVY0cHaK8G)eoI@PQ_jaz9=7>10EPq5+e zLwLhG{)$p5$drzA5@5Pe*g21LkIdaFh3jg^HM!Yo=Wboz-q2;fugv9SK-P|~L{Qnp z_kTRhFK!;eo7Fg_VY(8X>86iB5_^=NprY>Md6ynT$ZoV$bMU;_k2k}wZrVbU41f;s zNeze*8-b0%$KcbJ=V|@zZ6`%61O1wH>xZz(XqQM`Z?H6T3U=%7z4zc1^#oEM;Ftvg z(A{r6cToY(1x!>MpV9hU@~U6G001BWNkl>dxB^BjDbgI=s7+y!3(dsf-7Hq z1ry`dOihgoTW~h-yI-M{2 z`H7pK_dG#0voDZEQB0{^;>Mre#Q4}a14I3U#t>-T1^zS8AxaYjWkhuAHPL8eFh)=5 zT4z7+aHR;fV05;_s>M^URXOpoC-Alpya~~YG_RRVyzRKJyMg~yb%fm;a9X6pXCKf` z{qtVGKWRP7>%$v9)88vxE7cr2I#{c`trn@|qHPN6Jx-;FvS{VeN+#9tOAnu|7|w!}yNg_I$D2h;c z9{bqCv33uNybjkJ9A-*VY$osQ0N|P;fZnI5a+#3`_S7v%n4e2aqF}}C22I)c9049Bm@1D=0rf8z25FVL8Gzs z@q-`Viqhp~y@|Je(x&^~diR>|Z5>w|=js=1Vn8;dspjLC&Z+o5u7mTMNUh*UKYNH@ z|JEbnYGP}s#nI$=r?JL$-G|d@ba-bw-1$z&wR9=0muCO^Zt$-se{n(K{pkI%lY`R^4E?`utf z#Xg^&(_3}A*V5tp8y^(bW4yYwuqJ8D=}zy)zw-w`GvkB(eg%D@Ls3{^2QGoq6_oGU zy@zjq<9aH6{nVpq+C|UkphLNMp~#gW7e*Q%8RMuEj^>T;dIM-7cImWrj0bD5Fq_3) zPebx_1n9OETsPXnH3I(qWe!E+m-5Z;z|FS>Bz|c#_?^S!6>KIcvA#GHVc!;Ehp`-bP_y`*B|NeUqgtn zUaRx0=U%`||Li4krErIE58f*sQHoF7eebew};o-ptU@(4vF?EM>gq-g}f*RQfB74v%um8IR#T z*S&+V6jH6#A0o|dP6!l}>hKp-0zCj%=DXjz zfw6iDdA5smkTO&6{5L*doWrK$oPNd;EGd%#-JHN#h%3_#u#yKyGYhjVS!ZZ z+dfYmN;JmQ(QZ2zJ?D7(2O6awzT;45%WruBRbI=_Zt#5ndmyqJn>t*QE~;=__ z2m(Y2pZdsWrv&!x;D0g7Dzs7rK|q>X%6(;`dc;x39>w4P)7uzW(vN)L^u3)9^alrL#zj-$%z!2^Ai~SHF56x8IpyWhItyD$F?zO3BYX2i#hX zv#k6eS3G|Wp$wyJG9}PoI2Ch+(tX@^+crk_q*ymi*-ZoY=jr0DlzoRwV{B4o!^RS8 z*8vjeed^N%9qDWw6iEcus(c4|c7VTwzwjWEX#m6#GnN_2UJP%0IY5>TF~U(ttw2dU zGnpdowf`-*F2P~A>887ZeuPq1y0RdeAl;u0X_{i=5soqiw19jvxqTcM0Z@1u%&OkWLa{V6dO9_ukL9 zzWQw{{e4A1Q&?dI0dVlzq?(Gt9^DgqKWiu)?8n#3taVf>W$yU(t$gUc*RgErQoOMCF92KhBk&E3@|!C>o9W@cGUX^#t<0Y;X3Hl1ZV&_C4#dK5h1Ea9DBks zy!*p%$Ef^sjX{S&v#0X3-VH#ydr8l+=6@~#XKL?NGu@d&?z}~Gb}l+lASqF8glP8` zRJw!E>|tPFj8d?NX#7E2`M!CRg6Tz6gl>DxUQ1H@aj5?nmS(n^)bopA_peK-r;wMLBe6k3}}!=zdE;q$rd zgfxY*;RqqfIxebK=S2~^cGDXl()NENcob=*sg6OQL((YDgg`ybdC&lU^U)8A>*EYn z6h|Gs0v9&~5*wuzy~oz!K?24i_{k4`gGfIXhZhGvMR0SQuTwbK-19CMsl&&^Jm;bz z)~|d3#V$l8+0UAq46`Id2&lOg{OYzU*MGl;s%*r%uz52$>vIhU*Ys)UN$!*H*KSRK zoE)(Y&$K(7w^%HxPtmzzd{i4_V*Eks6I*f71BB`!`br~+-Hwg6W9%3u@q`|1Jwa)~ z`!x4Vgftt+wCT>-4qxcDgWZ}Z3;uj`4y84TwTz8VaPCFt@Rx6XHT7CO%Z@XGC{GY- zytf!*CIS71CQcBT);-=3?$cdeb(}^pO|2axf+4#XbuIuaA~@$zN)c7-eDa?@g;6Ri zN!bg)Ftb9+BP)LDUSAhlGiv{9ejVHzUyn4~*Lb#Q{#jUIh0W=KVXb3u#WH?+{g3$6 z@)i94AO0RAdm@6iV5Sj_Hd_y=#d*MZ?@>xquhlu~*rWJ|>)yeu{^T#27@cSaQ;%$O zI9taiibN#n|&R75A`)uC4jU_7& zrC!%lip}l2`K0MzvWhQ%We3+>TjIpSRv_sftecXb>H1^W0dcf*IJ=A0%Matc^NwKi zr?!IEAf2TuEn!H{{GPE2WdfpF!1u1dnHOGlCd!XwLV!~$;Wt{lwRL0=kJbrVjd11@ zkK{97+)AjyE5RpoFVUZRd|D@IV@M)FsbF^b=R#xwhrPlA)$rR8*K-9 zij(&jh>!3Rw?dXlYl&p1nKLF{9NuXxnui`7=8n4_M2DNeD?BN(VDLrf1OOlj z=!6wZ;lc}#Ae60$8VAo_fmi1S(3KPT=tsUs)q)9^;1ZjysmP)U8FvG|?9wZQlu}@E zb{J!;Jnq!>oPWtvIQr<-Y&>KYN<$b_xbL3r{Pg=b^RpZ7h5s;N6)M|CgK}fw; z=cH3lH$u@Ys0!(0Ng*AaLMII@x`y)$*$@MqK2AtCUp()xG=!ODKMo$ ze)+%m=HuAV9Z07_3tgxhoQ(;B5NG#r{PAlER2HzOiS2^!dk;!l=c-6kh-wb4f~-Tt zTq%acqqL?vVX4?uYmnnl7=W=IsPd#sJ~PqMHiXtM<;ELB zzWp^Q2WuFcsG`GC-u`fb0Dk^HE-w*j0CU%9+36f8h4Ts}ju=>B|LMQD{`$?B!HuBHI6ymQbq_Xl8XCfl zjk$fG?pB5-pjxeR&iPN}^>29%W7R4)b%dob+lh}SXMsrGaV8_PJLjX;iZsngAC$s7 z*FgecvR(dm62N=kBmuNmSnDX4OT6zL|Hz%U-_6oxONf&Mn_9Ffc6|#g?9pn420&qj z6*gEtbw6cp=z4~Il^tHDUsvT$82POG9j~eO$%0+5xuJmU|4b8{$VlxeYHR;?ypJ>U`_C zHFb>Cxaz9Y2-00>_O?b}uu`J#SMl+W-o#k7jK~r~6YQ0)!t6sp*Fg+4hRV2<&{Zk1 zgKMsSJYV|qYkAs}ms3%j>C^X9W*cR{gF(HU&~7Ji51^BK(dk`Wa?xRY;oo1&KYrxp zEU%29nLzurDNg!Gv(a6BztQFv>?Ig&J?f2g0YeBnR1?gJmEGEgQc8Nxsl7=^;;X`b26MW();weu43 zdA+wFT4j~ALbGmPy3+SmTVT^vKz1oUO|u$i?XfmxY;2U{PCkbBe(GIlrAbpeWesVw zHoDFC2E=!)SI+hD&iY$%o!(ue+k9^_541w8MT^JRcJi9PIFB!U>Hl!bNhNBdo579| zmKCNXh_)D=_i4xBv1nx}sX98|&Y7nU@tq&OlVwA@uyz<_8o|Gsb)9y54|(r;&*ygS zaJ<6k0kp4RU4>m^@X=5If)RfNrnDMwL7Yr4*x%j6J%3H5qg0~b^1|mHL8$9ElC0j< z3EoNbv!PMbZ)fnCMZRoOC$Www3HkQ*zd@T))|L*Jfo11A>e4e$@ixi47G=rYuEvrf z%@K#KA*zlNhNY}`x$h=Trgux{9QTdSmsYojx`i0M*Fe}gNJUX|0W}9!sZ4{Sds&-h z&2~1AT7A_epL?MRi0vq6J#ho2(oUkl3+Ezy*uHX z|FsimHc(GAeyRiQdfv3x)6eL+W`@sb(f%DhbW*&i$~*8K8rWPQi(K;2||rcV&3!C_wdUbe#y|_ zAZgzERcVFyuK8SV_~iSzQ}e=hd13q4yp|5{ekLsXCWe>}pDO}8&@>R{jDO~cAGLF^dd^1xvqJsPaVI{_cy)Bs=l za>SNx%dlz*epcJlU3~!q4ZcR`cX0L-*K_iTWo)#Apd@*EtwQ-m*M#|=qeGjlo?g@v ze(>E}5FLUNyp6N+aN2gKo4eHQyVZG@bQdGU9Dc+apoURufp&Y_K$K(~`BCKdI=D~@ z@S2IbC-y85NjX_f-=-;pMsv(D%fXLev?I%LZZJ*x@B8+S0BP$zB*kI4?RUGl?erSo-lS(r#_X^;2Es zrlY?k1nDr>Ui(De_}9;+qP7ysP7L*Iyt|3&u8s`K*60taB;$8->PZzo_No8Nx|J4D z6QnlgU^gX#(T1_nF)q0D0$%esucRJDhzQ06(~?FFB0$v4z^A#p-`(m@b!N-70Es4! zB9<;+%Jv7h^Nu&Yo4an`Or^hqJy@T4g%wuV;;diA5ENEeVS8(dO-Ygj?;YhzneTq{ zzxlvB{}JaLVGuThr1pTO9sC=ko579BgQ0HluVgaInq{lxnMztR7a<4?Nt~dRqFSwS z%<)I_p-;b`p(O)or4BmqugErUvkGTL99iOMoTr?9IB$B#OX%x^u)hL2KkZuP_@u+=50%SXQ3F707m{3DJv;+S6lebcrB}h4#9$z+r zN5iEB%C*Nb!9XSA1 zE~`J!$q|J^2_#XS^@ptC$fFK{B*GZ*X`3pdw|R*~w)3B5Sok&~AOn}by?HBcTw}ui zjA&X|_>T${q7$qg=BOitOD{c3%`&SCTlZn~+&H@*heb(S&B zyY=5xfSp#>59093ktpygr8@Ee7o2+xuY29a_~Z_>n#k(|Qj~Yu?sMJ&qGRW}A@Uc7 zU^L~v5^1!ZC!RUT-@orgNU{qdyWVq|%dPGU2Zn{AVPay8b1yiXH@xjNIA^go&350X zAUGN=a$En;7HzE*N-4C_O&3wkXJL0szXl1=C;^bX)Xv(Jfx$t3cgyX(`>pS1+t!B| z80f=0PZAycgiB$C6;==cg%wuV0br#(jj_=}u2e4Zy>ES=cfa{P)N1ufyBiJgKV8aH z2ly9<5ke|!>c%q&~=&D?fsA=^^*gj{W7rNvVYVb#)r z*Zk#`EFG+p)P}(&;8W7nb_V)A;hduqIL3Ew;g4T-K389THbzFXz}zps#<&)bPm?Td zSS(OM%tpTS<$JmBK1Iq3ybF710ptWg<9w|H1J-(y`ZmrxcMT^V37RTN5;xCVdDjzs z^u9I*W6FpGSXW|X*s^)^Fo`Q6TH}%$5Vv^(BzT|D=u_`~f_R(n$VVJ^!g4ebTGF0# zZ+7#qbB9-GWB^F3?cWr@w4+0SqeZYtMpx7$i%VMXKNTpQ4N%QUpcLn0T%O|XeTt>d zs#Sf6PQcZ2(k9OWXgD#gYzn};rZ&F!9=zcP-}yB#giu-p;W$McC)jiXi67+ROO9pD zvN~yfH)g=VUT94wK7eo?ANkM??5b(3AJ|VTyF+AxBQ`qT@HfwAz-*_a$Fs3kZ5bHN z2{O%zrXH7iY+S?WDlXc_3objI-+R&1Nb1|0M9uzMU*~JXJ9qKSXP?6x-|<%@d6t>h zS=-ybf&Qu2jjm^3uhil8EP!qjp!@sfD^_shPj2Mz-~1lN$HpN;J8X&b69T)$CN{o)UtHtQm*^LhX{gz zG_h!v^;33=rgzs+PZyYWzSi9rd}hGb?bFqmH*4a2Ltso$Vo9StJmBcz7^*m=_5gqPwre@|s3l0e8>7=KwRxV_>-jI`e^GR6^jdS(j9AOEx+RdS>wsXa0tB9&Qpz&Eg#S?e* zB;Q+4tiyVZ^$KKw?_Yl_VwU9uRNAWlcQ#8`cNWQO*)vb_*FX_-?6E@}vH@!KQEK&x zo`ApmXC4Kjn=WWXT8>x5b(ilL=3|<+x$Sb-{drji!1!1~Y%`)sBOQ7c?Yq>yw0#qg+o_^s|dF`8Dg>@;;rD&6Bt~i%qZA=hO&f-y8W%@OFS)9@`WtNuGjop1U zWjkFFpOf=QB7?$sk1$6{tr3;v!83oA9;9Ufi<#< z>v=$5-vR*^R#;&RX4ZS+Bw@+YA%1uB@A%9A^9Js}?|xRUTuzXuJ9_Ur$fsu7f4dIA z9BTfj4UQ!%6#$6hm`#Tt!bd;ucbhHk+mMuWi-pl53jmQSq1?{}=WSyBN(fc0)pxw3gkYWo zN8{j3i68vnHbk$^x&bIpW7ogiz0X|ol=V!wE@Egf;jm31aWqCyD)$6@vpdKZA>`2{ zN&ebYQ)a3R0N)~_vf)$2b&oLr0L^=k^&aa5=M`z`Tj|hgn_bRV*E~CeDzsec=0jB_06E_5SY$C zZB8J|l;VAj_gAfqo6=B)q`sR&*L$9S`C0gQqPgBBX|i|odmme90+J-*f{UNY8{hSY zran!jQf|&^XFK(7=5{?giS7=9yNd*fpt*tj4@OyS8C@ZZ`IX@URt^!LtA z6$;(f0%<09E@`UUM^Vht#~#T?|Mi~;OW`D8zyr}z??|n~Ck{z#oPPQtoO1GNY&@0; zvNZru!yD-g9ZS5LktFf?UQtNI6CS&izj@1*r0ED@r92~eUyzb&X|!g`kI^NVU@7UFZjhy*RYm}aPqx!hylH;(^7L-Kb4UWv`3tk&_)1n+I z(ERTW5Ae`KDcU4-Ec8LXM!`UJML znoobibq=Jf001BWNklS{9T3-$*UVFIQZvf>e>@0WN#)Y4laVrmkl@ybh4umBp|xtrQwY z$3}Sa)6e7$Z+%U3mr^TDy&mN)M_t}|CCdiTnpQ@eYVB_7woCw}bW;aFDVkXox~<-S zDiT1Hr&I~}(0f0^XRiBaO669mOJ~aeH0w}dg%ws10EHD+SYh+E#HJar_h93Mp}_%m zY}>(`UioHj`ti>ROqTZVeO61_z4O1&X=lzkv@w*+0i}{*WO$fE4?l!Y{oBWyLE3D9 z-oD(8>PSJgS(`eKMSR4wE^OOfoMA-~;>Zt)weBv7FW1Cww?c(NG zT3|@*Dz5v;FBpqJ)KZk1x#VLZ)+Fy8mR2plb)&fWAzuHc7qD@C0(KmuAhj`Ck*!0t z*Zy~b^GPIEPTr{Os|hSYkmA#woN~$rPCof?w6Z9rDTkGO%%*Q6w!bq3Mn*@YM+nS)s#KxQN@4@xF|G`{y?sK2D*WUZH9AQ5wLZ9TP#A8`zcgFm7UIKeRjvAWG z|E35Wh$QoARes=peHmc6z-PZSZN3tpF(a`MaHP4$$*e(WMU8RvwknzT%2r<;)kr_L zezaZ(v(Ca2WZEq8thh7AsgPRll-PR`>nQkwH?z`dhk#6SqzVBo34R|QLN>gFxqbcO zb+hpK%o-;5@{3Cpe*mB>7}qld$T=;!#ju%}nSs(lwoLgpEF<56fi{LNMe4^>__|I5 z_z}SoeM7R|dO+d?m3J}V+R*%2z}eme_Hh9!`;*qHMp`@+&R*!w@U^@`f-<=581y(D zl!#1f^XBGLSFf!MFUuevP=FeArNrx-KT=V8l0sfpp|c@SXpqg2uRqlRqhp_2F|Tix z=0Maph*=UwZNCLm`1GZD?xNLG1Lpxwk-Ik^52@1=UYCt$Z$9Xt!>U9rfs4OM%a6Mf zUx6t(90Qy7E1%O=AyVoupG{ud*D2~2+n)t))zw2&n3t0*ApjHj(5TmXot5^^vzkWB zk!Dexq3RkyJ-(L)RysDWvM7{F8fWBv`Un`{o3c8dv9x%Wx0{z1576hc^6c`=d0~^k zR7BXH#PWC>MT2GuGe|g1@i#eY6zz4ck>f}fhUwOu-)zc#JwFmB1n0VbwKFsdjR6W%ex10RLz=zSjwsUjz{i>Sc%K)?#_~XeU=oAW))9A%i1kmo z3JV4&ZgWhIY*xvq9I}4e9)z|rj7q|FW(C zk+GP7=kNg}nuRgZI$^&-HL5`5YIAOpLH8W5I9h-yeZ?1F}t_#o*If7YfOi zMc>9Ii=wY8O`=$s_Ei#j32+sQUA~*ZnXe*oiRdsBKk~9Id)NoBHDk7Y;k2MAQDiX2 zLlUXr69e{3pAwaNQ4eV2LQDt9n7*-`Hr0Oo`!mb!98D>qDXmrH)SY%sweWlaKQOUq z4JVLT0#dsR5GeCm$=#V)()x(oOExK?a-3x3f*~{;ws`43dG7mMTk(5*@z4V|4%k}q zFp{GgT`gShTLnHg@Z;Cu+`hsZfECdQp8kbY>&_!LPa9N=dF*>f>b>3BvGhJ7lfk&8 zEXwwToJSwyMDhOrgkcj@ioVn&MD6pycAkpdPh3so=|c&FnpDY(!vNu zhJffS@NaOg<-70Hn9#LuqZyi(%l9TI(~gL5WCsIz>Z0r`qg zRo##9(sZF59OmVZY}2FvR#hvRWd8au9O@_lP8ykgTIEU@bCnV9k5<;t=b|N&$RhUT z=tb)|p1WD{ID09XC^wV=O9IMacV#-+Jf8)0Q!KAd*1!3BW@$_mzlp5mJGbBaD>Um& z&7?dm38e&}3f>M|q}$^|ZT5Fsi~*CkzZ4OMKSaN1yWD6p!GFM_c03%IykIuLORx$m zswV9ZqQoH;{Z`zPP7L$%hn0mR=fNC?7EXNYr`@(K#KnTY}gP1CPvx^}fK-rIxgMj5|_ZyX$TY4!60LBmf}PW<=8 zo6cLLCm?3Ey5$e5A_u`!8KuiH@TP6FyPq_IYkIJ9VL*O%h}YNGr$48p`};G7<5#oJ zvBjnALN=+0M9pU49L>a291GKM&35cXY8B)%rq${AB(B5c`rd2LX|f}if^|Wrti8nr zqI#Ts{j<{G(#n$aWFlM9+h)vjN4_bU$R4n8Z{>;u<_}!fF1^43TJltKSMs#%ubH z@`|NBGeZ=FypN6r(|(QW#j1fmjKwC^oqb)S)p{Zs?T*AJ|H(m4fh{TZ%JW2C17D*C z$RMl`Q^~Kw=+Y1MOg7CgRYeXts#vOF9y?IvfwLPEm9o~Kz^mqi3~sj+k?z1^nAHTI znrT=ceB!|)ZZ^Nh{fAf6Sq{|N0&j%}%tVs)s~-3;YYzq1w3_U*#-WZ|+7B;=j%SFufT5lR&;gM=UxPmAchRQw*PMIIP-aJ2hs$R_v=Jnf2|1K=G|T@ zB^1x57To>B1v`;Z^1UI$_=|K#6+?@1=k_5rbF^TUzX2wi5|A`il^MJo&t1oPV1`NL zegf`)%5Ow=^%fw^)RA(c6F-Y&;b+_^kM~2HFiZvCU_I1PAhKZF5{pZqe=*z31Q{sS z3{+;D_7iqyLlyQVUOKz{hPGj7ZzNOIHV+m&_ulWz)N#mn*~u(&2S|{BIN${*B)^6N zf4qK6P54>VW~OeTi4!#aM8d)bKFo1{`wtHNsIU2 zEGL!c7>UmbGEB68PEbJ|E9#VKE-EP&L~T5L4gYE*O2Nk>WYo97 z#MNw$4n}j4h}$tKs3?l4JHXD%wC3d|dIZqE?!8@-cZaY`bmCmmlPP(ADu+@cMg~Wzp6o%;UeS`0Me32Y)?Dn*50G zQW5^3s0sX8AIi=IfBm!bFVJl7IF0bzDON|jUZ47ghi%!FKo~GP18H2$@BQXckNYWL zM>#utZ&w_3cizb;3E#_Pd?^nchbtS%X51i&+R4xh+>x#EbdluSPT`4rsG9svMleH9 zDHFQmv2i$@k3xR)Iu9biXv=k|V ziUll8lk-1_6@znW>@@@l`mo*GFwjQ^56So1Y2PI^jruC$yI`howTeX=KO%+fh5c=< zaTJd#VOnay+b%v&91$O*IFa)teuBBs2c~;!}7Wf zD)hs%ld8tLGmp7rS`(qo)ttx6c8mWeo0?_Py6N!6Lk^N7*N&_di!d-#9(U&*(xorN zi5-}mf@)6BC-QAPcn5u<_}stRu6P>4YPuox3$5vStzqo40fB4|urRfKioAo3whvZoWF~e54f9yw&X3C_IN?Ozr>r@C=%Avj5t|ZNH8k z_`i2kglfVJTLD|)%SXji0E6pTM9$>+gKNE|2X*rdDiRzBQmPx%*QiDlJB2qK`&T64 z1g|M#<2nGW$S2T**Dp7FWKEcj;8UEdaJAE^7uyu%(k}1mCP`)Wt~6rich(##oYDz> zzbRsX6DLmkcKAfn*)2-?utEjxggve-xQateACDym@_eQFwH=+2Mha*}6Gi#EN%Pp6 zdYMRCIgVa`BZJ0SN=CXJxJ3M1i$w&CXopQ&xIx}z=O~#;3?dn;Cf1L6TgM(7?#E5R zFz|%7(&C|P84N4#oM}R`cg;uxplc#M%@)m@<||0jtF$PNMBk6&@H}iFt^FlKr9)Lic}7<2 zX$NC={UoBGFLZY{vIuj}wlj3`OyewFU1P=sPgU|hLfVW4uFmST<$lgh2yd*@3*2a+ zc%12>n;3g1ks4iNZofB>p5SBuKNrSh7+F69V8i44KDM3aZO=sC80>Z+j430*4@qvM zQs0aq$1vL&9g;Ii2dp3R3i8ggCa<^86EkLg@pjQAx|dm-kCiP7FEo%CjTZ$nt@gdR z2kF-W3oZYe_WtlJ^~m?BU?+jMAJKELjo0%{WxArw@T54yIt9(h2<(Ta^KeJ25@ep% zn*Bx+EFT9?9#IqD^v3-{YdvCgq4(=)LyHhXg_lIgI3DTD2xQ$8A|=-`@FpcEv^l(; z>dT|0Jzv~wTz2q-Ga3;tR zS4@~oM)*6{1c~{1D={{w0Epz{bW!ShMQJnG%FFaOSfQG6i+JZl+|1nVHbznMr~tf- ztX+1Grj2vS(-LQ{iwimxS-lntFTY(;8wuR19(c${uh8h|x2N+8!2K$!qBXuX=XSmSn@1()Mk6!Sd&gJlGzp0`n^Y1bdEX#?p7)@Z*hZqV@1^^yY~-PE3n!Kj=Eu_Qx|wC2rJgVRxBTZoqa znrhu{^N`J$Dm+prDKhQT&k7pe%PfqF{10w`A1T*%{^fbXuJ}fF<1X`riPB*wIZ@7V zdazJ^&?A-olKbW_lo6o#5ws=$X#;{H&fFKJnCFjecL(nfbe;~joEx@99y6J?*BX?Z1s3ZHcB{ni=cx>Ikgz=xZP(RIm8u86mM5)wVUVQDjCa> zZ$4L~k<;OZVj8PxD$g;h5{Z2MvLcvT+4->>u!Hv8i}-B%s3O(h&5mIx{^R=H;BGwm~p zB!ydE4+Iq=e(&FFzamk4NcsfwE+KP8Ff8BCIA_Tft9J+OYuT4}I}j*8sK!WnMO3Ro zZ?1bVWV0uqXWHk_%?x(@bdH<1{?;3177al2RCKCU%3RJkK7nPYlH&**9I%Nf$iMp- z^Np6h^EC5{^<{8DrGsgV7hGLoE+5EI6F2zEw71mJ-N(wFVz$JfvHn}%8cPiHNBvmC8G;xp8KGU^{tN;pQ~L$L!M9aSQmr#IK-EE3CHVq7OdX zXFT`ju%4kG!9mv;Ee@hh-|r@Mdd|0gaVu>wM({ix@vzBPK8v3tJ~S@gqh^l}TeSql z$A~*TiSMZTN_Z=(A{C>LpsoxsbQxlV>-P5tx-ST-Yg}IpU~>Fv#1h6gMfK;LY+G3d z#>w9}-wWF(`I~jMg7Isq`f@%uR17I=%C#^?Tr{0;FKqOoMKXD#>tJ;+PlME6za31m z=-($_1*TR^SBwaGsW$%(C14a>NlOnJRLvDk)u?fd&*O>{W*QkqPXaV%k1K=nd8iyamvKV;Ew0vJ$>Z*e zKrG`kqV1|wvr3kE;n7Iphes%N__sJ6(W5*FYvMv`D9AY31uQ0vG;R0BP`e;Rtq%}V z))JzdP*+;Yvpjo|<1scfe@+RhnFF#wJi877w-@T^h7~FsJh@oZh^Y)cXNq=l58KN7 znWE#?@BHsRb%w?f*kIYWX$>(l6ddRjW<;M+khGatDO3tE%?z_@+;_K8z+2u%8<Z+VnzrTJ3T5oM?%#7xyeNin3Wx^hxkxcHgbRvAvd}+$;$h& zBN4PfA1W?0E7&dv4=R0xHc|#1q1KRFp&uM{5Fhm3KW!(DPzBkXll%L#7;hMN6V449 zE|&p`RKNu$)Q>c$Kwn=UC8Rp^ZX|vRaO_g7;ia#b1|wAG@hVivF8&&ZKldk1P!3S` zXSZpr`X_i53@(R&w8w+Z&(0kBpw9SkSE)ufMcq;8U@lLXP^$q3Wt5q)>uZCzBQugw zUfcjI8u-AZ(8#EC7~;L!B}<1TV_6xsbJR+o?FFXK>&wKe3+A9y)u4_e{Hh=S6}p0_ z|GOT!8rG2t3d+uP>P+YU9MbEsn=n9nxF=t}z$C~5#1TYTC|^~oTWWU&QWy68BU*OQ z7T?!jR{V8kr^DbT>a~5osC^g8wglL{Yx9rUd3WSiyib0}mK%~$~x`x&MRQz2`u~rqcx}v1kwL>Zn8XsR$`FNdX}~T)Vq@TW?}-iB}`#jM3}| z$6F+B<1wy&28-_?^UVo}K8EbF$!fvTr4ksYVY4KExw6ESh4zw(5fL18pZ!r5d3=BjfJuo7gMKGi1qvWB6)_p7AIzcN&Ea zygQLz^MmSPSJ`C!M8j=3GGYJY7&;+UhBVHf#g?eC0I zB4rb2X}%o|3DScOaS4l0Pir&>)NRI#?i;L06!+YD8e3&4)q10S2DjK?bkNvw8tCT^ z`*KE|1?=087UPhx`#-NL{MVIhrmt4}0;vi+?$oGcuKc^RD?^7WqX)Ah7~*F5hXv zwR5x`PjfFwCA+@yQ$z9i-K_iNh(>vKBiZ}6%Qkmh08lrtefd6}?_%o1 zWgw|Ql!7*~%S!gGk90d+&2F}w`D^Nf$W0`IpDbm z9X2+CJ`(%4sE$b(!qfwduew588CwlM#ue@(u6PlJZn}J4Bl>vQ;Z079iN~v}-mUIF zJTxPF7GxqC(*Sk^Ni+b+=i=1epk3lyF$D2+TWSO~zUh;(F9f;?J zd((Y$+3myau{+y?KjQR`}zsjm6E}2Ax=R-d0KsE z6zV2|qx9w8X!C0|J3cs<_%O=+k_vN5^yM647!m?+%!$XCz;}MGD$@hHah*7fI9Xcw z^BLHD6U;@u>a+Ei=PAstg3}cpXcYs&0}}xSEXjCYI&A;yg@x|RL+-}sQ9S@MD#l8Q z4hV zhRnAN&kSH~kJvk$<2d*NZu*NOtngHYA*bSr!M%T-{| z^aS)_Zx5Ya*;!y^p{y<%Zzxt z0E{o*$W@1tE6`xq=iHdts$Z?BE6dcl^2ej|K0Ei77c)ljPQ54>mwj7Toatn8eBf1wJe+%mr_b%T{O zE`yQhff#c5=XoCe23cgYp%K7G9|711n;x$Q-KV5G^c@xfu;I4@rd|3Yf3!q>{3C%-#(Jr zCLXkY1_?4Eoy5X1~0genKtlIBD>6lDfaZ2u9!SjZJCw zLd?^w6uY>TgW`u~I7-FkO|in>_W9cPW45&M|2a3`pac=m^;r;|HL#}E>bNk^1ZoP0 z$d@I-&O^zWDF(M)uKn?c4&kD9>_q?Y9-`8fP5OziLq7d9jlGYBB9s3Kv(qE7kw(6s zb-*2>B^sqxEe32!yPP3~v(8nki^7O9jm2BwBjMc-?@*3N1$>JRKB{d0vSUNsSdRjE zj6Y5E1rk)z4F*1L=tNA>fvJDUv_7sL0rQi}F|Ku)NBTD&;ZCDG*FViz*wQWkv;=1s zzt0>614rjNO}>3Y*+ayYH#q?RKAEDxVx?cb#y9Jr@hVFOFP;Xk(Gr##IkCgB0?HoJ!X6QB*acfI+b$6OIwsRUBikI>16M}Bl&%U6--*xDjjFAQI`A) zZ$ofe&*)LnajpAD@!o(>ynBVdnxCck|8rCzMc3tU;++6k{v|B?7f#-|JDqR*NJuC@ zk2>7&ljts1IG!i(Nu)9KO4>72S@5%RvdrDLSd3n#q3wCBiI*iN_OE6%3)W;e;KrZS ztFfUJ%pqmpbDy@o8V=G{{Nt*|MRYNCrs)6TgOJ_bk8&(d;z1MX;x&qt8^y{50+{HE zPDe`(fdYxXlcI}s{Ik^wPZ#T#qBsW7bN)@!EfnUN#7JnWyMu^9;)lyg(!vSQ>ZHS94bKeS?p&lBi>4LU*Pszik7 zCif?@tvhY8LH>WMHeELI?;h_tjTX*dLH{+}faqXZAn@yozL1_j7(1No9o#B98-A7< zU6Gb+V>?rIce$@A$nI;uBQzjo?QRa~7C2M?0?fg@ZQ2V9KB0+yn#G5~t+GapwI%KtG2i4K zp~xBnOqg^WrQ<;6teXZSw_#E*0D$#7pz_Pmwj=Hl8nfEL;1LaZfUijzWgNB}VsGTh zY_6Krac%fs+8C9)TUn~f!j?kjNTeRF1!q|S?%pa|j3<#Z<)t0?$`5!NYW`Zi^>Ulw zTwN|Injo@D-=jUjS`o!GyWpY8=NOC$VZ`&a3V8pvcD^A|aX`!W>I+L(g0cOTN8L@T6!b!-a-k@& z6B?t6AilH=F>5&5U^VyhMB`G8Pa=Z1B287UT0=Gf=xaC0N>zLl@B-l#=^LU(CsHBo zuzMG*;OtJ?rLbW(=cm?HD|4w;C^K0gswgTmjfl`@L8R~qlvOImUEcwP;u8zKV3@wR z%ECRykP7JaOb>m`QmkGWHQ1lcVnJ2I8HAwSLc<$APe;;Hw6sRW z-$nW{u=A@*+9_}BUIjp)|HDKWF{ZK8hLPWfu&%Art14?#MvO_L_ot=MadH5E;6$(g z`mtG|SrBarNyJ@w&e&XTZl(0QUGsqc3 zwNAlpm;C&BkbBz9+C8a}D}L^q7islSNZxDjR`dNc z$WMVrD#UT7E2F~-(akc~`8Se_gu*OE+>thA=+%x%bU@^@$k&_;UX_9pz=XJkZO&At zcN+`2={$tjHA-F%SE8ah#jdp`Gj~#a;>yeilWjZ%_!^>*+}qb(Rp z3nhL1<&b16>~%_;;^*tn%Fb=L9I@`uLw* z*qJ|Np)kO8c4g-aU&|ntar$?Lk%O=d9|oH`oPe@##)3<_XGND2NM3a~rW}*%>Sh7O08?8S2hHmXbefJ{yaztN?Ep6NW{1v@73ja1+G0_KZkrknSrL8byZ8AQ_qCi34|fX#$DVzcUfl^1#deKB=;q%kDD0rq%6w9Iq-#)p3%ukfpezuCOP z*R120BdB7svkNTykZYcf6MO&s83)JElY3n!obEGBYig_jN2m*^P8QQ1>A*Fd(%W{2 z{}F-=b@cI%S|&%wz0>3{);8lzwE?c*<6(L^fbu|O4W~gZb=NN`-0N1U;gi1L`tY9W zuQvOtu{uukAGh|l1SdHqJ4xJ(m1Qy!KmI`$;Wyg1C)-%fnnq5WlJz$JPxnuqVz~O& zSSg`R;IANP8MY7&f(yT&LFas}QKk6aJOh7bVQ1=KN|N=7ey!Cn zNGNEJCqn2umP}UB=02`A3FQ!Nqa(InYdw&9=2JiqbVrN5jHXHaLo9ka%t8s`mjNBl?{gK!ulSFr z5M|^F@AVy_WYoYL18bQLK6@gH>v{GVM1upc07#3Et1jWIL}sCM;tem;xRw1kX6x;A zySe|L1++WD7Yif|?G+Mw{B`@(#@!eB@xdl*7YCd&6Pb2}JAT{twP`V@Zq45TDeLZ8 ze-L{Y#igjR-F%{~7KJx3!fZJ0!?;u;aRt+dBm$(N(`2lfg$h&*u@wLJa z91!n2BAi|Sxl(=?gIs3HVq0LL?jR*aG#%8Jz}y*%obg2-ddD_q%QR^6b7E9B7emPN z`k2X*(KL0~8SJ=l%V+_X)BeRVe0md4qU!OqxchBx-Ez~|KNIM~KV2(!_4|5;0(+m& zZ-ZR}V}HMk_p*JC!=Ga57Bg+5l&lYs;4W$=lXW$7?t}u?e%eNlw=euYN!%f&#da) zs^0aSgNKE)H`VHkKI4Cv_ZZTojceu*t}R0=BLg{tcwe?bYjs=9pWONpaOBKHj$|hL z1ER0v!PoF{JjkM4wOARzvApg-{XfN4fdByFe|tcSB$=@MAoF}+S3byZP3Q|Y`$u3F z=*DbDdPr>RQuS-0#G$WkzM2?CJZqOC76*WWbQK@V3^e+DCiX5M2S5dn>J0G85p5_= zCbZV3d)5g|`9LdcDr*u*7P!toEu5q=6XZ7TbZYotp99s91O~I=1@bc(xvSBC8_p;6 zPS|&QL^2`yegAF&9u8GQ%pN^NZ+|HM`C%Y*RnUMw4u%dR0Q(L4Ott<}y^15&-tA1j z+8i4)fdkEy*xW4n+or12R2Wl2NUme=wMH=(8MZGkSxueGTx z;jk`kS03D8HL)g;bqMv-i{5ZY)EIG!F8Igl5`?4#))q>^OL!Qsd1@rGS4FmZ{x9mo zmy2J&0Z4dr5peA{V~;Gvbzob(gHt=jfO$=m>(#NbpI zXVF5Zry1X0rym{=R{Jx6rD>5wGPiAW)UpuulXM^q7~ z{VW2U456Vhr_M{Oobwm z@i{zS&Mv+S%xdsa%NO3yH^M6fJC)A3`50ZsZOU8vPi$jRo#EAgJPTBcnBEB3j=*B6 zbngxR!l(+zZ^|0Od}+f|52U{oazk3G zw3@+nb}_`k4hyxUMoUZZ<7*M+Ja97N3+zI#`3ht%s{j+{qD1=MAnMPz8*bJ7dSm7I z;Fwr@0`01W(Bp#C!-7=58mrHN2>2_JCg3ILaiO=+?}iK&?0?MazhY%h=Hc{ApD3SK zC2CnDg(d0$6}}n zLm(a~#u4gyU*MJ-DkD2z%|k>YGe35m`z!`tT#!87ZEezqr7{!ZPS=KuxnG|`^b zdHd4D=z1>MiOJx4CF)DPJ>!Om8ra(OcNdY3S&N2&UX6o~cd7N36u^+*c9PRq$ z2;+gG7H>iYkMn&;;#PgRfLb)oih+^SwWVK$3w}!mE7Bh!dwijpby_JO+uVjj*(3&C zIiJtR*xd*5prLOW|L?cy&OAD0m{q*A=J|f9Z(oC^CxZC#vPc_{w7Ac~-=jX{J1ruz z@Dw)~b$Tizsfm2kW7j))6@!tQ+NF$Gd%cC3BarT-^CuLdK!IglnE{#17rG+f6W%KS zT^vwP-3%j@bkDtlthr)y#h*Lxt5R_vo0~l-=@BjmV$=#eR}ufwc9qd4^rc;OMH#!f zB?t;fcs=FhwE-f7S)n9H4&P*emJTI%*P=?TT3pl~PN?$(2=5|;;QIu3(ob!DmHzvIGR_&9` zo%Bph%BmrC@;*HlplnH%-he_`ZP2_0dTaR+p{YeubXC~%K5qHk zHd+BKq+v`~5~~e^0@64^Qfy_Z&eos5G`%@`Tuvs= z-od&p%WrvkDs7D)#+?u|9{MKxEXZ%*QYqoJ+JgOFUlF^d=bB7%8Scw+=M*i@Fm;^i zRAQk*S1Z>IPESa<2F{#@CYa0j71N46-F=STXW=jw{zms7E)3@0q00S{_;hr-thLKI`HN}b&WOK%m6FHuztEGPPCfI;l4}_9$qDJf862sFt^V zEmmXIjHs8CMMRH!cRA{CIfe*=@r&c1n2Dy9>I2-ZYd>b6K04ORI7$R4bOy?dx}&G` z`oBRgmb9}nD|WaeR*%GMjj^2M-O7D;IL0ZsYfta~h*zR2Sc@~^D}8n29T(2RRc<%{ zuDwixm&1Xx(qF+Q?s-)Bhd2eEJC2u$XXnU@_%{Qs`iq3p^s7zd)e?c&oQ_d@Q*D zLGl0DnRx#i!5TQ2+8&j^2t1rFFImdtGh)6F^K-Bw?k~G>ot_Be-FaQg>s*}4^$Kiu zR4LZb&@-w!J7HgPJL|oC{%mr|bF&jA3{&k)URoG67C;rg;ve0auQtpjuvy||m=GL< zvIfy&(VygbLf_fmskK^~LB+1NE9kv)P`l9Pv|`H!K-%{N(0@J+uDEW^1Wm;3nZ#wz zKSal~O0U;sFFd4t`btW*;fKY~zxt9JqO5?6KGcSqG}qA^q?DaDUkzp}8>1 z=ZD6nGfxedKW05-%8ot$?0HA{mEEdE;VvILN*n(Wqa8lXmojWTiD255qGsdZFZ<2}nH6#i33>eCo7rRV%N)X;(?zF&XN4J~3v82kDzv+_2yIaxPC9ISAh6wN zRw3SHVUs~~I@Vwo$|)9;CzqZbMve=0?{_%s&W7T43YMl+#Owr9p?}^+b&?GI;HbDr zu^e?#L{&nI<~=)^OKVqLt1Z<1Z8OeFXP$TZT+t-q%rwxE*0p7^(`L4UWWt6Dmpe`Y zxkeQgy1EdqyQi+}xWz%~J}$mZwf`?L;ZTEDSu6-X;cMXCup9GYfAj5dIdiEaO9W@N zsQ;KB!?ByVnLyGeb<0Wem8MnZmOGZNVnHz9#KRiFTjob(?T@dM!&u!)AMclCDK94D z))PBV+i3PN|J}qR+CU8)m>(p_r;Tc8q^Xf=E{Y$YG_Sy=QD*d09@a8sE_571^1hnX ziZMFYW!wbw>K_uJl%4;-T1=IeE?TsGo!Hi?Z4WcX(b2Wu0`ZM$v$J+`8QU?DZB00isq3y#gx( zcMu`ll$}~m1Ruk%c$LwjIC4?AgYZ8(?z<)kLY|~qeisj~rXJI`4&a^(t{b3O6fy$N zifOL^xt_qXeG20c8oJ1N?6#V{W|ob(d_9)Fpg2XycM>;uw#`eB#3R_*b`w)o|Tts^3R zt?o!@NUp>?pLz)m?JC0q_j@#v$vWgs?Jm_ZBX<4lqAb!&WH16?Zh& zni+IPiX0Z;)!nD1+MeeSERbMgV?YOtPCh6~K6m7k>RhGi5>(K1dKrGLRjCzeu+?zo zg;mmS(_dM$C*rf)aGtDxUBMYS{nw`VB5|H4q-uX^b|BW}==kOv%_%ZlOYi|3P5Y7D zK!S~KZN>)RGxXC=EK!Q@997Q!<=`(61dP}=|IBAtsIZjlbkb1R#68F}P;{2pl@See z0V#5Yor1QojYYP#1WUC9F!}N&e71;Gqz=>2-}=$7UU!t(xS$PP;0QM&=6bANH}`0x zO7iobHiIEol=j)Ty2F_kmO?01S>UMY9Epuy#!1`V=&2mhXh_x<95{02HV|dYz+D}R zcD3MA9sHk4l@1yq?b1Tw1+XhDK=TAAV)~S5bLnRd5~=#TO8s6uN9Yk2`p#&d*8}vt zK;ecV`dXT3xhWq9m)*=c^(lVMk~v8=I#~5l(4j;5>*0Es~;+V`Tw1ueOnWxGKlg2%Bl|}H}>_#a41qoQP$1M%Oh)L?X8&Y zlpc;)}n7|)iL&V8itLh4>7&iXyztx2QO|eiiy=?qWkXW=Wcb4XGtvO zqJ6~o?G&AOHSFvfr&67d294@d;}AG56mTgF(Ebo9e|9`2I#XWxE^4rMooFxvVwaR3 zbbKn97N~}czWqV#Dh!lhav}0%z>y&Z=Y>^Jgu5vE@3A7`SSqVd9Rq}S0@XH}#C#YEIbr%fMu-7T1nP}f4^V`^O!_F!m&-4}?qw_2Sg`m+8FEc{tT3@y1TC)RO;Ga` z6Ky%i%XKq$->mSk9CO4`i7l+P_bSV?2NT|D>!!6M%xu2$c$FN#dBO}29r0e`?%mn_ zz4tn&3y|<0kV};4U<7EK#z}E3s~$@OiO2q={9!p*qI{mT`jUMj$A`SlhJkJqx8r!& zYKTrpBg~tHeLm}ef-Za|r6P|MO=3Xqg3h|a8UBU-XrIrt#f4~iR2>_pxHeCy+w?PW z;may#n(Qm3Ow})@Tdy@8nU1XWxz?4Km1=A0BEHt#%$^78JeOs??D{xNNsUl3_?fg$ zf5q4LZ@lR~EMR<)REqoz8C0%uwb~=OpV#h8eOEPDlA7p0Y$l|*XaBr?_>;kc*V<~# z#Xw1MB4lUhm8jGG+5tab^PZK2(Lzf84sXIZybcAlAA9|8u!Z5^wv-#{YoD?(FLR_>pS&{LI;RIIZVoWjz9y0PBTTwDqj4SgCG{ zR~l`{xqX9L!P;w=m)U&-{a;L_*lrs^dOdPhHTC(dZuG36@oWqUBL&a-?5Hx}IeZVP zEU=LD?capJUN{hKROYy-vL_*_4ocsiH6)R`ydj9Vs~mJRLerD#6bRaS8`-n#S2Pdk z!(aD`-K)tpR0_zW&P4wABo>Km7FU?zb#5LLNyO4`1M=o_^1kJ+RLq<^LwaBQKeGUx zvv{n)a6$5v30OWNE=YDr7YhmwV4fSNQqaEH^{T9~ZT$SHPxsv9uV(4ySQ>##I22*5 zz}GIBG@T`c;DW+7-Z)jpHR))P{o;_C`pz&bp`*FgY@104-JE7s0EO%i+U16@u1h)a zr##bS>XL6>e`9nNl%Jw1c|pYWEqv~_FnjDK^iXk`}k6965B+TjM)lfzGcWC+EKj%?{SAz+|?{eHfsg znzV6o+U5v2-SdnCOC5+&neSLz6Cs3O9Rgu|rLCBG%p|HcoaHS@FIn!#&IO$*hs^6` zCckWG4P(9=IG8U|EaK;%jhz?Fn4|Z`Ik4V_#^@n(+8<4meZn~ILH9V+*I<7uX_`DK zUBcPBeAadfhyA9#1(()&3c&mHi!@`uS~8038z^^qd+I>>iN1RQ^-LEcHMke$@vCaw zg2BYTAZpQ1+Z5gT`x?Gbi<}Iy;N=L|-{CE2=zHXm#FOxeLLsGzf)T16;XuL?7R#}R zeE*~U2G{fB4fwZ$|IXw;1=jH$&=wE#150v(XldR{{o+Lnz|l6VMI^nEfCxh(D>B_gR^GXJ)9 zheq`q7WbLN{EXSd9@=zyC5}r}D8YgXN%Y7tEe>u$0nTG|Lc14FmCT+F~5NZ8Ro#y?G9~cT~X0z?U|n_n49mZaSYb>XXx#TmZsBcrVd1@AWai z!(j4{0gzGlFqBK??WRBMg6pCVtsm!CXjN?ZXe(qZ{OU`&RcdOT43gWi&;>o=dudsC z4fe)%`Z8#QPOsw}&*adj4Futm0!qJbI%>SqeJFpt4%`9-~Lr6lc;6e<-~jGHA|?F>Sa!)o0l!Z19GRr$gd(hzM57< z9^N68iQ900p>nJr4#a?gb5Yo@iaF>w_7Jcm_DMfKCwpM{6M`t zrh-mNIL33t=f*e-Cn zTElB_itivZUax(B%zu|a&obtSAo_x?H2WThAdZiH?i^3j9TPtm1y=|~go(1FjVRk3 z4W`j>B(R@Dd=ugLJzFCuSKo36AMyrTJK5LT_^MEC3=7qa-vch!ga+2P0*C~DAnp8B zhA0*mtUR)NRc@3ZSZGw_i_}c6Xz@}N?{v`j+TG`UIHs2xQ6rVKaszqgjy8Tg`jOT? zcZ_ss;vEI?R>jYaP?(^`M7rHjd0v+UK9fc3#>$WssH#dej@`-<+f*|$d9v92Y13b$yl|)ifsZ`1+Qc;oqATUS| zD6Apaqb9qz_S4gv8V0+8K73JERIqWph+sIxltH9T0}YzD58lPiMgM))Fj1$Q@)`Zh z3uTYfoX4)KyPm5%E;Euv=E6AEuOzKo6mXSJ@-!wXA`KMx2!5k8X}})#5RF8_Rfp(E11k0v+@2s|Y@^ z_xg8>eoW_kazfaTdc;(5XsZ&!1-9*irVM!_Ctf^K1@^4m?PD1CJ?87D!pf}qW7A8z zZi8)4M@hMd#m5U@Xx&#{yX_>I`CFF1sNgEISjyPy;lLbpv%d1G4 zVGPTa#!H)axpT}G5Y7EY*oI<|i;fQ}jRG^%&a4UymMt#=$U~HiQ?*CfMlYqd8AXuL z(!R!p$D`D<3GlV8aSD{d7!7#o)KI^{$G&_1&A(}M(;YD2Q=ymp6jdw*>V$jAIJ`;5pEe5nn38y+>gs_^dpBa1baW};{$rp| zm^5){z8UdK{;vL7Dc^f-S>ow%j>kv>q4$%%LqI?hu7`AAdb4-4!s#ovbO2OHVqu3UB3i>@M_F z@0qB{jlOftI4Wb6bbwxh#OmrNVA$$eAkqbrn(bg+ODry*dCQnU)L`Fo-OzcvSnHi* zv#`u|opNT*W_zpzJ(Gp^9k_ixLH-#>VN{x+MY0zLg!UX@^PY#H zjVVycG}B=_EnTkmkaB$9GPFCI!j#nn2R#k2r)4C9NgcJ7sijixZ9@Ryv9Oo}_QqRP z&3?BFqsFTuOavmsWIm9sUHqVGFC0(R&t>b3MF)%TdWspkNT4?(@p}|_W&K1h~tffDGiKAV{Th+o3 z6aVy+De|2fd~=>roiJD!tB^HgJgY#*NY8nP;oGAf;FtbY^4Av54SVWv>TYJQFi#-Q zA9(}Dt}3ubwO|1C4<);Dz{U6Aeia3bBc{yAxc_T?=zz>`)IS502X)2U^~uYrC~b~( z4iT*MZ*}%*W9qmIdjl~JU88w=&lKKvK%c-hWaTqWB(LELpD zM_oAV5(NtU<9w0a&{l->F>rgx0oT)4ZgbChrJbcXbb1_HuxG_EXQggTz;Z$`V_Y}F zaJ(Py5ZOx{#jT-}j!%4dE!ok|J`yawhBe*8bK-+!)3wX+&S2u9T>KO3&NCF*yf+>s zW}797P3I7M)F@)VxxV#E(>U1e=u;|vfwX$1VN@_DuE_q2;K1jMP!}IK<_qb=?M4f{ z`j-?HZ)Hl+d(`N5zfYJ}RSW9!kyn_*%?{KsN432QB=M3dr;_6xA_@7>buVQmYLH@+IrMnjcu=v zFf|7to!^jZ(VDjLE+K(-h;5iDRi-{HT}M$4;}O_sW%yzW&O$H95UM=!M(^9~mjQqY zBDcBK1v3`^z_vc!U^nz=sq*;Lzktt|dy3;T{fHXD6Fy9RezAyN=a%(AP!MOJksO=d zg}yw~$v`<#+wKd#Rqq-7K!Of07mSshq4rWU5TaB*mdU#6f*WsvjYyU4mYC`{6@l)g z0FXZ##s2`Y;S@S-`Wr@II%$y^Z@W<=BGDW*haN6UZ|}293h<~;;ts^GomBFRPR**p ze%v-e_`Let!?l@&OLS3s5|!-2CEVoQjdjM2!{_?b!vw2(b%XOORM&o0>+MOc;aDZI zuSMvbQA7yv68-lkRfsZb1bC|T%2)YC6-51!OKbWi&mZ>HA#Z*n0-9QzTlO1gniLCC z3J#mXx%C-7RETemL08UBjOMET;&)26^xJV{X}4Xxz?ghD>ZH&)>XizcTaDg65L*Q!v8h((G>MoC6F=P z_`$QwInXhp!$KDiF6-b75nSmqY!oPKWWOEO!{lu0mWhGzV;JS3He{*2oWVY6qqheJZV4Q{`Z-)ejN(szetH92OUey zBm{ZJ7>PDwCixw4DBV}Xgg(YJIv#i`W%vgnnm$qvXZo?}zW(2WyC99oBxb+Ci9jN~ zGC@SAB>zqGx`un&ow=lp^&~BpPj8i6A28(T8XEAq2V6v50A4S2_s2phDo$>t02Bxn zaNq(ZuEiP(t2_**h-%Mg@v-HI<g-Bd9zM?|b3h1I~T z%o=M5Whg7Z?SRSGKiU|3IidO4k;I-GE-a8LC^f&hpJd*;Zn)!H;8QyMoi07$$;eGD zV0A{KYqN)QbRuYp|GCCus2tV>A=bI8dMtdQ>zAY{YBKgPPjTu{|!@5EcbF9|mm}D7ZtSS+5R&uCf950|w1$8w_Wr{-s2ugkNmL zd1N`=pK3J%mYi}%*wg6BfA}P*ZG#^aIb@YET?mZCC(=1hPfd~z;7QU# zxbu(UUGc?#8Di}>W~uEhU=K;FscCnkVVdi$)n+?pAf6)pfy<0b+r7hi9(8|Qm~e|0 zVI5HU2W>-_5)u-K9eeLZx10~MsOf*E{g#-w7G=^Ts|@HeQP$%(~-&IaK4EQo80}o~MaEW8ucutYKf_ zH%FoSW~JMT0{Yu0z~D4C2BH_&jZ2ea1cZo_!0UrH+$RujDF1r!bGQ2E&U< zxI8LnJrZr;iWkyIL2{Y?oxyGebf0&}Fu_D#_|d8q8J?~HqYAeSa1VnONrf%c*>HX3 z-}*>rskb!?(_9$Kp@<5xL}fD!;<2@mRg)E20W0yS0Zkfxi|-)~Kh+K6#MP%Ti>%wf zrH}K;p2r}@X9&Voi2i&ZhEA`_4KBgKAlPxQ`TiM6btWjihm-m8#J&TfVuMb94thytWA_11~Vp;$rc2S1jdnXU3WjNop3LcO&3Au)-ACZEM z2W38z)Xofx;|%g=8zp+*w#Pk(;6pi^cy^!Q=l~+Oh8861wALIFt_v+)RM;o_m@-P-b*@D*W>%FxX%&95&^ z|DsvG&e%2(mG*or66f`nC!V1+=KSAT{+_T{RX*Le=Vi6F{>LId{{+$T6gy|?0bc00 z&clN!2-I77^3sV zMX%}ZfP>6J{B)Gj@yP2)QWgmZGYOp{ljh6cmj;9%EWK1@8>OUFh$2y8W?cE`20IRK^Py#}oF)+_f1pIyfMQ$8YA8PH5LnD9M` zI3y9{Cg6L9mh#~VZ%B*xJfk!!wlf(0&&@Q}bKgFFMgkQ@isn9kwjbWjjq!F5q|rm9 zGK}iYMi>f`u}h7azqrs5(X#039f`Ebl{kgB25oldX@h?7f$a=H859v@V5~rjnk09E z!;-|yCei1EPEzFp_-gkX>kW>uYSr(gM< zsZ=pjD1_BH2;L}R?O@)wXZ|S*(U}bKcHS*TDQta%4c_2vFu+I4Bsh`xFX>6D$ByVjBS^md!&HdwTz0!N~Y-Y?M zkGxWYyD`hLC}4&k*J@N*u@?;|^J zuTSe-sltB|W&+Jq0`ia8Fg%uY8Lt>L89OYT`FR?ZszQkx|0LVOi;vFB)4tzrSM03p zT7%MfAh_86xY=zcM{k*5Tk=f0K@rtxcK2Y3Oo(@Uv7jER^The4sZ(q;ZG;IQV zr`lm#JTgI}g`Q!$<$QYccC015)1{O<`5?e(zS-gN*LKk_blCxUf2IF(NgbT03$eGE zvf+I!8Iz1M3kWOl(zZ9>dksw4R&tP0qxazY-LKNMY(kZBkEp6wYuEkRWMe(;Y{q~X z3UMZ~_%yj40Icb}iV_L^S6o^6xa4$+i$sD#1_`tQ$1|;_Rxp#h`JwN-;0p9Q>t$2U z4#80SHfJE}>6_W}h#QcVwv)Mae2eud7n98Bc?@W$yC~9BRtWMA6ABGSVk=~1d4|E{ZHo~ zYTMD8B%-CDc7aP0QWD40zUq%H56i8jS~4dVJVEYzuwTwCe~x!N{z9N5Eiz8!T|xG7 zP-;S2W3bKUbs}Tz+FOg~rKW0EqS2cz$!D$=!b&8cu`Fp&#}HzMQ|*Y9N7LR{Pm^(<~TYT@TvqdS(XZy9Z7^uRI~hYRaZ;aglz z(V(PssuCnbO+0{%>%Zt&YbN=iw5j2 z8$L$nD07f{K9Sm6)BE7olQy&mjtZY$k$hgv{Cge7Ht50FCzDD3LNj=P_kxYMi%TU8 z3}Hj~v8nJmI}vt{2TR$G;&j6;>n!FkQztUzWg6&V>aA67C z|A=JJ?slb8Ez{@WA1X0i6fNu979PGeUsY|rXl!-pzb0)DE2Db(yO4ELTwsbSiye#Z83*kv`k|3k zsvstqB14s-rdUF%NL2XZajsuC#$G|p#TN=JGg+A<;T_<8K&wBHAHDy)V;%z&KwmP@Us!R~^#$Vd*>3=aAPuA#Yr4-5e-?ahVLj~TF_IX{L_Qf4E}fQSH=%#r zLQiuGc=-B~v<6$miIv}YAyk`I_d|$){T#uAybHL}IfIK@VU;t^pF&RQMEnqEl$(N$ zPfN=ILm;7#T2g*gF)|BgIQn9vE3*z^*A2{^$>6LGK{`jYdw;JczRMA}Iw{fz`_iKuj(Fd|IwUXuyJG0mkTnl?p)6(%H%Jx$$(x{f6_!cH3nI z!AhG+P*$TAyJxmHEMZzdnR-5|YC(vp)I3*67kRCgX60^<1!&Xrplm?@zZPp|=*y>7(hNE=HHShD!&sxZeEy478q} zcL&K>`X0~sUB~(F+HDu$&@it_701d;EX&Q$>Xw*~YcQwN^O6Ftq{(L*2sU+6Zfhr{ zF2i}XX44gmv{*Y;w~jg=fU{+CDV;tFzWs}=&pqR%<<{Y|G65Wwm`?AfKU9pq?sU>U z4^+5>JZi&{)Uc@L}HoxzZ9Mx@j@xZ%kb4+OB(JReceuKYbYAg zfN6=eS5f;6Fq93=G=6dSr=q_L25GJ-Yoz6GIt!&z7A`ybe^=qiEeBbikA59XH(aDh3)U{iEplW%&NG5(amaVj_ zH?$X-5}>#IIeL?Ag3xvRPLGoqWzYud&m?(YCW>nNDIcjY90gZxP!x)V)Z1HWS%?wMJsv|IrAzoQ?}0I(dqr zUd3_GKko5Zu-{fD$M3qBg#b*ayiMu?stLlK^F5^U8Esl}A9LnyKgYaQm@Fn5I$AbEA2-C%F@R%h+*J`L`%FmY4X`Z*(`y(oql(*+tiF6Ce{BY z*ffufm?9visbPpu_b-Q50wU}OiHZpy+D4$khE_h@UO!uBB0JE(e-H(aVZhI?uh}-k zqZ|IX{{GY#4sw07p<(sS=0il%7;C`9lPmgXg-R=Sn1a5)WO`JZe*PZnp?`yA@ZxkM||g<<4|?vqf8d`QCpM((%2&S5n^AQN*f=L>JH zej1yPtGQ%$x72QQ6%_vXP&;`Kae<-@)H(34lfYS>avHOfj3=x4Eqa9dT0t`mMtr42!rSuG!x)yOE z6@*&E<{nI?%p7KazDqs`I`cqtR~4wo_(86~ZmdTo4PEc_@IqHpZy`6np=WDBd$IPd zfqgh{2D%N9Ie`)cuI^ZnCfeO>M4O?JE*x-eLtd+UFCW@X;RaC2I_pe{g+C6f-A6;5 z{6GdP2cq))L9U{e^7=`X%73v??aka=rASl%(TSG^W0Dcc!8Vv9)q0KF3;At{`n@o; zk`D^m|2mUc=J?cmvUebd4G|*N-I#G=F&Kfm$_+K9Ql$Cl__yNr*D|sA-CauQ`j>di zA-ko5n+*vX5I;8Fe&2EHKzNvI1}JU0Wc)*bE1vijt7#` z6g!LQ`TXXIFTw<@4hCxSTYXoI#(LtMoflH;OuP8TW{?r@gJYWR01<$mw=&;gRL*SkNv!}vHVkNfYV0P;+di+Q5^I`+uMwu01F5JfpN9rp-)_f1AF3q(}v z-_|`3^k#Bwib4CRNGsbdK#mdCU_>WS{XiuQXTNyXm6An7#dp3>jH~@~Hzkwzj#1jt z#H9*zE(O%^_RVNlKn6B)$Wjo`Co&)5XW5`g0qiV%G)7F@U&$)|rxh6CnV+9$R%2hy z#j1>U;Pe}pkrRaSC0@o`;}RKFd`0tH1}q%`z?Vq7;`6 z%1_Jn-z|eXUz_1+qQvzT^U)(J>PJ#h5`j=QlSc%k@p?LSx$x_O@N`^PKWq zY!cf_V{^CYa*x~6(J|FI4x;Lw|1-5Jf!K7wSic_bK29C5>vQsuK~z7<5-uR$UYi7N zgtbHa{*l1*!lwZ+^ifb`Ce`tXB~#i;i^k>`7IaVMxUDAiF((8hed6&{q6J>(Wj#hD zkq7K+m$lM7r`h;k@t0?3fEE9vXakbC)S1HCt97%|3|#PUwiejz7PxX10JmLN$2k5< z7`YH#_LuAU=f^L>$JI2!$F0yJU7Kk~zVG!#t8fA(AO^g4a1qXcW%nSc1kQ5OqW%Bb zSA4~@Iecp|2{(vK2c=06rtZC3PT=C7fRnN~I@oO8^&JwL3QJ00{%=U!NnB_9&4X`Q z)n*@Btv?+#k(g|Tz{aqY&D@k1HiI&L^sJb(>4G0374w3O;Lz6V#khx-VBbVqC@&u1 z&u1rYe@9%SVQg?}84o>ya>Y1$L{|=X! zV^=PD)c;UpIpcn4XbFTrgD$DB38W`@DQ$49iyXR7K1IcHH*RoWdH)%Ro7aTR_8mvK zkGWIc>ap7p))qf>5%)VwIx=E?9*p9yvNoEE+nu-diC@phy#SjnG%)S%nL&!^^PvXW z3Bqqukuq=RpBmICp&*f-ffOl51f5RQ9JE&Qn!}gr4f*5lHMZWAThZNnT+T$A@sq?x z7^5alU0f!s^#fpChb4AHY1LJk;o0rRV@q&VNU?NGdDCAvxm>`wwxOa6u>a_IGh(1z zleA`wXhA2;T%`bEICz*@FL3P-qpcm-UrXe{^YjV9K=uZRC2~NzshiKNSOxCyG7Xn9tC-c zp#4=|Y6d{dY+zm$V5Q*tII9ML+Thm9^b*B{l)q6IL&>K5VV=qujZ5>%iYyyv9J|$9 zeCO*1u~ACGiu?My9-Wxy$i2kWoyXLZdo%0X>Tzd|toh=yR6lCi>2gkxaHJ906{6ZM zkxZ#w$hKs+F70wq{G;h1+rDR&UtXM@4$-VbfFD)WWz@m*E;crBkSk+T;+J)SqESI< z*bpv@LZ$`YA9`qj$BCexv;L{M)H833jh#RtLTxh!rAFc59gkg4TRn{ru8i$hUeYfX zzq|dM+w?;1#v`}_R!Ue%O)CoKIFqp5=PuQ#H-U?&JB9fP_jC2%kW3;Kid)|EeEOd6 zOz!SerK)r7KibDQoO6G#Dqz|X?0&>P)ZVoE0l%{s$;|Hqdp;gDlJ?jEn<$0{ zk6py4D};{D%pkja@9YUw=a7f$KM>X2x{FoAwRAaqM;rb9B*;y@AnQP)8Ug#qm2Si| zf(6&jTX*3X7^W0y5kv;sB+S#;^W9e_D?)UN?@1ht-uE(Tvhxa07Q{8d=ego;{p%Y7 z<>HI7zk4Rj99tjcM6ktkC)5v{9vE8s4|3#g3?_8cZtw@{=DcaT)hQqSip1Kf&LBa% z_Lm0Vr`PItM5=|d)$*sk?pYWmvw%RM9~5ZKaE+^WB|wY1!-(ztAyQ2iP#R12Oj7M; ztl-1BMW@5}sVs*idy$pJl~Z;|4RE{^$=91r5;*3k>%1ub7Mf~FZ$eJzQ9bjAs4>?9Yaoj7sEPPr z%*U!F(#3EK9T17!@Mb$**2;VLl`>HBL!NP$gE2e^P!GQH!?qg0o>t%-D-~2VXc%RF z&ybN!tykp%f$aRUvh&S;2NYJfJ$G9JB}fc#;7Sd;8HDchcg&nZo?+eXizF$7abZ}` z%?C4*iQj)tj4|k~po4Os;cA}l(o*?BDb(X-xw~O+p~iO)rRg65!60z@55YtSupV3g zJi;uDLhHydOx$V}l_=B@P6f8ClG4^l@t=NNG`|DT8;_=nG^=-tfQ|2*42?x1a=j?e z6J)Dz0Th`+4*ZpxHAkMkA4FPjC-m6cudI+Uz+A^1W=#Ksps&De^8M$?Q=NpBT)RE9 zN6x^{^sO1M5>VCyXzgOQjj43z_KqkI;C|jKyf8OP31Ec-$b$oB%m95#B$KnVs3Y1( z)7jHsXbo>9@iYAqFgw--k6*#xsMjql$@bql)>ZePPmmK8hS*oOueq&I8+{ghG(8H z{+VXA7=3WE<FmFgDt+Gn;P9 zCGqca?DO^K`qP+|ro_5!1_kHVWYHA;P;%nW55SSI60gj?Io2(zK*TX&nWQj;P=|IHBuAV8M42(g=A znznoqo5RT`EjI86IZXF8+`zd>oD3TBc1-BBj?M)yMt*lb|4?So5Rek?aVL%=aGdHB z1S{|;vfQvyo8<6NjtuyFVw6V?$WP8OV|(D9R}X@$UcSDG1siTFv{X*?T(djSD5hUhiimpRS;0|rQ}iNJ9^ww{Nu8)cpOJS3ZU=5wf^7F zIB)+ z`I8A3*8aN*SN=OeNDctu#WG%tR+(M=l-{Nz?8@%~bL(LVLe@4`*))+C_D)wdp5pEc z(^0@{C`kRo(No?thiflNgG)0@PR?{OFkbqRGg-STp}v-GtAO4l(3gz>P|jdZg=G^l zCEiaV@gXDO6(E&!EUa?Hu9<82oL+&cE>(+v*w4AEIgtJ=g`0de#%JQefB{r3%HW|! zVu`hekjgltzrb{N@CTujsd^-zB=Vk0p(b0BO6JtQcG-Iq=EhfJ0m>ja{-|236*dx@ z;+dB*TNO}IsD8C9jkj89Gtu|sM6{wlvkszLf1I+S|wN`qIA7i3}_3Ri^C2)+d!(%R|4kG zO)TR2bH)XZ;#l2cDN;8J3My$>AL{=D|Ga7d0IC8_^T|Vrt_+c6v*a2y}ReCOO_`@;#(}|1S z8B!%gfyIkT8$=V?!YS1R9|aVg z0M|frHx?CTuxwK>s?+yJYUv<^<*gcU@Soh_*Y^%>>}UxCvug zlJ=JCG6qG@Ye2^e0ing( z8?FdiM1Ny0Q?uTcsvEBsh$2v|pnuJ!bX)ZW^2XiSbB7Dlu`oA)G!<8e=Es+G^ot$_ z15mTS|8CSigOH9M!jDW&=4Do)_qcvCn|xX*4%{m_b0S9;!*=A!qf_(gl2^hczp45h z;A>in@7v`2t`8p#4h2l6g5$7yaQRywc{i^`IJVYor_*H(H)6e~on(OzCqZv7HvrNd zxS!rG!UL|1=rjdg>WD0lG9}G;RH_UhsPjd0#k^kiWW!wg$6;(e@z1~#K^EDZhC1tD z-Z}d44d}8AuCxW}jg26AsuC?xoGy{(EQ@4_nHtD7w{cuu9abEEh8Hy>LDGx!#+Dc7a!$nz2%eT4@$}?u z^DWHYI?lTa#3>%1A!P9tW($AbsK^m5uy%A;X(11O#&)bSg*pGVK(ok&)qpgCIfK!} zSVbcAJN}UAhU?E~abgT?wb7SN)o13FwZCPfG@g;97M_W_Kswvs<2U zB$f0ZGodY=IdzI%Q-4c707>{ewp(hvXrp-8d1wpr6bNyg>&8#3PB(OF!I-1AtG6A1<>s5NqIveMF2W`~ zKwzD}Qwp(0UXg!ywp>ce!nS-M-1j7)k0# zaGmKbxp4KTzG6%JsGJzuZrpN|k}bpYnkNv{w18qUxJCmTHi)MXM01U)NmiaR@Jvv( zPqZcVQh{vpJ5cc;xwQ>V5hVw2e|ALmJ9~$ZmFwpw1MhR@W5enAeJX!RLG$3znM%|V zVa0cEdT{f1VX_ge`eC*hMSB9{Z)=?fh4DZ>(A7x$##Z|-KtkwBov|KBmfD9O+CR%< zhkxa>ed2dL;O2ERi6Rmnqst{K6N&gu)(rK9OB?=dgBd;rFBKkEUBpQVc6;bTG(|?XkZjS)2W8mh}yWi(3SkR4i z<$o!v%%^*VW&AL?Kgz-6aS+^9>wOx5J;@B8eq3Mtm_zG&de4cP%hDf?{l)kuar!JR zEXXqMpH$Q6acomYycrY;LU9>Ax5HlzCn54O5YmkO>t-@yJus_9Wv*lQV-eIdM}-?wg78Q5L{)KmUau~_xpgh*NG3?u9>MW z1gB=yrfAkW#rf}<(cnb!S=;N@y0Qe{pL8!N0D$IpV%3kNS^ zZ$Yu3$2Fjc5}$pJ4R@%3`WsDV&}gpr_j~g1=`&s?;7i;N!sBLWBzE;)jc+=c22DZ; zdN{`Z%BLne)2+Qt;Q>w}x5?%Nnc>?WQI;@+=$gl~098#sJgGH3A3q##I2RbVexDP| zdfzFM!Ju3ZkCpv93j2&&>`@xda#K|(6?o^7O@+WRd<|U^M)#pK-6i-tW!~oo!m3Ml0C*>{EdQx#eYj{-4a9|| z^BqwLOMnX;`3js_HhUNb{1d|2I`>XA|6Y-cDTG+b_lBW$e+S$mW_=v z-v8G^O`kT)H5zwv*Zw^6>b*`dW92-Hmf<5~t3#ImgCe~u+9U@;?}#Mk#zJq6aDJ)% zs48C@wlD-xj5(s4EC*#!-Y`fk;QFHDwA+Os+$RKcDUK;gDJzBwTZDUV(U_Rb$mf3jyD#E;odi?wxJc@`<-tIIK7bXK)`AHr)7s$v%2HHLL0WQgVA3FR zULXbI7)Nri3jAP#1s?CMChU9GfoSGJdsO z=a(ijaZNWPzUvpDMv6MXSv(PAbU~P(sHsXrgn^_DL^`|i{{R4LEELjTI?Y4T07IQ15$))P{2W>tgqWmvvXi_} ztcED@qaQ_2ad(~_m?5G)H%1_r0Xi0o!~LK0i)iFfVGYIzzk6qVjIVBeaXgr$PvEP; z!h0D$Jx~0wq9=pVGmNa#b-!Fv!_9&1v8}o8%+Wtc1wja^Sm19-V1SHmt3jy_5H`Si zY>r+SS`PYe1Ev|HZ_hDQ7l^{@ZVLfudWCZ#YszlqvX4lQDB~UiZxLEL=fr`HMgQ{p zO6Tc5&r^+(qRZU=)av&V!txc65je1h1o3YwGXXwSYLpPjk;B{*35BfW%3y1_1H9DV5m7c?QG1_Pue^7*V2+QE%`*%< z2lzxwkoYHE*-4ja%T>iV!GD-B0d<=cb?P?#41G83cz23l>s^QCh<)ARWRnlgMZ=%X z*Jbr`4N@4mt$3k&5LcglQ9=?~PY3EW13-MWRZp?1nij6Q*64lC*LH@A4$5w?OrE|>4^pgj{@6{qF6;7&X-oHxx5nLu zp~%Gv==^Rni;~6Z*UOVN``3krBdK)oCiL-VRAauM1{fRGeufm^ZQz4*hF3A8hxx>{ zwE7e~okpDk{8>iQ-u)NvdksWk^cBy&mm!i>2`P#s)|k_UTf#DW3o`f`#d2hmGJPVQ zzYrZs`4LR|DJ$011eSnj{(!(k{Gy!BaLT%Y40Q-aZOD?W6GljK;8E7fNcRGv)r}tv zKzEY!H>!6KqrbE4Z#wp=${hD^69XBIgG38(13ns&@*tR-cDI!_KUf6#mp;atSQG>6E($weixoB$>dUe+R3ZnN zdcZ`jeDCKs`e3@1?;ZnLY!YL3?nFWWo*xf1gj6{zHSj6(nVsBG@e<4|vAQ7QyKu{F~lLJ8Hy@4^^A7vvO#mI~K%I8yH z#edsCHW^>MIpz@}l{5~#YB5aZK#vIC%3!Is)`{Luv-YsJ*OM)e{}Vy~`AnEyyLyhp zj9-*cty&W<1dxJP<$-I)Hj?qtiX59c%c=kl_z${teJ)L+R|U?x6+-m$Fi_vmIA>;C z75h^bE9gcYux38VefO@!W6Vk@yfy*xulmB;EbHfYHl1YE@};rlRR4$dSTiL*qFUYq zs(;ftF@;ji>|xyLw1{0sIS}x`_6Ty3cMf0vjwJTIrR>WchP)5xR((%|Vjy$V9F%OI zX zUVoIs`If)77Jd?xXzf**Y7qtUq)kSZc+kA#IhhR2Yv3`JBUd4JI()8MPmjcwAD6&q-87lC#s`^!axk!!O1+Qz$mT97CTd4CyXN%~Glzm}=Q{7Dy6 zhc>7d*P?R&XPUrwiC@#nMIF;BdFJ)k!!kUpZ<-+|LWWLRe8n9N=hIIqfuEODE*(AQ zPYIX}t<@}GKVmu_I=DsxDiCA4^J$;ZV+yRwZ;x%l1hB;Ukm@JSGUdi6H_McL z3Ks%B7}&^O@cb_x!_Z>LrbHock|ofG2-N+!5M>NsvsuFzjoxs0cVu#?@Yzv7#0*z+8`+QjYx?9^j%FMC?hXor^p^-DiXAAE@&(8t-2euG3h5Feaw!^0* zN}~_rQE=$f@6(j|bDzsx9FIMYE^QL>4|z2;lN|d+)ky`@j#CC1VT;gc%3+#b27KR9 zr``;6Hwu)zVog-5mMgk)HZOtv9NqDnFP`tdElo#5AWz4=5yGfe#w zOu=g`!5Br;DfyC0L5iAf5HNvzW7Ag`BYsiC+gk!?X&uk+wE;?~-WI`V@w<1Bs ztZ~l&kEyeaiaOc?{eNgg8tHE7?ov7w5$P0=?rsJU>29Q?r9rxp?(Qy!9!hG6H`n{_ zdhZJ#X0aB`+57A|zrDAVmOSUteAVtv>O%<8X`>(pp?&b&vc*%@5JsyV2vm~Vv0`pa z=Ns~ELijEq#F{;t&2Ol!lfDo2G~reF5u(u^fWjwyEPZA!_{{uAf-%6RVAX{;p*RB5uFu+R{& zhrdKYA_k!(+6J9RdZE+yL`L7?+B*L3H~x%-Vv7+!YD0F}!KoI;OoJm#7x(qU_-BTb zN`X-o53RX&$jv}SY2_)p_%Gfv%)@(MYYU4O*0q+KRhMj^@yiGJ7@PUv#g;u0TAnjX zy@GEZIwwc3Prpc3rN$*|aVizu+fSv+czv#;HgPm0HgaNqe)@5FTPD~}HcC2a4q#vu z#wBp}A~lSaHW;C(QNGme)+_BaH@6BX99Eg6YzUZOeLgOvj}DtH3|yR0cw=daXq=l( zuUM`2Rn?_UU9((2ko%i*fYa-MXCjO9iGWMP%I0lSxqhu&6D~#;u!AI>@j~;%o&;db zA?b5K(t!$Yb;H@%pym}6>U94}(uC@P0w~*N(EwaXz&D`F@u*vQCdsF3?Ar%O$8UdNFoNUm+XyBpF>QQYe!070 zFC$1O(Q{Y{?v6g?;s)$XhO4BO&i>JsPKHVOkHjy%Q_M3nuZ`|U7lcpVFuAOv-_R)+ zbM?ES*Tefq z8po%<(yXm}^>e*}+e}9ZFWLn*FVOjLHz&5Gh;3G$PJ74qhGsfcD&M##8w6MihTTY1 zoi?)JTMCFXWjWItJv_WJ^Jn8yQf5c?Mgb;O>&2h(kJBI z3!db+544wQ#L_$>;}sr-zUL3@54#o~&a>#)U)DZqJU)){DuJ#stPtZH_$SfSnd%Ervxz1Q~X)7*r# za5Q0g@;ce_6PLfsBX`@G?8bIX1-ecLC5_ytGdzGO{nuC9>y*<{97{~O6oe!;N$gS- zmIzfn8I{X*myIso92nQ>y0iG(;eAV)DKP&p`nBm|mfEV=FF(Pt5ih4c02RylO{j(h z!Mr$vOt)u}IRiS+jvV0z!Q6CjobusGRFp*v3D&DFE=%82IO^GCuXUiNhS2qt#w3;? zEoj$uM~!`~91~yr*V!?ht~RCSTrGilJ9OjjHPm;d_3liE3LbwnapI`P1cCpjjW_@?0w4`+cCFM^okSr)x`b6v#iHrukTS*?8>Vvk=e8n@47*duTfsSg z&k%CwLERXMYs-Gd{=w;T#}6&tVco{ING8HC3aZWP?+yd~ARvuh3ZEZU6Xq3KLx|fg z`~x?3(|HRcM>G~b5h*A?c1=d7j4cH5XVUQvjY0cYgG1lVxnT;@=_eZ=uQk3_QkRZz z&}LuK@FaQ3(;s|^=>E6uIicQnc>>-wK$l4)bHup><9kh)lXrs7V^slTyxDFj^L2{F z4W@W2S^E|MAZzD>p3sz8OUoj3D{SQ>N+w`WO}W%1OZ14xJ@)ohuJKnvABjkoloIal z3PHQAvuM-u4Nn45y`#JC3HQsu02ng#c7PRL%V^}8lfVm)p}l<9i{ix=+-;>fRKQ;R zn2~;8BL z3o+U)YLzD@5srG#a?8fqgs6=Je8A1VxBu`u0YBY}bal*U!{tS+Kbj?sF@YOHM|Y+r z$KoO71s!rgAka+pS!AVa{^!o!4v~HrW@B&3l1f6k++~6z_gjnj;RNKrCEi#~=sFU} zfH9vO-&^D;S4+9XOc<2|WJpyq`9#m5U<#)fwe|z|=a3FOe&!Pyb{i${l-naIsYaa< z`bQ;=HweI1xPqMKSxn;-8I=TNCW>EhXExXHY|aOb%Tq(m2n4jJ&NlaNbRYotx`hvo z!t=XVyp*zLm^!CrcLp!I=D!Gs+KYYlX*k(DGHMI!^zB)>n+=s1N)bIZG4n;J-qpck zDY+rGDU~1#`}hS7_@{r4aR4BV1*Spm0;5dIcKq-Wl%jG5*zAw0oHAM&H$LsmL9+R5 zc76#Qy1W!93Z>>5PLXStYme=9wnY@@JdvxAe~#*IZc}=5 zd_c9%!A)(Hai?1@9@K@n*{)e%+>ms3q26|0WjGXR^n|KXe@a^`yj|ZgJZ~_i<@Q&u zL;_oEw*711TPolK%)el>J;Aibo&Whf+~F*y6Gp9Ahg+yf1=;S(I19|wT)r`QjR=lP zQ~c;5I^j3{*>`&*C)#Ww&O)6p5smeQp$$12C0c#==U1JZK#7GA4k7xAKSjm9j3I1U z^|Bd5WS2aAv8Hsrx#Ocq9G_CFfTXc%cABD`83ti!w6O@H8@hmwG!t334R3iGq1(J% zcM19nvZl$Z)WF@+0Jdv<^KVZrO?SUHAne)cjbGkUjpI1Kd>wx)N3%P<($5K%xb$IrGwvGro@6&<3bIj z%PnFxNy&*|PBR5oFzBOz%gt53=eWx=cB;V&^_XB_jPWeBS@$UZP9MR7v-pnbyFkgC zIA#=1N=}sEJ}Z?Jy}UoxPZ!ZtyBIllDD&mNeL#r>*NxR9eLja0sabBTt^*!aOhFt@ zU)i0X(xdUEBCi5Cw=QD?uF2nvoX~Tc~ zzUsMeqVt(fC6hp|y4AVB(8hMm94awsxG!n&-O#c|on=p_PP=Yrfw{ zOQRqqYVmIZdId6!2D3gEAKVgU;&A3L{1UWNv+bC1Ik_Pb{r70zWh=x;ULnG#4#T0c zFRe9>S5M3rix$2Ob=byXFGkdU_w|IY*f&CsELsw~Z*z1|?;Ls7Vf)!+24H#Y8xBJZ z(#}ez=di>19;d;Z(s#3rP34DWs!%b&IZ!(C;keXp;Zx{`NTW)Px3+c zVCHXahgCsWL9ru)&TAESM&1Dw=D^?syE+uLm-ttLTrIBln4O2bzSjv~I%QC_Ktd~V zfj(YIVdca}6fQj!sp{eo#&+(_)6J*vVpO-A_)A>w;{A57Q|jIczUmmfKH%_IhSDW1 z@!{o`f5QGeob@}>(3NuI4!J}ap_nz|=c*Q~eMH63dk!%~DGeW&rh-pAXakG_G&iC; z;-`0w&z`KYcUg4lt4Lv%B8nfM)QL%~cqAlHv@1KdNyi_%25w=E9g$Q9SK_)>!^8YGj*mPkEaH$8qah-~ zl4br;K#2PIoD0#Isx#*^de{H;uEw1pz`J9KOkb8*|CE@5f~gj>h*8h?nQUXr<@rWg z-(}9NTyhsjvr2zNe8v6J!@)~%=0wa4=e7-j;9iR#TL#FLfLW<+7*@_RoTF_#&4;L{ z`cOW%c1ka=N57_cH zgT|CdjyCXOr4;`%Daj(zlOrHBOWy z3I;8iLloWETmMeA9TysH2SI-;ToUsv(3`!!t8{)#G8q01l2%yn`|e0$u_>sarXjEB z?O+(QZ7$7#<#0x_FxBmC&e=-r@>iR+J_W>Ci;|mU4qu(xT|^3B2t5Dx_R2f)F&{P6U9h zFv`+0*0OGB{=36o+!z^rB&XemEe6^oPP6)sKqeL&#){)lDawCS8N_Vr+1Txe3Jrtr{4Cx zH!aRwAN#4E&^6^vdvePQz>mlDar2D_9@a-)rQ)@2#oi3e@3*r7hP^B6XP-Z51~HZ_ zD$SR%Ue*iwF!ClH=%3#>7A-P*=DbOYZ>)kCI0y#{wmnUmRp^C15IC(SZ(%F}!rG{Z zlXYwA9H6*8VJBPa2+KyzNyYbD$yG46oObFyrh(0wAo;wwVEma4iT3NC^1qeL8-D%4 z5kwr2YE3j%368Ui|4UA#Fq>kO^7_h3QZ;}g1z>ZAHxNT8`E}+x*@Hx4IT6fdtE2K? zrDyb{w*f$&4hUR|)HP22VTUezO|)wJXQya)K)}OizS9Su6XF(nR^` zLS{%VP3B@x4FcS>5xCG;DN}&afQM=%V@04#D%A1AaG{PNUQ!`y+voufM`)m-HMsKu z^!&g-@85+cU~+T%h&@?TNggZF)jzU%sYfBEaM3}~RQ=X#Q)n{y<-9MDs@BCi4{ZkF zUu54b+J7mqdO_TqY@#w^ahBqvSL&B;H_S`A++#hCA8_;8_pB#i>=O*D)|W_;fG*Dy z8tI{>wT$)p~;NV_DW1NExq7%xmj5T?%h$^@O(ir}7$A#}69FoA`!>XM#uGOas8K@U3e3+`|HVowJR3` zikvW;rSNs zv&VllQH3D!;)}maOpN8kqp14gM6<6gXYivW3V>xE{qUZjZzRTghM@Jj^$!=VD zL=z{{%QiW<`mQz#IlM+Su4GAH4qa^>)dkNQ+8a0BCk)$t$y*i@5+3sWphzF|2uJkeM)l8Q* zA+^Z0%U2=4rbAW-+u<=wyL6_+WOhk*Kt4*`{y5q7W)?n*;A|lQP3n_?FBKWSiSqPi zZbu5BK?LWwR$3RU*UxHHpI%}6E|NAlw5P-m2K~{n^M;z~Bwm%->aZubTNs|;{SblY z@gNYyg$FXk_t~}&rLxr?HNdf-wb{q!TS6}B*4%{3lH?!{$~#FWAn28{8Qr#AR6Lsb zxf6ni(&mSKQ{nxk=+Vm`?nV$>pZ$Iw$kv{u$iy8{zA`tg^3tCpcs%zitKxY%BQRj>YM=_#N9s>zRSYtfMeQ4I1gC zPRRb)0>E`UGBD680NP0=ez3(}AL(@|s=YOvmNN=Ic^;E5@OKj7`~`RxrD*`=?d? zPl$}>>kYLI;?)sn4uOMD@Q5e&M1IaqdCawMn;W??G&xJ#erJzUJW4Cai{M>*jJ94_ z(^9k7SL;`8+lOUNNCo9xve>(sE>(YW*8NcDZRzKz_cRf}!3f5kILH|6Z=H73zULPK zq32qWXhc$f5j>=ChvSN`FEkdaFK4Pn}+dYkn;$e8+_x zo_9kTikctBr(I)BCkdN+C-(d^KrSN&m*HL4wvwJR6(TTf@v_!Ww&?549Ea|rF4Z4L z)8q0pi{=5TPbC6ZP?YMB7v1`iFXxqiWbnW$b)DCKT)0@|@2Emcv#K9x_8pXoF6tG-Q(7ufX#8QBE`9xIse*azOWq6}=@1JB> z-9$c86iBT-Mdq#2_m+qDY(fq{9`KsSkq-z=Dm59#t`478uVU- zddQk_O-O5_A--7rNO`sS5>1Bf5VJvd3hIC%c+}(wOOvyvv3hQQg!|m;&gIzYhGeVa zZ(Xq_d~thyQzx0Os~qE^(7W=jPQRT_+A-4mUjg3^=hV`Ffelny0 zR|XsKC1_2!xU!6K{qs$)TP}Ct;(dZq!F@u1fYIrVtE7L#l4Asf>y+A4Qa44MODmdU%BtQ-|_!+N$N0M0h(*^i+o0J1W`l^xs zHb)=xJznPfvtLvj-DMNLOcia4`OV+nAEG(}FG-*MPralIjj?ZtMd)j@#>zY#$AvjJ zujCuJ@9MROBJQ8d%N#%aq_vEGC|`?xt`!+qq!WT`b{)5ntZ5hpYLWAIP;svX3m}bWNoJ6UVTvwkh(!#p8zN@*VdtS$gYlX=DG0 z3Oiq-J>MO4ZkEvD=_cxhImv{OhFWXhzE`8lruN$mx=MH=Sz)*;2i$|1a31dlC&$@Y z`(ropsqRDqo*Sv8zc9=rfu)9YSaJWy0%$lfznzt*{)43i7*9SFDx+7jVlhZsqptlnBUQDyfhC>qV+$qXMd1urg*RtE=5>jN>m zQ0_#?mPoogOOH;Io)^xZ@1Y_iP{T4PY1Z%RV0p1aONon|0O?VJtZVVGp_@Dp7FUBo zl=RB2!wO8p>j+iHgXFl3p217{0?Fumjj&C;v1v-I^eoi#{v`1daN%@16&annq z_tQYv8updZpLHU~$DrM&)l0sL6FYaijiE{(cjQjj(hw)oVW#1ie9$w}Psy27*GpUO zPX6=_@sS)370rHB2EONKwEwRl&CYNq4j^D~b3!d3i9if~bf+{`s$#moHbV%Kk z)@Il{o=-;ZvY4{EhvTi-M6LDlbj^S(ajiadz>6czQoOa6P6+ncX*{q?dd5wQtmi#v zuZ!^|;?s8CADu573q~9R0=vNC#Bu0+C^(NLjN6l7g2Nr!mRu};$;~$=5Va9VlJthb zK(O}!=_h%2l|OI)v*)-+vkecZl~ncXId>2`3F0md^MFhrR4uC*^fU7xo<{XV0zy&UZY zgW1ERnQhq z7#pw!*)rmh4M>~l%%!f+#0=zO^X@ZejNDv>aArGEzUU390Jcg^5nelYWA7mlp>PaX z2#*U-53RY1SZ$h3a;OF$=RXO+`{MWkK0Xn_nElqGIS zf2LF|3&h2ROynyZs6hWL?!<0#cU~$;-2CJWj{d+OCU;fs!ubjS|L#82Q3shc9H(v9 z<~(L>1f1rF)kTN(C@kBt{tY>)5O&VkJhA?`G&pX{8*tI0dNHGn%yjfZR0|xS;WO%< zgQ_*X@`MRo9p#gMbqpCNwIG!4A^_Q=&$u-mEL+Ji2|qd&gKa+Z^;jAzt0r=+Gcm}1 z`MRLC{N!lA>WuV!c}L`QIZr%+TN3>NH%d+cO#Yk2Psf`N2iT_|qLEl&p4(_NlR(^f~t#$>;U5T3P=kWJ&^KY_G6%rjK&Ii^HfZ> z31*)%jMRGzW8b}Rhd{@c&t; zM@>wCsFlm(H#UR;bBP$X!#h+L`ePd`@LlYUUjd_edewrFo@L!laXrHN&8$SrYWyCE zd#qSQ9GbV$g>}dYBRg1izJa!%Zk+zTUin3OiCF(Q7dlE{Lo=eg$>arTeNNJ9zkj{z zz4j%InIQtX_-#sk_)g`z2j_tiE}!LOv!I4v$SbL-_3K}Ks#;u;thmBE4-Z5h7p1A0 zvMH277P7|ogd2Nv`xaC4;(n4v?og6%nFX`xa)KLG3dfr5-nlPk$5d*%$u6 z$TzLCr{5IBny+7HI8o?~&Mbtpz?L=GQFjZzNo_#zhVnuf&0cS z9xv}qAf1nEIl9(9tKS!}r7IdG8KY6u%))nw%j0YSEUut1n!k5>$Cq7RCPUsvQ_MvECpI#8PW@1cjQL#-z6pg4Zp#>@xJw?6f)SMVv$%1p-(R^RhF|%|O8X8z zRV_$b|7c`8#2v!r>9rtlu;Rz|oOEz{BI5Y{W-|J8YM;^!o9NoVE|l+gUazr9Wt%+) zywQ_EZ10A$yVo;O%;s|+>h-~(F>T}^vt1|UhFCmywhy|);|SDcE`{5iG^8xDv-C&%&Hx9eDJMhzgdyc; z=L82cQP}BMG&=mS--W4mkPGF?&2v>xF4KGz)d*aoeoK-EpAVuDR_n234YqLNpe;<< zA#kccc*r+cNU^J#l24w?0J&Rdz3Pqky&Dwa8@AP!P?9lIlw!Btj#I2~M!@4>&SX?$ z+~E6icV*Pye}|K-BRZAO1rL-{D{tE0zOM!-a?xCUwbAVgv!~`lod&Y9d7p179<~#X z)aDTsE7+8*V|Q9RwMe9DRZkxMA*%a$5J~Z-m7Vpr4g$n#L$%FiyNlICxJU5H7(+36 zOB2od=Q5io%kOx7h~rCf=X!-NLahzdHuhfZJ9Qk@pKq-i77JhOOMBfqNTy+Qx`M`Ksh!krtjt9w zeeV6u?aX@y2X*aR{Kz(7f0D(n(JiX|Wn=h75pTZMyDpPV-99$C+#@|*KsyE`{cd%O z)_(LxW0v&K1sS3B!Z$Kxd*5`wj)DmS)FP_URm?%`hpq2$zh0xBxBsoN9>$B{cplOx zXMTk}m?`RaqmF0Ztg8mz>*5$md-Th&SBb%S^>gx@O0g9^9UA(=D%X%8M_XX%(>R@u zM;Oz|sz&*BNWT+a;`gytX9(`Y1d(4bl@angfgQgfE!9(Ux%;j&-RD%{hx%L`yi=yA z^V9QnWFv4>zOaeawQ zE-tQdbX*x7iP1z#1*_-v%tpVP;Bg+ew-5*C-lIMzAl2Rt;rLAYABe*^00fv*6s`x~ z+=>nO6t3ClAJ))n!&^~mLM`5;OnGDeKkTCx0Wcu)l0yOi7Wdc)wkh89jU#D5vH5L( z%x2s1&s&fqofriH#BXY3hFcFot(P`uBCtiE)ZJy*N#0#wcM>Qt^gM&eL}I9lcz%OxJbJYXzEEV7!X z+ir!i`Ic333pch+(q^IZN1~31f0X3|ab<^9=EBnudMD%Z8_bNck4Sd(L~#ij*KdX zg%2l2YW(>`2IF%uulYu1jMGYp%~!;Vm{ z8BnD(sOF`^v}RCORDrZ4Iu z4|8~``_I-Pzy~U!omZ*}QJ)^i_k`8(5{pUTKZ<6+G>UTJ`NSFwCk0{Xn+cZ`NeMDM zsMEi{RM(4}C2EHw(D^<47Q*WxBqVDDM;E@jFuglUQZ$rc%!5_6DDF2<@d@%n^osr@41WF6E8oB9@Fk_8Jctj2~v*A4s zn^aHdB=pXx@dw6dXii(aw;2JZ=C^NXOn(A-v!g2}r9nz>%}DE3Nb<`BF4w5m#v+Yw zu|z~bA}>0qZ)Yy7VEt42J6F_uDT@J*v4}@^r*M&zEbs>37egspXw!u9rfjJh-{zsQ zn<3rw26Q$G1NSS&gP^Xqrwfz+1b3ZS|GDkeu>kN|dnoeWzim1_1MBtEIC3^!^dDs4 z;Z5b9_`j0PqfTPc4_-gIV##B(_2(ag@lnULsvgGCczt;NiMK&xRX&-wCK?+_?qwYv zhdzkQ%W1W;C*}B#jBgY8A2wlv@X$3NDNB7P%pktx4vl^f*$5R)gefeR71UqL~gv zf$f}?=D4()@5{~RGUm$l8WZ?!v>bP*vwZz2Z#fD&t z3Io~66^aO}b zUD{C&`3G3E4Nm<#n`*J^gA{_Q&QA~zmz7f#lNgzH>97ym@DXJ-0QlM~sYqx&95fVG zD4Ve{G?V>+GcE#Sknu*2UyJ$cG>FCmL`|P2L-RlOP8k~K(!b8)osmT*i=DJD6q2)y zqWs<1=|flLdjL~hV^J91?3B!fWYpQH=6XVtfVViMF zJp7M2fEV4xfv!C>ozS+dJRQvnecHf!t=Z9~aA^qDKjaTyMvoHd+OE^GW`6(jP}J-I z>%GJij)dcQned-9C--$1CKGa}{F0N2$kOMGS*mR{8?KH)pk%qKbz4&JyDl<)4@o4x zw5cs3QNR(3;WJ#nYyF7Ft5z|9E)~@E5+=pmwe|4Y?`e!`E)?(CGdIf zjH5i&1G+|+RQq7r)B*5=3fpWSxO>k&IRR3p$0zq0-Le%jV;{)!WRs_KLt@JI6@ycJ z#yg+Mh;xV;tX|ONM>~eD;}b2E5veB0B2M4bb$I3kuC=d!yK=1ZLQb_RQ55_(w3hfE zxf`^)JN1k;x{RIFvbF; zFmd9|m0aXSXuIP4OKW+E*C`Jhg0l>8nRjCYNen!0 zeK8*nwJ6B~>lN}9^Otht>CJS$X&F>gb#lVpqUis?PApoWYo4zu`uF(BN-!u9{PCU* znw}~9`RQ-uKkySv696(ZSg2D>_&d_+g~Y62?_(r+wh*cO@#QIhWw5P%tEY(LlQAHs zFD6_~1*Fhs-=+JnQr(gnVc4_Jw{AORyQl2D3$A$;>hyHixiuDWZDi(;cw6%}&jtzt z3g1;T((>Ta;-2uy@|zaW6l4j^%Xro_(e`+5!0kiR_5%Pc4s(Q_Z(3iA9D-D&6L&rJ zf232Ieb9<3H|gn@ZhA%^a6SxH-TI0To#4I^O)!mF9xOdqlat%Yj^8LWQ5W-aN=1R_ zPv#5IJW7tKNAVNa7*O1OoR6k$;e@r_;K=WN{G_&GfTLc50;xm$-IvCqP)3A)oH^Wd z8Pf`%++NmnUwU+VdgK55RcFVy?+z{L-iD|2DIYo60mFDV)4l^f>fD~?d zSq1%l-JmgzN17b|Nu8o?&~h4^R>C)$lY){79Q=FUq-rZiQHNoQd9r^?v{yT z+hP6Ms7vmvkR|$JiFIqO_}ag_eDemNj#|v{4qXP1l19o9;>+{NXUw_v4tCv|{RbBT z4Ca9=0Q)voY?Da&ZT<0r(eB+bn1DWu>VaAPW4lOHD1B`cAA?P?>vEQkXfC<_mxr5E zq_zRjwKMWGDydWIM&|~kjXf(KA}VaVgZ6xVF6jK*scc_mLoRdow)~Z*g+M?$ zQK7>QN6KLVX2g@FUjR-le^AZV6E9PV!Gs`ZM~GnCAM)u3F4|uyIVg{cfdfmo!y~!L5Oi1#@OW`)VoYjh6K5F9j~juW`?(|G6A;q{ zcqV3t2K$aZM)JTDYoiTLd}3;a=c8;@ZTaU;;*68WV+27?azpFx|fR!=vk0o+!2!7-ZrRmFck1#s~PoA7$esM0o=-iVmwExQi|dNJ z#WlM^rlkbFWz?E4Z1A55hkQjKDDef=G9aWdnc$fWA0CULN9H4eC&K5e@hZn_m;|Od zW~n04)oY;EU%Qp+1Nl8Rss+#~IX2*9I`am1J;>Ix@#R2o3j9k_tI1?r?Bexj>ulb^ z5DkBs-3(!)y@%Xb6gq%8-QHLQAR?h4pV zlPbc$flb^obhM^USueSD3;s4b70_(vXb=3!7ra>H_5Us}dA*hCcu_;{GWwY>8Z;Ks zA8l>-6T^1l&5oOc=-^|5E$`gxSqK=Nb_n&jQ}`dh z?0=T=bOZpu?w3l}Az-Vtu-!vqeDCNUH+uO0IaL|}M@RZp%xk_I5@Tyk+9oekGrxWD zIqtuwPU+ArJ@i4O+{nsn0cO;fuezZ!-$7E=ic^v3=+$ezNi=hxkzS>Q6y$onYG=|` z)^WtOD`Db0r{;SJFDWgjcIJ#y#LOhiivQ}4T%rTIjuRVyWd;T~Seqnc(zJIM8P}kh zARkdr=p-X1j!E8V{V6s9k`0{kihvA4O1GkdK42B&nTUaPz8cxTT*z#lb3Ow}i< zTFf6EzWt>ghKLC;Lr{ORf=k{M#@!{BdtwkOW4JY5lv{|$1>A3@tJrsDe5(s*sSN*6 zx}*I9z)=Dw=^assLlvS-k@!O|QUv`YE;}4wD>^fu)O`|iOg1h1V;mPj{x4^R`-{I0 zr4!ORU=ca3sz9LwtKhyAq~mu0^`aojzu!Ou$V=ePtq&rc<&N>r#nCEugGr`G$NL{D zC!R?jCDKN^-K3auO5JdCcJ0v~XCZb3axHS`$q*<~7$L$eFqDHyACm%HKz!rj`SKp7 zqXa5%1j$j(ITHNqZVVs^bOcwhirS`pq2Cr*IgMZ} zGA*iMX-aRFiMxD@vs6oCr)A0$Flp4r+&GUK| zrc{ZIKKiKLU2H`&#T9~cdCU7A(mdhAqUt?tD)cbPPp=k;Q<5W_^<$mooOU2-37$WP z#7xvqG%r8D+ju|mEdGt6)x+t$F`(-iw(I4)*A}_Bwu~7?YmV?;=2ooSQ$C(yf?~bd z3$hx*(s2wFC)qVrgBn+phK(HeUWt6^jvU-+SHbOcY*3oQj+vi3yGx za2*nXG`&Y=UKVbm_w_VVBbYjrS{3^GbDYHxmT+^Pm9r7X9lB)|^sP4UV$3`76?K|; ziC@@f0B$6%i$Xr+n99qMkWuw8a{ruAE7s7Mkdj(r=M29b1-rtDiGQL3Ap^ZN3?A9# zb2N4y)F+k1N=u*r73AR38sU0_1Ww%9UN`P_vrIjg=+vPIhjvvwG%ic1WFz#_jo zYc?;*aTyj2(9my0?-jBigNiR#CA~?RvRngb$=@n{-PFghQHieqy7M?lZ=t&rq zDeS8F`=&(7OTorLhV@ zVzHne(m_SV%N3!^5Y;Wa0GO|?`ogB39*MGC(2fxTkVXke6>4D`!|P~QC}tmPpNM`w zQPjZy%bnqkIm88jdA^$sv2BNObj=X!gt`c?A7uv4fZ%z<$tT2QU;YcS z*Z*SyxE&(aqKgDCw~55RHMXBDxfa>e$LJAH)2=Wl;dlkMm9#hd6$J4D;JoKiWMDHy zg{(I})Om;fKjmieRag1QDKp-5)j{8w^KWlAag5@G?_mn)|BXsUK)|fuwg`7F_qSD| zy-(3;O-t0)j5MqYh|**K*VJrEyZ{v5g}PQQ=iU=X$t>2p2ri#WkxMg`KgrM2Y7YK7 zHUt6pnCr*g8jfnSFxevpP7BNCa;^E0oC?uKHjlIVGaoLWDarHNI-j%c)T=uGxW|kU zVI#Qh{e zY^7mGma4GcHYrh?eD@@cvY0Kc)c5BXslH+C9%Om_x)F_QbRA!LKFPTM=6Pun);t<6mS1Ub8b>t71`O(-1J~ ztLVL#rKcJ|t|eVXT{F8ik~B)uJKKf}$*0@yhr5-hINOE4Q8Mag8770cHQLDaZ*00F z4}=2_n8r$Q=MD8%cT}xpiZJDvVm2dKnY{MWpL@p|$wsEwKNK5YDHt+$a20+)p zq|s3d5jb=Mk<jK%&LBslOA00I|MQq7U8ShS!N)+w(; zh`%lp_p-J&K+Ao()o`jVYa=)tziZFY6GnWTzu~6b@rKHevB}9^FXqY!9HL9XFo$g+ z-#PU@$Gs^xa+pBeZ7HfdX)YpaL`5l@x46&lTz|&&5mcA{0(vwW%e1N;K?bfmr8Q(R zW+>9-pt#!*WZ?c0)aS}{O&T09gl>$-kkgSqkmHXBeN5Gwsczi7v zh;wdtZMdQ9fcpCcc(8afdA7zJ*@GqhPMTLee+|fd=OPWpFAo_lw0t917gtj)`Sp>8 z7~Ne4c~(9Q4KMg6f;+;clG)4hd{I8LAH(p$HT$y7qSks&NQrE)?E0j_fD<#-K#G(1 z7t+2t?f^RSeL#e-oZvBEypT@{pgPswidr!m5mJON|JDKJFxN#m2B9gbff3lW_ zP@+-3Yb;OGTg4TTY*SFA;#3Jmjk_F-0=EFq(t@XaFv3~lR-f7Q9pvH=sXA=u z?Ma%cRoe2MRRTr1&tz!$P&5fyZMw*xx`@RTFj4E>!`y;jF6N;&>6isLV>siym8@B# zmN)ArL2#+cy&;{w&^eqS_?S&}l+A7pOpvXPRz|KF>=?X9RnZ7fkO~tNt?l##j60&R zlbxAeCtb;kCAZPm|E?2NJAc6#^T?}|&LnWQPC&IVsF$am9Mwbk0MpsQ!BOL)gpk6M z+7l1PwrdTlyCM*pRV+CnbNV zBKCk~@qTEGA)aWOq6w1=(mDnRxTfFS*Z={NUD3`+`9G9jEn`3s0qQ4p{t|?eN?$4i_Y1v^q{->O)Y< zRcKuT0~ml0vnU=AY*E%GB@zNfZ7V*;w;GP=pUt+>I{xmkKUAeYW6aEyg`2TAH{~f4 ztrCg2RQ|NxlmGy81W>!*$LOnZx4qkRx@s;bKB&HkCCKvwxO#R6dSZw3XFW%4`=SkU z!)&-36h;LQr+GbudUr`$=<_T*uysI%w0wx%yFts&h(KW{VIQv3|DowC9IE<)tqCa|ZjkQo?v#d0cYOEvzW4rwz0aB1Yu2opWeUs1Q%kP_ zp`8E!TPzL_sAu($q1u+irC59gX9p@G4`>2nRwVc5QQOvLu<#YC&5|-(N%(_-lzW z%!c>QJ}xfL23z23fG z5OoTH-Bemznkl$5;@m*b(+xQ zy`sz~$!n6RK?1c3WLDNKGFVvVLRcduK8W}p=)!aTq~i}zo^7VmmALCUk5{x^gWr;s z-tV=BmK1O+=d7m_!0!oSEnMI6!y&)1-dR?u^pc5ORvApf0JMaNm@Xb@l8v5C9<_h`r7^1^Di91vjP_yF8XUeYCtfw&8}JB2@fX{ojry33z#M#2I<8}0A( zCbs|Nd=$EWgF49mua4^j#Ij(F8d)X<{#0njwPK5NZE}IvRw3GOoJx%&{O=b33^VP= z9Y|QP;=^SVf@#jtl!?a&sG?-}^G-4$6IzeuF;VB;!2Nv0lY}_HRVA*HOn47QfGr5J zN%D%l{ox3&nZoxnhmII$5f_1`?Fi0rKoEXc7f?tUtGF3vt_hF4cIO^$zvOxfWEV^E z0e~%ilyK=ddg$?sEISfcmoe{+dkIA$E4Z1Bp9RqjVt`hCCR@;L5P`K5!2jf}Ty+#Kth>Mn_CPE=&N5S;wKW~&^ZfkKW6}AO z8Y)~IJFZvH%M?Cwdx_5;PSDNwYJ}971(ocL|{an*lHbvpx6ckkFf8)On zU}MQ}m;{d%H2B$-iB!Svi~_)eHx@9&t`a~0OOS|3YsM0myjc{+<9{dGs>h(-J0Sau zC&-Nmu&&lL23ojqxM(cfF=e+IthpH1rzOj}GF-ICo*+mP=mRiwz z`XW{W|93AJ&8;4sRdg#0CZ+*rk%t=Ii)sXgsbx>_g*=LiwQkBRR$w3VQ7RITyTtO;~pE z8qqWm7rsMlUV8Sp9Lsi4wdunn`ZhcnfV5+Y8oG0xG%!S9^>G~xUHlgK$&~rq2DH$m z#{$elsJlqakvFR`%}?{*+zC8v&^l+w<4fA(D@u?TWjM9Y<0mSds0yK&Sl_+Xmt8>- z40CEyE+YS538z(!$KV3ZAI(M-)>e#M8o+m3piKX#lx%}F6IJtCKn5%`Jf$qvLY3|y zmduZxIfSg9C$tm(r{ZxEr$Zf{9|nRaB>D|yv){OIS|vA_&7R1Ikc8O(ZLkUf24i3p za2JG*#9pS=2ftf@+jf~5%G%zAC)G6W-BN|R*n2U5rcr)`LWqZgg2IDJrl5m0hBNyt zwy5toeqP&poUR|$I^a6ZUCn4D(dUoEL5cDlvB%dNeBP0F?3sNZTk37U{o1eA#%-{(-$CZ)vg)f)2GLgi+;5>w=Ue(;gXHU7rm;fZulFKdyRkwO?;D=H zpr_ZYmm?*24r3n7R+yg`L6pqtLxItZThV>%WoXAn06-~>=00F8uzvLua7L~|XwAyesm*vJFy0%2dOm&F=Ba;x*NT+BHH`IcHJ z_9$2sK8JT-{^Me5ZSNhG(*`Gg1hT(ci7Vx+Uw>Byd9}0jDO^6XxXHF6xF-5MC`RKJ z()3M@Q1}Lkr~v|^E=||7y|h^X6{&$zLTb;hlF;xg$TOIH^F_YnF&gZ6q6F%1UIuWL zQ3Hx<1}Fq5&FV>y6Yi0KFA|u=js&^`D@e%w93l_#DE#Nm!eIkseTj0y7K_D8VRjh6 zpY{k9U8*i3kpIU(yx8%0N&@|U43PSE!v_$_=>)xf(o`@P5#TV;CszzHN`$JxO16YS zk=}QD+-%DnWd#vlA3PGM4g5v!sPr{XCbG2Z6}0OslZ6J(#Qe|Dfa=*7@43egR*n}t z>eBv*{(OfC1O_99t9iEduH-5|;#i?aJ{=Y*p&a@sli2EX+47sxaDsaL$6Vh$o#ipL zMdpLv#ms=L=EQi)F7?ZXoVdIGt(+em`9mY5tJL!WCjHa5Um_j(~id;3CH>tB(#n;e!rcrTxhaCVvZ7^4*DC zyDQsNbEOp$jvXROT97@@wbST=)caxA&%Ll+iGM>S2zu{%oNv8Zw_1wM#rfRGR~Arn zmb6uu)LHIQ$54noCwV^8!?^H@cBB&np(3wv9)IyD} z$o>z=QPfi+#%5d**awCV@F{I~U(p4>a}Ox^b9q>YS)1XAD_d&jT0>xjmV~>?#!4De zyI3GI1^YbE)rjbrWBt367>J9|ldW|+LGk|{MwA7O^uZCC}eJ3k)ccLNg1wfG=d_j@?Ezcm+ zdO6Yb_93VB&OEq)k3oVxFt`!PB~lzfn}~G92#1mu9RB!Y+AzU>`OC89JMB)GL?}IZ zl{O7?sL>$Qpn><#L-r-4u(UpeKy6gNFzFgHB#~Z1P>65?Zlno8k@D^sz^%EZj zLG~8NDk}^|D`PhUeKt=@_OEE04_9bDk1O_$?~?a0P$7Bzj#4o-`lt;nnHvmv)^WJ0 zb6$BgF|Y)z3sC+puKtQY?a_ffKnxuiQw6%>EANS!b~{v~>o5rI+a0I&W z5_g*xAOn}TZ<*d3*UQdF5+qx^(zHC6$MhAP_>VW0d}oKiY>=kH<^2Yc&5gQ&?yZ2O zGEk@$qf>>{E)(_VeAH;P1JZ=AiypCge<#kK5J}!JVX%bU+j3XqbsExkR^pZ zyBzC^%Cb?Np!)nxG>c!KxhKc<-UK897~e>QG#(f`y0na5#&2q@I^OMrox@En6) ziL+ggiP%pZlcuvnwL?|=wfk*k^QQFyf45gNPX5>QJ1Zz_Y+-|OfXmf};LXakGwU)3 z8dy>Ias+1NzV!{xdA<6vakr0gnMK?g$sVQJAG2Y7z^pU3;92L2-L)X{TRjtgf6R*n z<5KC}N91ExTa?3&-_KBATooa3_~+$d+{O<;9HnBkaj3tMCFO2%NZY^L#j9Il`(8C~ z>|sm~d`3Jew7g8~u{x6-M1Yv4Q|SXRW$;1)?@*e|*o+*K=UFc7hQ`MK&Kx6ZjMjQ4 zv;sTJf1tB?IxIES@=Gy6LkvOJs3=3qLFU8itM`1SLSX%?Zi6ZK_DRy_3s2-tjh%&;KrV6#?4jhxp?6ZD|(`X8H4gCB>__H;NKhJfyTz} zoQlAOt2TrD>foRG2V6CdQPsGi$b)?!e;(nhsIqMqH4I=t%bD(qrBP$>2X}T{;bq?L z9H?9n8EZ8LSwlc8m)y#nZ)J2>E7&vm9<;UDvLBSw{dXL;TjDFOv2F79c8mImnN%AY zAT3F1|1lfB$FcNIW#gss!*%U7x{yj02{oR#+hCe_Ab%zn;5v(Rsv7F)h*=BkMt;K?zf?odJoOA+p{?I3e`J#`@u& zaLs!W&Yry)Z07V~%B2w>OI5p#K=@$;g^H?Ga4qqbRZ`;`*Ji+0s zCt5Zkg$$wpb;IRFfZLeDL&cfphPVrt@>l*unnU}e!DYHNcro**D=f2_lKhrgb zUIhX<+?y9Z;Xxu)^v!z?6&Sz)DO7eP3{ORPq1zAery-Sr(joZ9hp}rin3EzUWLJXY zpp4zcpfy;SI_Ih^W>jk?@bTGC?|(8$(*fRQuM}2?s^ikT2Vpo#x(@y24P$tIFa=_9 z5>r^@)CzUfLfvA-tLN-0+rgT1j-)t4eV`nz#D_Kky7a2)mN5Y#inKVJ-Dc#Mx0vVV z<6H2a;35cxUX?ipTeEksxf=jJe0%8{RG}^37`0&&3_y85=F6`^xO_!+oFAY!dznqb zo`NiQfCZ`@lLDp@ox_*X?mM30MXvidp<06&0})pinxVhC$AnhE0dphiUIP2Fi1Gw= z;TBmBE*pNgHl8E#-dLZBFI`?T(<#t3&cy>4NP*lM(2Vrk_q%&9(N%1^hIaKGsJo`t zj?vS)L|DLfwuOhWe+}U5DRdnF_n|Xu^7TL~K&8zH*T$57xYWm2VbX7=jYPoI zjG$E5SP)6A&UJtdt8sT=v?X^;SZH*qrK2paG8@pfyURD@#UB`Z=N~cVYbG$m?c^#01KTQZ>x$^|weC<4=YdVQeA9Aio`47x z4}-F*MQzSL6azfYN^0idFGMNle&VjfKn5m(xq<{T!Bv#lha8BNNf~y zFy}>mU@)6vss?InIkiHE-{A{q53FP07PjO{URJI~Ky_&<7l?f0eM9%*#=_pr#S)_& zB>|CCR)Pu-arjZ!yzBl_+PnSYfgt=Fp^zEk=!rHb+g29%ZxqM}T)@0(M?h>R(Sm5m zgd1%?ccJdq>@KX=mMC9qu2H{gePbMSS~Tc)R131P<)*Eg72)lA^Q*M?%FJn3yjD^{ z#)_o+b10|?>Xs#OnRjpdfdZPrpnvxC4{+ zy6chtY!JCKv+#t&2n!2mp; z!h)`r_y|5IAu{|n{R;)iau#M0T8zgl{kt}NB>0dG5|9iSNJFgHabT$U!|=*z=;oYu zhU||-4Xvb%NW^!TtC8uvvApIvX!x)x`U=m|FI8~{54e!!C5&wIZgdr~%ftt^Dlaya^uFNC9iXeBT* z+| zjI4k63d+4*NIN^u?)dujJn5%vE)CgIjv61m3WP$yco(mq*L)}SrRF&Yg?16kNe6$L z)klo)Hv;}@&3{0GxYEd|lrp-dHE(t7CRe?+NVnmurExcz~F7=rdU%5$7j$+rGK~!GwDdO$h=( z!l9B~Bn*9)2Pd~nt5<@W9pieJj_{SH5<1R=miWi1RxYd^X8Y!2&bPDK z_r4#HA1+featN0((MwEt|gcr2$gDJQ&!kH#`Oxp)mgl6t>pQ3PUM zwZ-k5`1`CfA6!9g4e0t&qnI0SkPQ~~m18xNLf95`#hQZ`J|?j_Y6S7x^UCmjTl^tq zI(o2@&Ubqmi=qP>_D@RnE2O3{u+h1)F9+dnb~QWLjbI%l^en4`Eld+$n5pZKz@o6y zjEgf^d(V>)pDpSt(`lcJQ@(tb5!w#Pl)L64RG*{omPG*ISV3%6VJ$Co(@q8aJv-rr z2H>wRbm3zGc3V$au`0wsMJ(Xg1{P4_7w5mT0;87}sP&J(h;-E0=I-E`7c#K5#1~>j zX3Q5rPG264SeRJ>yl`>RYhn^;}U!PcX5{&Xi7=#4N>>0t3bf6Q(9y zkxzXmljuYDuAbvj(J~)LMAe6nrkgShS;5p9uopHtf2q>!t{7{qZ>++F-_e9+`U4^- z0!jiaoVMW(uEXCw*lc2jfe!)T0@g%LI-1$8%IoMGXCu~@`tBf`X*2FA7O0}cY$5wk zTlEJyLw?WXV;EJPX_q63d`J$BL|T+sY=jnA>(%)`7Q!44$i@D>MoT8D8yz;HVLN_d&%g{WbnzMN3&a;iWS_?tWJ#U0 z=zLLO{reDt>#HD+l5HLBsi>sfvR9i`0{6v407jj9bT&pL$s*GOdh=!Fo`86>Yv_U` zs)2=HKTG%|JmAopLJxcXcRSyw1+>fO&G!HqaK=!Z-4eFTWwJQP39v0d z^e0-b_|pYtfumcuz-DcHfFF;THzM+U+DpjBAHDf{h5ojg0~QM-tp6OGOJGC=SxW6M zP@KGXskWun*%rNiuie-2d?v0=*oza1-K`UOUeJ!0;toCELjKRu|F(M|SU%9C+)awK?4m)@-UMI~24PNSMiUP1p3pknXHli&$ zw^02qvU092dNHW&qS}At-HrrUz&Xl(BgfKfh~k2;fJtJ!3Ke|ou?P8n_?Ug=vOmFK zk}sEa0a0%=iA&Wu?(#`G-|sDUHnbfl#G1}y_msvK5)cbx9;`30hYI{WAo_Z!3t`8k z)XOj&H%2G7Hbow#vz>-izdp(e)#N5v^wH9O{z`IOp}+p%-Z#0CJZz{+Hq0^H)7L@Z^+4_DT=qg= z+_i;79VeBbSn3OyD94w>!p-XY<97Zn|AM4JvfgW#CsMr|z1M!a;0)w~0cpR%IrN zI562M{j=6<6m<$_lV;C4&s^}|RD0AS*)87aEjnJllId#gXo)^@ER@pFQV^~x99b;4 zRCoI6=$0jq_Q5wdGw0%Uvppa!2$TAX{x6GI2==R7{-UfSzUMFy*q}Z*YNS+<{x(*p`K+n z!O)zI8?|%qEd!y;jDfn7eZajmd&U$;f)fjkwGRc^QeSKh^Iqfu=zLqY=kpmiMn%K; z=y{Drg};kB@Cf-M58l^NQreFbh4@7c!R( z)_)4B+}g*c`yzd@GXnIyH_xoEEe5kdAjCqCB|+S9xrO!~b#|BQ@8`i+RwS}CZ$gY|ou6bP`Rj zX#Vzfv8suWO1+1{Bnf&~d1A0O#`hQ7UT@BWAY@k;fNNGCjqNYN+xgoiwUY+EeQqO% ziWPf7)`dG}mO`@CM(R@m zX*0ZMLH*(Uup{famLfAeMn8S$b8nEI!(B%6>qIAJaY3+UX2s42Y0rU;Mrq=)am*wAjw?h7`CVP#;&1|+DXXr zhnL5t{@{m;s}H`n=#40lLHAok^ygt9ODyGGP(tra@AE!MqGQh*099SQY9=?~kzncw z>%G$dcy&PhQ^Db`L?|^c%IycCxvOvWmk#+wB)(&dPbihCyXGiGpQyVPS5jb@NwV$ zJ?cs`_tjYHS(|B`FRa+=Sdx?;IyqF8NDb{0I<{EVe7RQtp+`$(pyVTYvYbctP<`C5 ze+i>EgfHX6{;bCEwW+1s{y|!fQ{4#8>nZOD4zT@HD&JSjWsf|y+;hJF>2*q$f~7}T z(2-)Q2R9Vr3L5{lsIIu)Z|Yz6M266_U$0+lN!6J?;}Vq?$VKFN5efcr9V1Tb)jo3E zxPL2aIgLo+;^CPXy7A`}E=AHSnhV%H8@)qil}^ z`UN70WaS7;p64sO45I2Fqbh~>0Sk&+w3+ZZopC+(lG1KCJbv`Jz>0oA;n1WtH%-43 zv}5<2JI=oUHrFQ_}B+v^&28!34 zXki&E*jv;m9C?3%j!d>n$M~l}=pa!NsCLvBgTf(xNYlfVyc7yL^@yZgF+61kyRP)zXM`B`!cB8#qw%~q)@$L~0p zpx|()KKH85KG$8>3x^o6cHOq%^l&sp}+KC{AHa_?-{Zh5z^Y`+_| zL~`Jf?X=n9l&vBy1p&F^td88$xH2``mdpi^Dp(%?JdwN56QIQc;jERo8jYidq%hO1 z6rhyReJU6i}(?G}2T{OX5n1Uk8KKjiu!exl2W zz`q74*uM?SCs!nABcvlVo8oL3W$eueZh!NEr`xVoR>0r-48<2=;9l3Pi|2iV!B*8a z-{PaM9F?9#>XDx9MCd$XC#G0Kgc@~N6GG0*-eSwmjyv`;bUwmcav{Y;%tT1R4$m98f6rv8k;J!FhKJ!F!^T=N_fcCGq>fMC(|$7N!OobwKt4S(ZGjE(TzUAJokk?RpU&`|>eG-wjTOrK@n( zekQn8xaU3ZLnvmLD<3T%F-Myb*~6$6|^bTjZxN70(^E zrLav!c&5ZWyLzgMV}J4Ed>HW^3Se=5dUPli{0-MKd_jIYTCXy_-scRF@bB%5+Mek^ zwP$TPZJy~+WMibO<(#NJsMSY|kX(Cc2M1o)EaYnV-wA!4L!EZdV52eqD-+;^-Z!xQ zA{WGXF+@#EM<bO_GOJ2Cf&W0BbX>im75^$e{l5Pl^)ylJ7S1(hFAbCsLYxgmiv{ z7cYK7;Gj%v3q#tu~@TM{_rDR`tM1-D*Y4o=-!x?L5X9^T)7vZBa%>8MY z1qWL3dz561_W10ANJtgvh*%8DbZ5;Vf!jeIHCX&6bijP~2F_wf&v5s$PZBF4BrM8E zrP<4sV&0E6?K`a4FS|Bfyu_RNKK^oU4>_D=Y?~MlWW)CDy0Hwk+(V?1Z^$~-3B>O$ zw&XfuZgk3sI^8!Nn$&h!kaIXm~(^WndK!nw8S*(K0(I56aA zvAW+{#`^5NE_yueH9MzyG-Gg(@Va1(b0t_q$IoVc!&F8j6|lmo;^?sMb*p`nAEXYcCW4Mog`E_AmGHZG)g>UxlCd(O!V4&R z(jnLTz^UpuzB|7~kS&NU14Z9mDghGU1i((JjYmQ}p15qQZEX#ozA_LbaVA2+KFwf6j;tz1c2F zjm=wi6cax#e4m3&M3!GT^fK%GDS-v9Tj~p1^4S)L0y6j~W=>yuaeoGn^qHQ5wkmxt zQS8m%=TSJe`8qz}zzm#3ShGJ0&LabJQ4I!wzpB1DJsi@3xCLMo=bq<9aHxj$#5TJI zLV$v*om>is)OS+)U@#{G;!Ht12Oy@W-Qfmex)jEiBub>;Vw4;B{W*yaxtob~EBkqn z-|-CYqbM&({)T_yyMI66^wt7dwHA5C@!?cmYFfR$Tu3j7+E)0Of!}J_{O%wgU&C$YF5@RH(E~!x=R)8ys88S-u({Mk`xF8>-!Gr-MLj z&IhO*xP9Mc$x!Uhi$uqk!Fy4dy!uFp3{;JMF- zc8fNLm(IZFWaM&QVj_^%2UqFc|F~PX5E&5(uPRHm=6AIvA1HP~M#~BI)fF5_>tjE4 zsu1b4dmpIF`qjbR;OicT=>KE0Xw_B0`s6jHKj7x#jd$+C#jyO9SM6KACXo`e9KV$A z3#E*@!Jt8F>_y#b#}E|4Is7??I*$aUna^VB-PfGE4FB&FO8#`DoyIj>Ci`WdN%<|r zn4P7OeI8feCu(U?rcCH@)oouEsS3=!>K$2IzJ^Df1wfmYl`pxHkvH$_hXv6`6#BtW z(jTa(N|2l!&+4((HM8AD0G7g(K@a;_!0gI~c-2SQ^$E*-3D-8gd*)9JVnK(P3Z(+L zks<{(GLM6x7w@<8=nrbF#6oZe1GhQEuMh;B`-YxG<#7yEbTQa^F45NF3Kp}ADkG!Z zc%>LwZ+C022O2^KiX`}_cB)x>b`X&zT3&!bvIcU)2XIsM_nO!x?3KcNcqwdiP!*Cy zvG>>E@5c|eQvRcHH)hTmdIOPcYH3tcu?Qetlb2V5lghzM;U|mn z-nsAfD=r8*uwSMNvnF_Y9%t`I$(S0+6oM4;k&_gtV66QEWEvP@Lp;BJrC0RbFL=55 zaKGi<`dgR+TY`Exon)X%o=VeAU4NB75CaEqN`<{z>dH5X ze(8si-4~S2z5|h0&xn3csl3QV%c8=$-{&;jn3uTJ&NAVdC#pZ24SjnRj~_M|qJPX> zkBTv^@x%ZGH_wp!74%;!r7ZnUVVvza=q3U5Hs;nw>-TP^$A1-G&# z-@H;|ueP`H4KvKhzijha)6JkZiarXWT)>sSYWAYb@70m#Ms2-B*yogo1f(0lLDxgT zB~1CwU?x}1oZ0E`-u^s}>yxL9csu~|S{X?>)pJVAuQ}WL%qAF=%yb`i{hkbyQSChm zugc63&0<$C$i^yo2y26y4Cyr->=j)+R<(uD%A=Yv=0+wqD}A;8Ibu5#pr>ei(lP_Z zd{}SHHBgpkdel-0f8%nN6Jb}nB8FfpWb=?QCVgPPq_ui@t?PD3y?(a{#Za|T6&;;h z+?_-pqxrbfw9y|Q%4(i}!!BM!pheh4$1;~Fm%sFuq$WvVw;zaNFgE&jtVcIcTEdZL zgVnOPLsN*Q&ndc90sfmXWNYH$LQsL%Pm$XAtjTn#y|%*t-0~m!kjQ~4fm8QWCv;}^ zx&>}(Vyh(%#qbe3MyHNk#ggb>kJB)1%8@D*CPG!msgFa(G`Gk2-(`&Ya9Vogmu)W^&#~J%4f9NpP*3t>FI~_ z-PK4tjE9@qj$gZ6CD4p0(0%a?0v8-O8QmI=^Jr1H0}po?B7Im0XNo00Y{hcWj0Blw z=MHt>cxYYTHOf5hLpQ=PqGI5Y21Cc&5h2a4^!TVcJ%{aY<-&I!Ja*clkrE>~1y;>z zk_;hb(gZmF*g%bwO_1$o-wl^ecL4&B6OKAgBlyp2rGchx7fC98&%IB5#FF=OCf=LFD;sb;3MdH2BeF>P-^8%F$NE&g^Y~0^&i2M_G zA>xg(CGrsX&xmd~lft*`B5&j=Tt)2C;>ju#Gebv8sh>ad9_;cagVoStjlJH=i>8Xc ztkH~P#g!0EP1bQ&U|6w2>wv*t|32RiRk@Ugeym?_O~D7ORs(^f=cfI|duG$QUOsU0 zF2jVI(qlDY4D>;Ito=$>eP{Sb3bp>O&k9>o1*+nB``1f2WRv{c+%MT?iE`vfamVY}_byo*L$MmZ zuLVT6dhgE*Tr_n)mQV!{^uWcC@!fkDNEPy%S?kDzp-8bxwjPnEvnCR0@(9BonIDIs zFSygj6RK)>QDypM_bN!tXoh@)Re}Gvb()eX{e57{vPU({DbdiVF}s+DSjt#Rn_(LP z*_79Djiao2e-_mH#WAXjDNhd< z;A|c%0@A9@bBGn&Q~&qu-?NvA5mLpsgJC_E{;5cA-?p01e_~XdJnChBizT}S6p$dkComX;XNZN-9?OZL+;6EV3VM=|+gBH$F{})5&s= znf*G6^@Q!B0gJX$0dxa{l$?%7q6IqvqjR4D%QNF%$oA{CX!5luG->2?iLKws5-V{( z&2)3v4k7)I{$tK9hl#DegIM?V!StXU`GTE`!~ioX*Q$p0aVlWPH!x;3F4$DC)T0Aa zW8ho66Xj;xn_an{L6=m!T`*%%3mLD8aamOpBLrJ0gQ6`+0**}rz ztKWv8#K(?;V)6QXof2$Z=>PJ;!sBeda@Tm%+62;Ic97a^&1Gk&ibZ{`r*Zx9o-YO|ob(X)naOAk%{a|T# zVGD6u&NwU0RY{#5UMku3wB?!J@NGr3c9qqrES6dY&$NftY<(sEMuD?RQ2zXT z1~(qk-xosUeN>EpI4}7S&O75ye!NgL)}rT+57!7P(ukcJIllCISDL(ixms`7`uaur z;+jD;MNc5riQDwY*@`6pYgD468?~Ggn`){Sbn3tM!rh(2r|0I0zlAI*B?g!UR-6)H}6S~X+7o8FBMvxCUKY*2+MjV_!-L2WpH z?@uM)7uCDXUhE|fT$Q$Ok!pcu^SF4#-nv=g*Hsm=s1=CxF!M>5zxXH81^6Q3_lj&b zz^KjELZJ(dH!TfbV1znPrAN^P|tT%T{hh$eAd;(R-~w*P#M2uDl*j}qIy;lK%zD`;Qsc8Sg%&w?rQ z`&bEe!V?IR=91}$~a>Y8}kr07jh1C21ltkDyFBPD3NQ|~2GM^Z@nLQ=5A z8tiV=AM~$OrDc${EiQyGl*)V@4iZK+VUUPXuHt3LN7}aG_^5+q!^Yt&$G&PB`TZTOq)(N8jnVFeAR z1{s__v*=XQ$j~m#^C@TF_0LyTd#Xsq;>>Q9Uh8iD+Cmx|x1@oZ7a=&5?H%xQEc z`7NrpU%`H)$%jzTo4Nhl$Nn>S7O1>KBdA*KugK76se-3PnPi@P#;h$j+A5bq7`r4N zbJ~8vVCX3{2n1*%cPeK-AsQhNNG6TN3$Q(MpZr&M@S0Lw3IykMOR3Cm7BWgmdLtQY zahq%hnq@xja4C!Bx=MUnQl7K3t{f76fB@$9+WQgVrTebr;3Nhu3qL}R6Nz$$ApD-I z-);908%X&{VdxKbF~TJ@o}jFx$jXM*)j6BV1i(DHQ?X&aKrPG*1E%hzroD1@@Xes% zq#%~ZO=#1lQ-<{WAw2pL6z`$JO;ZTCm~<*}{n)oaK~Pn zwZmDGk(jVhY$D*yBg4)ztc7%jZNed1NK1_HCW!72`2>Gd+7pRU0c?TEPtXZ!QO+&1 zCbKTC%q&j{KO@)xa`5$pnMgc#T*OI{TaVlb^p^^3X&&usJiqCHmK-fT2TqXdba?6| zLbW-nZbql-!290sw5N+PH(Nb6Orxo1jC9e>B7{yjTv_{d)CaO`yCW`lKXyFlYI_Rb zMkTu84|yx~y$vl*);JtfS8wU7xnWb*Bp-w)o<&?3K9aPv&5yv-ARdYyo7@EeJE&DH zR}zdk1p&k9&ckM*>Ieyrzx@w7FI*zJ*#~rb_g8bY{ce_K@e>1V;pn2{`b?g>d+%(?IYFr!Md5R6$`LHZ z6Z^&q9@*33kR4k*M_gU`dwY`3VsCTRC3C&t#Izn9#j8*w8vT?3rS*?01r00*G=Eyf zU6m~oA_OYSfo_p5i>g;|MeS#=^S>5c#d*#8I;te7by`tq(<6h+90m_NT>KARz8CCa z3tT35BeW^YjNa!RTuP$n^d0Z(Ia{7^`Na|RRaR*ve8SC`n9`ITC>Kd9``0Qwd*~unNFq`lZHxD~<@kotrs%u;ejMh#q`dA$6W$}F%fbd&kggoU6~ zEFF?O#!uLKW?5riR9f;91yvtAAQ5)0_+Sl*iVfVCK4= zHnF!V-G)_y@k48GMA6p_MEFI!ZYxt%H|?NI>(i7az+RcMSlwo3QF+|Xq12V*gOy!b zJq)6yA8+5En9FaM-p&xw@pko>L`~z2==H{}Xq)$y<4s{0%%!j1`X5;OXcz4nZc}~) zTJ0k^sO)yo`JS!3smFAF&#uBBSAEv~YcG(3yZ)na2Crb6uRf_dS>|E!K4;P%^oZhj z4^m?PRPRlK(($qFS8Yq-6)b58pB*8>dcoB=^l9uvT7%2f%+GJn6D=in&7|zd z`TZT^B2|Q^Pmub-jV@g}d8;I1NGXUtTUE*}lX`)qX97j$8a)Xjm{;J(G#;*uOk=q1 zF>U2E=KFxgXfpZl_sg|68(kcl2WJ4)G0GRq+kA9vo4MZBbAhDD6HW8Vl+ezc=R4w^ zO*#p&gwDvwrnf4EXP(AL35WF!nF>;*SV`Z=I3fay5=*G?zqadg_YIzd3X@Y75i-2H zohcs-1q$+~mKICOzmu*VVg94AQtl3nROTGWf5g$dUhXveQ4_eNFx(ov&*#5f&xkT9 zde9PuZu1FS)cIU>3_|{Jh*kWH>%B7*SZeMI&_>Ab=Ia~Bjiye4OeL9l(c$7S{g*iT z;!ieet|pS@S!b)nirN7kE8J=mu<#U@5&sXVFj}rata_Hq3HPkt4R0ySffeGog~Ayi ze`0%ESpN1FM`UxLI`K!|!LK*T!{>6@&d-l0;ZPN&u9DT2sob~7^XtucD5~SODldsE zzc}9XRa-aWBND2&N=qs-37#l17sLTa<5&Acc*zwgDE;uqr{==rn@TH?{*n2O`^qL7 z`|-_q9kxn|P&by+dNi-zEfpQ({jR#yAM4$(0oS`|4%jjq&4kBaw7Ra$M)}TeEcyd* z#N&j5tYi8h^^abo_5z77O$3$7r`r;7bet5}CO0E{A68w%wimoT&TkAJ&U`z?z1N8C zkTTfHL5!2${aY;xvaqB!uv6QXv6bL!_cB+yZ-pKL#07Eu3v|*+DNCoNHME1;DqAj* z0raHUe@zC<6`{}yp(f_qmRa+;o;wwUT~0uW=$Vg@CHz;j{y)zYXTGG^>w=0|Goejd z3Ems0^|P!^Use}z{TsK$irnWas|Nn~7hEO2+$K-%?q~S*D>MlqW79}?9z#)#ed2)F z$Ljn73j{{*B;enH9dbF?IRA+rkp7-v)jMvNlQyF>8&&d8zGB765$}3 z@HkD0eBO9l;BxpE%3w(x`X-;q6YqtYj66N@6_Fbq=xQM_DuzP4%~5P1sZG^$otjd9Y(3zN-gB$VwJ^gA;$o3G;REc8Y$y1 zj(kQFYy7COxJ#ZzS*e{4=#lOQV}XyoPjS=%L#YZO zM@%kzREa+U5uHo))ucnEOprLXG?Tc&=63n&~GSxTqTi=1d;g6L4r z?d`=Wh2oSKu&rD2o2e&|`ppZGQ^KthBAzp$#{TFK!oS=N7?Fz7Tlifa=}y-~sSpQBjN zs)9_h z?lDI>WIQvpt75*B#3#93w@;+L?0Qy36L6!hdEQP1q3chkSRiK!I|^lBm_rSa@m8~; z!@wEbEIcywK#&m}NS>*hD%l0@mK<%^;|N*|xgi087?6WFQV`M{Mxy$*l}OBglc4eL z+dcIMkF*R!0jhcTjgyb*@sxtKGqtt)!Z(O0Z4L-q&dJWve13i9wjd!vfSkO!Kr5}h1BjnuG zc(%kWZ;Cd4FOuQ41h(FwtDLFK_3Tj<+{UG=%c8UXty$W;PJ$}3d9$n0nE$)1ZXaj6 zX~SsQP-x)<>@sR2EVaJl-JEzF^?)Sw`RYhK$Cu|$XGFP!F^9L_IdqfhOM0?W_+CH? zy5SCZe(ULa9)kZCua$~zyVNN5CpU31VGwZ~F~FoV8EgI^zA=nOwpS97!G9rwH+_)Z z5;vDya_h4v^4~h}I=EpQ<>&F26V|{#`e?l7umG>)zR;`U9&u}er(o})!)-EMt65JObnVotvWcqLA0_{ZY-1Rh9C}Ur+KHy8F>rv z>^dPEf#wWwkbRY9C{)#}SvCmFRiui4NkM+~zeHrz=L~T!(m+B!pU3{MJ;k&LuX)uI zqZjaSk7jGg3>L}DF$+@7;np=iSNB=Q-RAqExU46amw!#do!|QwfAYpDc2Ls?*>cmGYHzx+XWJS>*t zZt7Bnj7Y762$#T7$F%xNg9M3CCo1m5Tl=#*d1Rf+P4HW`R?P@=iAV_5XbaNZf%k~B zU63m;tvaL<15|co8y?_GT$3GR=K9T^<;UG#o!`YKPci0`K_3ISltG})zf({6ek_8= zOek3^vmjzB_G8FG_MeQenGi^UYAj>dI=PDIz+RT!;{dYpVTdh>Ym4YAQx_Le>Wc_P zPnP?-BR1;{p}zVMU+f{|&g)#}-682fIOJFU6;Irb)=Gy5N zVfo*6AF}8W%ve4t_(cWVmcoDF@e{mWOx3lIzTf=Mg@A;eo?Ng-&2z%%7x_RhlZMGL zca^WNXY_rE>pM$W+4}&VW#}#Y(x;v}w|-8+YJ6D2;{+5AN4K=MOK3X42;pMNF84o# zRt4%0jPr;-mm+7nT}+K}uL)s;(FFq@l`73cshSK??^~?2r>)Jnrm}KAbR+fqqA%9H zkgIb=na5f>1DUc0ROvLu%^*P8oXlN|0bZQb>;#Xya6ab{c;1Pn#iOifv5~L47YF-C zJRM2%h1U_GPRiTPARM@JgWUh{Le%*Xc!v4{2H1snh%+|M=2k5SVS! zofezf+V`Xlr{0KT=8z}k3h-U`q`;CYCy>IC@cNb_TC$wQD=NdFSO+Q&LgRTqn5&t?>v5QRs!IM)EMFD3TX!_ zY|YoGPPd8>BVQHCX8)A{qP*dGtK5t$9({GaluBk9^7cIO92OZt%AqBR!gCwA~W7k!cF z0qtYOGl9n@o9$&H@Nfw@Bq#wqNVE1;I39p%@!4cSk zfi}`yF1KbsspwuskN#FT^DeBj8eyw(>SI_j{SvwpxN^#(298YYDQI>2lV{GHKw3I} z##R0-(`e3Qd zUlNrJCjfx~@Z^}R;zgPJxRMxGY(kpj=s4qRR9C|x{|uz&+Y4q(RQbL=;wj@3O3bi& zzVCvS$_Xqt{el0G0K)g$hTdn{U)`5%^6bPUu=grOg4)qUHtj4tMmabBzRJO6OKLrJqnw?8;;)L0~*;x%1bN8zwVJH*sYB>D?>4N}Y8lS=f$*LJeYA7m9EfxJ7 zuzzYE-B6;L?g)^YC|9{=Txcy8`ARno{NB^rVsocEJ4Cizt+9W4L<>9`z5fCm#g}gsba~~9;hu;8?AC~S7Ii%k`Po~=iFkj^xUZ8myVETw~(LHCXIzbVnvUzR5NhbtRi@X!U?NwVN)r1tk=g;unOtI1`>dPi&y z_HhNfw|LYpK(QZ+<(^!9orBWp_?c2KV#?3A*zExn#G1#Sw_R|xUT!-8QoCrs6}yVa+c_6 zbgrjdwB;(eTLvY|+mvr!{l5adw-+*eD##{{9VZmntz_9P8g%ChYm=9-DLQ`nj(sYf zc>h9Et2K9--8^zx-q@+v^n88SPId@$UnyyoI*ErMnd@en`2;@cnud0Me1~-+DN<29 z9Cm)v=S~F6;o&v}rWya|&^-N!~i1R(+vS})17v|XQIqUp4YtbXM z==syeaTvD9CJ&;*L^+4KC`#coZ7tWHG7fB==at0J85BfMxOXIdhbjd9dlYDDLcz%r zZqA7>$Dm~^!jj}#dGFjEQ+TxOe=2zK8xO&QRh8ch0~U<*pNuwFO0*sQUzNl$%|C?@{DUPx(>K#T}A=}bKgj^i>Q#< zR@!E`^a$>(Z3ft!C8B>M@*?y>6MD>b&3X4Q7nJI*M9j#Zb=Q_vXFl#qg1=P^r8-d_ zSA?SMX48J%nx-63c5d%ksy2oh&nFaUphj+52IHR-ggDju`)_u_r5TpLjPc-sJAb$UZI;N%vqP{ zH=<^o|0ug$x`d(b>&;mQ5xe|s!7jcoYJKoyCa{BAPNn0};mOM9OlP+JAW6OI@j$-b z{jk!Fb5;OW9`Ud21m5+12>SwwnI|>!eLq)rG?uq%XW}F|DC4o9a1dyszS@yPW*Xx1z_Bz1Iq&Ncvuo7XsE`mRc2N0nWklK@pAfw;uZ3&;P~pH*9T3aLB;$tsV4 z2j>fgv;AqIj22{KMq6WCw5~}s;bp7LkLr=ptif=boW#aHn zvgIQ=_~(BzUa>Lg~`;W%dcBLy7Hk`BrLpHyOiPfR94|pWG)2MB4gWq_ z*q@XM0-$5VfY&RW=3w1_jYvwp?!!)`kjf%0j!L3JT&d{#j)gvMo>nDtkRA?-odL|U zbu_NgwDz@{qj99+d~uAJV=Jxb`x2AxMlko}l$>yR81FdEyK%J4?iY{9{!;Flm)Mh- zLvlpNVk@JhFgD89;#Qv{ zi7kcvL66t$LdyZd>ebA{i2G7V8{_y0(llS7BW_UoHfvm5_rMXN#&qAV%EabV`^L+b zwhaagSMyDw1-o{R11}AVb3EU*>(n>D`<~Ctt6bOyjM`g}FNVr_FRkL93Az_j$F|28 ziV!v$xtyhS@$|CKPPbG{h|uF;(x%2lc9FLNYCo`{@D?F1`TobNxdf?i>68DPIVYw` z%Vq22+C(BtQsT}`pADd!qV9|Jt8gldAGf8*rxldpg>Fg>0Buj5O%(Qvrc+ahk;C@_ zwynQo?$SY2FgX0vt?=>11U2)S`uejscOyhsQ2*VR+ z!e3F^9VwWO0hf!yZyCn!J!i959%ma-%Wu}r$st5@zOqT*sG2o(g+IS9)4mLTez08k zTam8VNg*VYN$(QDnazlhH#ypSw$;l0T|sbX6BEG@mp1;%jn||Rs+aRa`#{AiZ-!{+ zy+#1ol_+*eL3K|ynIusrjngK}_2k?DbndD=Bu~;BcZCVcaR?D{-OUZaYWEpuhCPi< zhjWpE&|7uR4p-qwnUdm6IrLso+Zpq0D3H3+G>wO=-z{AoPp^VXIv#+CT+GVnPdqrc z9T->P;DV7WG_4@QtLwVVqeOuE+Gq<9_!0=-AqaZ1@B_w>HeNg#T=ri$98eW!k9KA> zSDbw%wM^&ir^EC}H9qKG#gPgwn_KzLwh{PA*!7w1CeM@#@w0B^$}gO0VHid*Y+(qT z><;o#G&K4AFHgqHlqr;Qq;@By7{e#TQRSV->3#G=P z2BQ*XDFgAQ0eQMT{jPm~Wc)~JP!aB5rv-~K(0eF{9)yKW9wR27WWRLb)UmSVvMOyF zBAXuW6VildChCfuVMBlUtS255)9`Q`b84EAdW%p`hM<)RK{u|IJA|yCOaA=bDWBwO zoz>(lH|)Jw}R{OgDRshAS@PGOeWA(yeLl<`ThxLFxbY{77NLvQ;Gx zk|>nhbUjq-y`+EX%{KCXI($j(_{6Ie-op=*u;Z5!DP%i@yg-T*k;BY%I10?llwcb# z46%yvDpS*Hv?acvV;EtI+@@w}8{WCg;F_-0D6 zBxf{sJwNcAMah)icInpVe0nmP3lA1PSeernkK<@aE&S=wddHS6&kr)($Lsr^ zr^lAc=Ak=8`pp3Vnk7}cOjw>6J-qY#XiVV)Iyqwj%RJ#Z?4zpWM}h9ZSi)9!N%?W@0s_92@@V;eo@<%JVUoRs_*p#-lxk;gjt z><+rcNBKTi-2TUm5Z*6S1Ed#vw|JC@v*kT6N3=q5l>%o0+I_@`9OY7emhmhY6!sVX zG=4VZH3bZIDl6`QhLXEo36$1X!BtUR<$3w~!1NCpCO6LDbRR4)RvB418e$WlZyxFu zR$Z=%cgMo7QbMl{Mqt0b2Rk8r=O`u36wJ;XS6e!bD3YTofgzV|KE}M??^>-m#G!AJ zMeEX4nw~`g8A^V~Qu;TKJhwSK{Xx5*BPY$}-K48qk!@mIKe5zH88}1b zk=bFXCp-t<1^ANCA%B86fqX?9{8pS_t`FZ7{C0c5{hyhImD_crRPQR8lyo^xjIb!i zn}3nBcg-jH<5EKY9OHJwZHzV5_zDKSdKY7?;pRszVZMrigV5~f7KaSfbuV@>IU;KN zwnp7q1Kz9f|5hY2OXxzVV-mLQBJrMQ= zo()|eh9;0cE1;7)O%l@EcI}*2h9sI?gWhmc2dU-im#3PzVmjxOi`}|sfaokScmXZL zB$s_4KApg{vU#{*7W_FkiCsG>i00t24a}pmRk_0S7bRJ{DA2YCkFGcWJ9$^HN~p?@ zQ|yZD6HmiOxxFWf4+!ay)g==);oa}y+YzHiq-nLMZ>>WxYOBD}XJNxvKicpNg9&Pq zIO=FKEJtZ`Z1u$bd!tuKC9frhm(u~i@G2h)c7G_&EA1&?fErHSi4;ao^j;QCEy+ax z7{ShKM&Q90l3Uq+_uH#1WBwh9H9FZ;RIvT@a@5o60O;fBb^?LVYxtV}F|G4=8CMny zZ~SpkxmQ1AKr=4zb_-O$OY+(O*5!x&4z>IX`u)15U|c1*6pNE>+$1FZi^zll=_nBu zQ4MXLo7fkY=46xd(suyU*!wlZN7Tv3!SugliCqzTjSyqPCfoyY9eo?iA*}q2l#;HM zHngvK7{`1ty@Anfv_P~ST)D5s;L`;L+-h+cQ5~p8@o21HPUvtdq4R&El?1|D3cN@U z#to}-)X%;oT294V)@gks6oJCu-In}ze8@>KKh8Jj#_)d5g`DiXM=Xjjl7bPY5wgLZ zu{LyH2>K#$VI4RZ&x535K|9-BB!@JFV~oa(*|I^Y_~Mv|(?AssO-f;Rm_vGbkXi7r zviE}6XtW8HauHu|3{GWeI3L@e>y*lpF-}xt)xqmZ`)RrOTt#6)TrbBB=Q$W=h3+7V zEwPe?job#K&=-%=OKXDX#C!e+aF(O+jc)XchBH=sQUjCM4nwi$^P6^rdF?Kla?!pZ zTIa|3ruRZ=;n;-mD_aO%5qldhDNpk6AY66a$8z@`@Fk#zR5p#LKw*Tmc$&wemTgT7 zOe_|qg8w(dBZ%(oiJ?wB`vM!WNf`4KB_pWbZV&oDEdZn>-9suCOr8A2i>82KkFcB; zbbL72f$71&s_%-fBzUjk_wmkvK+n(i=Z{ysYRcdQMy5xW#W!twagU7OdB08lvB7~H zk>1CPJ7Ce^IGNr&RSEWC@`Y_?dXTk+)&+T*Zgn;3KID8&(z6c)itoh8?@;F}w~EE) z=5!huHSB|95_0xgK0vpe7A7u#KbP%C*i*MsG*@^;)cI z&`sz?V=**IN<#yP1&)3_AMRV({mAt^$UzQERv^}CTcp|@S8d!%lC;d9Z970JMYQhq zT=Btq#hiHC^4uwxp>~;_h_98bhK50`p>n}F@q2|AI2C0%IgkT}8Pg()$wQx*=EY)S zmz4#-YDBdNu_swJwR?%h{ZeU~xEVukIJ%tZO8^AC4&2&Nj~^(cXk`DE&TQ8ZcJOs* z-EHuUs$c>sNWPk1KT*IX*ZSrEnbThCtrSVYlvvMN6s>br(C?@eeeF2ZS+L#^)(SRX z<{AxA8f3x@yBGL7r1risO){eD@@4UldV3*%T>G(d(|eU$n}%}qdaK2K#3LDFMgVAX zw#Vx{8*q}a?WtO0U#Ze?*!SbGcC_(=>IDa!<|CK3rrn(E)+RHsdjlg9{Wye`T@YV? znTbW6;Dn1(sUW{rHXLAhn`>pZru5Sk%8VaxB{;(yY3c)k!4`chtLf$CIKmeR2Qir0 zrKtSUJ?B>#1`O5QVO3INzGp88yZ2xWSftBOfNcjyRIyCpC$%rTy0idoOf4P6O&}uP z=EE-Bvwa(KGLSqF2=q2iG;tWhn&{VPrjUZ*?RFI&Jl(?f zkzRe}U8Lmk}yD>z9IiN~QyA6oO0&8O2sh zV6s&nh~nu=j|`zgTUYlq;AmQ5i7LQfj<4eHeImHfip#0VYrlDW_&!p4%!9m@NC zvHPC*ehUO~6z9of!E>JAogp42DgKg5CpW8aa1mBSm_`Vx&fheJx#c?bOwoiqrGado z|0)3KuE=k}pcMEtV*T2)u-lG3wVpnQCxAoGD#$f@msJ0oz3sZpvkLhS;J@IZ5+x74=i(6ZKasf1t|s88ysC_BKJ$ ze0lj9=f+Inr!k9?(Eanj%0$0{Di?~m$e3}2Rui9(ktbN$&MZTBj#Sl5v6KbhM$T_5Hd`AOEqVx5o%Y*|CZgYrC ztxrudg-8%xqnSds&^4-5{tcB51tRD-L<6@}mJIh%zpNg273#^hst1bQm~IQIuvBt? zB~cM+XJG8R7Ip=>&hew$S}Nl%El)@5njTIg(Hw!~;P)Scjj0V^tJ!hFlc}J&KPfYZ z&$LEjB5R7XbZ@F{-akg?cJ327J7_qhZW$>Z6;ua^hjKH)bArR=v-&9rx<{AYmr{xcyrF zLAYQw7g$E;Xv(Wy1Ecoc{g3+2%qOn!lL^V-FdJ<6 zv3m<|XkA>M=C4HKA{c<%B(;v)8KZ8}&w!meuY5>w49)nf?S~sfM?odq)vuzWbpiC6 z2-#UvyP87oakTC8wu;bj$r+;Eo7F)S<3FQjvu^xK(!NZD8@obE{HT~^vSLl6CK;F+ zeUh~maE)`zJsNloa-g5kVw^3DXLoQ(lXIynI4tn;(O6Pw^b;;;_^S+eeIXY2S~a52|gSZ4IN%5t3kj2&AP0zhlW zz3V;=Dqcu=>JI6gT6H64HQ(qTw0VS~sf3G*l}?kMj{C5H4-SXd?2$>!#`HX~{GuG4 zdFnz0mqFg&w68?z17heU^3<^5fq6?^bxsgi-C{&2QAS00*GF9df2zhHak3Ja{bCIg zTqR?B*@ed2lpA_$@T6DR6;ilwzY^w;ox_#gw~Kc54LLgz$l}WocNx|+17%b8w2T@s z@ZyvY5La)t&-hkpW5hbn+z+aC+&s{3qj$K06N*tw`vap}RiYsxuT@{P&jOrR!}Xzl zG;mSDiwe6G+XJ|291B%}1fM{@sL?a#P?=pLHRBO+yprEFk17G`#Q`mNAz{LTpc z?lXB&2*7*dD4q_!1Hb2^?!Ssf7OH48rsg#zeI|7o{qa8#Cfp)JG8Cr}GiE;;Cf@S~ zc=K*1+K>f6-drtsFm;?KhZTcws<2ZyA+hWBq2N4Vn1AoP{n0@7F*U}GVUwC(=Ofzw zOb z9t`{Xzoe&~OiZ5YK5B|swtog;Ds1~yj5Qe7bkq$KKgva9y%J$$UC&6M)&ULg=ZA>1 zkhP;!v*YBNX}Cmds0?oOHzPF-M?INF*%^=Q<@4(Q1FF1!=-O2+UY!@r4q@bG$hkX> z?O3_zF1jjPs?B#(^9gc{VDSB%El%(2M`jKlH*B}ZG`u^QFika3w+o?}H7u8$wAr>A z6;Z}D!?`FiV`r(7Qg9B6m}5ec5%q#73Yt$*^rm;+-7{W- zZCY1ZOnlC!b0Aqt!pTf^B=^1k^Sk7_bOS*!S^lw$0`4Qpqs(r`9ORWzO!M-_{g2Ju zf+Fks_^sX;dUDAx|Hdlw!66z9v*UtMY&GW*yTGlL4OA{lT>+~L8J6BXJt{`kUz*8m z@*hkg?p^yi)Us)GDp(Nz^I-~Gu3z}A#dx$t2IcEs;Cmu`i>p-_Dju0ESrX*+?*Q@W zzM)lOrC<_gRDy$VWZ}&LSe}F=h%hJE2vBww*wOqMpD$C-x1~gQh^6_wh^{+md`Ai<3jBrM9NqaXdmd1UU;mrx!L^pO-C#%Ii`? zw;;jQt!>E8_`Aj;tk5{^aDYy9X;jaT9B^>MK9+=kHhyEEi%#ck# zr`fB?>f*@18}}2=HW>C11u4Nf;zO+s1F3(5=F7flE`Gj?S+*&q;>Gy1_7&qR55Z~l zIvgS!b%qKDPP~X8o0~cgitC_Z=3WKYH9NM~0L0<1+^97r_MZh288%j&FcUG%%?3kO zg0Q`>Y=vg5p98QH1>d~1dkO4NgorwJ9-Z%jGw=sO7Swu&>t0UZAGTx9lLYHETv&z4 z+umMZZfkWL-)(ay$Y?vN995IQ1N@%|{Py2#q_ae+Y>Y;JT5|rz446*7Rp)x7nxYvK ze7>}z^cjn#hkpXJunvfijD;~PP9z{kOl1Y=zsOH71XH6;$9$K6n;Ne;^LwgD7x*a; zxsy2GVF&PGpS-1(JOx;MvHPk`v+|rM5KbffPY$X`JUW_2iKF1KN_TLv;FXMGJGA)E zWc`_{r?trp{$~@_1)Kf{JjiI*_De4!6**9btP2jVGPi5rDj~D~lvw;vG;g&|#J{0U ztD3)B1fM;ctt}--LX<6IA$FF#DM#)NFA$zvUqiE@fX;HUIwV1ztI%XX19tfrQYn#T zwyih#@UBz*5GO6Jgs?_aR>qq$+O14ApaJx*2-v9@Qo|sCK+gQc+rc&+p~oh!64tsJ zzVL2jS~?9=j$-g1&ec*v6}2(Jw{I26rQv%XAfVarjEs=DI4bhl)1K!N!)|yoEF3am z2fz~fW3{4I4-}Y}gK~7Hn|nH@!>NjJuXlu}YXaXumwFgj)&D$D(_AjL@MZPHsK4jP z1$O3o4xAuk*h!cT!QuJUAPqKtg$O`73Y=l%Clfa=X+mG`E58hi86pbJspYKn?DR?~ zZJ)W1vl4_c%Q1MSmnCHW(+z7N6rI_z!Ii6UkcD~(y9aq9ND+dHvaE|Ms@xF8RQL41 zz;@)MqedkHJ|vQ07ZPa6Si12Nys|S;K*pbx+YKc~MMz);=!}JY+y|3V`>0aEIe3Ob z`t~gMZ$pB|n0_Ke{m;y+BfG?}EwR;?e|SUQa>{(!=zce`evh_1WNp~erIfBnAJVJc zCR3k{L^s%RXP75{3>QSH2Tu5wzKQN3&KMTFuF&T0 zZ;K)&dpzey{{%%&y23R3WyeJtN0Eb%Qi?E7FD_HrPVt7#Mx~~2%G*F$rDTuPZFPRm z6<#CU+Nj3v?yS+r%LHOIZRz)LCDmd>@h0h>=(AR=3;AHmQCrv(3eL4D;Kvhumo3op zTqer}qR#p7t+n1c{ixZ=`Y7sYcjePcf$Pksbdo;87r`z5#d)*^nCGg zFcV5Knv&?HuW5^PwO&)JWHc;BlrLK@jhCG}s&zC!nFihW6fI#pV=t9Y`T)}wCX)Bt zWLP=$n?Epz^SmIud={K(|2eE`mP1vKDIHFS(#W(~{xFSXWGa~G)}Xd^!*Rs28^ApD zfM{H)++@~yx=5>H+(flk#CIUc`qx1ou5Z^}4QuY7=CA3MJ{kmfsAI2j4w7Ie^l>f@ zEs(Py2^t<^2e8|A#CyMO_$!Iqc<3#o#a|HuDCPf8MgL=@SQm=TTsky6oYS9+h0bs- zE%*2-igzEPGE?R#$j4=MKgqd556a&#AVY2HI_~iIe)Yhm&(b>0@oBjZjd@~eCQvjM z&Ejh{lnM>iise6OM0S4EslW06*w$)Hzk0uq?Eme_+!tLjw}*(Y9l>sN>{xXtELk!T z`muvPQ13I3KU4|WKtjYA<7bH~3Pkx@Q&fl9m zK4=k#RTFSPK}msNuBhKrwKb8C{t3(tzGX2##uZ!sR(OwG2+yQ!22-AU#6IWP*5~5T z(G1OC!|J5gb05E3lkQqwxBjp;?XEFu?RWGLgVsIav`$B^QJC|Z=T;OV7x{^?Mrf#O z0>SW&&;vW9=Xx#P0RxJRg_s~*O7PX>Yv8*gjFUrDnmU#f^@^Y@dS?oRb}?-0c56@k z*N}zn)_Lv^q6e?sc1}o@@86ca9>{2{v}O{t)nOk)7S)m1x<S+4l6b?$g$wc%s|4>5Ax2l zGE~&#`pf+-Rc(ss@w;ei{&Yjt{m#J4_diQ$yZ`6z?XrmEWgS4 zbs=x2hy>8qNdO;5)M5oQ(Wb&{#6XqONqDzMK+ybZcFfaVTIE)vwiuQU}LHq9usA*N27X}qm=&LJ=?2>`Z z34zz08F}xUNA)3s$$7k~TVizrPGS9j*IkD7FT=MLKZzfWht(t5A-T7PJy~`qXoUk{ z)3XnqQQ>zyE@JsWj!ZsK|1g2<2tS~R$_5_$D1xdzaxx3<+f4K0o1~3-L`*C5s;Nob z(aMmHc|UGR`$ye*m{c7#WlU#N{wj4elI0aZ)%n+f*vid7akj!@n3(L>dMS4qpEGBs+z;) z+?Q9brC2wFWY>xghiKjNYRLX@_urM1g1glNO85dvucp01yP0kc0NqO?$~9T>I!($r zsFEp8q2~a*HB7`fAN_^PbH|35ShMp-IA#yd^Z2c|#~n zlnCwjlS+PtS|4)(0Uv*)Mz%gKP4ue=As0&c;HaS(4VB!XrX7VDZleHowygx+%p}u*Rb}qOme=?83H+IUEf&Zglk#e`d)LNsqf3d{A>C95Vqpq7vvTb3PNjAtl>)B`#VbWaz791FZ#LoQ%tC`R%G0-=%V=A! z7dd5~g>AV^n)Z2p`T`0Rf1?Gp(-Von?J=CG?d1UN~)s0bRY1(Hr0-ZnlounAxTdJ_&b7^08t0~*z92~-EB-X90_1^Qg6ZhoT@h4b5RVlzM zV<4iSjhW4RtUL$I?ok;%%$L=rJFVg`!GyITap+mDb(;}MU9HL5PtzdC=h(HakMbur z!4YX+=Ez+x@iXgDrPIQIcWpGhCn2-Ev$cDm>_mR^u0?Bb@Io9U$pi`2zt^Y`o25_* z?1(lGf)sZp<1KY}I08QzY7c%&M(N+xP4lP|46d|LkqnEh2@m{sEp*2NPrGtu-UF6w zJ*@{9Km*`6eI!9SyhwCt23t}yschQX@2FIo2x^kRrKmv z9z#$WX5lb`UqCh#J8^dkI`G;H?m{^bY$bq)`D`GP2CSG0o#r#OabOt9JDtI?6ly7JyklnD$pF$wyQewTQ}@u>{oa7nI?Wz&z;55sIp06trC_q636~O5MPDC zBe~0b+eY{H=KAQMV)IA^#CA{6Bk4ho!pDc>Zmb0|eHRk)Lavgv+lvjy2R^;ZK@!47 z=wIN_%dl)MBvcVThx%5L6qDpwrtReobj%4$g!Z|}zVo1J74+CddNTZqmwdW&_!~#c z*oqBJ8Ixn`O-jL3tA+e*FEXy9NFOOZeJ7?fR4JAi6OT#S^pT3}QTN>31!i^IH`N~P zu#zfIf1cH+#@VnR1b?ThyHZrG^O+O_cu~L z8udJ->R2LgKd9iIoEQ2m*L!7Hkr72}@C^CUjTxQ>a9s=4WH~F7I&4o~_5lKC7hDy3 zHN+T3J2`I$!K_@d02YFjQ?H#j&rhN%l{$xt{-sL-*AdGok~`k2rB~*2WgK~k;&ROz z_8#l@?T$I5D22e%E`LoVGdc6dpcN~J2g``(Gln^v{tiPE4v#1?9EmU!5sT`n1=IZS zK40<}RU>P-O8rS!@tPK~wpvjAO_%QuELPEwWkyb{-yX>5Z|y`MM!f%b$zcdWZ=RQu zqrkXazzG6AC#m||06H~;l=Sn~bOS+sQt1TFh#8JaLLCRZ*JY`v%h+-cTRH?0U3|DV zYx@4%nD#~G?4&m$O_j?@|F+-f`32vbGrIIAj|@Uc5saZS-*U`pw%x5!VBR0 znU$I#9KqAOUssesw-F(?z-Nlt`#la^F=y%lUa`s98?D6A?HylMw7+a}D5`T!lOM4I zO6RtcLQ!C4<Vu8eQeqnbwcTDV z5l1y`A`od`e~$-SwDoED11P%nZ=aRXVB63BPYZxB?+tBk!Uv?h+_Xz&&sPw-SmNAI zkV@wCa5){;p3!vJK=j3#U9fh?R(QFWD@2uS# zdL^%MEz$e;`$W4d!YkJ|ik}&NTveJ`I|2&jxd%kJkW(OKZD3(2XA@c73i6exVz1*EN9Bc;;o2o7$R+XYH>4wY()$b z0~feANSom3fA2Eqw*}oeU;5r~se#Ut6HlW(4gB0^@Ol6UXYhP{=z-&*8lV?pREKJ? z;K=8Kqf9sRsy!I8>@^vskgKSH_7s+nvc^&2_6Ae=y+kUSzG9{yr0 z_*)eFL^@(o>9{|IK4qFNtOhOW&HFZjyc`L%a{o_9J1_lX?tP5n=ui!WI`W z9{mBe<13A_1DAKtVq{V5ur44tVrmvWxWs-$=YU))xhlyPr1?8tJG4n-RJWb9BeZi} zybmkPq!MZiDXC?;-)J&1E)8ML?*Y#arxBRIuR`<`uVp*=PXhf}Mhf87_#@o`h#N;8 zN0EWIFAV5QQ~UgDSG%i(Z}~m$pF_ChHUexpK6B$1*EqURV5ZpG#E(H3-V>dyf!I`~ zb;U7yRQFEaNraPhjgdo36ikud;YJ_d8v;if)LItCo?Ge00yLiIv7md5IG%=Yf`0Pg7umvBHuMxQ@8VH)C zWt)_>8E%C5?f1|d{|nVCrBWhe1P0GaE5YcUI-X?+<3wb3fd}eT5VCeSV`KwQ-i6(J z^=kOxUB1_lPxB9QL00wqA#;o9ms^b|4*1*$qRajR3TB)w0H^C`CXq3~o^-yC<;*FJa zA7(YbxSe3*;gRZXT`Cuk0)F_e0e5}vj8z;PXI$n2^~a|J-zM`K&5*tTOeEeIcCN~m zv{YW6N76t0r=)L5aBWR2b$R#06zmF`E)8OSO~}}caxLRt0jjXdVZP!9xXrdvHT|%g zZ#lHbIV_Ia)36>#G@+(iahvqJZ4gR7={^0W@BNa<3)}+S#y13{>eYV|_?|rdm6e;q zf6L?_eS4__34V(b68!|yVxdOVT3;KF=$AWPI}5C%C`%wTanDR5283Irfs1}H1I|%} zgNq{To=VX*3FK%_K`OfI=IM*sbo56vO3m7a+C`zJaW}Z6<)O z;PHo0l4BX7O$b|c#E?VD737v6pzKZcO#L#AUL9*m5@!F_?~XFl<-|}l+6ggh&@@H; z_I91)MRP%n*fm(!Buph~j@)dJi*4DOo75ktO+oG7DE0(1txWC+nGc?zCDN`9`7c9m zVteB?LgKMt5P7^0^gwIYF~@JiqFT%g9o_T!q)}N6yZu-G;UgYHZ!Dqp?LI|Y z(&s>85p|2H!0sKifR*HnSfFd~WHR#3^EK@Yu&K(nXpoKHX6spKv-7aJPn|*UCl$AW zmDXD3sxNgEg~#?I2N=LR{vB23OQ;D3XOaK8x^QEfT^W_7mcLroQ7jiH!|{43y#}xd zoPR8B=XSo$^>kftpE4C!lnSEiJ_ zuVU*pY#3te7JKv=23yjRl66^KPm*)*ziyrve!Ll;LuLg=^gM5cwnc=$ct2ftMQ%Fu zqzd*tH$7kXEnUQlkEvrTp?_1DFa|N+?s&DQ8#DyFnty*X8+MA5~?;#ycTU^{0z9{1EwSsX*lb z-g7OoV22?OU~TcnrfFUxif6{6X`u62ad@{7Vrc!c=JJ5&vF#(JWjfgA+5(3e>S-;6 zLOskeVg>kQW(k;_RDY!hjfwmFBOKP-fx2g2*yx5xLAaknyS6FD;%pwtzT0m<`~8<^ zgOnJ^Y99g)d+y!+Ufz+t&KC=UEO8mQZPpq|i^OY&_4|UkG7b~@b5axd2>~O2?0_+} z>R9eGp!=fqPfP%H_~+{IuJh$%z-U{yzzr=5fN)%?=BZ$p7DMtIpuIIq6BK1IT(Iri zb#$!P`7-|h0OUX$zgxKGdq3pji!R~%pIp!4;;u}ssuD<*Ac$zx>)4F>EWHa$lmtMD zbpsu+vG{emQ=0;TFh8GQgMj%sM!ja1WHY!Q^6Yia^4S}+FDsxDuf9OsPJ%P^{zw0R zQMf5u?hm%wz@X#$SsKPUJ}p<~u|Yr>3eGvEY5}L8dIG1PdIC>++Ar|txBVX1|MUhv z|Jkqc?;rgPKmOr0Ktv_lgcp}F`g#~Lz#kd~4KWxaC%Nz)RGas0=YVE&iEwrk-~IY-zIV+$5*?L#c6f^*Suq2fWzM&Sgi{B} zh)vr6IfY(*3{-?kL%wPsQBeIl1kVLoo#xt%?C>_!4X zb0ol;$i1Ket^5TZLs;3yx4v~Fcig$3Q;wU0#zMF9FLWi5M25uR%Q6Xo%&TSK`Fnmd zxJ;lbh&FiOgHOc#*R`2~YKM>ggMdpssywp`^z~%I;|clU5S*j%uxnA0g*=dmxzp8;I%< zMSff{#}EA6Qfgan?=APfgHf`J_u6^B?*BJ3oIqGdg#u}xW7anEN`34TDq+Mi zJC9-4ZFjUeAN7hiYvF;4Atn{}&%-w^{1Ny5_4^|<((sse@`9p`)Abpno%4z=(*r>J zjbGVyt)&pA=N4|fFP-pKkM}(1Vl6Ix?CRDRA4~C8!F<8k~!94ic9piEYfMq)iY69CO?@ zUh*5y=huGo`P}xiU3}#WU+44ZUBHE3x(M$Z2sMn^Xew9deQ8#ls;mI8jtBr0(LS-~-D zMtn~9KO>#^3jJ0#b3vot%NqB;T`Ig%2bv5;v&XO)_*!; z!Icr;A1v`;x~wiO1+u%ZL*V|WZo$NNqcnReTlAIE%!mzTQXf^Bu;ir_;&R#qLYi?} zw93+A$VC@ki{ThdQUy#pE=Ah?-I*4{(~`LrB7V9B{OodG!G>(wz7<=or18KiRII^! zP?y%D@7}{rp)8*ty3hI*&S5Ze?JLAE3*ZvPh-KUM9qhSd7h()v4Pg+F#BPAndMGwK z53&Tuf6ZcfaH5E!P$>k;)WHRkg*uS8wVj>bT#LWb;5$YOz<}+X85s!ROmZ;OL zs31COc20T~Kjt<3kneiJ1x+UgKCo$-RS`oJn)d9Jfl@*-OMS1*Fx>53m$k3DeovQ| zDRNm=)BFdTGbZ?+Lw(P7;S1O64)Yp$|NXv~KAsALo-;EvDB!$DG6h~U`60^odrcfk z;+UUXBn(52+IloEdHD-@(Mw;*wLiR`b3S@5=lsimvaoO)u!%6J;C)J{d+#wOOz%Nj z;b&_l`K%dZ-zpG}h5bl$yya{)?hn?sVWy~`7+p(mw?v6mhF4wHHnG5e`J#QJ@gw}7@jYt~#ApRH+A zMZHC2mhXS>7OuZ;H!uZ0SOuYExr49sw|dkjf@*{p@CEnyzU%2a;A4KDWYChH3w% z`1?Msi%{ryy`tpp+MflFc!yD+t#wU~-fEeehI(UxAX@qLtgHO3TA;>x=bn#8+9@p8 zlP{GX=Hu5M%mm*rCp2QeH@d%Sb2UXuBc>{%le+!E_+ww5T7F1e;x(AjIGc<3V^fb$DjQ38{OldPX{?(x!Vfe{fz6t4@MaN;&LZ<#_#5Tkt* z5c(@0E7rz1caba@bZ-huL{K}13op2urZ-flYJCK9gZAAPhuz~U2vVv@C1h% z_Wi~t^o`N)*CtZ_bAOA9e^L4G=S?9VAz|y*i0wPTC(Qy8GN>&NmFZ3V^t$W0=<63F z*nv5RR#SnWq|xW|`cO>=Q(UX}b~_=XgF;6)n!II$0BbF(N|K}m;=X(4*mutyGn;Gt z*}GoLWk0!y7r*S~cwHcA&QqJ20c}D%?50GCk^m^NRv_IAb{XGO;z+@KBLO3Xfx-FI zVLwUWUMFtnYnOb6SN!Hnsn_=q7@@hih*7t$p3isH-AY|5Q&-%1j^0tj`(akt)fRUr zJa=BPDS8Ts2Mw58sBpnoen4nvmtCTAwK2PC2C6Ny^|A9*vVhK?Jw9=+ezJdl4g@2s^Br;*ES)bQi$1e2`2Es0Zu*rK}1!ck!1<* z-^1OC`h*sY;me0X#<4?b{$5<}|GMk3s}H&EraC|PfhTqWVYNb>Bx~w@n84z_r!qZ7 z6h&FmEI-6Dv@tk*FKJkVb4sOJW7AQaIIw3QhpP^wc8$T7vJ7ZS00ibZF249i>dB+A zK}etp2JI|JrjyvLhoI78l0Y+<02`<5t>j|+DUAF4`k5;~s9w{1%<^ms9-%?S%yG)S zH*?{|3u%6d6)uHYRBV!^d0>k3&%1!Pzw0e*+d7pdy~}zgSmhC*%lzK2psdla4Be=Q zA7(M`C!391Jtwcw5AUDVD?Hdr7&+`7M}>I~I=1*+gPyY}-l)@|{U}~Li9J+O6aV%p-uA|Ku`qupCfI~eJVBI};9Tn@im@gmdpyp0 z!cwGOS13IIN*v_kT&mV3Ha@ytAzO+-f+4}Er{R=8dFyNVoADEz z{Z$pQ8`~&FFa(xW*qauBmf-edM)?q)_oFi_4jVaGee@5#ijjg z?={4Z>FH^rN<fzhxx}>CZZWv8T3a1J|z}<68Tzctsz-*Rp z(t(rIJ;-b7{iB%`zddIr7{gK^5JpC8`yunO@O}0wOww3F zy%JTY%5E%C5&$LE;PO_o03{A)+(`d_udn+znL=Qw1Qwq-Ebv$FeG_M$`9zlLd#KhT zY_N_LIN#Y4jCvwq+tym9KPm%Ow%;sn#YQ)Zq#E-hbbUvWYkm z!l1gg-hmiU$EK~DN#b}zCn0UU<3U3)wU!w)S5x{>TNEXy{G%of>FGkmkAJim^;=PI z!8bdx`BKOQ&@VIpC>3-Z4}reN(Ps|!k^`6 z3tQU=R{90|9+_sc5iqG+W|=k9bnj@A3sy#!xru8_3~Gf7VnjOj1l2WP<|7#)2CVyJ zDk3Kldgf!X&Z(G=b!V)xBg5ahGZ6Y9riv96S)k@Wbe0i-~GR9xjU!z)(`A&aP5!pKsgp;LcCMFxDkX% z|8bbt*_XWu2bRQxZ#JkMy^X{x-dY6diLve7@49bv(mKK`UUztcckQM3c~TEE4`Q*A zVRqXV=I*!)Y=nv;nZVJKJ7kqrIPtc9uU$hlNeJyU-@ockcJB>2Zc_z(BPUT6UGuYX zT1*n^b@cch`x$-z_tB?XfOO5zY#^uusdL|xw_&-K*!htrqGgi=2FPlm3pM`cUH`yi zKKuWp&4fxN=y9bVwc6e;8KU?lyD#=Xmz>p3Z&l za{|Bd!r$fgTX$iIutCuGSy!DT9-+Y--@SyXPj{?N?gSmEuC$I{9{gnD#0ncOWt0H0 z#yvqx4}cN}w=TGnJKu=#s$ESBZAHco>o_^1nzl92o8IMf+tC$1{K<^U*BBEMyuj`-bl@SNwJ#p$O#fTTIM_UE&9yJH&(@ZM3WLKqsHixJaGK$i+A z>Rr3_S44^mi9bY*ULwFNF7aTdx%uY1*}quFgmz5{uP#ZmvYRNNQmvp~k2udZqx}hi zs1i}BRlz4~u1|~&*nJP&aQ&U>SZc-jYXZLYcfzE9lQc7;jidM%$_e@;W@b}`Fal-u zlaRHorCyQ1lCZ?zzWW2jKv;>&4W`5q1(zAt=H?eT{=}X9$N3-Qfe(5B&E^4mbAFU4 zQ4#H5QboaT$HSi$}T>zhYs-@ zDO_6>)Sx=WuDg~{@_8B7Mveyc398!V=2@<<8J{CUwORoathsP4pB1UR`G(zqLE~If zOym3btGC8uu8ktJhAQ}sKE5aGzwf@sb{J2pSa{!{Iu&38kV7CIwum1wg7<}_+^T9#+#8FX!re(ilJT}Qa9~Ro`C5oq3PgBPkcC! zeatU#@g?8FnoXz?!YCqcw&u$kVewlr@M^o>Ql$n_#bfZP71KyNpjCPX*q~>zS&;k$ z?|jnn*7f_MTYp+oPE3tNr@89Nn-C%pLB-W zd0(;Xt#}ON^6qa`7(m=?5=0>;3YH&Gy%Gv*j)YJNF{a9oue%fE=+sNbXIB1JD*LYb zfqmveSB!q&1#^t~-Jq@rXp*9^-se-A4^NPqYqRg$xm{e6% z47I6gmgX1u^Ebbn&wTMCRDswf7-L%lZldMFkCjB^(CvRC_s8M|i#fI?oCC!iB9rE9 zP(SMYexmhd(&W*_^A+!ZxjAq~-T$QTznBxC_k4cjcr@0ux3hXH3)~um-`Ir3s8UX&Hn_EBhxDV8o|%<>pQ8jW z)3srG7V0smA*_YG{C~cXi!b>WB7(6Nmn5u*%GMqwtibS%c?rx@7m{_h`9IW zoJEaURX{-%#Z)Mu(Ac6@^8iJ8JV8-g5yu9B2D$-Jp99*?+O4hfe66CbjopZL<8wd| zl-XjeE}@{Hs4=sqtjxTbcZi5{_TKCLaZXGp;>L-%afi5>dwo9pb2Bm`PMl%wwb%OX zwSFs#`+^Iv8#wziJ~$AwGyTNI2116Gt#R_<781od`CJz;*nJK6UNLcmF#$Xs(k>&2 z=IP++T5G;$j~W~e9(;HW>pRZWn5-95%%jeH5I=!A@Uy)?L;&b|WQ4xxQXrlAEl3P% zGm@GqI81gzC!_A~cn5Lq5P$b~@8P2#zJ+Ih&65W<6*|4tg9Mob&=?B%OaAE)Kr$l$ zu;pSr-e(*?!TXLnF^)*CEgeS{60BW5b%cAaZR2p(6Dc=s00G5Y$I1ARs z+FmgEnplSD<)jJSNy&*Wi^8rShIijeBu*4f*^HVs$`(CC_ z2>yed=N`qte05nyv$@Xd>Izq0bs2x~j^CkHTf|w54^#j_0ss(X+l`ULQ0VR8Zm4uh7d)ztC^k7bAUhjn zsJB?}h-x)H^_h?H*8lt`BymEsoegxOap+JB+YKP3iV-BpoU8)A$N!?Ls8hUmBuPRP z8Wai)3ZT)@gs=jY3&adxP5!jkGnArN{=9eN zkEg@I#VqGz+~5I?iSgc}POuTY;9H-^KfM3%F?pVC-X~W8XNu!j=*I-^?N->wP8R?z zfPmOQT%-nTw@%p?_33?2-RbyLhOq&J+2VddWexxj#^5#Lfm11GSHO3dyeYW}wo4)b zVrokZq@&8GTt$lOE<0M=&MDvo>pf9q3LXldx73~s=OO`BR?foeisj&8gXxR!XCm&{(MDpBLHYDw(LP063cwM+PH@ z_{lf_DldG=3%UH#{aEWrBFO`d!uq+H^>;P{TUGJsro4GW{*ao1ZY5Jwr1(KAH*~!v zJ*wK&nYG4Wr+i=Clr{dP2jVJH{D<$?;QO|U2HnuV@g8Fmn(NTE`I6VOUAte0p)MyXBrKGkMEUh zB(*tg3nw))3(cm*NUaFC%k8>0NZB8HZ<;oj6Am50{$J&Ich7BSU=}~(m9rj`)Ht{H zFmL#`KTTUT6&N9GgJ`dSy>K!IB7$bNWyrqrjl*!uxc4a>L zVT%Yhg|*ctB*~!uc}g4rFm(<85RX8AST{xaz`O>yeg6{mi1xCPob@E}A>R3pzu@ow z@&ha`HBd3Am=M?oxxmnY90(9Gg)iaw@t=DWjm4u_yG9&E*leSn|0vwQAOQdf5+q2F zoeB&9jviU$vSXJHwFjD8Kcg?JN4LH0T1D{E1pzAVbrCcOeqcae&ukJyyOrVQam!Hi zH0!(%ddR5Kfs76v(8+lufD@DNM7Nt6L`iRVzQa+WuE+||9%#|YVCfKRP8`2)Cb*y8 z?sz{x*?l=kC%p3I|Bh2<*7pdk5hTd=`4y_l%Gp&ex$I(o<*h#pq*&V;0ceN7Kgf6q zw8KHREMVw&H~MSyeV-#IQ`UrUy?e8@GAn{MGY7(!1YzUOuk;_bd;%350%m9RuS&nl z@B51DHx+C)DmpfhF~F6NJjUm4`8<)t`3|%c_k9=QnIX=W(!W#{LGtYQPT&Q-;<9Pg zwUVm1x-atBLfvtPk2-bO%KBP62LQ~Rk?Tpmt0N+%mXbl{Q3gO7H4x?O9%oLiLaPY$ zCF{)-H)_&!wz&QHIVBs+6`5ky)6(}wzNTmi2oZwm0syK-GtTvDqGPjl`ZU4%Hhg|M{&l%VxLjtWeyZQ|I%Q4$R&xSO z=`Rhn`J3{754Q>QWA$s6=UE0kM_<41mP4(tuJhb)x{(*W_(lBHJO3K*IvfDg=(mpb z4}t^$AV`og25WS{TLk4ZuU9VlvVewmX4|nhPD{o)b=~dJ%tDEux%^7H21rW%F z&o%$1CF0=013(*N7OFVic7INLMGCD@^?9%?+dO+VDiri)CZI&T&w&i@^0>@_iNW;; zJ`(m5=Wk7^o?r7PrU0sZPiLhF7=}<#)75_&XMsn}y-8~L%QsNX@b~)lzGRb}*SX=N zsMQ*H4-5M);{W@f@8UoI<}-NpYyLHB){&UKr6ECfV7;yY466JsHv|CqvBxhXP_qCY zqXB@?0F8(tN?>*M9IyZJSMd*j_W_#CzAi%@n-kWzAoCkO1vy_FwNdYDAPNi;3@rN( z9$@YhS0{B8%0zq<2l`|3o4px5#(sCQ;hn|%4Cj0|Ss({Bd~l9}Y(d0f`W)_p5?d{( zMYSkl8LHzib5~SwTWrVy;3~#c7cy)T=Ah)lSx1^>w6hdbKg^q7_ZB|*!Ozf$Bhu6k z;2Z%82y!9#-2J|F+RnK6l1K2SpL#9co-1q)gFt|WFfT{|0D^3>*r5ceAVGrkOAHu^ zfduRKR>0|Hb{6U_IuoYunO^+c`wZtj-uZk(RhdgLrrU;5LhIVSBoY;*nKG_6eMne;~`H^!p<(rhHzz?;hrD2yHKj@yg3y@^85N#3|x>ef&9YyK@rU=ODWT z1~^5Xk|c)Jl@-4ECC}&jCp{T;1^5@h?I08Sr|0(qUDalH&pTUXX7Rp0-NyCpq;1`T ztlk&3!qw|+RnSxJ6Yr+Y7^v8VjJA0fW4B#^stk*&U7Tq-2#gFB6^{M9t#DLLdEdsC zf3M;WmlwLB8n*3yLT(%ffG8<&ZuxAki*eowK&J3bx7$1!b=@Fe1tKCGiud3$tuD!^ z%mUB~BfP~mwjF&G9SD4y?X0J9-e8O`u6uVE0AnKX9`TBaOy0L!I;yReXE&>#x7(XT zRU91>3z|7pJ+7#k7v?C#RSgP65qa&u*y5zj2>mvr^B{&5WH7Rri0^3}lh%7Ra)GZNL9El{7}uk!c&RA)3q+E=!STkX&KjR62(ELNvaodM#xWqCK+ zbCkU*1|e&MwIf>VO{*=t8KEfyV2g}ctYwZ!C#27j(g}k93r!63m*%ZLUtw|_$M($a z00_`;tEM!_GDGVM@s%7cqa?G6vzs{A9gr-w)E5qN^QS(^zj@gk_~SqQ6}&MtQ%4d< z%v+-fA0}~-DFXf~|F`^k=kmJ$)pKY0s%L!_PkrVy_|(TfNv*NJ3z+VtA0Y~n%levf=a=sSBBE$7m|IVTq1^(TjtVHW)Y@#jdyr3= z{y&t)oWC~R)RJzW62#AEo20;-vX>lsPXw4q@!saoP8Icvc6MKf>i0L(%CAD-H{=Uo z>N%RF8SS>4diQI)9o5e8nZ>ZgpZ)hgjw<86c#sb*AS52@(K+AVDgeQ(p}2I67lSkRTWCWHCJa&@y-5 zc`pD_H2(=#Mg)(cm4R~|0-;d^Wb@dOdpN+!gD{_`X7?V;+-75h;D4{pafb+qu>(5R z74dmBih|&~WOWmf)`_c_W7B0vP2&Cu5zZ<$1(DiZ5s0bxsP{N)F;PTBgJ1ilU*tD_ z`TrpT+y-}4wcut9`w>Crz`llxOlgIHk`RgR_o(2N!tn&9{)I%hVc2u{P^-wC6>7H$c=Nv49hrtB~2gCS@J+ybgnY zUjS5jd&F;_l4|wb7tOf%;){p{94-(3)#RlQW_P0kV+5BuveY7xDT>K=4lvklUdnP! z?5OMM*pRfH&esAczFT8*=*T{h(?vafukD`b7#$TDB0U+>Z5=Z|Ol6jx=(s+f+6(X% zP(e&YYb~R>21aVQIoU<@xw7=uUDaH7{9%6b&Hn)tHTd^$_+FY;i4YQ#uMI;r+{$P~{Qco9LXTHP#GndM9P?e@`|aHF$20$IzU2VJw5Mw}AHPj@J{1l|6^V^Alg7srT|ts0SZhh@ z2>}22o(}ZC&xKU9M!@CGfEANkfj;U&0|WdV{soWEXMSgmw-p`<5`9)6|pMI#&5YM zi%DpwS_I05c}^qtM3{IH?-PFFjX%M@rA1!-%5S6Tzrn3bS(OPfgT@j2(#r)i5g^g+nMk7cKul z00C4EvatjXfb;c{>h2(aqiU4S>jfeB7C{Ca0FQHy#l?Mm;JqK=-n;L@#5L-T#{32U z8_Zu!#HxeUb(e=1(rG7F-jSSShq7XaUncqf21Cb@n&q!e1P04u&66ZxI^?BN0zh^v zxb+I&zrY4#0)v$gSNzqOw3W93^?N`Iv*tRs)gr3bXw(|K`At8{FTCY9snsHo;o!e~ z`+Xm$(a7OTLm3!B=6EgM2KBAdmnu%IX>0t4iM7j|0Wz@WiBZljpWy{BdLAZT*l}N2 zCAW(Cy$`F{yvv@?0MM!o)ESNRP+v!xH>K`7A1y9ei_|?IvDFa06lrzNIdC3hN-9*^ z@prntB>|N+<`oJe6$&!R)1DHpF-5Q-+WMCs$lCeQbpt@f!#D zgTd{hKSfKp)KJ%TLwaz*#M7gS|15Lm-c=+Ka>mGT5N*G`>j;-vr+H`*~5KjYKe#vo3M z=VRI=@jAz@fZLAy2~($cD1Z?GpMxOnW|MlojyJx`O>x0z7&*l#*eu1wW{@*a%`Srg z9$4eDOD@BhjL0~8WiiG6`A|Tv)5MqC4zP5y5K!)q(eM9PPMmm2p$E=_9-atBnLQGjA;|An z_R_KPe#$9HZH>oXx5WEz$`BD8zKCpg_nCNARPvqJ4u6F4A@J`9NZRc-s7IY3^Hk4m zEYReUkG>d##Y8^OXc6tHm6UbWPiKF2)D{>dI>3YXpG5lk3dD^$-}y`PP1BUPc7VTs z_uupUulqKBC zCE-DkAjJ+^t~$=UnMS$Bsj?=oTyr&4BUKu{VpQ~Ht^WP({r=2j%9^yQbNt&iydikYXbM2#1-^$mQAVJEuGTee7(}=Ye=WJe2geQt; z3j+NfCkm|E)ipDIf9CU>vc{{r&#m85pVn2@D9M&y@6h#aQvFnNf8VO7mNZZm>h&78 zeeO6v`kJ4nRy*2FZ(?RE8`sC2 z>hxh9+vj}R`7xZ@FCr{71j}ij^u$YuN@qs22m|0 zbFR9C=zMePy%NU}S!QX}YNV|e?N%ETlee^*gZtraMpCaMnsT54*I&0E;URqLDO2L0 zRj;dZpev*HisMbt2VNl0WvZich~vjsff|tkFM^D96Lk)CDCb(q(vM_Er5XDvx1f`Z_$GZ)9+uTsXER)nBcHT_i?bd@RVGQLK>MAF3 zUoO4hZnj8j2~nKS*=OJQHGJ};|3sE%#7VsUr2s(!01)JS6*Cml?zTaxB!aG3z|71! z{-(kKsJ^4ydxJLdh^IK7ZZXN`@a;ESd%b45^RIG%;wZ*Ci_-|(_Ppu`{ypohHR9R= zW{G72v9s1ah8IC0NuR?am!yg$lstk)9r{)`CFKx@IRQue;u!vfq;pP{!NaD)&htKX*;F8wq69?elALAj1jbm^3^rD=pwlM ziWu)!a(qk&*Nyq_JKvi#o|slzZ4B?K+nvPnR?oFLb@D8S_jkRsK0{Ch@LO_O<5~qy*~h$`0bmsC zT6o}*S6{?sS1xe+wkC1C!8}yJbvDq97G&(Ujcuox#RV=@@!n&M&|GiPZnlWxXxD(| zj{WbX#ES@VB6-$^Tj!c<;mAc9uC<;!Q=P!yZ`J_sMiiNo7b-DpE#bcVAu0#MAv-=Efn8{D<_TXVWI z+~8@ot16|k3=mY}H{$>zHN;8G;(mU;C zo4Xxa6V*Ape2xQgLS!ynjtvnZ%QAdwiEByM`Q6pW)oDT4RT|4;tUsB9bn0F2gt5h!QwoU7C4JQZK)Ix$L|R-bL2xs(^vhHM zeI=k#_4{Q&VTkk=@?}G@&a(f&ejatrl{|3#UTTdcvea&;8R=Wd{Py^ZL3S4an72N# z^G=#np3qwz4D|P9cM5{w)#a?oqJuS4n|9LsC$`{G+F44HM3u*P+YAtt`_4M&$DV)r z{znxqWo3eatqLGz8|IUAug%DKR_4kQ)AEjiA9!>4e*3}us3W|k0iEB#Iq5zJ9X~Is z?tl4vRe*~xv-+36->uR2#oJNw8~N*2nvx_jX_|51z(H15GG6sVuj5_sd;TPd~TWr!rW^(Kw|OG7oV<&vXW+mP;7w}H-O z_&$9y3f?=68oVlYWfd_7Z?ir-w{sq)(ZMby9gYlDh{ccq&wbW4EZWmZ9O10N_zn@E z(CyFdm|^oxX8e4~|0pjA5s($9Ya;GB(Wcb~kz_8qWK;)?W@77OByZ~C0WpiLwA#Go z$9{%C{{46I%Gdt@-}SvO=4f3wyPoEtp^1p&h-R~uJIirYfKWx2z({~@5S%a1zs`K? z1ah7Acf*a8KK)a#h*t3u^yzDdr)7*2;8mv5pK>7D`T9nF-q5L1!Lh(FK;QqFp@6!K zPz_}`lmcpDtUaB!Ym_S=-<16&84C!M&!>vgqx|{dE=s=lS3FS+*FE7HKK6l+j_@}J zv;Q`0BR%=10eay9)}m9Y&kjq~`X4d?&O7Nd06VaFSCZIl`|n?9&$qnTZ^0ej^bUgW zE>K-SV6HPl%#`;Q+AWJQN)+WlfOpWc8JAvh3AcReR(|MTzn(AMb{qTmUCQd}3YglS z*D&W5Vz*}IJwl>j2K2=Ac_@DC;+HM9g<=#boUn)%PkPeD`J!kt;^+`=-g00i zCndA(jI5Rt)e<}x4ilhwPZUR-K7E#)h0f2@$>j>|EJVuw7#{P;Lx?>NWCa$br8(kZ zk32t5!soy60B02HQJo};MgZ!2-!tk3QVI2i%enK*CwRk;ypgy5r{CvwZ+r#c_MIPEMlW|v!6t-^O@d9hfA>fNe6qM z*D?OtbT#VbW4rEaKhgKAQ{-jnvs+0NJ!=jB(Dg7INuVG4e#P%saU7_2O{S`&)oOCp zqpqO4Q?(u0D{4Sr2r``yB&KJWy;M4A+wHuTckIsp)^sC! zOAYMG?o9BX^OO1B0$2PsRT&Lklvv6San^3u1NmLM1%7*iel0<#jUq6wRp%O z-ceCAf}M2byM^SCGO8ley0knG_TIUJJkhfNWj>vC-z!6;s4W%BgJ zFhypEh~zu+;xqE%~L3&5u`Xa~9R#%UDpZ`^l_YOZvpY9#5R~$}pu1!$Y zxBvhk07*naRQP?ES-t_9%(pqm(tJkpBo_D&Z zs(51VzT1-7eW;Ie=fNxZIht>(gJTSECbG`K1LwHmDFS{4@3OgTvu8x`syOdyx7s*s z_W*~)4A-l@;9r19gtMp5LTpgGyCig8dZTOioKTAe#{+!jS6)IbT0^Ck@6--(Q0@^Z zP;>0nc<|J?>+aK77I2n=JLCLcLesXRxz?nycrjv*@}=AF{w{Bn1I-A4t?c`T-;iUOJpfgTqg-S<$O-@TE1(&+N|oxic#ho`3m1 z2if=;TgQ>3M_623#HMMuAwdQtWB>%2N}RJ;YsoU#bHK+2d^>FLDuU2aec-(+-g&$# z_4;%v-2KPvBJO>#Y`IQwsL!yQjU&QONP;nxDG_yx5lasFBrN*cy{=BiMPvqR`bQJ8 z^>o6OY6x`$mhU8rhXT{V=UvgJFOEj=2=vu_BZ$d&bDVWpXE7#WaiPJ&VuM3R_j7i+ z&HLW_A^zlT|AY7b^?MPM5Z5jN!1{tAyvTNwuENI~J1@Hkh*VGTe1Az*snzy#--$Ck zd`j4NaShE{-RItUZc^Pwq5A#)gAC2j@th6ge@}W&rFabM#N5M;Uw0MneCORnlHyGb znL`{rFE{a=K6#ppE`3CoywaB2K2>DE%)40YSg6-=-Vq_3K6M%?Y~uWspnE#W&Rz}b zSJ{W>8*aP`?@y3))Kbef#xutBY|r{QEhv}M1z}8sG^_K)+s~j}j8mbT%r$mRaC=MP zM$Gk`_&?4&0bn3n02WwYY4i3!c{^|alRxI_D<8`@Joni=|Ao)xh8v#9rI#O}7UfU4 z-G5#iP?(r0 zTnEpmoV!1;fNXqBuQ~5Phw)m)jRr2p2DYxdaCh6FZLk|sZVMd8amjtC(7%xmPVcvp zB*A8uI7wJK(7+f&WMb~W|3Pm52yO^Gj3u+Fm z)OhegICWZi)Wx+TZ3=HHA;N}hDx*#$QW)_ZKW!c~{T zPtvr=yr?bm@WUx*&R8%@8`|UZPX_ChwQqe;frW*O5%t`C z@7=uZ_x_N#{oWsPWd9?0{FAQdtDpT0o_*u9xa87Hxa` zv-#TE4e+$^qc&KFbKlBAoD7!1Ou6%ID|9f@acjWFB!a#S;Ok^10AqbC9p*IBK^n)L zfUzLsL`@Dr^p#o+`wx~Vit*mDw%#O(BjO}M#NfTllL<;`^Q99Wdp)iqg}cuBY>3e#3N`%EgaZaE#kjFX3+H zKiePt&^^_X|FZA z88jWcyz3ba;_;={aMmsJwKvwd>VwZ?K<|BN4otq* zm$`U4>uuStmGd_uKjPVrTx9!d^<^B1;rJc%!IK=m#-*@GyHoc7Gv!u6ioM=%hx$_mth2n(NHSo1O!K_ ze#2zTEXz=r^|?ZwPu8e44&YR9?IvP`_S#yplN{$e|8WdX2NwBxYm++DJSQdtKKJGoVn$!0;GjUGr|H?0VQYM$*C*J6D8 zV9x(itb6yJc!2v(+{Qqz1Q!o1ap>?tu6*P%F1_qBp7xBV@PsEn zp2ejl*4New_tB5^OGIP_E31?Mpo=Yp9AMdSsQvv9f00J2zJ(hvZC+|HX!lA@STVl| zS!%nVQEQQoZWP2>nss)>`|A(~&mijFUpMl8&a;EAF^ z>idY-V-F$V8o2mUZoc^oIQD_Yc(E50VYrNT82bEyyw|-5IOiaaNS*7h75UmQ!ksQk z

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≫hyG$#9-# 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 o&4LZZfO9asxWycvMN)78&qol`;+057q0@Bjb+ literal 0 HcmV?d00001 diff --git a/docs/_static/img/laboratory.png b/docs/_static/img/laboratory.png new file mode 100644 index 0000000000000000000000000000000000000000..aec03dec37b1c7ac3d00b311aaf72d3a8905180c GIT binary patch literal 33952 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 MZ)+>RQ?v^GAK0uL*Z=?k literal 0 HcmV?d00001 diff --git a/docs/_static/img/optimize-experiment-loop.png b/docs/_static/img/optimize-experiment-loop.png new file mode 100644 index 0000000000000000000000000000000000000000..b343b1cba221411e4ea5e95a1ebd7d4f76667e61 GIT binary patch literal 20405 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ȟ!Q= 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 b5_3YQ_S#0RYroV00zL-1Cb;L?&cy!$zeKy} literal 0 HcmV?d00001 diff --git a/docs/_static/img/tasks-devices.png b/docs/_static/img/tasks-devices.png new file mode 100644 index 0000000000000000000000000000000000000000..8c04596b822c39a7d4d82625dead026e8b10b08c GIT binary patch literal 52132 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 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/user-guide/campaigns.rst b/docs/user-guide/campaigns.rst new file mode 100644 index 0000000..9eb748a --- /dev/null +++ b/docs/user-guide/campaigns.rst @@ -0,0 +1,126 @@ +Campaigns +========= +A campaign in EOS is an experiment that is executed multiple times in sequence. The parameters of the experiments usually +differ. A campaign has some goals, such as to optimize some objectives by searching for optimal parameters. +Campaigns are the highest-level execution unit in EOS, and can be used to implement autonomous (self-driving) labs. + +The DMTA loop is a common paradigm in autonomous experimentation and EOS campaigns can be used to implement it. EOS has +built-in support for running campaigns of an experiment. In addition, EOS has a built-in Bayesian optimizer that can +be used to optimize parameters. + +.. figure:: ../_static/img/dmta-loop.png + :alt: The DMTA Loop + :align: center + +Optimization Setup (Analyze and Design Phases) +---------------------------------------------- +Both the "analyze" and "design" phases of the DMTA loop can be automated by optimizing the parameters of experiments over time. +This is natively supported by EOS through a built-in Bayesian optimizer that integrates with the campaign execution module. +It is also possible to customize the optimization to incorporate custom algorithms such as reinforcement learning. + +Let's look at the color mixing experiment to see how a campaign with optimization can be set up. There are six dynamic +parameters, which are the inputs of the optimization problem: + +.. code-block:: yaml + + # In the "dispense_colors" task + cyan_volume: eos_dynamic + magenta_volume: eos_dynamic + yellow_volume: eos_dynamic + black_volume: eos_dynamic + + # In the "mix_colors" task + mixing_time: eos_dynamic + mixing_speed: eos_dynamic + +Looking at the task specification of the `score_color` task, we also see that there is an output parameter called "loss". + +:bdg-primary:`task.yml` + +.. code-block:: yaml + + type: Score Color + description: Score a color based on how close it is to an expected color + + input_parameters: + red: + type: integer + unit: n/a + description: The red component of the color + green: + type: integer + unit: n/a + description: The green component of the color + blue: + type: integer + unit: n/a + description: The blue component of the color + + output_parameters: + loss: + type: decimal + unit: n/a + description: Total loss of the color compared to the expected color + +Taking all these together, we see that this experiment involves selecting CMYK color component volumes, as well as a +mixing time and mixing speed and trying to minimize the loss of a synthesized color compared to an expected color. + +This setup is also summarized in the `optimizer.py` file adjacent to `experiment.yml`. + +:bdg-primary:`optimizer.py` + +.. code-block:: python + + from typing import Type, Tuple, Dict + + from bofire.data_models.acquisition_functions.acquisition_function import qNEI + from bofire.data_models.enum import SamplingMethodEnum + from bofire.data_models.features.continuous import ContinuousOutput, ContinuousInput + from bofire.data_models.objectives.identity import MinimizeObjective + + from eos.optimization.sequential_bayesian_optimizer import BayesianSequentialOptimizer + from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + + def eos_create_campaign_optimizer() -> Tuple[Dict, Type[AbstractSequentialOptimizer]]: + constructor_args = { + "inputs": [ + ContinuousInput(key="dispense_colors.cyan_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.magenta_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.yellow_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.black_volume", bounds=(0, 5)), + ContinuousInput(key="mix_colors.mixing_time", bounds=(1, 15)), + ContinuousInput(key="mix_colors.mixing_speed", bounds=(10, 500)), + ], + "outputs": [ + ContinuousOutput(key="score_color.loss", objective=MinimizeObjective(w=1.0)), + ], + "constraints": [], + "acquisition_function": qNEI(), + "num_initial_samples": 50, + "initial_sampling_method": SamplingMethodEnum.SOBOL, + } + + return constructor_args, BayesianSequentialOptimizer + +The `eos_create_campaign_optimizer` function is used to create the optimizer for the campaign. We can +see that the inputs are composed of all the dynamic parameters in the experiment and the output is the "loss" output parameter +from the "score_color" task. The objective of the optimizer (and the campaign) is to minimize this loss. + +More about optimizers can be found in the Optimizers section. + +Automation Setup (Make and Test Phases) +--------------------------------------- +Execution of the automation is managed by EOS. The tasks and devices must be implemented by the user. Careful setup of +the experiment is required to ensure that a campaign can be executed autonomously. + +Some guidelines: + +* Each experiment should be standalone and should not depend on previous experiments. +* Each experiment should leave the laboratory in a state that allows the next experiment to be executed. +* Dependencies between tasks should be minimized. A task should have a dependency on another task only if it is necessary. +* Tasks should depend on any devices that they may be interacting with, even if they are not operating them. For example, + if a robot transfer task takes a container from device A to device B, then the robot arm and both devices A and B should be required + devices for the task. +* Branches and loops are not supported. If these are needed, they should be encapsulated inside large tasks that may use + many devices and may represent several steps in the experiment. diff --git a/docs/user-guide/configuration.rst b/docs/user-guide/configuration.rst new file mode 100644 index 0000000..bf23d3d --- /dev/null +++ b/docs/user-guide/configuration.rst @@ -0,0 +1,28 @@ +Configuration +============= + +After installation, you need to configure external services such as MongoDB and MinIO as well as EOS itself. + +1. Configure External Services +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +We provide a Docker Compose file that can run all external services for you. + +Copy the example environment file: + +.. code-block:: shell + + cp docker/.env.example docker/.env + +Edit `docker/.env` and provide values for all fields. + +2. Configure EOS +^^^^^^^^^^^^^^^^ +EOS reads parameters from a YAML configuration file. + +Copy the example configuration file: + +.. code-block:: shell + + cp config.example.yml config.yml + +Edit `config.yml`. Ensure that credentials are provided for the MongoDB and MinIO services. diff --git a/docs/user-guide/devices.rst b/docs/user-guide/devices.rst new file mode 100644 index 0000000..d1e5e12 --- /dev/null +++ b/docs/user-guide/devices.rst @@ -0,0 +1,107 @@ +Devices +======= +In EOS, a device is an abstraction for a physical or virtual apparatus. A device is used by one or more tasks to +run some operations. Each device in EOS is managed by a dedicated process which is created the moment +a laboratory definition is loaded. This process is usually implemented as a server and tasks call various functions +from it. For example, there could be a device called "magnetic mixer", which communicates with a physical magnetic mixer via +serial and provides functions such as `start`, `stop`, `set_time` and `set_speed`. + +.. figure:: ../_static/img/tasks-devices.png + :alt: EOS Tasks and Devices + :align: center + +In the figure above, we illustrate an example of devices and a task that uses these devices. The task in this example is +Gas Chromatography (GC) sampling, which is implemented with a GC and a mobile manipulation robot for automating the +sample injection with a syringe. Both the GC and the robot are physical devices, and each has a device implementation +in EOS, which runs as a persistent process. Then, the GC Sampling task uses both of the EOS devices to automate the +sample injection process. + +Most often, an EOS device will represent a physical device in the lab. But this need not always be the case. A device +in EOS can be used to represent anything that needs persistent state throughout one or more experiments. This could +be an AI module that records inputs given to it. Remember that a device in EOS is a persistent process. + +Device Implementation +--------------------- +* Devices are implemented in the `devices` subdirectory inside an EOS package +* Each device has its own subfolder (e.g., devices/magnetic_mixer) +* There are two key files per device: `device.yml` and `device.py` + +YAML File (device.yml) +~~~~~~~~~~~~~~~~~~~~~~ +* Specifies the device type, description, and initialization parameters +* The same implementation can be used for multiple devices of the same type +* Initialization parameters can be overridden in laboratory definition + +Below is an example device YAML file for a magnetic mixer: + +:bdg-primary:`device.yml` + +.. code-block:: yaml + + type: magnetic_mixer + description: Magnetic mixer for mixing the contents of a container + + initialization_parameters: + port: 5004 + +Python File (device.py) +~~~~~~~~~~~~~~~~~~~~~~~ +* Implements device functionality +* All devices implementations must inherit from `BaseDevice` +* The device class name must end with "Device" to be discovered by EOS + +Below is a example implementation of a magnetic mixer device: + +:bdg-primary:`device.py` + +.. code-block:: python + + from typing import Dict, Any + + from eos.containers.entities.container import Container + from eos.devices.base_device import BaseDevice + from user.color_lab.common.device_client import DeviceClient + + class MagneticMixerDevice(BaseDevice): + def _initialize(self, initialization_parameters: Dict[str, Any]) -> None: + port = int(initialization_parameters["port"]) + self.client = DeviceClient(port) + self.client.open_connection() + + def _cleanup(self) -> None: + self.client.close_connection() + + def _report(self) -> Dict[str, Any]: + return {} + + def mix(self, container: Container, mixing_time: int, mixing_speed: int) -> Container: + result = self.client.send_command("mix", {"mixing_time": mixing_time, "mixing_speed": mixing_speed}) + if result: + container.metadata["mixing_time"] = mixing_time + container.metadata["mixing_speed"] = mixing_speed + + return container + +Let's walk through this example code: + +There are functions required in every device implementation: + +#. **_initialize** + + * Called when device process is created + * Should set up necessary resources (e.g., serial connections) + +#. **_cleanup** + + * Called when the device process is terminated + * Should clean up any resources created by the device process (e.g., serial connections) + +#. **_report** + + * Should return any data needed to determine the state of the device (e.g., status and feedback) + +The magnetic mixer device also has the function `mix` for implementing the mixing operation. This function will be called +by a task to mix the contents of a container. The `mix` function: + +* Sends a command to lower-level driver with a specified mixing time and speed to operate the magnetic mixer +* Updates container metadata with mixing details diff --git a/docs/user-guide/experiments.rst b/docs/user-guide/experiments.rst new file mode 100644 index 0000000..2dcfc05 --- /dev/null +++ b/docs/user-guide/experiments.rst @@ -0,0 +1,286 @@ +Experiments +=========== +Experiments are a set of tasks that are executed in a specific order. Experiments are represented as directed +acyclic graphs (DAGs) where nodes are tasks and edges are dependencies between tasks. Tasks part of an experiment can +pass parameters and containers to each other using EOS' reference system. Task parameters may be fully defined, with +values provided for all task parameters or they may be left undefined by denoting them as dynamic parameters. Experiments with +dynamic parameters can be used to run campaigns of experiments, where an optimizer generates the values for the +dynamic parameters across repeated experiments to optimize some objectives. + +.. figure:: ../_static/img/experiment-graph.png + :alt: Example experiment graph + :align: center + +Above is an example of a possible experiment that could be implemented with EOS. There is a series of tasks, each +requiring one or more devices. In addition to the task precedence dependencies with edges shown in the graph, there can +also be dependencies in the form of parameters and containers passed between tasks. For example, the task "Mix Solutions" +may take as input parameters the volumes of the solutions to mix, and these values may be output from the "Dispense Solutions" +task. Tasks can reference input/output parameters and containers from other tasks. + +Experiment Implementation +------------------------- +* Experiments are implemented in the `experiments` subdirectory inside an EOS package +* Each experiment has its own subfolder (e.g., experiments/optimize_yield) +* There are two key files per experiment: `experiment.yml` and `optimizer.py` (for running campaigns with optimization) + +YAML File (experiment.yml) +~~~~~~~~~~~~~~~~~~~~~~~~~~ +Defines the experiment. Specifies the experiment type, labs, container initialization (optional), and tasks + +Below is an example experiment YAML file for an experiment to optimize parameters to synthesize a specific color: + +:bdg-primary:`experiment.yml` + +.. code-block:: yaml + + type: color_mixing + description: Experiment to find optimal parameters to synthesize a desired color + + labs: + - color_lab + + tasks: + - id: retrieve_container + type: Retrieve Container + description: Get a random available container from storage and move it to the color dispenser + devices: + - lab_id: color_lab + id: robot_arm + - lab_id: color_lab + id: container_storage + containers: + c_a: c_a + c_b: c_b + c_c: c_c + c_d: c_d + c_e: c_e + parameters: + target_location: color_dispenser + dependencies: [] + + - id: dispense_colors + type: Dispense Colors + description: Dispense a color from the color dispenser into the container + devices: + - lab_id: color_lab + id: color_dispenser + containers: + beaker: retrieve_container.beaker + parameters: + cyan_volume: eos_dynamic + magenta_volume: eos_dynamic + yellow_volume: eos_dynamic + black_volume: eos_dynamic + dependencies: [retrieve_container] + + - id: move_container_to_mixer + type: Move Container + description: Move the container to the magnetic mixer + devices: + - lab_id: color_lab + id: robot_arm + - lab_id: color_lab + id: magnetic_mixer + containers: + beaker: dispense_colors.beaker + parameters: + target_location: magnetic_mixer + dependencies: [dispense_colors] + + - id: mix_colors + type: Magnetic Mixing + description: Mix the colors in the container + devices: + - lab_id: color_lab + id: magnetic_mixer + containers: + beaker: move_container_to_mixer.beaker + parameters: + mixing_time: eos_dynamic + mixing_speed: eos_dynamic + dependencies: [move_container_to_mixer] + + - id: move_container_to_analyzer + type: Move Container + description: Move the container to the color analyzer + devices: + - lab_id: color_lab + id: robot_arm + - lab_id: color_lab + id: color_analyzer + containers: + beaker: mix_colors.beaker + parameters: + target_location: color_analyzer + dependencies: [mix_colors] + + - id: analyze_color + type: Analyze Color + description: Analyze the color of the solution in the container and output the RGB values + devices: + - lab_id: color_lab + id: color_analyzer + containers: + beaker: move_container_to_analyzer.beaker + dependencies: [move_container_to_analyzer] + + - id: score_color + type: Score Color + description: Score the color based on the RGB values + parameters: + red: analyze_color.red + green: analyze_color.green + blue: analyze_color.blue + dependencies: [analyze_color] + + - id: empty_container + type: Empty Container + description: Empty the container and move it to the cleaning station + devices: + - lab_id: color_lab + id: robot_arm + - lab_id: color_lab + id: cleaning_station + containers: + beaker: analyze_color.beaker + parameters: + emptying_location: emptying_location + target_location: cleaning_station + dependencies: [analyze_color] + + - id: clean_container + type: Clean Container + description: Clean the container by rinsing it with distilled water + devices: + - lab_id: color_lab + id: cleaning_station + containers: + beaker: empty_container.beaker + dependencies: [empty_container] + + - id: store_container + type: Store Container + description: Store the container back in the container storage + devices: + - lab_id: color_lab + id: robot_arm + - lab_id: color_lab + id: container_storage + containers: + beaker: clean_container.beaker + parameters: + storage_location: container_storage + dependencies: [clean_container] + +Let's dissect this file: + +.. code-block:: yaml + + type: color_mixing + description: Experiment to find optimal parameters to synthesize a desired color + + labs: + - color_lab + +Every experiment has a type. The type is used to essentially identify the class of experiment. When an experiment is running +then there are instances of the experiment with different IDs. Each experiment also requires one or more labs. + +Now let's look at the first task in the experiment: + +.. code-block:: yaml + + - id: retrieve_container + type: Retrieve Container + description: Get a random available container from storage and move it to the color dispenser + devices: + - lab_id: color_lab + id: robot_arm + - lab_id: color_lab + id: container_storage + containers: + c_a: c_a + c_b: c_b + c_c: c_c + c_d: c_d + c_e: c_e + parameters: + target_location: color_dispenser + dependencies: [] + +The first task is named `retrieve_container` and is of type `Retrieve Container`. This task uses the robot arm to get +a random container from storage. The task requires two devices, the robot arm and the container storage. There are five +containers passed to it, "c_a" through "c_e". There is also a parameter `target_location` that is set to `color_dispenser`. +This task has no dependencies as it is the first task in the experiment and is essentially a container feeder. +There are five containers in storage, and one of them is chosen at random for the experiment. All five containers in our +"color lab" are passed to this task, as any one of them could be chosen. + +Let's look at the next task: + +.. code-block:: yaml + + - id: dispense_colors + type: Dispense Colors + description: Dispense a color from the color dispenser into the container + devices: + - lab_id: color_lab + id: color_dispenser + containers: + beaker: retrieve_container.beaker + parameters: + cyan_volume: eos_dynamic + magenta_volume: eos_dynamic + yellow_volume: eos_dynamic + black_volume: eos_dynamic + dependencies: [retrieve_container] + +This task takes the container from the `retrieve_container` task and dispenses colors into it. The task has an +input container called "beaker" which references the output container named "beaker" from the `retrieve_container` task. +If we look at the `task.yml` file of the task `Retrieve Container` we would see that a container named "beaker" is +defined in `output_containers`. There are also four parameters, the CMYK volumes to dispense. All these parameters are +set to `eos_dynamic`, which is a special keyword in EOS for defining dynamic parameters, instructing the system that +these parameters must be specified either by the user or an optimizer before an experiment is run. + +Optimizer File (optimizer.py) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Contains a function that returns the constructor arguments for and the optimizer class type for an optimizer. + +As an example, below is the optimizer file for the color mixing experiment: + +:bdg-primary:`optimizer.py` + +.. code-block:: python + + from typing import Type, Tuple, Dict + + from bofire.data_models.acquisition_functions.acquisition_function import qNEI + from bofire.data_models.enum import SamplingMethodEnum + from bofire.data_models.features.continuous import ContinuousOutput, ContinuousInput + from bofire.data_models.objectives.identity import MinimizeObjective + + from eos.optimization.sequential_bayesian_optimizer import BayesianSequentialOptimizer + from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + + def eos_create_campaign_optimizer() -> Tuple[Dict, Type[AbstractSequentialOptimizer]]: + constructor_args = { + "inputs": [ + ContinuousInput(key="dispense_colors.cyan_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.magenta_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.yellow_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.black_volume", bounds=(0, 5)), + ContinuousInput(key="mix_colors.mixing_time", bounds=(1, 15)), + ContinuousInput(key="mix_colors.mixing_speed", bounds=(10, 500)), + ], + "outputs": [ + ContinuousOutput(key="score_color.loss", objective=MinimizeObjective(w=1.0)), + ], + "constraints": [], + "acquisition_function": qNEI(), + "num_initial_samples": 50, + "initial_sampling_method": SamplingMethodEnum.SOBOL, + } + + return constructor_args, BayesianSequentialOptimizer + +The `optimizer.py` file is optional and only required for running experiment campaigns with optimization managed by EOS. +More on optimizers can be found in the Optimizers section of the User Guide. diff --git a/docs/user-guide/index.rst b/docs/user-guide/index.rst new file mode 100644 index 0000000..4260be1 --- /dev/null +++ b/docs/user-guide/index.rst @@ -0,0 +1,25 @@ +User Guide +========== + +.. toctree:: + :caption: Getting Started + + installation + configuration + running + +.. toctree:: + :caption: Concepts + + packages + devices + laboratories + tasks + experiments + campaigns + optimizers + +.. toctree:: + :caption: Advanced + + jinja2_templating \ No newline at end of file diff --git a/docs/user-guide/installation.rst b/docs/user-guide/installation.rst new file mode 100644 index 0000000..736ce25 --- /dev/null +++ b/docs/user-guide/installation.rst @@ -0,0 +1,59 @@ +Installation +============ + +EOS should be installed on a capable computer in the laboratory. We recommend a central +computer that is easily accessible. + +.. note:: + If EOS will be connecting to other computers to run automation, then you must ensure that the computer where EOS + is installed has bi-directional network access to the other computers. + + We strongly recommend that the laboratory has its own isolated network for security and performance reasons. + See our infrastructure setup guide for more information. + +EOS also requires a MongoDB database, a MinIO object storage server, and (for now) Budibase for the web UI. +We provide a Docker Compose file that can set up all of these services for you. + +1. Install PDM +^^^^^^^^^^^^^^ +PDM is used as the project manager for EOS, making it easier to install dependencies and build it. + +.. tab-set:: + + .. tab-item:: Linux/Mac + + .. code-block:: shell + + curl -sSL https://pdm-project.org/install-pdm.py | python3 - + + .. tab-item:: Windows + + .. code-block:: shell + + (Invoke-WebRequest -Uri https://pdm-project.org/install-pdm.py -UseBasicParsing).Content | py - + +2. Clone the EOS Repository +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: shell + + git clone https://github.com/aangelos28/eos + +3. Install Dependencies +^^^^^^^^^^^^^^^^^^^^^^^ +Navigate to the cloned repository and run: + +.. code-block:: shell + + pdm install + +(Optional) If you wish to contribute to EOS development: + +.. code-block:: shell + + pdm install -G dev + +(Optional) If you also wish to contribute to the EOS documentation: + +.. code-block:: shell + + pdm install -G docs diff --git a/docs/user-guide/jinja2_templating.rst b/docs/user-guide/jinja2_templating.rst new file mode 100644 index 0000000..ae9e428 --- /dev/null +++ b/docs/user-guide/jinja2_templating.rst @@ -0,0 +1,24 @@ +Jinja2 Templating +================= +The EOS YAML files used to define labs, devices, experiments, and tasks support Jinja2 templating. This allows easier +authoring of complex YAML files by enabling the use of variables, loops, and conditionals. Jinja2 templates are evaluated +with Python, so some of the expressions are the same as in Python. + +.. note:: + Jinja2 templates are evaluated during loading of the YAML file, not during runtime. + +Below is the "containers" portion of a lab YAML file that uses Jinja2 templating: + +:bdg-primary:`lab.yml` + +.. code-block:: yaml+jinja + + containers: + - type: beaker + location: container_storage + metadata: + capacity: 300 + ids: + {% for letter in ['a', 'b', 'c', 'd', 'e'] %} + - c_{{ letter }} + {% endfor %} diff --git a/docs/user-guide/laboratories.rst b/docs/user-guide/laboratories.rst new file mode 100644 index 0000000..8900428 --- /dev/null +++ b/docs/user-guide/laboratories.rst @@ -0,0 +1,267 @@ +Laboratories +============ +Laboratories are the space in which devices and containers exist and where tasks, experiments, and campaigns +of experiments take place. + +A laboratory in EOS is a collection of: + +* Locations (e.g., physical stations around the lab) +* Computers (e.g., devices capable of controlling equipment) +* Devices (e.g., equipment/apparatuses in the laboratory) +* Containers (e.g., vessels for holding samples) + +.. figure:: ../_static/img/laboratory.png + :alt: Contents of a laboratory + :align: center + +Laboratory Implementation +------------------------- +* Laboratories are implemented in the `laboratories` subdirectory inside an EOS package +* Each laboratory has its own subfolder (e.g., laboratories/color_lab) +* The laboratory is defined in a YAML file named `laboratory.yml` + +Below is an example laboratory YAML file for a solar cell fabrication lab: + +:bdg-primary:`lab.yml` + +.. code-block:: yaml + + type: solar_cell_fabrication_lab + description: A laboratory for fabricating and characterizing perovskite solar cells + + locations: + glovebox: + description: Nitrogen-filled glovebox + metadata: + map_coordinates: + x: 10 + y: 20 + theta: 0 + fume_hood: + description: Fume hood for solution preparation and coating + annealing_station: + description: Hotplate for thermal annealing + evaporation_chamber: + description: Thermal evaporation chamber for electrode deposition + characterization_room: + description: Room for solar cell performance testing + + computers: + xrd_computer: + description: XRD system control and data analysis + ip: 192.168.1.101 + solar_sim_computer: + description: Solar simulator control and J-V measurements + ip: 192.168.1.102 + robot_computer: + description: Mobile manipulation robot control + ip: 192.168.1.103 + + devices: + spin_coater: + description: Spin coater for depositing perovskite and transport layers + type: spin_coater + location: glovebox + computer: eos_computer + + uv_ozone_cleaner: + description: UV-Ozone cleaner for substrate treatment + type: uv_ozone_cleaner + location: fume_hood + computer: eos_computer + + thermal_evaporator: + description: Thermal evaporator for metal electrode deposition + type: thermal_evaporator + location: evaporation_chamber + computer: eos_computer + initialization_parameters: + max_temperature: 1000C + materials: [Au, Ag, Al] + + solar_simulator: + description: Solar simulator for J-V curve measurements + type: solar_simulator + location: characterization_room + computer: solar_sim_computer + initialization_parameters: + spectrum: AM1.5G + intensity: 100mW/cm2 + + xrd_system: + description: X-ray diffractometer for crystal structure analysis + type: xrd + location: characterization_room + computer: xrd_computer + + mobile_robot: + description: Mobile manipulation robot for automated sample transfer + type: mobile_robot + location: characterization_room + computer: robot_computer + initialization_parameters: + locations: + - glovebox + - fume_hood + - annealing_station + - evaporation_chamber + - characterization_room + + containers: + - type: vial + location: glovebox + metadata: + solvent: 20 #ml + ids: + - precursor_vial_1 + - precursor_vial_2 + - precursor_vial_3 + + - type: petri_dish + location: glovebox + metadata: + capacity: 100 #ml + ids: + - substrate_dish_1 + - substrate_dish_2 + + - type: crucible + location: evaporation_chamber + metadata: + capacity: 5 #ml + ids: + - au_crucible + - ag_crucible + +Locations (Optional) +"""""""""""""""""""" +Locations are physical stations around the lab where devices and containers are placed. They are defined in the +`locations` section of the laboratory YAML file. You can define metadata for each location, such as map coordinates +for a mobile robot. Defining locations is optional. + +.. code-block:: yaml + + locations: + glovebox: + description: Nitrogen-filled glovebox + metadata: + map_coordinates: + x: 10 + y: 20 + theta: 0 + fume_hood: + description: Fume hood for solution preparation and coating + annealing_station: + description: Hotplate for thermal annealing + evaporation_chamber: + description: Thermal evaporation chamber for electrode deposition + characterization_room: + description: Room for solar cell performance testing + +Computers (Optional) +"""""""""""""""""""" +Computers control devices and host EOS devices. Each computer that is required to interface with one or +more devices must be defined in this section. The IP address of each computer must be specified. + +There is always a computer in each lab called **eos_computer** that has the IP "127.0.0.1". This computer is the computer +that runs the EOS orchestrator, and can be thought of as the "central" computer. No other computer named "eos_computer" +is allowed, and no other computer can have the IP "127.0.0.1". The "computers" section need not be defined unless +additional computers are required (e.g., if not all devices are connected to eos_computer). + +.. figure:: ../_static/img/eos-computers.png + :alt: EOS computers + :align: center + +.. code-block:: yaml + + computers: + xrd_computer: + description: XRD system control and data analysis + ip: 192.168.1.101 + solar_sim_computer: + description: Solar simulator control and J-V measurements + ip: 192.168.1.102 + robot_computer: + description: Mobile manipulation robot control + ip: 192.168.1.103 + +Devices (Required) +"""""""""""""""""" +Devices are equipment or apparatuses in the laboratory that are required to perform tasks. Each device must have a unique +name inside the lab and must be defined in the `devices` section of the laboratory YAML file. + +.. code-block:: yaml + + devices: + spin_coater: + description: Spin coater for depositing perovskite and transport layers + type: spin_coater + location: glovebox + computer: eos_computer + + uv_ozone_cleaner: + description: UV-Ozone cleaner for substrate treatment + type: uv_ozone_cleaner + location: fume_hood + computer: eos_computer + + thermal_evaporator: + description: Thermal evaporator for metal electrode deposition + type: thermal_evaporator + location: evaporation_chamber + computer: eos_computer + initialization_parameters: + max_temperature: 1000C + materials: [Au, Ag, Al] + +**type**: Every device must have a type, which matches a device specification (e.g., defined in the `devices` subdirectory +of an EOS package). There can be multiple devices with different names of the same type. + +**location** (optional): The location where the device is at. + +**computer**: The computer that controls the device. If not "eos_computer", the computer must be defined in the +"computers" section. + +**initialization_parameters** (optional): Parameters required to initialize the device. These parameters are defined +in the device specification and can be overridden here. + +Containers (Optional) +""""""""""""""""""""" +Containers are vessels for holding samples and are how samples go around the lab (e.g., for batch processing). They are +defined in the `containers` section of the laboratory YAML file. + +.. code-block:: yaml + + containers: + - type: vial + location: glovebox + metadata: + capacity: 20 #ml + ids: + - precursor_vial_1 + - precursor_vial_2 + - precursor_vial_3 + + - type: petri_dish + location: glovebox + metadata: + capacity: 100 #ml + ids: + - substrate_dish_1 + - substrate_dish_2 + + - type: crucible + location: evaporation_chamber + metadata: + capacity: 5 #ml + ids: + - au_crucible + - ag_crucible + +**type**: Every container must have a type, which can be used to group together containers of the same type. + +**location** (optional): The location where the container starts out at. + +**metadata** (optional): Any additional information about the container, such as its capacity or contained sample. + +**ids**: A list of unique identifiers for each container. These are used to identify and refer to specific containers. diff --git a/docs/user-guide/optimizers.rst b/docs/user-guide/optimizers.rst new file mode 100644 index 0000000..1f7e8c1 --- /dev/null +++ b/docs/user-guide/optimizers.rst @@ -0,0 +1,171 @@ +Optimizers +========== +Optimizers are key to building an autonomous laboratory. In EOS, optimizers give intelligence to experiment campaigns +by optimizing task parameters to achieve objectives over time. Optimizers in EOS are *sequential*, meaning they iteratively +optimize parameters by drawing insights from previous experiments. One of the most common sequential optimization +methods is **Bayesian optimization**, and is especially useful for optimizing expensive-to-evaluate black box functions. + +.. figure:: ../_static/img/optimize-experiment-loop.png + :alt: Optimization and experiment loop + :align: center + +EOS has a built-in Bayesian optimizer powered by `BoFire `_ +(based on `BoTorch `_). This optimizer supports both constrained single-objective and multi-objective +Bayesian optimization. It offers several different surrogate models, including Gaussian Processes (GPs) and +Multi-Layer Perceptrons (MLPs), along with various acquisition functions. + +Distributed Execution +--------------------- +EOS optimizers are created in a dedicated Ray actor process. This actor process can be created in any computer with an +active Ray worker. This can enable running the optimizer on a more capable computer than the one running +the EOS orchestrator. + +Optimizer Implementation +------------------------ +EOS optimizers are defined in the `optimizer.py` file adjacent to `experiment.yml` in an EOS package. Below is an example: + +:bdg-primary:`optimizer.py` + +.. code-block:: python + + from bofire.data_models.acquisition_functions.acquisition_function import qNEI + from bofire.data_models.enum import SamplingMethodEnum + from bofire.data_models.features.continuous import ContinuousOutput, ContinuousInput + from bofire.data_models.objectives.identity import MinimizeObjective + + from eos.optimization.sequential_bayesian_optimizer import BayesianSequentialOptimizer + from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + + def eos_create_campaign_optimizer() -> tuple[dict, type[AbstractSequentialOptimizer]]: + constructor_args = { + "inputs": [ + ContinuousInput(key="dispense_colors.cyan_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.magenta_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.yellow_volume", bounds=(0, 5)), + ContinuousInput(key="dispense_colors.black_volume", bounds=(0, 5)), + ContinuousInput(key="mix_colors.mixing_time", bounds=(1, 15)), + ContinuousInput(key="mix_colors.mixing_speed", bounds=(10, 500)), + ], + "outputs": [ + ContinuousOutput(key="score_color.loss", objective=MinimizeObjective(w=1.0)), + ], + "constraints": [], + "acquisition_function": qNEI(), + "num_initial_samples": 50, + "initial_sampling_method": SamplingMethodEnum.SOBOL, + } + + return constructor_args, BayesianSequentialOptimizer + +Each `optimizer.py` file must contain the function `eos_create_campaign_optimizer`. This function must return: + +#. The constructor arguments to make an optimizer class instance +#. The class type of the optimizer + +In this example, we use EOS' built-in Bayesian optimizer. However, it is also possible to define custom optimizers in this +file, and simply return the constructor arguments and the class type from `eos_create_campaign_optimizer`. + +.. note:: + All optimizers must inherit from the class `AbstractSequentialOptimizer` under the `eos.optimization` module. + +Input and Output Parameter Naming +""""""""""""""""""""""""""""""""" +The names of input and output parameters must reference task parameters. The EOS reference format must be used: + +**TASK.PARAMETER_NAME** + +This is necessary for EOS to be able to associate the optimizer with the experiment tasks and to forward parameter values +where needed. + +Example Custom Optimizer +------------------------ +Below is an example of a custom optimizer implementation that randomly samples parameters for the same color mixing problem: + +:bdg-primary:`optimizer.py` + +.. code-block:: python + + import random + from dataclasses import dataclass + from enum import Enum + import pandas as pd + + from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + + class ObjectiveType(Enum): + MINIMIZE = 1 + MAXIMIZE = 2 + + + @dataclass + class Parameter: + name: str + lower_bound: float + upper_bound: float + + + @dataclass + class Metric: + name: str + objective: ObjectiveType + + + class RandomSamplingOptimizer(AbstractSequentialOptimizer): + def __init__(self, parameters: list[Parameter], metrics: list[Metric]): + self.parameters = parameters + self.metrics = metrics + self.results = [] + + def sample(self, num_experiments: int = 1) -> pd.DataFrame: + samples = [] + for _ in range(num_experiments): + sample = {param.name: random.uniform(param.lower_bound, param.upper_bound) for param in self.parameters} + samples.append(sample) + return pd.DataFrame(samples) + + def report(self, inputs_df: pd.DataFrame, outputs_df: pd.DataFrame) -> None: + for _, row in pd.concat([inputs_df, outputs_df], axis=1).iterrows(): + self.results.append(row.to_dict()) + + def get_optimal_solutions(self) -> pd.DataFrame: + if not self.results: + return pd.DataFrame( + columns=[param.name for param in self.parameters] + [metric.name for metric in self.metrics] + ) + + df = pd.DataFrame(self.results) + optimal_solutions = [] + + for metric in self.metrics: + if metric.objective == ObjectiveType.MINIMIZE: + optimal = df.loc[df[metric.name].idxmin()] + else: + optimal = df.loc[df[metric.name].idxmax()] + optimal_solutions.append(optimal) + + return pd.DataFrame(optimal_solutions) + + def get_input_names(self) -> List[str]: + return [param.name for param in self.parameters] + + def get_output_names(self) -> List[str]: + return [metric.name for metric in self.metrics] + + def eos_create_campaign_optimizer() -> tuple[dict, type[AbstractSequentialOptimizer]]: + constructor_args = { + "parameters": [ + Parameter(name="dispense_colors.cyan_volume", lower_bound=0, upper_bound=5), + Parameter(name="dispense_colors.magenta_volume", lower_bound=0, upper_bound=5), + Parameter(name="dispense_colors.yellow_volume", lower_bound=0, upper_bound=5), + Parameter(name="dispense_colors.black_volume", lower_bound=0, upper_bound=5), + Parameter(name="mix_colors.mixing_time", lower_bound=1, upper_bound=15), + Parameter(name="mix_colors.mixing_speed", lower_bound=10, upper_bound=500), + ], + "metrics": [ + Metric(name="score_color.loss", objective=ObjectiveType.MINIMIZE), + ], + } + + return constructor_args, RandomSamplingOptimizer \ No newline at end of file diff --git a/docs/user-guide/packages.rst b/docs/user-guide/packages.rst new file mode 100644 index 0000000..02bdd6e --- /dev/null +++ b/docs/user-guide/packages.rst @@ -0,0 +1,31 @@ +Packages +======== +Code and resources in EOS are organized into packages, which are discovered and loaded at runtime. +Each package is essentially a folder. These packages can contain laboratory, device, task, and experiment definitions, +code, and data, allowing reuse and sharing across the community. For example, a package can contain task and device +implementations for equipment from a specific manufacturer, while another package may only contain experiments that +run on a specific lab. + +.. figure:: ../_static/img/package.png + :alt: EOS package + :align: center + +Using a package is as simple as placing it in a directory that EOS loads packages from. By default, this directory +is called `user` and is located in the root of the EOS repository. + +Below is the directory tree of an example EOS package called "color_lab". It contains a laboratory called "color_lab", +an experiment called "color_mixing", and various devices and tasks. It also contains additional files +such as a Python script for launching low-level device drivers, a device client under `common`, and a README file. + +.. figure:: ../_static/img/example-package-tree.png + :alt: Example package directory tree + :align: center + +Create a Package +---------------- +.. code-block:: shell + + eos pkg create my_package + +This command is a shortcut to create a new package with all subdirectories. Feel free to delete subdirectories you don't +expect to use. diff --git a/docs/user-guide/running.rst b/docs/user-guide/running.rst new file mode 100644 index 0000000..cda8308 --- /dev/null +++ b/docs/user-guide/running.rst @@ -0,0 +1,26 @@ +Running +======= +1. Start External Services +^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: shell + + cd docker + docker compose up -d + +2. Source the Virtual Environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: shell + + source env/bin/activate + +3. Start the EOS Orchestrator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: shell + + eos orchestrator + +4. Start the EOS REST API +^^^^^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: shell + + eos api diff --git a/docs/user-guide/tasks.rst b/docs/user-guide/tasks.rst new file mode 100644 index 0000000..5274741 --- /dev/null +++ b/docs/user-guide/tasks.rst @@ -0,0 +1,254 @@ +Tasks +===== +A task in EOS encapsulates an operation and can be thought of as a function. Tasks are the elementary building block +in EOS. A task is ephemeral, meaning it is created, executed, and terminated. A task takes some inputs and returns some +outputs, and may use one or more devices. + +There are two kinds of inputs: **parameters** and **containers**. + +#. **Parameters**: Data such as integers, decimals, strings, booleans, etc that are passed to the task. +#. **Containers**: Vessels that may contain one or more samples. + +There are three kinds of outputs: **parameters**, **containers**, and **files**. + +#. **Parameters**: Data such as integers, decimals, strings, booleans, etc that are returned by the task. +#. **Containers**: Vessels that may contain one or more samples. +#. **Files**: Raw data or reports generated by the task, such as output files from analysis. + +.. figure:: ../_static/img/task-inputs-outputs.png + :alt: EOS Task Inputs and Outputs + :align: center + +Parameters +---------- +Parameters are values that are input to a task or output from a task. Every parameter has a specific data type. +EOS supports the following parameter types: + +* **integer**: An integer number; equivalent to Python's `int` +* **decimal**: A decimal number; equivalent to Python's `float` +* **string**: A string (series of text characters); equivalent to Python's `str` +* **boolean**: A true/false value; equivalent to Python's `bool` +* **choice**: A value that must be one of a set of predefined choices. The choices can be any type. +* **list**: A list of values of a specific type. Equivalent to Python's `list`. +* **dictionary**: A dictionary of key-value pairs. Equivalent to Python's `dict`. + +Tasks can have multiple parameters of different types. EOS will ensure that the parameters passed to a task are of the +correct type and have values that meet their constraints. + +Containers +---------- +Containers are referenced by a unique identifier called a **container ID**. A container ID is a string that uniquely +identifies a container. Every container in EOS must have an ID, and these can be specified in the laboratory +definition. Containers are treated as `global` objects and can move across labs. However, every container must +have a "home" lab from which it originates. + +In order to pass a container to a task or return a container from a task, its container ID is used. Every task +may accept specific types of containers, such as beakers, or vials. Multiple different containers can be passed. Users +can create their own types of containers, such as `beaker_500ml` or `vial_2ml`, which specify a unique container +type. EOS will ensure that only container types that are compatible with the task are passed to it. + +Files +----- +Files may be generated by a task and EOS will store them in an object storage (MinIO). Output files can be used to +record raw data for future reference, and can be downloaded by the user. + +.. note:: + Files cannot currently be passed as inputs to tasks via the EOS runtime and its object storage. + This is planned to be supported in the future. It is still possible to pass them using an external object + storage (e.g., MinIO), but this has to be implemented and managed manually. + +Task Implementation +------------------- +* Tasks are implemented in the `tasks` subdirectory inside an EOS package +* Each task has its own subfolder (e.g., tasks/magnetic_mixing) +* There are two key files per task: `task.yml` and `task.py` + +YAML File (task.yml) +~~~~~~~~~~~~~~~~~~~~ +* Specifies the task type, description, and input/output parameters and containers +* Acts as the interface contract (spec) for the task +* This contract is used to validate tasks, and EOS enforces the contract statically and dynamically during execution +* Useful as documentation for the task + +Below is an example task YAML file for a GC analysis task for GCs made by SRI Instruments: + +:bdg-primary:`task.yml` + +.. code-block:: yaml + + type: SRI GC Analysis + description: Perform gas chromatography (GC) analysis on a sample. + + device_types: + - sri_gas_chromatograph + + input_parameters: + analysis_time: + type: integer + unit: seconds + value: 480 + description: How long to run the GC analysis + + output_parameters: + known_substances: + type: dictionary + description: Peaks and peak areas of identified substances + unknown_substances: + type: dictionary + description: Peaks and peak areas of substances that could not be identified + +The task specification makes clear that: + +* The task is of type "SRI GC Analysis" +* The task requires a device of type "sri_gas_chromatograph". EOS will enforce this requirement. +* The task takes an input integer parameter `analysis_time` in seconds. It has a default value of 480, making this an + optional parameter. +* The task outputs two dictionaries: `known_substances` and `unknown_substances`. + +Parameter Specification +""""""""""""""""""""""" +Parameters are defined in the `input_parameters` and `output_parameters` sections of the task YAML file. + +Below are examples and descriptions for each parameter type: + +Integer +""""""" +.. code-block:: yaml + + sample_rate: + type: integer + description: The number of samples per second + value: 44100 + unit: Hz + min: 8000 + max: 192000 + +Integers must have a unit (can be n/a) and can also have a minimum and maximum value. + +Decimal +""""""" +.. code-block:: yaml + + threshold_voltage: + type: decimal + description: The voltage threshold for signal detection + value: 2.5 + unit: volts + min: 0.0 + max: 5.0 + +Decimals must have a unit (can be n/a) and can also have a minimum and maximum value. + +String +"""""" +.. code-block:: yaml + + file_prefix: + type: string + description: Prefix for output file names + value: "experiment_" + +Boolean +""""""" +.. code-block:: yaml + + auto_calibrate: + type: boolean + description: Whether to perform auto-calibration before analysis + value: true + +Booleans are true/false values. + +Choice +"""""" +.. code-block:: yaml + + column_type: + type: choice + description: HPLC column type + value: "C18" + choices: + - "C18" + - "C8" + - "HILIC" + - "Phenyl-Hexyl" + - "Amino" + +Choice parameters take one of the specified choices. + +List +"""" +.. code-block:: yaml + + channel_gains: + type: list + description: Gain values for each input channel + value: [1.0, 1.2, 0.8, 1.1] + element_type: decimal + length: 4 + min: [0.5, 0.5, 0.5, 0.5] + max: [2.0, 2.0, 2.0, 2.0] + +List parameters are a sequence of values of a specific type. They can have a specific length and minimum and maximum +per-element values. + +Dictionary +"""""""""" +.. code-block:: yaml + + buffer_composition: + type: dictionary + description: Composition of a buffer solution + value: + pH: 7.4 + base: "Tris" + concentration: 50 + unit: "mM" + additives: + NaCl: 150 + KCl: 2.7 + CaCl2: 1.0 + temperature: 25 + +Dictionaries are key-value pairs. The values can be any type. + +Python File (task.yml) +~~~~~~~~~~~~~~~~~~~~~~ +* Implements the task +* All task implementations must inherit from `BaseTask` +* The task class name must end with "Task" to be discovered by EOS + +:bdg-primary:`task.py` + +.. code-block:: python + + from eos.tasks.base_task import BaseTask + + + class MagneticMixingTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + magnetic_mixer = devices.get_all_by_type("magnetic_mixer")[0] + mixing_time = parameters["mixing_time"] + mixing_speed = parameters["mixing_speed"] + + containers["beaker"] = magnetic_mixer.mix(containers["beaker"], mixing_time, mixing_speed) + + return None, containers, None + +Let's walk through this example code: + +`_execute` is the only required function in a task implementation. It is called by EOS to execute a task. The function +takes three arguments: + +#. `devices`: A data structure supporting lookup of specific lab devices assigned to a task. In this case, only one +device is given, a magnetic mixer. The devices are represented as wrappers to Ray actor references, and the task +implementation can call functions from the device implementation. + +#. `parameters`: A dictionary of the input parameters. Keys are the parameter names and values are the parameter values. + +#. `containers`: A dictionary of the input containers. Keys are the container IDs and values are the `Container` objects. diff --git a/eos/__init__.py b/eos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/campaigns/__init__.py b/eos/campaigns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/campaigns/campaign_executor.py b/eos/campaigns/campaign_executor.py new file mode 100644 index 0000000..34b460a --- /dev/null +++ b/eos/campaigns/campaign_executor.py @@ -0,0 +1,332 @@ +import asyncio +from typing import Any, TYPE_CHECKING + +import pandas as pd + +from eos.campaigns.campaign_manager import CampaignManager +from eos.campaigns.campaign_optimizer_manager import CampaignOptimizerManager +from eos.campaigns.entities.campaign import CampaignStatus, Campaign, CampaignExecutionParameters +from eos.campaigns.exceptions import EosCampaignExecutionError +from eos.experiments.entities.experiment import ExperimentStatus, ExperimentExecutionParameters +from eos.experiments.exceptions import EosExperimentCancellationError, EosExperimentExecutionError +from eos.experiments.experiment_executor_factory import ExperimentExecutorFactory +from eos.logging.logger import log +from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer +from eos.tasks.task_manager import TaskManager +from eos.utils import dict_utils + +if TYPE_CHECKING: + from eos.experiments.experiment_executor import ExperimentExecutor + + +class CampaignExecutor: + def __init__( + self, + campaign_id: str, + experiment_type: str, + execution_parameters: CampaignExecutionParameters, + campaign_manager: CampaignManager, + campaign_optimizer_manager: CampaignOptimizerManager, + task_manager: TaskManager, + experiment_executor_factory: ExperimentExecutorFactory, + ): + self._campaign_id = campaign_id + self._experiment_type = experiment_type + self._execution_parameters = execution_parameters + self._campaign_manager = campaign_manager + self._campaign_optimizer_manager = campaign_optimizer_manager + self._task_manager = task_manager + self._experiment_executor_factory = experiment_executor_factory + + self._optimizer = None + self._optimizer_input_names: list[str] = [] + self._optimizer_output_names: list[str] = [] + + self._experiment_executors: dict[str, ExperimentExecutor] = {} + + self._campaign_status: CampaignStatus | None = None + + def _setup_optimizer(self) -> None: + if self._optimizer: + return + + self._optimizer = self._campaign_optimizer_manager.create_campaign_optimizer_actor( + self._experiment_type, + self._campaign_id, + self._execution_parameters.optimizer_computer_ip, + ) + self._optimizer_input_names, self._optimizer_output_names = ( + self._campaign_optimizer_manager.get_input_and_output_names(self._campaign_id) + ) + + def cleanup(self) -> None: + """ + Clean up resources when the campaign executor is no longer needed. + """ + if self._execution_parameters.do_optimization: + self._campaign_optimizer_manager.terminate_campaign_optimizer_actor(self._campaign_id) + + async def start_campaign(self) -> None: + """ + Start the campaign or handle an existing campaign. + """ + campaign = self._campaign_manager.get_campaign(self._campaign_id) + if campaign: + await self._handle_existing_campaign(campaign) + else: + self._create_new_campaign() + + self._campaign_manager.start_campaign(self._campaign_id) + self._campaign_status = CampaignStatus.RUNNING + log.info(f"Started campaign '{self._campaign_id}'.") + + async def _handle_existing_campaign(self, campaign: Campaign) -> None: + """ + Handle cases when the campaign already exists. + """ + self._campaign_status = campaign.status + + if not self._execution_parameters.resume: + def _raise_error(status: str) -> None: + raise EosCampaignExecutionError( + f"Cannot start campaign '{self._campaign_id}' as it already exists and is '{status}'. " + f"Please create a new campaign or re-submit with 'resume=True'." + ) + + status_handlers = { + CampaignStatus.COMPLETED: lambda: _raise_error("completed"), + CampaignStatus.SUSPENDED: lambda: _raise_error("suspended"), + CampaignStatus.CANCELLED: lambda: _raise_error("cancelled"), + CampaignStatus.FAILED: lambda: _raise_error("failed"), + } + status_handlers.get(self._campaign_status, lambda: None)() + + await self._resume_campaign() + + def _create_new_campaign(self) -> None: + """ + Create a new campaign. + """ + self._campaign_manager.create_campaign( + campaign_id=self._campaign_id, + experiment_type=self._experiment_type, + execution_parameters=self._execution_parameters, + ) + + if self._execution_parameters.do_optimization: + self._setup_optimizer() + + async def _resume_campaign(self) -> None: + """ + Resume an existing campaign. + """ + self._campaign_manager.delete_current_campaign_experiments(self._campaign_id) + + if self._execution_parameters.do_optimization: + self._setup_optimizer() + await self._restore_optimizer_state() + + log.info(f"Campaign '{self._campaign_id}' resumed.") + + async def _restore_optimizer_state(self) -> None: + """ + Restore the optimizer state for a resumed campaign. + """ + completed_experiment_ids = self._campaign_manager.get_campaign_experiment_ids( + self._campaign_id, status=ExperimentStatus.COMPLETED + ) + + inputs_df, outputs_df = await self._collect_experiment_results(completed_experiment_ids) + + await self._optimizer.report.remote(inputs_df, outputs_df) + + log.info( + f"CMP '{self._campaign_id}' - Restored optimizer state with {len(completed_experiment_ids)} " + f"completed experiments." + ) + + async def cancel_campaign(self) -> None: + """ + Cancel the campaign and all running experiments. + """ + campaign = self._campaign_manager.get_campaign(self._campaign_id) + if not campaign or campaign.status != CampaignStatus.RUNNING: + raise EosCampaignExecutionError( + f"Cannot cancel campaign '{self._campaign_id}' with status " + f"'{campaign.status if campaign else 'None'}'. It must be running." + ) + + log.warning(f"Cancelling campaign '{self._campaign_id}'...") + self._campaign_manager.cancel_campaign(self._campaign_id) + self._campaign_status = CampaignStatus.CANCELLED + + await self._cancel_running_experiments() + + log.warning(f"Cancelled campaign '{self._campaign_id}'.") + + async def _cancel_running_experiments(self) -> None: + """ + Cancel all running experiments in the campaign. + """ + cancellation_tasks = [executor.cancel_experiment() for executor in self._experiment_executors.values()] + try: + await asyncio.wait_for(asyncio.gather(*cancellation_tasks, return_exceptions=True), timeout=30) + except asyncio.TimeoutError as e: + raise EosCampaignExecutionError( + f"CMP '{self._campaign_id}' - Timed out while cancelling experiments. " + f"Some experiments may still be running." + ) from e + except EosExperimentCancellationError as e: + raise EosCampaignExecutionError( + f"CMP '{self._campaign_id}' - Error cancelling experiments. Some experiments may still " + f"be running." + ) from e + + async def progress_campaign(self) -> bool: + """ + Progress the campaign by executing experiments. + Returns True if the campaign is completed, False otherwise. + """ + try: + if self._campaign_status != CampaignStatus.RUNNING: + return self._campaign_status == CampaignStatus.CANCELLED + + await self._progress_experiments() + + campaign = self._campaign_manager.get_campaign(self._campaign_id) + if self._is_campaign_completed(campaign): + if self._execution_parameters.do_optimization: + await self._compute_pareto_solutions() + self._campaign_manager.complete_campaign(self._campaign_id) + return True + + await self._create_experiments(campaign) + + return False + except EosExperimentExecutionError as e: + self._campaign_manager.fail_campaign(self._campaign_id) + self._campaign_status = CampaignStatus.FAILED + raise EosCampaignExecutionError(f"Error executing campaign '{self._campaign_id}'") from e + + async def _progress_experiments(self) -> None: + """ + Progress all running experiments sequentially and process completed ones. + """ + completed_experiments = [] + + for experiment_id, executor in self._experiment_executors.items(): + is_completed = await executor.progress_experiment() + if is_completed: + completed_experiments.append(experiment_id) + + if self._execution_parameters.do_optimization and completed_experiments: + await self._process_completed_experiments(completed_experiments) + + for experiment_id in completed_experiments: + del self._experiment_executors[experiment_id] + self._campaign_manager.delete_campaign_experiment(self._campaign_id, experiment_id) + self._campaign_manager.increment_iteration(self._campaign_id) + + async def _process_completed_experiments(self, completed_experiments: list[str]) -> None: + """ + Process the results of completed experiments. + """ + inputs_df, outputs_df = await self._collect_experiment_results(completed_experiments) + await self._optimizer.report.remote(inputs_df, outputs_df) + self._campaign_optimizer_manager.record_campaign_samples( + self._campaign_id, completed_experiments, inputs_df, outputs_df + ) + + async def _collect_experiment_results(self, experiment_ids: list[str]) -> tuple[pd.DataFrame, pd.DataFrame]: + """ + Collect the results of completed experiments. + """ + inputs = {input_name: [] for input_name in self._optimizer_input_names} + outputs = {output_name: [] for output_name in self._optimizer_output_names} + + for experiment_id in experiment_ids: + for input_name in self._optimizer_input_names: + reference_task_id, parameter_name = input_name.split(".") + task = self._task_manager.get_task(experiment_id, reference_task_id) + inputs[input_name].append(float(task.input.parameters[parameter_name])) + for output_name in self._optimizer_output_names: + reference_task_id, parameter_name = output_name.split(".") + output_parameters = self._task_manager.get_task_output(experiment_id, reference_task_id).parameters + outputs[output_name].append(float(output_parameters[parameter_name])) + + return pd.DataFrame(inputs), pd.DataFrame(outputs) + + async def _create_experiments(self, campaign: Campaign) -> None: + """ + Create new experiments if possible. + """ + while self._can_create_more_experiments(campaign): + iteration = campaign.experiments_completed + len(self._experiment_executors) + new_experiment_id = f"{self._campaign_id}_exp_{iteration + 1}" + + experiment_dynamic_parameters = await self._get_experiment_parameters(iteration) + + experiment_execution_parameters = ExperimentExecutionParameters() + experiment_executor = self._experiment_executor_factory.create( + new_experiment_id, self._experiment_type, experiment_execution_parameters + ) + self._campaign_manager.add_campaign_experiment(self._campaign_id, new_experiment_id) + self._experiment_executors[new_experiment_id] = experiment_executor + experiment_executor.start_experiment(experiment_dynamic_parameters) + + async def _get_experiment_parameters(self, iteration: int) -> dict[str, Any]: + """ + Get parameters for a new experiment. + """ + campaign_dynamic_parameters = self._execution_parameters.dynamic_parameters + + if campaign_dynamic_parameters and len(campaign_dynamic_parameters) > iteration: + return campaign_dynamic_parameters[iteration] + if self._execution_parameters.do_optimization: + log.info(f"CMP '{self._campaign_id}' - Sampling new parameters from the optimizer...") + new_parameters = await self._optimizer.sample.remote(1) + new_parameters = new_parameters.to_dict(orient="records")[0] + log.debug(f"CMP '{self._campaign_id}' - Sampled parameters: {new_parameters}") + return dict_utils.unflatten_dict(new_parameters) + + raise EosCampaignExecutionError( + f"CMP '{self._campaign_id}' - No dynamic parameters provided for iteration {iteration}." + ) + + def _can_create_more_experiments(self, campaign: Campaign) -> bool: + """ + Check if more experiments can be created. + """ + num_executors = len(self._experiment_executors) + max_concurrent = self._execution_parameters.max_concurrent_experiments + max_total = self._execution_parameters.max_experiments + current_total = campaign.experiments_completed + num_executors + + return num_executors < max_concurrent and (max_total == 0 or current_total < max_total) + + def _is_campaign_completed(self, campaign: Campaign) -> bool: + """ + Check if the campaign is completed. + """ + max_experiments = self._execution_parameters.max_experiments + return ( + max_experiments > 0 + and campaign.experiments_completed >= max_experiments + and len(self._experiment_executors) == 0 + ) + + async def _compute_pareto_solutions(self) -> None: + """ + Compute and store Pareto solutions for the campaign. + """ + log.info(f"Computing Pareto solutions for campaign '{self._campaign_id}'...") + try: + pareto_solutions_df = await self._optimizer.get_optimal_solutions.remote() + pareto_solutions = pareto_solutions_df.to_dict(orient="records") + self._campaign_manager.set_pareto_solutions(self._campaign_id, pareto_solutions) + except Exception as e: + raise EosCampaignExecutionError(f"CMP '{self._campaign_id}' - Error computing Pareto solutions.") from e + + @property + def optimizer(self) -> AbstractSequentialOptimizer: + return self._optimizer diff --git a/eos/campaigns/campaign_executor_factory.py b/eos/campaigns/campaign_executor_factory.py new file mode 100644 index 0000000..d9be256 --- /dev/null +++ b/eos/campaigns/campaign_executor_factory.py @@ -0,0 +1,45 @@ +from eos.campaigns.campaign_executor import CampaignExecutor +from eos.campaigns.campaign_manager import CampaignManager +from eos.campaigns.campaign_optimizer_manager import CampaignOptimizerManager +from eos.campaigns.entities.campaign import CampaignExecutionParameters +from eos.configuration.configuration_manager import ConfigurationManager + +from eos.experiments.experiment_executor_factory import ExperimentExecutorFactory + +from eos.tasks.task_manager import TaskManager + + +class CampaignExecutorFactory: + """ + Factory class to create CampaignExecutor instances. + """ + + def __init__( + self, + configuration_manager: ConfigurationManager, + campaign_manager: CampaignManager, + campaign_optimizer_manager: CampaignOptimizerManager, + task_manager: TaskManager, + experiment_executor_factory: ExperimentExecutorFactory, + ): + self._configuration_manager = configuration_manager + self._campaign_manager = campaign_manager + self._campaign_optimizer_manager = campaign_optimizer_manager + self._task_manager = task_manager + self._experiment_executor_factory = experiment_executor_factory + + def create( + self, + campaign_id: str, + experiment_type: str, + execution_parameters: CampaignExecutionParameters, + ) -> CampaignExecutor: + return CampaignExecutor( + campaign_id, + experiment_type, + execution_parameters, + self._campaign_manager, + self._campaign_optimizer_manager, + self._task_manager, + self._experiment_executor_factory, + ) diff --git a/eos/campaigns/campaign_manager.py b/eos/campaigns/campaign_manager.py new file mode 100644 index 0000000..3cc076a --- /dev/null +++ b/eos/campaigns/campaign_manager.py @@ -0,0 +1,178 @@ +from datetime import datetime, timezone +from typing import Any + +from eos.campaigns.entities.campaign import Campaign, CampaignStatus, CampaignExecutionParameters +from eos.campaigns.exceptions import EosCampaignStateError +from eos.campaigns.repositories.campaign_repository import CampaignRepository +from eos.configuration.configuration_manager import ConfigurationManager +from eos.experiments.entities.experiment import ExperimentStatus +from eos.experiments.repositories.experiment_repository import ExperimentRepository +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.tasks.repositories.task_repository import TaskRepository + + +class CampaignManager: + """ + Responsible for managing the state of all experiment campaigns in EOS and tracking their execution. + """ + + def __init__(self, configuration_manager: ConfigurationManager, db_manager: DbManager): + self._configuration_manager = configuration_manager + self._campaigns = CampaignRepository("campaigns", db_manager) + self._campaigns.create_indices([("id", 1)], unique=True) + self._experiments = ExperimentRepository("experiments", db_manager) + self._tasks = TaskRepository("tasks", db_manager) + + log.debug("Campaign manager initialized.") + + def create_campaign( + self, + campaign_id: str, + experiment_type: str, + execution_parameters: CampaignExecutionParameters, + metadata: dict[str, Any] | None = None, + ) -> None: + """ + Create a new campaign of a given experiment type with a unique id. + + :param campaign_id: A unique id for the campaign. + :param experiment_type: The type of the experiment as defined in the configuration. + :param execution_parameters: Parameters for the execution of the campaign. + :param metadata: Additional metadata to be stored with the campaign. + """ + if self._campaigns.get_one(id=campaign_id): + raise EosCampaignStateError(f"Campaign '{campaign_id}' already exists.") + + experiment_config = self._configuration_manager.experiments.get(experiment_type) + if not experiment_config: + raise EosCampaignStateError(f"Experiment type '{experiment_type}' not found in the configuration.") + + campaign = Campaign( + id=campaign_id, + experiment_type=experiment_type, + execution_parameters=execution_parameters, + metadata=metadata or {}, + ) + self._campaigns.create(campaign.model_dump()) + + log.info(f"Created campaign '{campaign_id}'.") + + def delete_campaign(self, campaign_id: str) -> None: + """ + Delete a campaign. + """ + if not self._campaigns.exists(id=campaign_id): + raise EosCampaignStateError(f"Campaign '{campaign_id}' does not exist.") + + self._campaigns.delete(id=campaign_id) + + log.info(f"Deleted campaign '{campaign_id}'.") + + def start_campaign(self, campaign_id: str) -> None: + """ + Start a campaign. + """ + self._set_campaign_status(campaign_id, CampaignStatus.RUNNING) + + def complete_campaign(self, campaign_id: str) -> None: + """ + Complete a campaign. + """ + self._set_campaign_status(campaign_id, CampaignStatus.COMPLETED) + + def cancel_campaign(self, campaign_id: str) -> None: + """ + Cancel a campaign. + """ + self._set_campaign_status(campaign_id, CampaignStatus.CANCELLED) + + def suspend_campaign(self, campaign_id: str) -> None: + """ + Suspend a campaign. + """ + self._set_campaign_status(campaign_id, CampaignStatus.SUSPENDED) + + def fail_campaign(self, campaign_id: str) -> None: + """ + Fail a campaign. + """ + self._set_campaign_status(campaign_id, CampaignStatus.FAILED) + + def get_campaign(self, campaign_id: str) -> Campaign | None: + """ + Get a campaign. + """ + campaign = self._campaigns.get_one(id=campaign_id) + return Campaign(**campaign) if campaign else None + + def get_campaigns(self, **query: dict[str, Any]) -> list[Campaign]: + """ + Query campaigns with arbitrary parameters. + + :param query: Dictionary of query parameters. + """ + campaigns = self._campaigns.get_all(**query) + return [Campaign(**campaign) for campaign in campaigns] + + def _set_campaign_status(self, campaign_id: str, new_status: CampaignStatus) -> None: + """ + Set the status of a campaign. + """ + update_fields = {"status": new_status.value} + if new_status == CampaignStatus.RUNNING: + update_fields["start_time"] = datetime.now(tz=timezone.utc) + elif new_status in [ + CampaignStatus.COMPLETED, + CampaignStatus.CANCELLED, + CampaignStatus.FAILED, + ]: + update_fields["end_time"] = datetime.now(tz=timezone.utc) + + self._campaigns.update(update_fields, id=campaign_id) + + def increment_iteration(self, campaign_id: str) -> None: + """ + Increment the iteration count of a campaign. + """ + self._campaigns.increment_campaign_iteration(campaign_id) + + def add_campaign_experiment(self, campaign_id: str, experiment_id: str) -> None: + """ + Add an experiment to a campaign. + """ + self._campaigns.add_current_experiment(campaign_id, experiment_id) + + def delete_campaign_experiment(self, campaign_id: str, experiment_id: str) -> None: + """ + Remove an experiment from a campaign. + """ + self._campaigns.remove_current_experiment(campaign_id, experiment_id) + + def delete_current_campaign_experiments(self, campaign_id: str) -> None: + """ + Delete all current experiments from a campaign. + """ + campaign = self.get_campaign(campaign_id) + + for experiment_id in campaign.current_experiment_ids: + self._experiments.delete(id=experiment_id) + self._tasks.delete(experiment_id=experiment_id) + + self._campaigns.clear_current_experiments(campaign_id) + + def get_campaign_experiment_ids(self, campaign_id: str, status: ExperimentStatus | None = None) -> list[str]: + """ + Get all experiment IDs of a campaign with an optional status filter. + + :param campaign_id: The ID of the campaign. + :param status: Optional status to filter experiments. + :return: A list of experiment IDs. + """ + return self._experiments.get_experiment_ids_by_campaign(campaign_id, status) + + def set_pareto_solutions(self, campaign_id: str, pareto_solutions: dict[str, Any]) -> None: + """ + Set the Pareto solutions for a campaign. + """ + self._campaigns.update({"pareto_solutions": pareto_solutions}, id=campaign_id) diff --git a/eos/campaigns/campaign_optimizer_manager.py b/eos/campaigns/campaign_optimizer_manager.py new file mode 100644 index 0000000..d3c19c9 --- /dev/null +++ b/eos/campaigns/campaign_optimizer_manager.py @@ -0,0 +1,123 @@ +import pandas as pd +import ray +from ray.actor import ActorHandle + +from eos.campaigns.entities.campaign import CampaignSample +from eos.configuration.plugin_registries.campaign_optimizer_plugin_registry import CampaignOptimizerPluginRegistry +from eos.logging.logger import log +from eos.optimization.sequential_optimizer_actor import SequentialOptimizerActor +from eos.persistence.db_manager import DbManager +from eos.persistence.mongo_repository import MongoRepository + + +class CampaignOptimizerManager: + """ + Responsible for managing the optimizers associated with experiment campaigns. + """ + + def __init__(self, db_manager: DbManager): + self._campaign_samples = MongoRepository("campaign_samples", db_manager) + self._campaign_samples.create_indices([("campaign_id", 1), ("experiment_id", 1)], unique=True) + + self._campaign_optimizer_plugin_registry = CampaignOptimizerPluginRegistry() + + self._optimizer_actors: dict[str, ActorHandle] = {} + + log.debug("Campaign optimizer manager initialized.") + + def create_campaign_optimizer_actor(self, experiment_type: str, campaign_id: str, computer_ip: str) -> ActorHandle: + """ + Create a new campaign optimizer Ray actor. + + :param experiment_type: The type of the experiment. + :param campaign_id: The ID of the campaign. + :param computer_ip: The IP address of the optimizer computer on which the actor will run. + """ + constructor_args, optimizer_type = ( + self._campaign_optimizer_plugin_registry.get_campaign_optimizer_creation_parameters(experiment_type) + ) + + resources = {"eos-core": 0.01} if computer_ip in ["localhost", "127.0.0.1"] else {f"node:{computer_ip}": 0.01} + + optimizer_actor = SequentialOptimizerActor.options(name=f"{campaign_id}_optimizer", resources=resources).remote( + constructor_args, optimizer_type + ) + + self._optimizer_actors[campaign_id] = optimizer_actor + + return optimizer_actor + + def terminate_campaign_optimizer_actor(self, campaign_id: str) -> None: + """ + Terminate the Ray actor associated with the optimizer for a campaign. + + :param campaign_id: The ID of the campaign. + """ + optimizer_actor = self._optimizer_actors.pop(campaign_id, None) + + if optimizer_actor is not None: + ray.kill(optimizer_actor) + + def get_campaign_optimizer_actor(self, campaign_id: str) -> ActorHandle: + """ + Get an existing Ray actor associated with the optimizer for a campaign. + + :param campaign_id: The ID of the campaign. + :return: The Ray actor associated with the optimizer. + """ + return self._optimizer_actors[campaign_id] + + def get_input_and_output_names(self, campaign_id: str) -> tuple[list[str], list[str]]: + """ + Get the input and output names from an optimizer associated with a campaign. + + :param campaign_id: The ID of the campaign associated with the optimizer. + :return: A tuple containing the input and output names. + """ + optimizer_actor = self._optimizer_actors[campaign_id] + + input_names, output_names = ray.get( + [optimizer_actor.get_input_names.remote(), optimizer_actor.get_output_names.remote()] + ) + + return input_names, output_names + + def record_campaign_samples( + self, + campaign_id: str, + experiment_ids: list[str], + inputs: pd.DataFrame, + outputs: pd.DataFrame, + ) -> None: + """ + Record one or more campaign samples (experiment results) for the given campaign. + Each sample is a data point for the optimizer to learn from. + + :param campaign_id: The ID of the campaign. + :param experiment_ids: The IDs of the experiments. + :param inputs: The input data. + :param outputs: The output data. + """ + inputs_dict = inputs.to_dict(orient="records") + outputs_dict = outputs.to_dict(orient="records") + + campaign_samples = [ + CampaignSample( + campaign_id=campaign_id, + experiment_id=experiment_id, + inputs=inputs_dict[i], + outputs=outputs_dict[i], + ) + for i, experiment_id in enumerate(experiment_ids) + ] + + for campaign_sample in campaign_samples: + self._campaign_samples.create(campaign_sample.model_dump()) + + def delete_campaign_samples(self, campaign_id: str) -> None: + """ + Delete all campaign samples for a campaign. + + :param campaign_id: The ID of the campaign. + """ + self._campaign_samples.delete(campaign_id=campaign_id) diff --git a/eos/campaigns/entities/__init__.py b/eos/campaigns/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/campaigns/entities/campaign.py b/eos/campaigns/entities/campaign.py new file mode 100644 index 0000000..2eda803 --- /dev/null +++ b/eos/campaigns/entities/campaign.py @@ -0,0 +1,71 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import Any + +from pydantic import BaseModel, field_serializer, Field, model_validator + + +class CampaignExecutionParameters(BaseModel): + max_experiments: int = Field(0, ge=0) + max_concurrent_experiments: int = Field(1, ge=1) + + do_optimization: bool + optimizer_computer_ip: str = "127.0.0.1" + dynamic_parameters: list[dict[str, dict[str, Any]]] | None = None + + resume: bool = False + + @model_validator(mode="after") + def validate_dynamic_parameters(self) -> None: + if not self.do_optimization: + if not self.dynamic_parameters: + raise ValueError("Campaign dynamic parameters must be provided if optimization is not enabled.") + if len(self.dynamic_parameters) != self.max_experiments: + raise ValueError( + "Dynamic parameters must be provided for all experiments up to the max experiments if " + "optimization is not enabled." + ) + return self + + +class CampaignStatus(Enum): + CREATED = "CREATED" + RUNNING = "RUNNING" + COMPLETED = "COMPLETED" + SUSPENDED = "SUSPENDED" + CANCELLED = "CANCELLED" + FAILED = "FAILED" + + +class Campaign(BaseModel): + id: str + experiment_type: str + + execution_parameters: CampaignExecutionParameters + + status: CampaignStatus = CampaignStatus.CREATED + experiments_completed: int = Field(0, ge=0) + current_experiment_ids: list[str] = [] + + pareto_solutions: list[dict[str, Any]] | None = None + + metadata: dict[str, Any] = {} + + start_time: datetime | None = None + end_time: datetime | None = None + + created_at: datetime = datetime.now(tz=timezone.utc) + + @field_serializer("status") + def status_enum_to_string(self, v: CampaignStatus) -> str: + return v.value + + +class CampaignSample(BaseModel): + campaign_id: str + experiment_id: str + + inputs: dict[str, Any] + outputs: dict[str, Any] + + created_at: datetime = datetime.now(tz=timezone.utc) diff --git a/eos/campaigns/exceptions.py b/eos/campaigns/exceptions.py new file mode 100644 index 0000000..861b624 --- /dev/null +++ b/eos/campaigns/exceptions.py @@ -0,0 +1,10 @@ +class EosCampaignError(Exception): + pass + + +class EosCampaignStateError(EosCampaignError): + pass + + +class EosCampaignExecutionError(EosCampaignError): + pass diff --git a/eos/campaigns/repositories/__init__.py b/eos/campaigns/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/campaigns/repositories/campaign_repository.py b/eos/campaigns/repositories/campaign_repository.py new file mode 100644 index 0000000..f7acb9a --- /dev/null +++ b/eos/campaigns/repositories/campaign_repository.py @@ -0,0 +1,30 @@ +from eos.campaigns.exceptions import EosCampaignStateError +from eos.persistence.mongo_repository import MongoRepository + + +class CampaignRepository(MongoRepository): + def increment_campaign_iteration(self, campaign_id: str) -> None: + result = self._collection.update_one({"id": campaign_id}, {"$inc": {"experiments_completed": 1}}) + + if result.matched_count == 0: + raise EosCampaignStateError( + f"Cannot increment the iteration of campaign '{campaign_id}' as it does not exist." + ) + + def add_current_experiment(self, campaign_id: str, experiment_id: str) -> None: + self._collection.update_one( + {"id": campaign_id}, + {"$addToSet": {"current_experiment_ids": experiment_id}}, + ) + + def remove_current_experiment(self, campaign_id: str, experiment_id: str) -> None: + self._collection.update_one( + {"id": campaign_id}, + {"$pull": {"current_experiment_ids": experiment_id}}, + ) + + def clear_current_experiments(self, campaign_id: str) -> None: + self._collection.update_one( + {"id": campaign_id}, + {"$set": {"current_experiment_ids": []}}, + ) diff --git a/eos/cli/__init__.py b/eos/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/cli/orchestrator_cli.py b/eos/cli/orchestrator_cli.py new file mode 100644 index 0000000..095e4ab --- /dev/null +++ b/eos/cli/orchestrator_cli.py @@ -0,0 +1,185 @@ +import asyncio +import contextlib +import functools +import os +import signal +from contextlib import AbstractAsyncContextManager +from pathlib import Path +from typing import Annotated + +import typer +import uvicorn +from litestar import Litestar, Router +from litestar.di import Provide +from litestar.logging import LoggingConfig +from omegaconf import OmegaConf, DictConfig + +from eos.logging.logger import log, LogLevel +from eos.orchestration.orchestrator import Orchestrator +from eos.persistence.service_credentials import ServiceCredentials +from eos.web_api.orchestrator.controllers.campaign_controller import CampaignController +from eos.web_api.orchestrator.controllers.experiment_controller import ExperimentController +from eos.web_api.orchestrator.controllers.file_controller import FileController +from eos.web_api.orchestrator.controllers.lab_controller import LabController +from eos.web_api.orchestrator.controllers.task_controller import TaskController +from eos.web_api.orchestrator.exception_handling import global_exception_handler + +default_config = { + "user_dir": "./user", + "labs": [], + "experiments": [], + "log_level": "INFO", + "web_api": { + "host": "localhost", + "port": 8070, + }, + "db": { + "host": "localhost", + "port": 27017, + "username": None, + "password": None, + }, + "file_db": { + "host": "localhost", + "port": 9004, + "username": None, + "password": None, + }, +} + +eos_banner = r"""The Experiment Orchestration System + ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ +▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ +▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀▀▀ +▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ +▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ +▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▀▀▀▀▀▀▀▀▀█░▌ +▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌ ▄▄▄▄▄▄▄▄▄█░▌ +▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ + ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ +""" + + +def load_config(config_file: str) -> DictConfig: + if not Path(config_file).exists(): + raise FileNotFoundError(f"Config file '{config_file}' does not exist") + return OmegaConf.merge(OmegaConf.create(default_config), OmegaConf.load(config_file)) + + +def parse_list_arg(arg: str | None) -> list[str]: + return [item.strip() for item in arg.split(",")] if arg else [] + + +@contextlib.asynccontextmanager +async def handle_shutdown( + orchestrator: Orchestrator, web_api_server: uvicorn.Server +) -> AbstractAsyncContextManager[None]: + class GracefulExit(SystemExit): + pass + + loop = asyncio.get_running_loop() + shutdown_initiated = False + + def signal_handler(*_) -> None: + nonlocal shutdown_initiated + if not shutdown_initiated: + log.warning("Shut down signal received.") + shutdown_initiated = True + raise GracefulExit() + + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, functools.partial(signal_handler)) + + try: + yield + except GracefulExit: + pass + finally: + log.info("Shutting down the internal web API server...") + web_api_server.should_exit = True + await web_api_server.shutdown() + + log.info("Shutting down the orchestrator...") + orchestrator.terminate() + + log.info("Shutdown complete.") + + +async def run_all(orchestrator: Orchestrator, web_api_server: uvicorn.Server) -> None: + async with handle_shutdown(orchestrator, web_api_server): + orchestrator_task = asyncio.create_task(orchestrator.spin()) + web_server_task = asyncio.create_task(web_api_server.serve()) + + await asyncio.gather(orchestrator_task, web_server_task) + + +def start_orchestrator( + config_file: Annotated[ + str, typer.Option("--config", "-c", help="Path to the EOS configuration file") + ] = "./config.yml", + user_dir: ( + Annotated[str, typer.Option("--user-dir", "-u", help="The directory containing EOS user configurations")] | None + ) = None, + labs: ( + Annotated[str, typer.Option("--labs", "-l", help="Comma-separated list of lab configurations to load")] | None + ) = None, + experiments: ( + Annotated[ + str, + typer.Option("--experiments", "-e", help="Comma-separated list of experiment configurations to load"), + ] + | None + ) = None, + log_level: Annotated[LogLevel, typer.Option("--log-level", "-v", help="Logging level")] = None, +) -> None: + + typer.echo(eos_banner) + + file_config = load_config(config_file) + cli_config = {} + if user_dir is not None: + cli_config["user_dir"] = user_dir + if labs is not None: + cli_config["labs"] = parse_list_arg(labs) + if experiments is not None: + cli_config["experiments"] = parse_list_arg(experiments) + if log_level is not None: + cli_config["log_level"] = log_level.value + config = OmegaConf.merge(file_config, OmegaConf.create(cli_config)) + + log.set_level(config.log_level) + + # Set up the orchestrator + db_credentials = ServiceCredentials(**config.db) + file_db_credentials = ServiceCredentials(**config.file_db) + orchestrator = Orchestrator(config.user_dir, db_credentials, file_db_credentials) + orchestrator.load_labs(config.labs) + orchestrator.load_experiments(config.experiments) + + # Set up the web API server + logging_config = LoggingConfig( + configure_root_logger=False, + loggers={ + "litestar": {"level": "CRITICAL"}, + }, + ) + os.environ["LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD"] = "0" + + def orchestrator_provider() -> Orchestrator: + return orchestrator + + api_router = Router( + path="/api", + route_handlers=[TaskController, ExperimentController, CampaignController, LabController, FileController], + dependencies={"orchestrator": Provide(orchestrator_provider)}, + exception_handlers={Exception: global_exception_handler}, + ) + web_api_app = Litestar( + route_handlers=[api_router], + logging_config=logging_config, + exception_handlers={Exception: global_exception_handler}, + ) + config = uvicorn.Config(web_api_app, host=config.web_api.host, port=config.web_api.port, log_level="critical") + web_api_server = uvicorn.Server(config) + + asyncio.run(run_all(orchestrator, web_api_server)) diff --git a/eos/cli/pkg_cli.py b/eos/cli/pkg_cli.py new file mode 100644 index 0000000..50bff34 --- /dev/null +++ b/eos/cli/pkg_cli.py @@ -0,0 +1,28 @@ +from pathlib import Path +from typing import Annotated + +import typer + +pkg_app = typer.Typer() + + +@pkg_app.command(name="create") +def create_package( + name: Annotated[str, typer.Argument(help="Name of the package to create")], + user_dir: Annotated[ + str, typer.Option("--user-dir", "-u", help="The directory containing EOS user configurations") + ] = "./user", +) -> None: + """Create a new package with the specified name in the user directory.""" + package_dir = Path(user_dir) / name + subdirs = ["common", "devices", "tasks", "labs", "experiments"] + + try: + package_dir.mkdir(parents=True, exist_ok=False) + for subdir in subdirs: + (package_dir / subdir).mkdir() + typer.echo(f"Successfully created package '{name}' in {package_dir}") + except FileExistsError: + typer.echo(f"Error: Package '{name}' already exists in {user_dir}", err=True) + except Exception as e: + typer.echo(f"Error creating package: {e!s}", err=True) diff --git a/eos/cli/web_api_cli.py b/eos/cli/web_api_cli.py new file mode 100644 index 0000000..99f108e --- /dev/null +++ b/eos/cli/web_api_cli.py @@ -0,0 +1,30 @@ +import os +import subprocess +import sys +from typing import Annotated + +import typer + + +# ruff: noqa: S603 + + +def start_web_api( + host: Annotated[str, typer.Option("--host", help="Host for the EOS web API server")] = "0.0.0.0", + port: Annotated[int, typer.Option("--port", help="Port for the EOS web API server")] = 8000, + orchestrator_host: Annotated[ + str, typer.Option("--orchestrator-host", help="Host for the EOS orchestrator server") + ] = "localhost", + orchestrator_port: Annotated[ + int, typer.Option("--orchestrator-port", help="Port for the EOS orchestrator server") + ] = 8070, +) -> None: + env = os.environ.copy() + env["EOS_ORCHESTRATOR_HOST"] = str(orchestrator_host) + env["EOS_ORCHESTRATOR_PORT"] = str(orchestrator_port) + + subprocess.run( + [sys.executable, "-m", "uvicorn", "--host", str(host), "--port", str(port), "eos.web_api.public.server:app"], + env=env, + check=True, + ) diff --git a/eos/configuration/__init__.py b/eos/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/configuration/configuration_manager.py b/eos/configuration/configuration_manager.py new file mode 100644 index 0000000..af3688f --- /dev/null +++ b/eos/configuration/configuration_manager.py @@ -0,0 +1,230 @@ +import os +from typing import TYPE_CHECKING + + +from eos.configuration.exceptions import ( + EosConfigurationError, +) +from eos.configuration.package_manager import PackageManager +from eos.configuration.plugin_registries.campaign_optimizer_plugin_registry import CampaignOptimizerPluginRegistry +from eos.configuration.plugin_registries.device_plugin_registry import DevicePluginRegistry +from eos.configuration.plugin_registries.task_plugin_registry import TaskPluginRegistry +from eos.configuration.spec_registries.device_specification_registry import DeviceSpecificationRegistry +from eos.configuration.spec_registries.task_specification_registry import ( + TaskSpecificationRegistry, +) +from eos.configuration.validation.experiment_validator import ExperimentValidator +from eos.configuration.validation.lab_validator import LabValidator +from eos.configuration.validation.multi_lab_validator import MultiLabValidator +from eos.logging.logger import log + +if TYPE_CHECKING: + from eos.configuration.entities.lab import LabConfig + from eos.configuration.entities.experiment import ExperimentConfig + + +class ConfigurationManager: + """ + The configuration manager is responsible for the data-driven configuration layer of EOS. + It allows loading and managing configurations for labs, experiments, tasks, and devices. + It also invokes the validation of the loaded configurations. + """ + + def __init__(self, user_dir: str): + self._user_dir = user_dir + self._package_manager = PackageManager(user_dir) + + self.labs: dict[str, LabConfig] = {} + self.experiments: dict[str, ExperimentConfig] = {} + + task_configs, task_dirs_to_task_types = self._package_manager.read_task_configs() + self.task_specs = TaskSpecificationRegistry(task_configs, task_dirs_to_task_types) + self.tasks = TaskPluginRegistry(self._package_manager) + + device_configs, device_dirs_to_device_types = self._package_manager.read_device_configs() + self.device_specs = DeviceSpecificationRegistry(device_configs, device_dirs_to_device_types) + self.devices = DevicePluginRegistry(self._package_manager) + + self.campaign_optimizers = CampaignOptimizerPluginRegistry(self._package_manager) + + log.debug("Configuration manager initialized") + + def get_lab_loaded_statuses(self) -> dict[str, bool]: + """ + Returns a dictionary where the lab type (name of directory) is associated + with a boolean value indicating if it's currently loaded. + """ + all_labs = set() + + for package in self._package_manager.get_all_packages(): + labs_dir = package.labs_dir + if labs_dir.is_dir(): + package_labs = [d for d in os.listdir(labs_dir) if (labs_dir / d).is_dir()] + all_labs.update(package_labs) + + return {lab: lab in self.labs for lab in all_labs} + + def load_lab(self, lab_type: str, validate_multi_lab=True) -> None: + """ + Load a new laboratory to the configuration manager. + + :param lab_type: The type of the lab. This should match the name of the lab's directory in the + user directory. + :param validate_multi_lab: Whether to validate the multi-lab configuration after adding the lab. + """ + lab_config = self._package_manager.read_lab_config(lab_type) + + lab_validator = LabValidator(self._user_dir, lab_config) + lab_validator.validate() + + self.labs[lab_type] = lab_config + + if validate_multi_lab: + multi_lab_validator = MultiLabValidator(list(self.labs.values())) + multi_lab_validator.validate() + + log.info(f"Loaded lab '{lab_type}'") + log.debug(f"Lab configuration: {lab_config}") + + def load_labs(self, lab_types: set[str]) -> None: + """ + Load multiple laboratories to the configuration manager. + + :param lab_types: A list of lab types (names). Each type should match the name of the lab's directory in the + user directory. + """ + for lab_name in lab_types: + self.load_lab(lab_name, validate_multi_lab=False) + + multi_lab_validator = MultiLabValidator(list(self.labs.values())) + multi_lab_validator.validate() + + def unload_labs(self, lab_types: set[str]) -> None: + """ + Unload multiple labs from the configuration manager. Also unloads all experiments associated with the labs. + + :param lab_types: A list of lab types (names) to remove. + """ + for lab_type in lab_types: + self.unload_lab(lab_type) + + def unload_lab(self, lab_type: str) -> None: + """ + Unload a lab from the configuration manager. Also unloads all experiments associated with the lab. + + :param lab_type: The type (name) of the lab to remove. + """ + if lab_type not in self.labs: + raise EosConfigurationError( + f"Lab '{lab_type}' that was requested to be unloaded does not exist in the configuration manager" + ) + + self._unload_experiments_associated_with_labs({lab_type}) + + self.labs.pop(lab_type) + log.info(f"Unloaded lab '{lab_type}'") + + def get_experiment_loaded_statuses(self) -> dict[str, bool]: + """ + Returns a dictionary where the experiment type (name of directory) is associated + with a boolean value indicating if it's currently loaded. + """ + all_experiments = set() + + for package in self._package_manager.get_all_packages(): + experiments_dir = package.experiments_dir + if experiments_dir.is_dir(): + package_experiments = [d for d in os.listdir(experiments_dir) if (experiments_dir / d).is_dir()] + all_experiments.update(package_experiments) + + return {exp: exp in self.experiments for exp in all_experiments} + + def load_experiment(self, experiment_type: str) -> None: + """ + Load a new experiment, making it available for execution. + + :param experiment_type: The name of the experiment. This should match the name of the experiment + configuration file in the lab's directory. + """ + if experiment_type in self.experiments: + raise EosConfigurationError( + f"Experiment '{experiment_type}' that was requested to be loaded is already loaded." + ) + + try: + experiment_config = self._package_manager.read_experiment_config(experiment_type) + + experiment_validator = ExperimentValidator(experiment_config, list(self.labs.values())) + experiment_validator.validate() + + self.campaign_optimizers.load_campaign_optimizer(experiment_type) + self.experiments[experiment_type] = experiment_config + + log.info(f"Loaded experiment '{experiment_type}'") + log.debug(f"Experiment configuration: {experiment_config}") + except Exception: + self._cleanup_experiment_resources(experiment_type) + raise + + def unload_experiment(self, experiment_name: str) -> None: + """ + Unload an experiment from the configuration manager. + + :param experiment_name: The name of the experiment to remove. + """ + if experiment_name not in self.experiments: + raise EosConfigurationError( + f"Experiment '{experiment_name}' that was requested to be unloaded is not loaded." + ) + + self._cleanup_experiment_resources(experiment_name) + self.experiments.pop(experiment_name) + log.info(f"Unloaded experiment '{experiment_name}'") + + def load_experiments(self, experiment_types: set[str]) -> None: + """ + Load multiple experiments to the configuration manager. + + :param experiment_types: A list of experiment names. Each name should match the name of the experiment's + configuration file in the experiments directory. + """ + for experiment_type in experiment_types: + self.load_experiment(experiment_type) + + def unload_experiments(self, experiment_types: set[str]) -> None: + """ + Unload multiple experiments from the configuration manager. + + :param experiment_types: A list of experiment names to remove. + """ + for experiment_type in experiment_types: + self.unload_experiment(experiment_type) + + def _cleanup_experiment_resources(self, experiment_name: str) -> None: + """ + Clean up resources associated with an experiment. + + :param experiment_name: The name of the experiment to clean up. + """ + try: + self.campaign_optimizers.unload_campaign_optimizer(experiment_name) + except Exception as e: + raise EosConfigurationError( + f"Error unloading campaign optimizer for experiment '{experiment_name}': {e!s}" + ) from e + + def _unload_experiments_associated_with_labs(self, lab_names: set[str]) -> None: + """ + Unload all experiments associated with a list of labs from the configuration manager. + + :param lab_names: A list of lab names. + """ + experiments_to_remove = [] + for experiment_name in self.experiments: + for lab_name in lab_names: + if lab_name in self.experiments[experiment_name].labs: + experiments_to_remove.append(experiment_name) + + for experiment_name in experiments_to_remove: + self.unload_experiment(experiment_name) + log.info(f"Unloaded experiment '{experiment_name}' as it was associated with lab(s) {lab_names}") diff --git a/eos/configuration/constants.py b/eos/configuration/constants.py new file mode 100644 index 0000000..6dc6ccd --- /dev/null +++ b/eos/configuration/constants.py @@ -0,0 +1,18 @@ +LABS_DIR = "labs" +EXPERIMENTS_DIR = "experiments" +TASKS_DIR = "tasks" +DEVICES_DIR = "devices" +COMMON_DIR = "common" + +EXPERIMENT_CONFIG_FILE_NAME = "experiment.yml" +LAB_CONFIG_FILE_NAME = "lab.yml" +DEVICE_CONFIG_FILE_NAME = "device.yml" +TASK_CONFIG_FILE_NAME = "task.yml" + +DEVICE_IMPLEMENTATION_FILE_NAME = "device.py" +TASK_IMPLEMENTATION_FILE_NAME = "task.py" + +CAMPAIGN_OPTIMIZER_FILE_NAME = "optimizer.py" +CAMPAIGN_OPTIMIZER_CREATION_FUNCTION_NAME = "eos_create_campaign_optimizer" + +EOS_COMPUTER_NAME = "eos_computer" diff --git a/eos/configuration/entities/__init__.py b/eos/configuration/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/configuration/entities/device_specification.py b/eos/configuration/entities/device_specification.py new file mode 100644 index 0000000..612ceb4 --- /dev/null +++ b/eos/configuration/entities/device_specification.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import Any + + +@dataclass +class DeviceSpecification: + type: str + description: str | None = None + initialization_parameters: dict[str, Any] | None = None diff --git a/eos/configuration/entities/experiment.py b/eos/configuration/entities/experiment.py new file mode 100644 index 0000000..48fbfc8 --- /dev/null +++ b/eos/configuration/entities/experiment.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import Any + +from eos.configuration.entities.task import TaskConfig + + +@dataclass +class ExperimentContainerConfig: + id: str + description: str | None = None + metadata: dict[str, Any] | None = None + tags: list[str] | None = None + + +@dataclass +class ExperimentConfig: + type: str + description: str + labs: list[str] + tasks: list[TaskConfig] + containers: list[ExperimentContainerConfig] | None = None diff --git a/eos/configuration/entities/lab.py b/eos/configuration/entities/lab.py new file mode 100644 index 0000000..8ebefec --- /dev/null +++ b/eos/configuration/entities/lab.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class Location: + description: str + metadata: dict[str, Any] | None = None + + +@dataclass +class LabComputerConfig: + ip: str + description: str | None = None + + +@dataclass +class LabDeviceConfig: + type: str + computer: str + location: str | None = None + description: str | None = None + initialization_parameters: dict[str, Any] | None = None + + +@dataclass +class LabContainerConfig: + type: str + location: str + ids: list[str] + description: str | None = None + metadata: dict[str, Any] | None = None + + +@dataclass +class LabConfig: + type: str + description: str + devices: dict[str, LabDeviceConfig] + locations: dict[str, Location] = field(default_factory=dict) + computers: dict[str, LabComputerConfig] = field(default_factory=dict) + containers: list[LabContainerConfig] = field(default_factory=list) diff --git a/eos/configuration/entities/parameters.py b/eos/configuration/entities/parameters.py new file mode 100644 index 0000000..9c31b7c --- /dev/null +++ b/eos/configuration/entities/parameters.py @@ -0,0 +1,209 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Any, ClassVar + +from omegaconf import ListConfig + +from eos.configuration.exceptions import EosConfigurationError + +AllowedParameterTypes = int | float | bool | str | list | dict + + +def is_dynamic_parameter(parameter: AllowedParameterTypes) -> bool: + return isinstance(parameter, str) and parameter.lower() == "eos_dynamic" + + +class ParameterType(Enum): + integer = "integer" + decimal = "decimal" + string = "string" + boolean = "boolean" + choice = "choice" + list = "list" + dictionary = "dictionary" + + def python_type(self) -> type: + mapping = { + "integer": int, + "decimal": float, + "string": str, + "boolean": bool, + "choice": str, + "list": list, + "dictionary": dict, + } + return mapping[self.value] + + +@dataclass(kw_only=True) +class Parameter: + type: ParameterType + description: str + value: Any | None = None + + def __post_init__(self): + self._validate_type() + + def _validate_type(self) -> None: + try: + self.type = ParameterType(self.type) + except ValueError as e: + raise EosConfigurationError(f"Invalid task parameter type '{self.type}'") from e + + +@dataclass(kw_only=True) +class NumericParameter(Parameter): + unit: str + min: int | float | None = None + max: int | float | None = None + + def __post_init__(self): + super().__post_init__() + self._validate_unit() + self._validate_min_max() + self._validate_value_range() + + def _validate_unit(self) -> None: + if not self.unit: + raise EosConfigurationError("Task parameter type is numeric but no unit is specified.") + + def _validate_min_max(self) -> None: + if self.min is not None and self.max is not None and self.min >= self.max: + raise EosConfigurationError("Task parameter 'min' is greater than or equal to 'max'.") + + def _validate_value_range(self) -> None: + if self.value is None or is_dynamic_parameter(self.value): + return + + if not isinstance(self.value, int | float): + raise EosConfigurationError("Task parameter value is not numerical.") + if self.min is not None and self.value < self.min: + raise EosConfigurationError("Task parameter value is less than 'min'.") + if self.max is not None and self.value > self.max: + raise EosConfigurationError("Task parameter value is greater than 'max'.") + + +@dataclass(kw_only=True) +class BooleanParameter(Parameter): + def __post_init__(self): + super().__post_init__() + self._validate_value() + + def _validate_value(self) -> None: + if not isinstance(self.value, bool) and not is_dynamic_parameter(self.value): + raise EosConfigurationError( + f"Task parameter value '{self.value}' is not true/false but the declared type is 'boolean'." + ) + + +@dataclass(kw_only=True) +class ChoiceParameter(Parameter): + choices: list[str] + + def __post_init__(self): + super().__post_init__() + self._validate_choices() + + def _validate_choices(self) -> None: + if not self.choices: + raise EosConfigurationError("Task parameter choices are not specified when the type is 'choice'.") + + if ( + not self.value + or len(self.value) == 0 + or self.value not in self.choices + and not is_dynamic_parameter(self.value) + ): + raise EosConfigurationError( + f"Task parameter value '{self.value}' is not one of the choices {self.choices}." + ) + + +@dataclass(kw_only=True) +class ListParameter(Parameter): + element_type: ParameterType + length: int | None = None + min: list[int | float] | None = None + max: list[int | float] | None = None + + def __post_init__(self): + super().__post_init__() + self._validate_element_type() + self._validate_list_attributes() + self._validate_elements_within_bounds() + + def _validate_element_type(self) -> None: + if isinstance(self.element_type, str): + try: + self.element_type = ParameterType[self.element_type] + except KeyError as e: + raise EosConfigurationError(f"Invalid list parameter element type '{self.element_type}'") from e + if self.element_type == ParameterType.list: + raise EosConfigurationError("List parameter element type cannot be 'list'. Nested lists are not supported.") + + def _validate_list_attributes(self) -> None: + for attr_name in ["value", "min", "max"]: + attr_value = getattr(self, attr_name) + if attr_value is None: + continue + + if not isinstance(attr_value, list) and not isinstance(attr_value, ListConfig): + raise EosConfigurationError( + f"List parameter '{attr_name}' must be a list for 'list' type parameters.", + EosConfigurationError, + ) + if not all(isinstance(item, self.element_type.python_type()) for item in attr_value): + raise EosConfigurationError( + f"All elements of list parameter '{attr_name}' must be of the same type as specified " + f"by 'element_type'." + ) + if self.length is not None and len(attr_value) != self.length: + raise EosConfigurationError(f"List parameter '{attr_name}' length must be {self.length}.") + + def _validate_elements_within_bounds(self) -> None: + if self.value is None or is_dynamic_parameter(self.value) or self.min is None and self.max is None: + return + + if self.length is None and (self.min is not None or self.max is not None): + raise EosConfigurationError( + "List parameter 'min' and 'max' can only be specified when 'length' is specified." + ) + + _min = self.min or [float("-inf")] * self.length + _max = self.max or [float("inf")] * self.length + for i, val in enumerate(self.value): + if not _min[i] <= val <= _max[i]: + raise EosConfigurationError( + f"Element {i} of the list with value {val} is not within the the bounds [{_min[i]}, {_max[i]}]." + ) + + +@dataclass(kw_only=True) +class DictionaryParameter(Parameter): + pass + + +class ParameterFactory: + _TYPE_MAPPING: ClassVar = { + ParameterType.integer: NumericParameter, + ParameterType.decimal: NumericParameter, + ParameterType.string: Parameter, + ParameterType.boolean: BooleanParameter, + ParameterType.choice: ChoiceParameter, + ParameterType.list: ListParameter, + ParameterType.dictionary: DictionaryParameter, + } + + @staticmethod + def create_parameter(parameter_type: ParameterType | str, **kwargs) -> Parameter: + if isinstance(parameter_type, str): + parameter_type = ParameterType(parameter_type) + + parameter_class = ParameterFactory._TYPE_MAPPING.get(parameter_type) + if not parameter_class: + raise EosConfigurationError(f"Unsupported parameter type: {parameter_type}") + + if "type" not in kwargs: + kwargs["type"] = parameter_type + + return parameter_class(**kwargs) diff --git a/eos/configuration/entities/task.py b/eos/configuration/entities/task.py new file mode 100644 index 0000000..eca8b1c --- /dev/null +++ b/eos/configuration/entities/task.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class TaskDeviceConfig: + lab_id: str + id: str + + +@dataclass +class TaskConfig: + id: str + type: str + devices: list[TaskDeviceConfig] = field(default_factory=list) + containers: dict[str, str] = field(default_factory=dict) + parameters: dict[str, Any] = field(default_factory=dict) + dependencies: list[str] = field(default_factory=list) + + max_duration_seconds: int | None = None + description: str | None = None diff --git a/eos/configuration/entities/task_specification.py b/eos/configuration/entities/task_specification.py new file mode 100644 index 0000000..7f5e232 --- /dev/null +++ b/eos/configuration/entities/task_specification.py @@ -0,0 +1,110 @@ +import re +from dataclasses import dataclass, field +from typing import Any + +from eos.configuration.entities.parameters import ( + ParameterFactory, + ParameterType, +) +from eos.configuration.exceptions import EosConfigurationError + + +@dataclass +class TaskSpecificationContainer: + type: str + + def __post_init__(self): + self._validate_type() + + def _validate_type(self) -> None: + if not self.type.strip(): + raise EosConfigurationError("Container 'type' field must be specified.") + + +@dataclass +class TaskSpecificationOutputParameter: + type: ParameterType + description: str + unit: str | None = None + + def __post_init__(self): + self._validate_type() + self._validate_unit_specified_if_type_numeric() + self._validate_unit_not_specified_if_type_not_numeric() + + def _validate_type(self) -> None: + try: + self.type = ParameterType(self.type) + except ValueError as e: + raise EosConfigurationError(f"Invalid task output parameter type '{self.type}'") from e + + def _validate_unit_specified_if_type_numeric(self) -> None: + if self.type not in [ParameterType.integer, ParameterType.decimal]: + return + if self.unit is None or self.unit.strip() == "": + raise EosConfigurationError("Task output parameter type is numeric but no unit is specified.") + + def _validate_unit_not_specified_if_type_not_numeric(self) -> None: + if self.type in [ParameterType.integer, ParameterType.decimal]: + return + if self.unit is not None: + raise EosConfigurationError("Task output parameter type is not numeric but a unit is specified.") + + +@dataclass +class TaskSpecification: + type: str + description: str + device_types: list[str] | None = None + + input_containers: dict[str, TaskSpecificationContainer] = field(default_factory=dict) + input_parameters: dict[str, Any] = field(default_factory=dict) + + output_parameters: dict[str, TaskSpecificationOutputParameter] = field(default_factory=dict) + output_containers: dict[str, TaskSpecificationContainer] = field(default_factory=dict) + + def __post_init__(self): + if not self.output_containers: + self.output_containers = self.input_containers.copy() + + self._validate_parameters() + self._validate_parameter_names() + self._validate_container_names() + + def _validate_parameters(self) -> None: + for parameter in self.input_parameters.values(): + _ = ParameterFactory.create_parameter(ParameterType(parameter["type"]), **parameter) + + def _validate_parameter_names(self) -> None: + valid_name_pattern = re.compile(r"^[a-zA-Z0-9_.]*$") + + for name in self.input_parameters: + if not valid_name_pattern.match(name): + raise EosConfigurationError( + f"Invalid task parameter name '{name}'. " + f"Only characters, numbers, dots, and underscores are allowed." + ) + + for name in self.output_parameters: + if not valid_name_pattern.match(name): + raise EosConfigurationError( + f"Invalid task parameter name '{name}'. " + f"Only characters, numbers, dots, and underscores are allowed." + ) + + def _validate_container_names(self) -> None: + valid_name_pattern = re.compile(r"^[a-zA-Z0-9_.]*$") + + for name in self.input_containers: + if not valid_name_pattern.match(name): + raise EosConfigurationError( + f"Invalid task input container name '{name}'. " + f"Only characters, numbers, dots, and underscores are allowed." + ) + + for name in self.output_containers: + if not valid_name_pattern.match(name): + raise EosConfigurationError( + f"Invalid task output container name '{name}'. " + f"Only characters, numbers, dots, and underscores are allowed." + ) diff --git a/eos/configuration/exceptions.py b/eos/configuration/exceptions.py new file mode 100644 index 0000000..a15fed2 --- /dev/null +++ b/eos/configuration/exceptions.py @@ -0,0 +1,38 @@ +class EosConfigurationError(Exception): + pass + + +class EosMissingConfigurationError(Exception): + pass + + +class EosExperimentConfigurationError(Exception): + pass + + +class EosLabConfigurationError(Exception): + pass + + +class EosContainerConfigurationError(Exception): + pass + + +class EosTaskValidationError(Exception): + pass + + +class EosDynamicParameterConfigurationError(Exception): + pass + + +class EosTaskGraphError(Exception): + pass + + +class EosTaskHandlerClassNotFoundError(Exception): + pass + + +class EosCampaignOptimizerNotFoundError(Exception): + pass diff --git a/eos/configuration/experiment_graph/__init__.py b/eos/configuration/experiment_graph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/configuration/experiment_graph/experiment_graph.py b/eos/configuration/experiment_graph/experiment_graph.py new file mode 100644 index 0000000..2801796 --- /dev/null +++ b/eos/configuration/experiment_graph/experiment_graph.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass +from typing import Any + +import networkx as nx + +from eos.configuration.entities.experiment import ExperimentConfig +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.configuration.exceptions import EosTaskGraphError +from eos.configuration.experiment_graph.experiment_graph_builder import ExperimentGraphBuilder +from eos.configuration.spec_registries.task_specification_registry import TaskSpecificationRegistry + + +@dataclass +class TaskNodeIO: + containers: list[str] + parameters: list[str] + + +class ExperimentGraph: + def __init__(self, experiment_config: ExperimentConfig): + self._experiment_config = experiment_config + self._task_specs = TaskSpecificationRegistry() + + self._graph = ExperimentGraphBuilder(experiment_config).build_graph() + + self._task_subgraph = self._create_task_subgraph() + self._topologically_sorted_tasks = self._stable_topological_sort(self._task_subgraph) + + if not nx.is_directed_acyclic_graph(self._task_subgraph): + raise EosTaskGraphError(f"Task graph of experiment '{experiment_config.type}' contains cycles.") + + def _create_task_subgraph(self) -> nx.Graph: + return nx.subgraph_view(self._graph, filter_node=lambda n: self._graph.nodes[n]["node_type"] == "task") + + def get_graph(self) -> nx.DiGraph: + return self._graph + + def get_task_graph(self) -> nx.DiGraph: + return nx.DiGraph(self._task_subgraph) + + def get_tasks(self) -> list[str]: + return list(self._task_subgraph.nodes) + + def get_topologically_sorted_tasks(self) -> list[str]: + return self._topologically_sorted_tasks + + def get_task_node(self, task_id: str) -> dict[str, Any]: + return self._graph.nodes[task_id] + + def get_task_config(self, task_id: str) -> TaskConfig: + return TaskConfig(**self.get_task_node(task_id)["task_config"]) + + def get_task_spec(self, task_id: str) -> TaskSpecification: + return self._task_specs.get_spec_by_type(self.get_task_node(task_id)["task_config"].type) + + def get_task_dependencies(self, task_id: str) -> list[str]: + return [pred for pred in self._graph.predecessors(task_id) if self._graph.nodes[pred]["node_type"] == "task"] + + def _get_node_by_type(self, task_id: str, node_type: str, direction: str) -> list[str]: + if direction == "in": + nodes = self._graph.predecessors(task_id) + elif direction == "out": + nodes = self._graph.successors(task_id) + else: + raise ValueError("direction must be 'in' or 'out'") + + return [node for node in nodes if self._graph.nodes[node]["node_type"] == node_type] + + def get_task_inputs(self, task_id: str) -> TaskNodeIO: + return TaskNodeIO( + containers=self._get_node_by_type(task_id, "container", "in"), + parameters=self._get_node_by_type(task_id, "parameter", "in"), + ) + + def get_task_outputs(self, task_id: str) -> TaskNodeIO: + return TaskNodeIO( + containers=self._get_node_by_type(task_id, "container", "out"), + parameters=self._get_node_by_type(task_id, "parameter", "out"), + ) + + def get_container_node(self, container_id: str) -> dict[str, Any]: + return self._graph.nodes[container_id] + + @staticmethod + def _stable_topological_sort(graph: nx.Graph) -> list[str]: + nodes = sorted(graph.nodes()) + return list(nx.topological_sort(nx.DiGraph((u, v) for u, v in graph.edges() if u in nodes and v in nodes))) diff --git a/eos/configuration/experiment_graph/experiment_graph_builder.py b/eos/configuration/experiment_graph/experiment_graph_builder.py new file mode 100644 index 0000000..ab7ea2b --- /dev/null +++ b/eos/configuration/experiment_graph/experiment_graph_builder.py @@ -0,0 +1,108 @@ +import networkx as nx + +from eos.configuration.entities.experiment import ExperimentConfig +from eos.configuration.spec_registries.task_specification_registry import ( + TaskSpecificationRegistry, +) +from eos.configuration.validation import validation_utils + + +class ExperimentGraphBuilder: + """ + Builds an experiment graph from an experiment configuration and lab configurations. + """ + + def __init__(self, experiment_config: ExperimentConfig): + self._experiment = experiment_config + self._task_specs = TaskSpecificationRegistry() + + def build_graph(self) -> nx.DiGraph: + graph = nx.DiGraph() + + self._add_begin_and_end_nodes(graph) + self._add_task_nodes_and_edges(graph) + self._add_container_nodes(graph) + self._add_parameter_nodes(graph) + self._connect_orphan_task_nodes(graph) + self._remove_orphan_nodes(graph) + + return graph + + def _add_begin_and_end_nodes(self, graph: nx.DiGraph) -> None: + graph.add_node("Begin", node_type="begin") + graph.add_node("End", node_type="end") + + first_task = self._experiment.tasks[0].id + last_task = self._experiment.tasks[-1].id + graph.add_edge("Begin", first_task) + graph.add_edge(last_task, "End") + + def _add_task_nodes_and_edges(self, graph: nx.DiGraph) -> None: + for task in self._experiment.tasks: + graph.add_node(task.id, node_type="task", task_config=task) + for dep in task.dependencies: + graph.add_edge(dep, task.id) + + @staticmethod + def _connect_orphan_task_nodes(graph: nx.DiGraph) -> None: + for node, node_data in list(graph.nodes(data=True)): + if node_data["node_type"] == "task" and node not in ["Begin", "End"]: + if graph.in_degree(node) == 0: + graph.add_edge("Begin", node) + if graph.out_degree(node) == 0: + graph.add_edge(node, "End") + + def _add_container_nodes(self, graph: nx.DiGraph) -> None: + container_mapping = {} + used_containers = set() + + for task in self._experiment.tasks: + for container_name, container_id in task.containers.items(): + # Determine the container ID for this task + if container_id not in used_containers: + input_container_id = container_id + else: + input_container_id = f"{container_id}_{task.id}" + + # If this container is used as output by a previous task, update the input_container_id + if container_id in container_mapping: + previous_output_container_id = container_mapping[container_id] + input_container_id = previous_output_container_id + + # Add container node as input for the current task + if input_container_id not in graph: + graph.add_node(input_container_id, node_type="container", container={container_name: container_id}) + graph.add_edge(input_container_id, task.id) + + # Add container node as output for the current task + output_container_id = f"{container_id}_{task.id}" + graph.add_node(output_container_id, node_type="container", container={container_name: container_id}) + graph.add_edge(task.id, output_container_id) + + # Update the container mapping to link the output of this task to the next task's input + container_mapping[container_id] = output_container_id + + used_containers.add(container_id) + + def _add_parameter_nodes(self, graph: nx.DiGraph) -> None: + for task in self._experiment.tasks: + for param_name, param_value in task.parameters.items(): + if validation_utils.is_parameter_reference(param_value): + parameter_reference = param_value + producer_task_id, parameter_name = parameter_reference.split(".") + graph.add_node(parameter_reference, node_type="parameter") + graph.add_edge(parameter_reference, task.id, mapped_parameter=param_name) + graph.add_edge(producer_task_id, parameter_reference) + + # Add output parameters based on task specs + task_spec = self._task_specs.get_spec_by_config(task) + for param_name in task_spec.output_parameters: + ref_param_name = f"{task.id}.{param_name}" + graph.add_node(ref_param_name, node_type="parameter") + graph.add_edge(task.id, ref_param_name) + + @staticmethod + def _remove_orphan_nodes(graph: nx.DiGraph) -> None: + orphan_nodes = [node for node in graph.nodes if graph.in_degree(node) == 0 and graph.out_degree(node) == 0] + for node in orphan_nodes: + graph.remove_node(node) diff --git a/eos/configuration/package.py b/eos/configuration/package.py new file mode 100644 index 0000000..31e81f1 --- /dev/null +++ b/eos/configuration/package.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from eos.configuration.constants import COMMON_DIR, EXPERIMENTS_DIR, LABS_DIR, DEVICES_DIR, TASKS_DIR + + +class Package: + """ + A collection of user-defined common files, experiments, labs, devices, and tasks. + """ + + def __init__(self, name: str, path: str): + self.name = name + self.path = Path(path) + self.common_dir = self.path / COMMON_DIR + self.experiments_dir = self.path / EXPERIMENTS_DIR + self.labs_dir = self.path / LABS_DIR + self.devices_dir = self.path / DEVICES_DIR + self.tasks_dir = self.path / TASKS_DIR diff --git a/eos/configuration/package_manager.py b/eos/configuration/package_manager.py new file mode 100644 index 0000000..02298e8 --- /dev/null +++ b/eos/configuration/package_manager.py @@ -0,0 +1,332 @@ +import os +from dataclasses import dataclass +from enum import Enum, auto +from pathlib import Path +from typing import TypeVar, Generic, Any + +import jinja2 +import yaml +from omegaconf import OmegaConf, ValidationError + +from eos.configuration.constants import ( + LABS_DIR, + EXPERIMENTS_DIR, + TASKS_DIR, + DEVICES_DIR, + LAB_CONFIG_FILE_NAME, + EXPERIMENT_CONFIG_FILE_NAME, + DEVICE_CONFIG_FILE_NAME, + TASK_CONFIG_FILE_NAME, +) +from eos.configuration.entities.device_specification import DeviceSpecification +from eos.configuration.entities.experiment import ExperimentConfig +from eos.configuration.entities.lab import LabConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.configuration.exceptions import EosConfigurationError, EosMissingConfigurationError +from eos.configuration.package import Package +from eos.configuration.package_validator import PackageValidator +from eos.logging.logger import log + +T = TypeVar("T") + + +class EntityType(Enum): + LAB = auto() + EXPERIMENT = auto() + TASK = auto() + DEVICE = auto() + + +@dataclass +class EntityInfo: + dir_name: str + config_file_name: str + config_type: type + + +@dataclass +class EntityLocationInfo: + package_name: str + entity_path: str + + +ENTITY_INFO: dict[EntityType, EntityInfo] = { + EntityType.LAB: EntityInfo(LABS_DIR, LAB_CONFIG_FILE_NAME, LabConfig), + EntityType.EXPERIMENT: EntityInfo(EXPERIMENTS_DIR, EXPERIMENT_CONFIG_FILE_NAME, ExperimentConfig), + EntityType.TASK: EntityInfo(TASKS_DIR, TASK_CONFIG_FILE_NAME, TaskSpecification), + EntityType.DEVICE: EntityInfo(DEVICES_DIR, DEVICE_CONFIG_FILE_NAME, DeviceSpecification), +} +ConfigType = LabConfig | ExperimentConfig | TaskSpecification | DeviceSpecification + + +class EntityConfigReader(Generic[T]): + """ + Reads and parses entity configurations from files. + + The EntityConfigReader class provides static methods to read and parse configuration + files for various entity types (labs, experiments, tasks, and devices) in the EOS system. + It handles the loading, validation, and structuring of configuration data using OmegaConf. + """ + + @staticmethod + def read_entity_config(file_path: str, entity_type: EntityType) -> ConfigType: + entity_info = ENTITY_INFO[entity_type] + return EntityConfigReader._read_config(file_path, entity_info.config_type, f"{entity_type.name}") + + @staticmethod + def read_all_entity_configs(base_dir: str, entity_type: EntityType) -> tuple[dict[str, ConfigType], dict[str, str]]: + entity_info = ENTITY_INFO[entity_type] + configs = {} + dirs_to_types = {} + + for root, _, files in os.walk(base_dir): + if entity_info.config_file_name not in files: + continue + + entity_subdir = Path(root).relative_to(base_dir) + config_file_path = Path(root) / entity_info.config_file_name + + try: + structured_config = EntityConfigReader.read_entity_config(str(config_file_path), entity_type) + entity_type_name = structured_config.type + configs[entity_type_name] = structured_config + dirs_to_types[entity_subdir] = entity_type_name + + log.debug( + f"Loaded {entity_type.name.lower()} specification from directory '{entity_subdir}' of type " + f"'{entity_type_name}'" + ) + log.debug(f"{entity_type.name} configuration '{entity_type_name}': {structured_config}") + except EosConfigurationError as e: + log.error(f"Error loading {entity_type.name.lower()} configuration from '{config_file_path}': {e}") + raise + + return configs, dirs_to_types + + @staticmethod + def _read_config(file_path: str, config_type: type[ConfigType], config_name: str) -> ConfigType: + try: + config_data = EntityConfigReader._process_jinja_yaml(file_path) + + structured_config = OmegaConf.merge(OmegaConf.structured(config_type), OmegaConf.create(config_data)) + _ = OmegaConf.to_object(structured_config) + + return structured_config + except OSError as e: + raise EosConfigurationError(f"Error reading configuration file '{file_path}': {e!s}") from e + except ValidationError as e: + raise EosConfigurationError(f"Configuration is invalid: {e!s}") from e + except jinja2.exceptions.TemplateError as e: + raise EosConfigurationError(f"Error in Jinja2 template processing for '{config_name}': {e!s}") from e + except Exception as e: + raise EosConfigurationError(f"Error processing {config_name} configuration: {e!s}") from e + + @staticmethod + def _process_jinja_yaml(file_path: str) -> dict[str, Any]: + """ + Process a YAML file with Jinja2 templating, without passing any variables. + + This method: + 1. Reads the YAML file + 2. Renders the Jinja2 template without any variables + 3. Parses the rendered content back into a Python dictionary + """ + try: + with Path(file_path).open() as f: + raw_content = f.read() + except OSError as e: + raise EosConfigurationError(f"Error reading file '{file_path}': {e}") from e + + try: + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(Path(file_path).parents[3]), # user directory + undefined=jinja2.StrictUndefined, + autoescape=True, + ) + + template = env.from_string(raw_content) + rendered_content = template.render() + + return yaml.safe_load(rendered_content) + except yaml.YAMLError as e: + raise EosConfigurationError(f"Error parsing YAML in {file_path}: {e}") from e + except jinja2.exceptions.TemplateError as e: + raise EosConfigurationError(f"Error in Jinja2 template processing: {e}") from e + + +class PackageDiscoverer: + """ + Discovers packages in the user directory. + """ + + def __init__(self, user_dir: str): + self.user_dir = Path(user_dir) + + def discover_packages(self) -> dict[str, Package]: + packages = {} + if not self.user_dir.is_dir(): + raise EosMissingConfigurationError(f"User directory '{self.user_dir}' does not exist") + + for item in os.listdir(self.user_dir): + package_path = self.user_dir / item + + if package_path.is_dir(): + packages[item] = Package(item, package_path) + + return packages + + +class PackageManager: + """ + Manages packages and entity configurations within the user directory. + + The PackageManager class provides facilities to discover, read, add, and remove packages, + as well as read entity configurations (labs, experiments, tasks, and devices) from these packages. + It also maintains efficient lookup indices for quick access to entities across all packages. + """ + + def __init__(self, user_dir: str): + self.user_dir = user_dir + + self.packages: dict[str, Package] = {} + self.entity_indices: dict[EntityType, dict[str, EntityLocationInfo]] = { + entity_type: {} for entity_type in EntityType + } + + self._discover_packages() + log.info(f"Found packages: {', '.join(self.packages.keys())}") + + log.debug("Package manager initialized") + + def read_lab_config(self, lab_name: str) -> LabConfig: + entity_location = self._get_entity_location(lab_name, EntityType.LAB) + config_file_path = self._get_config_file_path(entity_location, EntityType.LAB) + return EntityConfigReader.read_entity_config(config_file_path, EntityType.LAB) + + def read_experiment_config(self, experiment_name: str) -> ExperimentConfig: + entity_location = self._get_entity_location(experiment_name, EntityType.EXPERIMENT) + config_file_path = self._get_config_file_path(entity_location, EntityType.EXPERIMENT) + return EntityConfigReader.read_entity_config(config_file_path, EntityType.EXPERIMENT) + + def read_task_configs(self) -> tuple[dict[str, TaskSpecification], dict[str, str]]: + return self._read_all_entity_configs(EntityType.TASK) + + def read_device_configs(self) -> tuple[dict[str, DeviceSpecification], dict[str, str]]: + return self._read_all_entity_configs(EntityType.DEVICE) + + def get_package(self, name: str) -> Package | None: + return self.packages.get(name) + + def get_all_packages(self) -> list[Package]: + return list(self.packages.values()) + + def _find_package_for_entity(self, entity_name: str, entity_type: EntityType) -> Package | None: + entity_location = self.entity_indices[entity_type].get(entity_name) + if entity_location: + return self.packages.get(entity_location.package_name) + return None + + def find_package_for_lab(self, lab_name: str) -> Package | None: + return self._find_package_for_entity(lab_name, EntityType.LAB) + + def find_package_for_experiment(self, experiment_name: str) -> Package | None: + return self._find_package_for_entity(experiment_name, EntityType.EXPERIMENT) + + def find_package_for_task(self, task_name: str) -> Package | None: + return self._find_package_for_entity(task_name, EntityType.TASK) + + def find_package_for_device(self, device_name: str) -> Package | None: + return self._find_package_for_entity(device_name, EntityType.DEVICE) + + def add_package(self, package_name: str) -> None: + package_path = Path(self.user_dir) / package_name + if not package_path.is_dir(): + raise EosMissingConfigurationError(f"Package directory '{package_path}' does not exist") + + new_package = Package(package_name, package_path) + PackageValidator(self.user_dir, {package_name: new_package}).validate() + + self.packages[package_name] = new_package + self._update_entity_indices(new_package) + + log.info(f"Added package '{package_name}'") + + def remove_package(self, package_name: str) -> None: + if package_name not in self.packages: + raise EosMissingConfigurationError(f"Package '{package_name}' not found") + + package = self.packages[package_name] + del self.packages[package_name] + self._remove_package_from_indices(package) + + log.info(f"Removed package '{package_name}'") + + def _discover_packages(self) -> None: + self.packages = PackageDiscoverer(self.user_dir).discover_packages() + PackageValidator(self.user_dir, self.packages).validate() + self._build_entity_indices() + + def _build_entity_indices(self) -> None: + for entity_type in EntityType: + self.entity_indices[entity_type] = {} + for package_name, package in self.packages.items(): + entity_dir = Path(getattr(package, f"{ENTITY_INFO[entity_type].dir_name}_dir")) + if entity_dir.is_dir(): + self._index_entities(entity_type, package_name, str(entity_dir)) + + def _index_entities(self, entity_type: EntityType, package_name: str, entity_dir: str) -> None: + for root, _, files in os.walk(entity_dir): + if ENTITY_INFO[entity_type].config_file_name in files: + entity_path = Path(root).relative_to(Path(entity_dir)) + entity_name = Path(entity_path).name + self.entity_indices[entity_type][entity_name] = EntityLocationInfo(package_name, str(entity_path)) + + def _update_entity_indices(self, package: Package) -> None: + for entity_type in EntityType: + entity_dir = Path(getattr(package, f"{ENTITY_INFO[entity_type].dir_name}_dir")) + if entity_dir.is_dir(): + self._index_entities(entity_type, package.name, str(entity_dir)) + + def _remove_package_from_indices(self, package: Package) -> None: + for entity_type in EntityType: + self.entity_indices[entity_type] = { + entity_name: location + for entity_name, location in self.entity_indices[entity_type].items() + if location.package_name != package.name + } + + def _get_entity_location(self, entity_name: str, entity_type: EntityType) -> EntityLocationInfo: + entity_location = self.entity_indices[entity_type].get(entity_name) + if not entity_location: + raise EosMissingConfigurationError(f"{entity_type.name} '{entity_name}' not found") + return entity_location + + def _get_config_file_path(self, entity_location: EntityLocationInfo, entity_type: EntityType) -> str: + entity_info = ENTITY_INFO[entity_type] + package = self.packages[entity_location.package_name] + config_file_path = ( + Path(getattr(package, f"{entity_info.dir_name}_dir")) + / entity_location.entity_path + / entity_info.config_file_name + ) + + if not config_file_path.is_file(): + raise EosMissingConfigurationError( + f"{entity_type.name} file '{entity_info.config_file_name}' does not exist for " + f"{entity_type.name.lower()} '{entity_location.entity_path}'", + EosMissingConfigurationError, + ) + + return config_file_path + + def _read_all_entity_configs(self, entity_type: EntityType) -> tuple[dict[str, T], dict[str, str]]: + all_configs = {} + all_dirs_to_types = {} + for package in self.packages.values(): + entity_dir = Path(getattr(package, f"{ENTITY_INFO[entity_type].dir_name}_dir")) + if not entity_dir.is_dir(): + continue + configs, dirs_to_types = EntityConfigReader.read_all_entity_configs(entity_dir, entity_type) + all_configs.update(configs) + all_dirs_to_types.update({Path(package.name) / k: v for k, v in dirs_to_types.items()}) + return all_configs, all_dirs_to_types diff --git a/eos/configuration/package_validator.py b/eos/configuration/package_validator.py new file mode 100644 index 0000000..cf3137b --- /dev/null +++ b/eos/configuration/package_validator.py @@ -0,0 +1,173 @@ +import os +from pathlib import Path + +from eos.configuration.constants import ( + COMMON_DIR, + EXPERIMENTS_DIR, + LABS_DIR, + DEVICES_DIR, + TASKS_DIR, + LAB_CONFIG_FILE_NAME, + EXPERIMENT_CONFIG_FILE_NAME, + TASK_CONFIG_FILE_NAME, + TASK_IMPLEMENTATION_FILE_NAME, + DEVICE_CONFIG_FILE_NAME, + DEVICE_IMPLEMENTATION_FILE_NAME, +) +from eos.configuration.exceptions import EosMissingConfigurationError, EosConfigurationError +from eos.configuration.package import Package +from eos.logging.logger import log + + +class PackageValidator: + """ + Responsible for validating user-defined packages. + """ + + def __init__(self, user_dir: str, packages: dict[str, Package]): + self.user_dir = user_dir + self.packages = packages + + def validate(self) -> None: + if not self.packages: + raise EosMissingConfigurationError(f"No valid packages found in the user directory '{self.user_dir}'") + + for package in self.packages.values(): + self._validate_package_structure(package) + + def _validate_package_structure(self, package: Package) -> None: + """ + Validate the structure of a single package. + """ + if not any( + [ + package.common_dir.is_dir(), + package.experiments_dir.is_dir(), + package.labs_dir.is_dir(), + package.devices_dir.is_dir(), + package.tasks_dir.is_dir(), + ] + ): + raise EosMissingConfigurationError( + f"Package '{package.name}' does not contain any of the directories: " + f"{COMMON_DIR}, {EXPERIMENTS_DIR}, {LABS_DIR}, {DEVICES_DIR}, {TASKS_DIR}" + ) + + if package.labs_dir.is_dir(): + self._validate_labs_dir(package) + + if package.experiments_dir.is_dir(): + self._validate_experiments_dir(package) + + if package.devices_dir.is_dir(): + self._validate_devices_dir(package) + + if package.tasks_dir.is_dir(): + self._validate_tasks_dir(package) + + @staticmethod + def _validate_labs_dir(package: Package) -> None: + """ + Validate the structure of the labs directory. + """ + for file in os.listdir(package.labs_dir): + file_path = package.labs_dir / file + if not file_path.is_dir(): + raise EosConfigurationError( + f"Non-directory file found in '{package.labs_dir}'. Only lab directories are allowed." + ) + + for lab in os.listdir(package.labs_dir): + lab_file_path = package.labs_dir / lab / LAB_CONFIG_FILE_NAME + if not lab_file_path.is_file(): + raise EosMissingConfigurationError(f"Lab file '{LAB_CONFIG_FILE_NAME}' does not exist for lab '{lab}'") + + log.debug(f"Detected lab '{lab}' in package '{package.name}'") + + @staticmethod + def _validate_experiments_dir(package: Package) -> None: + """ + Validate the structure of the experiments directory. + """ + for file in os.listdir(package.experiments_dir): + file_path = package.experiments_dir / file + if not file_path.is_dir(): + raise EosConfigurationError( + f"Non-directory file found in '{package.experiments_dir}'. Only experiment directories " + f"are allowed." + ) + + experiment_config_file = file_path / EXPERIMENT_CONFIG_FILE_NAME + if not experiment_config_file.is_file(): + raise EosMissingConfigurationError( + f"Experiment configuration file '{EXPERIMENT_CONFIG_FILE_NAME}' does not exist for " + f"experiment '{file}'" + ) + + log.debug(f"Detected experiment '{file}' in package '{package.name}'") + + @staticmethod + def _validate_tasks_dir(package: Package) -> None: + """ + Validate the structure of the tasks directory. + Ensure each subdirectory represents a task and contains the necessary files. + """ + task_types = [] + for current_dir, _, files in os.walk(package.tasks_dir): + if TASK_CONFIG_FILE_NAME not in files: + continue + + task_dir = Path(current_dir) + task_name = task_dir.relative_to(package.tasks_dir) + + config_file = task_dir / TASK_CONFIG_FILE_NAME + implementation_file = task_dir / TASK_IMPLEMENTATION_FILE_NAME + + if not config_file.is_file(): + raise EosMissingConfigurationError( + f"Task configuration file '{TASK_CONFIG_FILE_NAME}' not found for task '{task_name}' " + f"in package '{package.name}'." + ) + + if not implementation_file.is_file(): + raise EosMissingConfigurationError( + f"Task implementation file '{TASK_IMPLEMENTATION_FILE_NAME}' not found for task " + f"'{task_name}' in package '{package.name}'." + ) + + task_types.append(task_dir) + + log.debug(f"Detected tasks '{task_types}' in package '{package.name}'") + + @staticmethod + def _validate_devices_dir(package: Package) -> None: + """ + Validate the structure of the devices directory. + Ensure each subdirectory represents a device and contains the necessary files. + """ + device_types = [] + for current_dir, _, files in os.walk(package.devices_dir): + if DEVICE_CONFIG_FILE_NAME not in files: + continue + + device_dir = Path(current_dir) + device_name = device_dir.relative_to(package.devices_dir) + + config_file = device_dir / DEVICE_CONFIG_FILE_NAME + implementation_file = device_dir / DEVICE_IMPLEMENTATION_FILE_NAME + + if not config_file.is_file(): + raise EosMissingConfigurationError( + f"Device configuration file '{DEVICE_CONFIG_FILE_NAME}' not found for device " + f"'{device_name}' in package '{package.name}'." + ) + + if not implementation_file.is_file(): + raise EosMissingConfigurationError( + f"Device implementation file '{DEVICE_IMPLEMENTATION_FILE_NAME}' not found for device " + f"'{device_name}' in package '{package.name}'." + ) + + device_types.append(device_dir) + + log.debug("Detected devices '%S' in package '%s'", device_types, package.name) diff --git a/eos/configuration/plugin_registries/__init__.py b/eos/configuration/plugin_registries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/configuration/plugin_registries/campaign_optimizer_plugin_registry.py b/eos/configuration/plugin_registries/campaign_optimizer_plugin_registry.py new file mode 100644 index 0000000..291168f --- /dev/null +++ b/eos/configuration/plugin_registries/campaign_optimizer_plugin_registry.py @@ -0,0 +1,116 @@ +import importlib.util +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from eos.configuration.constants import ( + CAMPAIGN_OPTIMIZER_FILE_NAME, + CAMPAIGN_OPTIMIZER_CREATION_FUNCTION_NAME, +) +from eos.configuration.package_manager import PackageManager +from eos.configuration.plugin_registries.plugin_registry import PluginRegistry, PluginRegistryConfig +from eos.logging.logger import log +from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + +class CampaignOptimizerPluginRegistry( + PluginRegistry[Callable[[], tuple[dict[str, Any], type[AbstractSequentialOptimizer]]], Any] +): + """ + Responsible for dynamically loading campaign optimizers from all packages + and providing references to them for later use. + """ + + def __init__(self, package_manager: PackageManager): + config = PluginRegistryConfig( + spec_registry=None, # Campaign optimizers don't use a specification registry + base_class=None, # Campaign optimizers don't have a base class + config_file_name=None, # Campaign optimizers don't have a separate config file + implementation_file_name=CAMPAIGN_OPTIMIZER_FILE_NAME, + class_suffix="", # Campaign optimizers don't use a class suffix + error_class=Exception, # Using generic Exception for simplicity + directory_name="experiments_dir", + ) + super().__init__(package_manager, config) + + def get_campaign_optimizer_creation_parameters( + self, experiment_type: str + ) -> tuple[dict[str, Any], type[AbstractSequentialOptimizer]] | None: + """ + Get a function that can be used to get the constructor arguments and the optimizer type so it can be + constructed later. + + :param experiment_type: The type of the experiment. + :return: A tuple containing the constructor arguments and the optimizer type, or None if not found. + """ + optimizer_function = self.get_plugin_class_type(experiment_type) + if optimizer_function: + return optimizer_function() + return None + + def _load_single_plugin(self, package_name: str, dir_path: str, implementation_file: str) -> None: + module_name = Path(dir_path).name + spec = importlib.util.spec_from_file_location(module_name, implementation_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if CAMPAIGN_OPTIMIZER_CREATION_FUNCTION_NAME in module.__dict__: + experiment_type = module_name + self._plugin_types[experiment_type] = module.__dict__[CAMPAIGN_OPTIMIZER_CREATION_FUNCTION_NAME] + self._plugin_modules[experiment_type] = implementation_file + log.info(f"Loaded campaign optimizer for experiment '{experiment_type}' from package '{package_name}'.") + else: + log.warning( + f"Optimizer configuration function '{CAMPAIGN_OPTIMIZER_CREATION_FUNCTION_NAME}' not found in the " + f"campaign optimizer file '{self._config.implementation_file_name}' of experiment " + f"'{Path(dir_path).name}' in package '{package_name}'." + ) + + def load_campaign_optimizer(self, experiment_type: str) -> None: + """ + Load the optimizer configuration function for the given experiment from the appropriate package. + If the optimizer doesn't exist, log a warning and return without raising an error. + """ + experiment_package = self._package_manager.find_package_for_experiment(experiment_type) + if not experiment_package: + log.warning(f"No package found for experiment '{experiment_type}'.") + return + + optimizer_file = ( + Path(experiment_package.experiments_dir) / experiment_type / self._config.implementation_file_name + ) + + if not Path(optimizer_file).exists(): + log.warning( + f"No campaign optimizer found for experiment '{experiment_type}' in package " + f"'{experiment_package.name}'." + ) + return + + self._load_single_plugin(experiment_package.name, experiment_type, optimizer_file) + + def unload_campaign_optimizer(self, experiment_type: str) -> None: + """ + Unload the optimizer configuration function for the given experiment. + """ + if experiment_type in self._plugin_types: + del self._plugin_types[experiment_type] + del self._plugin_modules[experiment_type] + log.info(f"Unloaded campaign optimizer for experiment '{experiment_type}'.") + + def reload_plugin(self, experiment_type: str) -> None: + """ + Reload a specific campaign optimizer by its experiment type. + """ + self.unload_campaign_optimizer(experiment_type) + self.load_campaign_optimizer(experiment_type) + log.info(f"Reloaded campaign optimizer for experiment '{experiment_type}'.") + + def reload_all_plugins(self) -> None: + """ + Reload all campaign optimizers. + """ + experiment_types = list(self._plugin_types.keys()) + for experiment_type in experiment_types: + self.reload_plugin(experiment_type) + log.info("Reloaded all campaign optimizers.") diff --git a/eos/configuration/plugin_registries/device_plugin_registry.py b/eos/configuration/plugin_registries/device_plugin_registry.py new file mode 100644 index 0000000..87f9dd3 --- /dev/null +++ b/eos/configuration/plugin_registries/device_plugin_registry.py @@ -0,0 +1,23 @@ +from eos.configuration.constants import DEVICE_CONFIG_FILE_NAME, DEVICE_IMPLEMENTATION_FILE_NAME +from eos.configuration.package_manager import PackageManager +from eos.configuration.plugin_registries.plugin_registry import PluginRegistry, PluginRegistryConfig +from eos.configuration.spec_registries.device_specification_registry import DeviceSpecificationRegistry +from eos.devices.base_device import BaseDevice +from eos.devices.exceptions import EosDeviceClassNotFoundError + + +class DevicePluginRegistry(PluginRegistry[BaseDevice, DeviceSpecificationRegistry]): + def __init__(self, package_manager: PackageManager): + config = PluginRegistryConfig( + spec_registry=DeviceSpecificationRegistry(), + base_class=BaseDevice, + config_file_name=DEVICE_CONFIG_FILE_NAME, + implementation_file_name=DEVICE_IMPLEMENTATION_FILE_NAME, + class_suffix="Device", + error_class=EosDeviceClassNotFoundError, + directory_name="devices_dir", + ) + super().__init__(package_manager, config) + + def get_device_class_type(self, device_type: str) -> type[BaseDevice]: + return self.get_plugin_class_type(device_type) diff --git a/eos/configuration/plugin_registries/plugin_registry.py b/eos/configuration/plugin_registries/plugin_registry.py new file mode 100644 index 0000000..8859083 --- /dev/null +++ b/eos/configuration/plugin_registries/plugin_registry.py @@ -0,0 +1,118 @@ +import importlib +import inspect +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Generic, TypeVar + +from eos.configuration.package_manager import PackageManager +from eos.logging.batch_error_logger import batch_error, raise_batched_errors +from eos.logging.logger import log +from eos.utils.singleton import Singleton + +T = TypeVar("T") +S = TypeVar("S") # Specification registry type + + +@dataclass +class PluginRegistryConfig: + spec_registry: S + base_class: type[T] + config_file_name: str + implementation_file_name: str + class_suffix: str + error_class: type[Exception] + directory_name: str + + +class PluginRegistry(Generic[T, S], metaclass=Singleton): + """ + A generic registry for dynamically discovering and managing plugin-like implementation classes. + Supports on-demand reloading of plugins. + """ + + def __init__(self, package_manager: PackageManager, config: PluginRegistryConfig): + self._package_manager = package_manager + self._config = config + self._plugin_types: dict[str, type[T]] = {} + self._plugin_modules: dict[str, str] = {} # Maps type_name to module path + + self._load_plugin_modules() + + def get_plugin_class_type(self, type_name: str) -> type[T]: + """ + Get the plugin class type for the given type name. + """ + if type_name in self._plugin_types: + return self._plugin_types[type_name] + + raise self._config.error_class(f"Plugin implementation for '{type_name}' not found.") + + def _load_plugin_modules(self) -> None: + self._plugin_types.clear() + self._plugin_modules.clear() + + for package in self._package_manager.get_all_packages(): + directory = getattr(package, self._config.directory_name) + + if not Path(directory).is_dir(): + continue + + for current_dir, _, files in os.walk(directory): + if self._config.config_file_name not in files: + continue + + dir_path = Path(current_dir).relative_to(Path(directory)) + + implementation_file = Path(current_dir) / self._config.implementation_file_name + + self._load_single_plugin(package.name, dir_path, implementation_file) + + raise_batched_errors(root_exception_type=self._config.error_class) + + def _load_single_plugin(self, package_name: str, dir_path: Path, implementation_file: Path) -> None: + module_name = Path(dir_path).name + spec = importlib.util.spec_from_file_location(module_name, implementation_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + found_implementation = False + for name, obj in module.__dict__.items(): + if inspect.isclass(obj) and obj is not self._config.base_class and name.endswith(self._config.class_suffix): + type_name = self._config.spec_registry.get_spec_by_dir(Path(package_name) / dir_path) + self._plugin_types[type_name] = obj + self._plugin_modules[type_name] = implementation_file + found_implementation = True + log.debug( + f"Loaded {self._config.class_suffix.lower()} plugin '{name}' for type '{type_name}' from package " + f"'{package_name}'" + ) + break + + if not found_implementation: + batch_error( + f"{self._config.class_suffix} plugin for '{module_name}' in package '{package_name}' not found." + f" Make sure that its name ends in '{self._config.class_suffix}'.", + self._config.error_class, + ) + + def reload_plugin(self, type_name: str) -> None: + """ + Reload a specific plugin by its type name. + """ + if type_name not in self._plugin_modules: + raise self._config.error_class(f"Plugin '{type_name}' not found.") + + implementation_file = self._plugin_modules[type_name] + package_name = Path(implementation_file).parent.parent.name + dir_path = os.path.relpath(Path(implementation_file).parent, Path(implementation_file).parent.parent) + + self._load_single_plugin(package_name, dir_path, implementation_file) + log.info(f"Reloaded plugin '{type_name}'") + + def reload_all_plugins(self) -> None: + """ + Reload all plugins. + """ + self._load_plugin_modules() + log.info("Reloaded all plugins") diff --git a/eos/configuration/plugin_registries/task_plugin_registry.py b/eos/configuration/plugin_registries/task_plugin_registry.py new file mode 100644 index 0000000..842fc05 --- /dev/null +++ b/eos/configuration/plugin_registries/task_plugin_registry.py @@ -0,0 +1,23 @@ +from eos.configuration.constants import TASK_CONFIG_FILE_NAME, TASK_IMPLEMENTATION_FILE_NAME +from eos.configuration.exceptions import EosTaskHandlerClassNotFoundError +from eos.configuration.package_manager import PackageManager +from eos.configuration.plugin_registries.plugin_registry import PluginRegistry, PluginRegistryConfig +from eos.configuration.spec_registries.task_specification_registry import TaskSpecificationRegistry +from eos.tasks.base_task import BaseTask + + +class TaskPluginRegistry(PluginRegistry[BaseTask, TaskSpecificationRegistry]): + def __init__(self, package_manager: PackageManager): + config = PluginRegistryConfig( + spec_registry=TaskSpecificationRegistry(), + base_class=BaseTask, + config_file_name=TASK_CONFIG_FILE_NAME, + implementation_file_name=TASK_IMPLEMENTATION_FILE_NAME, + class_suffix="Task", + error_class=EosTaskHandlerClassNotFoundError, + directory_name="tasks_dir", + ) + super().__init__(package_manager, config) + + def get_task_class_type(self, task_type: str) -> type[BaseTask]: + return self.get_plugin_class_type(task_type) diff --git a/eos/configuration/spec_registries/__init__.py b/eos/configuration/spec_registries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/configuration/spec_registries/device_specification_registry.py b/eos/configuration/spec_registries/device_specification_registry.py new file mode 100644 index 0000000..e69fdb6 --- /dev/null +++ b/eos/configuration/spec_registries/device_specification_registry.py @@ -0,0 +1,9 @@ +from eos.configuration.entities.device_specification import DeviceSpecification +from eos.configuration.entities.lab import LabDeviceConfig +from eos.configuration.spec_registries.specification_registry import SpecificationRegistry + + +class DeviceSpecificationRegistry(SpecificationRegistry[DeviceSpecification, LabDeviceConfig]): + """ + The device specification registry stores the specifications for all devices that are available in EOS. + """ diff --git a/eos/configuration/spec_registries/specification_registry.py b/eos/configuration/spec_registries/specification_registry.py new file mode 100644 index 0000000..d9ee45c --- /dev/null +++ b/eos/configuration/spec_registries/specification_registry.py @@ -0,0 +1,38 @@ +from typing import Generic, TypeVar + +from eos.utils.singleton import Singleton + +T = TypeVar("T") # Specification type +C = TypeVar("C") # Configuration type + + +class SpecificationRegistry(Generic[T, C], metaclass=Singleton): + """ + A generic registry for storing and retrieving specifications. + """ + + def __init__( + self, + specifications: dict[str, T], + dirs_to_types: dict[str, str], + ): + self._specifications = specifications.copy() + self._dirs_to_types = dirs_to_types.copy() + + def get_all_specs(self) -> dict[str, T]: + return self._specifications + + def get_spec_by_type(self, spec_type: str) -> T | None: + return self._specifications.get(spec_type) + + def get_spec_by_config(self, config: C) -> T | None: + return self._specifications.get(config.type) + + def get_spec_by_dir(self, dir_path: str) -> str: + return self._dirs_to_types.get(dir_path) + + def spec_exists_by_config(self, config: C) -> bool: + return config.type in self._specifications + + def spec_exists_by_type(self, spec_type: str) -> bool: + return spec_type in self._specifications diff --git a/eos/configuration/spec_registries/task_specification_registry.py b/eos/configuration/spec_registries/task_specification_registry.py new file mode 100644 index 0000000..9b9c160 --- /dev/null +++ b/eos/configuration/spec_registries/task_specification_registry.py @@ -0,0 +1,24 @@ +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.configuration.spec_registries.specification_registry import SpecificationRegistry + + +class TaskSpecificationRegistry(SpecificationRegistry[TaskSpecification, TaskConfig]): + """ + The task specification registry stores the specifications for all tasks that are available in EOS. + """ + + def __init__( + self, + task_specifications: dict[str, TaskSpecification], + task_dirs_to_task_types: dict[str, str], + ): + updated_specs = self._update_output_containers(task_specifications) + super().__init__(updated_specs, task_dirs_to_task_types) + + @staticmethod + def _update_output_containers(specs: dict[str, TaskSpecification]) -> dict[str, TaskSpecification]: + for spec in specs.values(): + if not spec.output_containers: + spec.output_containers = spec.input_containers.copy() + return specs diff --git a/eos/configuration/validation/__init__.py b/eos/configuration/validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/configuration/validation/container_registry.py b/eos/configuration/validation/container_registry.py new file mode 100644 index 0000000..f27d6c5 --- /dev/null +++ b/eos/configuration/validation/container_registry.py @@ -0,0 +1,28 @@ +from eos.configuration.entities.experiment import ( + ExperimentConfig, +) +from eos.configuration.entities.lab import ( + LabConfig, + LabContainerConfig, +) + + +class ContainerRegistry: + """ + The container registry stores information about the containers in labs. + """ + + def __init__(self, experiment_config: ExperimentConfig, lab_configs: list[LabConfig]): + self._experiment_config = experiment_config + self._lab_configs = [lab for lab in lab_configs if lab.type in self._experiment_config.labs] + + def find_container_by_id(self, container_id: str) -> LabContainerConfig | None: + """ + Find a container in the lab by its id. + """ + for lab in self._lab_configs: + for container in lab.containers: + if container_id in container.ids: + return container + + return None diff --git a/eos/configuration/validation/container_validator.py b/eos/configuration/validation/container_validator.py new file mode 100644 index 0000000..b026a79 --- /dev/null +++ b/eos/configuration/validation/container_validator.py @@ -0,0 +1,36 @@ +from eos.configuration.entities.experiment import ( + ExperimentConfig, + ExperimentContainerConfig, +) +from eos.configuration.entities.lab import LabConfig +from eos.configuration.exceptions import EosContainerConfigurationError +from eos.configuration.validation.container_registry import ContainerRegistry + + +class ExperimentContainerValidator: + """ + Validate the containers of an experiment. + """ + + def __init__(self, experiment_config: ExperimentConfig, lab_configs: list[LabConfig]): + self._experiment_config = experiment_config + self._lab_configs = lab_configs + + self._container_registry = ContainerRegistry(experiment_config, lab_configs) + + def validate(self) -> None: + self._validate_containers() + + def _validate_containers(self) -> None: + if not self._experiment_config.containers: + return + for container in self._experiment_config.containers: + self._validate_container_exists(container) + + def _validate_container_exists(self, container: ExperimentContainerConfig) -> None: + for lab in self._lab_configs: + for lab_container in lab.containers: + if container.id in lab_container.ids: + return + + raise EosContainerConfigurationError(f"Container '{container.id}' does not exist.") diff --git a/eos/configuration/validation/experiment_validator.py b/eos/configuration/validation/experiment_validator.py new file mode 100644 index 0000000..15d99ce --- /dev/null +++ b/eos/configuration/validation/experiment_validator.py @@ -0,0 +1,39 @@ +from eos.configuration.entities.experiment import ExperimentConfig +from eos.configuration.entities.lab import LabConfig +from eos.configuration.exceptions import EosExperimentConfigurationError +from eos.configuration.validation.container_validator import ( + ExperimentContainerValidator, +) + +from eos.configuration.validation.task_sequence_validator import ( + TaskSequenceValidator, +) + + +class ExperimentValidator: + def __init__( + self, + experiment_config: ExperimentConfig, + lab_configs: list[LabConfig], + ): + self._experiment_config = experiment_config + self._lab_configs = lab_configs + + def validate(self) -> None: + self._validate_labs() + ExperimentContainerValidator(self._experiment_config, self._lab_configs).validate() + TaskSequenceValidator(self._experiment_config, self._lab_configs).validate() + + def _validate_labs(self) -> None: + lab_types = [lab.type for lab in self._lab_configs] + invalid_labs = [] + for lab in self._experiment_config.labs: + if lab not in lab_types: + invalid_labs.append(lab) + + if invalid_labs: + invalid_labs_str = "\n ".join(invalid_labs) + raise EosExperimentConfigurationError( + f"The following labs required by experiment '{self._experiment_config.type}' do not exist:" + f"\n {invalid_labs_str}" + ) diff --git a/eos/configuration/validation/lab_validator.py b/eos/configuration/validation/lab_validator.py new file mode 100644 index 0000000..e5ae2e1 --- /dev/null +++ b/eos/configuration/validation/lab_validator.py @@ -0,0 +1,164 @@ +from pathlib import Path + +from eos.configuration.constants import LABS_DIR, EOS_COMPUTER_NAME +from eos.configuration.entities.lab import LabConfig +from eos.configuration.exceptions import EosLabConfigurationError +from eos.configuration.spec_registries.device_specification_registry import DeviceSpecificationRegistry +from eos.configuration.spec_registries.task_specification_registry import ( + TaskSpecificationRegistry, +) +from eos.logging.batch_error_logger import batch_error, raise_batched_errors + + +class LabValidator: + """ + Validates the configuration of a lab. It validates the locations, devices, and containers defined in the + lab configuration. + """ + + def __init__(self, config_dir: str, lab_config: LabConfig): + self._lab_config = lab_config + self._lab_config_dir = Path(config_dir) / LABS_DIR / lab_config.type.lower() + self._tasks = TaskSpecificationRegistry() + self._devices = DeviceSpecificationRegistry() + + def validate(self) -> None: + self._validate_lab_folder_name_matches_lab_type() + self._validate_locations() + self._validate_computers() + self._validate_devices() + self._validate_containers() + + def _validate_locations(self) -> None: + self._validate_device_locations() + self._validate_container_locations() + + def _validate_lab_folder_name_matches_lab_type(self) -> None: + if Path(self._lab_config_dir).name != self._lab_config.type: + raise EosLabConfigurationError( + f"Lab folder name '{Path(self._lab_config_dir).name}' does not match lab type " + f"'{self._lab_config.type}'." + ) + + def _validate_device_locations(self) -> None: + locations = self._lab_config.locations + for device_name, device in self._lab_config.devices.items(): + if device.location and device.location not in locations: + batch_error( + f"Device '{device_name}' has invalid location '{device.location}'.", + EosLabConfigurationError, + ) + raise_batched_errors(EosLabConfigurationError) + + def _validate_container_locations(self) -> None: + locations = self._lab_config.locations + for container in self._lab_config.containers: + if container.location not in locations: + raise EosLabConfigurationError( + f"Container of type '{container.type}' has invalid location '{container.location}'." + ) + + def _validate_computers(self) -> None: + self._validate_computer_unique_ips() + self._validate_eos_computer_not_specified() + + def _validate_computer_unique_ips(self) -> None: + ip_addresses = set() + + for computer_name, computer in self._lab_config.computers.items(): + if computer.ip in ip_addresses: + batch_error( + f"Computer '{computer_name}' has a duplicate IP address '{computer.ip}'.", + EosLabConfigurationError, + ) + ip_addresses.add(computer.ip) + + raise_batched_errors(EosLabConfigurationError) + + def _validate_eos_computer_not_specified(self) -> None: + for computer_name, computer in self._lab_config.computers.items(): + if computer_name.lower() == EOS_COMPUTER_NAME: + batch_error( + "Computer name 'eos_computer' is reserved and cannot be used.", + EosLabConfigurationError, + ) + if computer.ip in ["127.0.0.1", "localhost"]: + batch_error( + f"Computer '{computer_name}' cannot use the reserved IP '127.0.0.1' or 'localhost'.", + EosLabConfigurationError, + ) + raise_batched_errors(EosLabConfigurationError) + + def _validate_devices(self) -> None: + self._validate_devices_have_computers() + self._validate_device_initialization_parameters() + + def _validate_devices_have_computers(self) -> None: + for device_name, device in self._lab_config.devices.items(): + if device.computer.lower() == EOS_COMPUTER_NAME: + continue + if device.computer not in self._lab_config.computers: + batch_error( + f"Device '{device_name}' has invalid computer '{device.computer}'.", + EosLabConfigurationError, + ) + raise_batched_errors(EosLabConfigurationError) + + def _validate_device_initialization_parameters(self) -> None: + for device_name, device in self._lab_config.devices.items(): + device_spec = self._devices.get_spec_by_config(device) + if not device_spec: + batch_error( + f"No specification found for device type '{device.type}' of device '{device_name}'.", + EosLabConfigurationError, + ) + continue + + if device.initialization_parameters: + spec_params = device_spec.initialization_parameters or {} + for param_name in device.initialization_parameters: + if param_name not in spec_params: + batch_error( + f"Invalid initialization parameter '{param_name}' for device '{device_name}' " + f"of type '{device.type}' in lab type '{self._lab_config.type}'. " + f"Valid parameters are: {', '.join(spec_params.keys())}", + EosLabConfigurationError, + ) + + raise_batched_errors(EosLabConfigurationError) + + def _validate_containers(self) -> None: + self._validate_container_unique_types() + self._validate_container_unique_ids() + + def _validate_container_unique_types(self) -> None: + container_types = [] + for container in self._lab_config.containers: + container_types.append(container.type) + + unique_container_types = set(container_types) + + for container_type in unique_container_types: + if container_types.count(container_type) > 1: + batch_error( + f"Container type '{container_type}' already defined." + f" Please add more ids to the existing container definition.", + EosLabConfigurationError, + ) + raise_batched_errors(EosLabConfigurationError) + + def _validate_container_unique_ids(self) -> None: + container_ids = set() + duplicate_ids = set() + for container in self._lab_config.containers: + for container_id in container.ids: + if container_id in container_ids: + duplicate_ids.add(container_id) + else: + container_ids.add(container_id) + + if duplicate_ids: + duplicate_ids_str = "\n ".join(duplicate_ids) + raise EosLabConfigurationError( + f"Containers must have unique IDs. The following are not unique:\n {duplicate_ids_str}" + ) diff --git a/eos/configuration/validation/multi_lab_validator.py b/eos/configuration/validation/multi_lab_validator.py new file mode 100644 index 0000000..2d9523d --- /dev/null +++ b/eos/configuration/validation/multi_lab_validator.py @@ -0,0 +1,51 @@ +from collections import defaultdict + +from eos.configuration.entities.lab import LabConfig +from eos.configuration.exceptions import EosLabConfigurationError + + +class MultiLabValidator: + """ + Cross-checks all lab configuration. It validates that all container IDs are globally unique. + """ + + def __init__(self, lab_configs: list[LabConfig]): + self._lab_configs = lab_configs + + def validate(self) -> None: + self._validate_computer_ips_globally_unique() + self._validate_container_ids_globally_unique() + + def _validate_computer_ips_globally_unique(self) -> None: + computer_ips = defaultdict(list) + + for lab in self._lab_configs: + for computer in lab.computers.values(): + computer_ips[computer.ip].append(lab.type) + + duplicate_ips = {ip: labs for ip, labs in computer_ips.items() if len(labs) > 1} + + if duplicate_ips: + duplicate_ips_str = "\n ".join( + f"'{ip}': defined in labs {', '.join(labs)}" for ip, labs in duplicate_ips.items() + ) + raise EosLabConfigurationError( + f"The following computer IPs are not globally unique:\n {duplicate_ips_str}" + ) + + def _validate_container_ids_globally_unique(self) -> None: + container_ids = defaultdict(list) + for lab in self._lab_configs: + for container in lab.containers: + for container_id in container.ids: + container_ids[container_id].append(lab.type) + + duplicate_ids = {container_id: labs for container_id, labs in container_ids.items() if len(labs) > 1} + + if duplicate_ids: + duplicate_ids_str = "\n ".join( + f"'{container_id}': defined in labs {', '.join(labs)}" for container_id, labs in duplicate_ids.items() + ) + raise EosLabConfigurationError( + f"The following container IDs are not globally unique:\n {duplicate_ids_str}" + ) diff --git a/eos/configuration/validation/task_sequence/__init__.py b/eos/configuration/validation/task_sequence/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/configuration/validation/task_sequence/base_task_sequence_validator.py b/eos/configuration/validation/task_sequence/base_task_sequence_validator.py new file mode 100644 index 0000000..27b2551 --- /dev/null +++ b/eos/configuration/validation/task_sequence/base_task_sequence_validator.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + +from eos.configuration.entities.experiment import ( + ExperimentConfig, +) +from eos.configuration.entities.lab import LabConfig +from eos.configuration.entities.task import TaskConfig +from eos.configuration.spec_registries.task_specification_registry import ( + TaskSpecificationRegistry, +) + + +class BaseTaskSequenceValidator(ABC): + def __init__( + self, + experiment_config: ExperimentConfig, + lab_configs: list[LabConfig], + ): + self._experiment_config = experiment_config + self._lab_configs = lab_configs + self._tasks = TaskSpecificationRegistry() + + @abstractmethod + def validate(self) -> None: + pass + + def _find_task_by_id(self, task_id: str) -> TaskConfig | None: + return next((task for task in self._experiment_config.tasks if task.id == task_id), None) diff --git a/eos/configuration/validation/task_sequence/task_input_container_validator.py b/eos/configuration/validation/task_sequence/task_input_container_validator.py new file mode 100644 index 0000000..1832e49 --- /dev/null +++ b/eos/configuration/validation/task_sequence/task_input_container_validator.py @@ -0,0 +1,113 @@ +from eos.configuration.entities.lab import LabContainerConfig +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification, TaskSpecificationContainer +from eos.configuration.exceptions import EosTaskValidationError +from eos.configuration.validation import validation_utils +from eos.configuration.validation.container_registry import ContainerRegistry +from eos.logging.batch_error_logger import batch_error, raise_batched_errors + + +class TaskInputContainerValidator: + """ + Validates that the input containers of a task conform to the task's specification. + """ + + def __init__( + self, + task: TaskConfig, + task_spec: TaskSpecification, + container_registry: ContainerRegistry, + ): + self._task_id = task.id + self._input_containers = task.containers + self._task_spec = task_spec + self._container_registry = container_registry + + def validate_input_containers(self) -> None: + """ + Validate the input containers of a task. + Check whether the types of containers match the task's requirements and whether the quantities are correct. + """ + self._validate_input_container_requirements() + raise_batched_errors(root_exception_type=EosTaskValidationError) + + def _validate_input_container_requirements(self) -> None: + """ + Validate that the input containers of a task meet its requirements in terms of types and quantities. + """ + required_containers = self._get_required_containers() + provided_containers = self._get_provided_containers() + + self._validate_container_counts(required_containers, provided_containers) + self._validate_container_types(required_containers, provided_containers) + + def _get_required_containers(self) -> dict[str, TaskSpecificationContainer]: + """ + Get the required containers as specified in the task specification. + """ + return self._task_spec.input_containers + + def _get_provided_containers(self) -> dict[str, str]: + """ + Get the provided containers, validating their existence if not a reference. + """ + provided_containers = {} + for container_name, container_id in self._input_containers.items(): + if validation_utils.is_container_reference(container_id): + provided_containers[container_name] = "reference" + else: + lab_container = self._validate_container_exists(container_id) + provided_containers[container_name] = lab_container.type + return provided_containers + + def _validate_container_exists(self, container_id: str) -> LabContainerConfig: + """ + Validate the existence of a container in the lab. + """ + container = self._container_registry.find_container_by_id(container_id) + + if not container: + batch_error( + f"Container '{container_id}' in task '{self._task_id}' does not exist in the lab.", + EosTaskValidationError, + ) + + return container + + def _validate_container_counts( + self, required: dict[str, TaskSpecificationContainer], provided: dict[str, str] + ) -> None: + """ + Validate that the total number of containers matches the requirements. + """ + if len(provided) != len(required): + batch_error( + f"Task '{self._task_id}' requires {len(required)} container(s) but {len(provided)} were provided.", + EosTaskValidationError, + ) + + def _validate_container_types( + self, required: dict[str, TaskSpecificationContainer], provided: dict[str, str] + ) -> None: + """ + Validate that the types of non-reference containers match the requirements. + """ + for container_name, container_spec in required.items(): + if container_name not in provided: + batch_error( + f"Required container '{container_name}' not provided for task '{self._task_id}'.", + EosTaskValidationError, + ) + elif provided[container_name] != "reference" and provided[container_name] != container_spec.type: + batch_error( + f"Container '{container_name}' in task '{self._task_id}' has incorrect type. " + f"Expected '{container_spec.type}' but got '{provided[container_name]}'.", + EosTaskValidationError, + ) + + for container_name in provided: + if container_name not in required: + batch_error( + f"Unexpected container '{container_name}' provided for task '{self._task_id}'.", + EosTaskValidationError, + ) diff --git a/eos/configuration/validation/task_sequence/task_input_parameter_validator.py b/eos/configuration/validation/task_sequence/task_input_parameter_validator.py new file mode 100644 index 0000000..3ea6bf2 --- /dev/null +++ b/eos/configuration/validation/task_sequence/task_input_parameter_validator.py @@ -0,0 +1,139 @@ +import copy +from typing import Any + +from omegaconf import DictConfig, ListConfig, OmegaConf + +from eos.configuration.entities.parameters import ParameterType, ParameterFactory +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.configuration.exceptions import ( + EosTaskValidationError, + EosConfigurationError, +) +from eos.configuration.validation import validation_utils +from eos.logging.batch_error_logger import batch_error, raise_batched_errors + + +class TaskInputParameterValidator: + """ + Validates that the input parameters of a task conform to the task's specification. + """ + + def __init__(self, task: TaskConfig, task_spec: TaskSpecification): + self._task_id = task.id + self._input_parameters = task.parameters + self._task_spec = task_spec + + def validate_input_parameters(self, allow_non_concrete_parameters=True) -> None: + """ + Validate the input parameters of a task. + Ensure that all required parameters are provided and that the provided parameters conform to the task's + specification. + + :param allow_non_concrete_parameters: Whether to allow non-concrete parameters: reference or dynamic parameters. + """ + for parameter_name in self._input_parameters: + self._validate_parameter_in_task_spec(parameter_name) + raise_batched_errors(root_exception_type=EosTaskValidationError) + + self._validate_all_required_parameters_provided() + + for parameter_name, parameter in self._input_parameters.items(): + if allow_non_concrete_parameters: + self._validate_parameter(parameter_name, parameter) + else: + self._validate_concrete_parameter(parameter_name, parameter) + raise_batched_errors(root_exception_type=EosTaskValidationError) + + def _validate_parameter_in_task_spec(self, parameter_name: str) -> None: + """ + Check that the parameter exists in the task specification. + """ + if parameter_name not in self._task_spec.input_parameters: + batch_error( + f"Parameter '{parameter_name}' in task '{self._task_id}' is invalid. " + f"Expected a parameter found in the task specification.", + EosTaskValidationError, + ) + + def _validate_parameter(self, parameter_name: str, parameter: Any) -> None: + """ + Validate a parameter according to the task specification. Ignore parameter references and special parameters. + """ + if validation_utils.is_parameter_reference(parameter) or validation_utils.is_dynamic_parameter(parameter): + return + + self._validate_parameter_spec(parameter_name, parameter) + + def _validate_concrete_parameter(self, parameter_name: str, parameter: Any) -> None: + """ + Validate a parameter according to the task specification. Expect that the parameter is concrete. + """ + if validation_utils.is_parameter_reference(parameter): + batch_error( + f"Input parameter '{parameter_name}' in task '{self._task_id}' is a parameter reference, which is not " + f"allowed.", + EosTaskValidationError, + ) + elif validation_utils.is_dynamic_parameter(parameter): + batch_error( + f"Input parameter '{parameter_name}' in task '{self._task_id}' is 'eos_dynamic', which is not " + f"allowed.", + EosTaskValidationError, + ) + else: + self._validate_parameter_spec(parameter_name, parameter) + + def _validate_parameter_spec(self, parameter_name: str, parameter: Any) -> None: + """ + Validate a parameter to make sure it conforms to its task specification. + """ + parameter_spec = copy.deepcopy(self._task_spec.input_parameters[parameter_name]) + + if isinstance(parameter, ListConfig | DictConfig): + parameter = OmegaConf.to_object(parameter) + + if not isinstance(parameter, ParameterType(parameter_spec.type).python_type()): + batch_error( + f"Parameter '{parameter_name}' in task '{self._task_id}' has incorrect type {type(parameter)}. " + f"Expected type: '{parameter_spec.type}'.", + EosTaskValidationError, + ) + return + + parameter_spec["value"] = parameter + + try: + parameter_type = ParameterType(parameter_spec.type) + ParameterFactory.create_parameter(parameter_type, **parameter_spec) + except EosConfigurationError as e: + batch_error( + f"Parameter '{parameter_name}' in task '{self._task_id}' validation error: {e}", + EosTaskValidationError, + ) + + def _validate_all_required_parameters_provided(self) -> None: + """ + Validate that all required parameters are provided in the parameter dictionary. + """ + missing_parameters = self._get_missing_required_task_parameters() + + if missing_parameters: + raise EosTaskValidationError( + f"Task '{self._task_id}' is missing required input parameters: {missing_parameters}" + ) + + def _get_missing_required_task_parameters(self) -> list[str]: + """ + Get all the missing required parameters in the parameter dictionary. + """ + required_parameters = self._get_required_input_parameters() + return [ + parameter_name for parameter_name in required_parameters if parameter_name not in self._input_parameters + ] + + def _get_required_input_parameters(self) -> list[str]: + """ + Get all the required input parameters for the task. + """ + return [param for param, spec in self._task_spec.input_parameters.items() if "value" not in spec] diff --git a/eos/configuration/validation/task_sequence/task_sequence_input_container_validator.py b/eos/configuration/validation/task_sequence/task_sequence_input_container_validator.py new file mode 100644 index 0000000..84bf328 --- /dev/null +++ b/eos/configuration/validation/task_sequence/task_sequence_input_container_validator.py @@ -0,0 +1,92 @@ +from eos.configuration.entities.experiment import ExperimentConfig +from eos.configuration.entities.lab import LabConfig +from eos.configuration.entities.task import TaskConfig +from eos.configuration.exceptions import EosTaskValidationError +from eos.configuration.validation import validation_utils +from eos.configuration.validation.container_registry import ContainerRegistry +from eos.configuration.validation.task_sequence.base_task_sequence_validator import BaseTaskSequenceValidator +from eos.configuration.validation.task_sequence.task_input_container_validator import TaskInputContainerValidator + + +class TaskSequenceInputContainerValidator(BaseTaskSequenceValidator): + """ + Validate the input containers of every task in a task sequence. + """ + + def __init__( + self, + experiment_config: ExperimentConfig, + lab_configs: list[LabConfig], + ): + super().__init__(experiment_config, lab_configs) + self._container_registry = ContainerRegistry(experiment_config, lab_configs) + + def validate(self) -> None: + for task in self._experiment_config.tasks: + self._validate_input_containers(task) + + def _validate_input_containers( + self, + task: TaskConfig, + ) -> None: + """ + Validate that a task gets the types and quantities of input containers it requires. + """ + task_spec = self._tasks.get_spec_by_config(task) + if not task.containers and task_spec.input_containers: + raise EosTaskValidationError(f"Task '{task.id}' requires input containers but none were provided.") + + input_container_validator = TaskInputContainerValidator(task, task_spec, self._container_registry) + input_container_validator.validate_input_containers() + + self._validate_container_references(task) + + def _validate_container_references(self, task: TaskConfig) -> None: + for container_name, container_id in task.containers.items(): + if validation_utils.is_container_reference(container_id): + self._validate_container_reference(container_name, container_id, task) + + def _validate_container_reference( + self, + container_name: str, + container_id: str, + task: TaskConfig, + ) -> None: + """ + Ensure that a container reference is valid and that it conforms to the container specification. + """ + referenced_task_id, referenced_container = container_id.split(".") + + referenced_task = self._find_task_by_id(referenced_task_id) + if not referenced_task: + raise EosTaskValidationError( + f"Container '{container_name}' in task '{task.id}' references task '{referenced_task_id}' " + f"which does not exist." + ) + + referenced_task_spec = self._tasks.get_spec_by_config(referenced_task) + + if referenced_container not in referenced_task_spec.output_containers: + raise EosTaskValidationError( + f"Container '{container_name}' in task '{task.id}' references container '{referenced_container}' " + f"which is not an output container of task '{referenced_task_id}'." + ) + + task_spec = self._tasks.get_spec_by_config(task) + if container_name not in task_spec.input_containers: + raise EosTaskValidationError( + f"Container '{container_name}' is not a valid input container for task '{task.id}'." + ) + + required_container_spec = task_spec.input_containers[container_name] + referenced_container_spec = referenced_task_spec.output_containers[referenced_container] + + if required_container_spec.type != referenced_container_spec.type: + raise EosTaskValidationError( + f"Type mismatch for referenced container '{referenced_container}' in task '{task.id}'. " + f"The required container type is '{required_container_spec.type}' which does not match the referenced " + f"container type '{referenced_container_spec.type}'." + ) + + def _find_task_by_id(self, task_id: str) -> TaskConfig | None: + return next((task for task in self._experiment_config.tasks if task.id == task_id), None) diff --git a/eos/configuration/validation/task_sequence/task_sequence_input_parameter_validator.py b/eos/configuration/validation/task_sequence/task_sequence_input_parameter_validator.py new file mode 100644 index 0000000..19fa902 --- /dev/null +++ b/eos/configuration/validation/task_sequence/task_sequence_input_parameter_validator.py @@ -0,0 +1,95 @@ +from eos.configuration.entities.parameters import ( + ParameterType, +) +from eos.configuration.entities.task import TaskConfig +from eos.configuration.exceptions import ( + EosTaskValidationError, +) +from eos.configuration.validation import validation_utils +from eos.configuration.validation.task_sequence.base_task_sequence_validator import ( + BaseTaskSequenceValidator, +) +from eos.configuration.validation.task_sequence.task_input_parameter_validator import ( + TaskInputParameterValidator, +) + + +class TaskSequenceInputParameterValidator(BaseTaskSequenceValidator): + """ + Validate the input parameters of every task in a task sequence. + """ + + def validate(self) -> None: + for task in self._experiment_config.tasks: + self._validate_input_parameters(task) + + def _validate_input_parameters(self, task: TaskConfig) -> None: + task_spec = self._tasks.get_spec_by_config(task) + + if task_spec.input_parameters is None and task.parameters is not None: + raise EosTaskValidationError( + f"Task '{task.id}' does not accept input parameters but parameters were provided." + ) + + parameter_validator = TaskInputParameterValidator(task, task_spec) + parameter_validator.validate_input_parameters() + + self._validate_parameter_references(task) + + def _validate_parameter_references(self, task: TaskConfig) -> None: + for parameter_name, parameter in task.parameters.items(): + if validation_utils.is_parameter_reference(parameter): + self._validate_parameter_reference(parameter_name, task) + + def _validate_parameter_reference( + self, + parameter_name: str, + task: TaskConfig, + ) -> None: + """ + Ensure that a parameter reference is valid and that it conforms to the parameter specification. + """ + parameter = task.parameters[parameter_name] + referenced_task_id, referenced_parameter = str(parameter).split(".") + + referenced_task = self._find_task_by_id(referenced_task_id) + if not referenced_task: + raise EosTaskValidationError( + f"Parameter '{parameter_name}' in task '{task.id}' references task '{referenced_task_id}' " + f"which does not exist." + ) + + referenced_task_spec = self._tasks.get_spec_by_config(referenced_task) + + referenced_parameter_spec = None + if ( + referenced_task_spec.output_parameters + and referenced_task_spec.output_parameters + and referenced_parameter in referenced_task_spec.output_parameters + ): + referenced_parameter_spec = referenced_task_spec.output_parameters[referenced_parameter] + elif ( + referenced_task_spec.input_parameters + and referenced_task_spec.input_parameters + and referenced_parameter in referenced_task_spec.input_parameters + ): + referenced_parameter_spec = referenced_task_spec.input_parameters[referenced_parameter] + + if not referenced_parameter_spec: + raise EosTaskValidationError( + f"Parameter '{parameter_name}' in task '{task.id}' references parameter '{referenced_parameter}' " + f"which does not exist in task '{referenced_task_id}'." + ) + + task_spec = self._tasks.get_spec_by_config(task) + parameter_spec = task_spec.input_parameters[parameter_name] + + if ( + ParameterType(parameter_spec.type).python_type() + != ParameterType(referenced_parameter_spec.type).python_type() + ): + raise EosTaskValidationError( + f"Type mismatch for referenced parameter '{referenced_parameter}' in task '{task.id}'. " + f"The required parameter type is '{parameter_spec.type}' which does not match referenced the parameter " + f"type '{referenced_parameter_spec.type.value}'." + ) diff --git a/eos/configuration/validation/task_sequence_validator.py b/eos/configuration/validation/task_sequence_validator.py new file mode 100644 index 0000000..45f6c8d --- /dev/null +++ b/eos/configuration/validation/task_sequence_validator.py @@ -0,0 +1,111 @@ +import re +from collections import Counter + +from eos.configuration.entities.experiment import ExperimentConfig +from eos.configuration.entities.lab import LabConfig +from eos.configuration.exceptions import EosTaskValidationError +from eos.configuration.validation.task_sequence.base_task_sequence_validator import BaseTaskSequenceValidator +from eos.configuration.validation.task_sequence.task_sequence_input_container_validator import ( + TaskSequenceInputContainerValidator, +) +from eos.configuration.validation.task_sequence.task_sequence_input_parameter_validator import ( + TaskSequenceInputParameterValidator, +) +from eos.logging.batch_error_logger import batch_error, raise_batched_errors + + +class TaskSequenceValidator(BaseTaskSequenceValidator): + def __init__( + self, + experiment_config: ExperimentConfig, + lab_configs: list[LabConfig], + ): + super().__init__(experiment_config, lab_configs) + self._valid_task_id_pattern = re.compile("^[A-Za-z0-9_]+$") + + def validate(self) -> None: + self._validate_tasks_exist() + self._validate_task_dependencies_exist() + self._validate_unique_task_ids() + self._validate_task_id_format() + self._validate_devices() + TaskSequenceInputContainerValidator(self._experiment_config, self._lab_configs).validate() + TaskSequenceInputParameterValidator(self._experiment_config, self._lab_configs).validate() + + def _validate_tasks_exist(self) -> None: + for task in self._experiment_config.tasks: + if not self._tasks.spec_exists_by_config(task): + raise EosTaskValidationError( + f"Task '{task.id}' in experiment '{self._experiment_config.type}' does not exist." + ) + + def _validate_task_dependencies_exist(self) -> None: + for task in self._experiment_config.tasks: + for task_id in task.dependencies: + if not any(task.id == task_id for task in self._experiment_config.tasks): + raise EosTaskValidationError( + f"Task '{task_id}' in experiment '{self._experiment_config.type}' does not exist." + ) + + def _validate_unique_task_ids(self) -> None: + task_ids = [task.id for task in self._experiment_config.tasks] + if len(task_ids) != len(set(task_ids)): + raise EosTaskValidationError("All task IDs in the task sequence must be unique.") + + def _validate_task_id_format(self) -> None: + for task in self._experiment_config.tasks: + if not self._valid_task_id_pattern.match(task.id): + raise EosTaskValidationError( + f"Task ID '{task.id}' is invalid. Task IDs can only contain letters, numbers, " + f"and underscores, with no spaces." + ) + + def _validate_devices(self) -> None: + experiment_type = self._experiment_config.type + + for task in self._experiment_config.tasks: + task_spec = self._tasks.get_spec_by_config(task) + required_device_types = Counter(task_spec.device_types or []) + provided_device_types = Counter() + used_devices = set() + + for device in task.devices: + lab_id = device.lab_id + device_id = device.id + if device_id in used_devices: + batch_error( + f"Duplicate device '{device_id}' in lab '{lab_id}' requested by task '{task.id}' of experiment " + f"'{experiment_type}' is not allowed.", + EosTaskValidationError, + ) + continue + + lab_config = self._find_lab_by_id(lab_id) + if not lab_config or device_id not in lab_config.devices: + batch_error( + f"Device '{device_id}' in lab '{lab_id}' requested by task '{task.id}' of experiment " + f"{experiment_type} does not exist.", + EosTaskValidationError, + ) + continue + + device_type = lab_config.devices[device_id].type + provided_device_types[device_type] += 1 + used_devices.add(device_id) + + # Check if all required device types are provided + missing_device_types = required_device_types - provided_device_types + if missing_device_types: + missing_counts_str = ", ".join( + [f"\n{count}x '{device_type}'" for device_type, count in missing_device_types.items()] + ) + batch_error( + f"Task '{task.id}' of experiment '{experiment_type}' does not have all required device types " + f"satisfied. Missing device types: {missing_counts_str}", + EosTaskValidationError, + ) + + raise_batched_errors(EosTaskValidationError) + + def _find_lab_by_id(self, lab_id: str) -> LabConfig: + return next((lab for lab in self._lab_configs if lab.type == lab_id), None) diff --git a/eos/configuration/validation/validation_utils.py b/eos/configuration/validation/validation_utils.py new file mode 100644 index 0000000..58d4c12 --- /dev/null +++ b/eos/configuration/validation/validation_utils.py @@ -0,0 +1,33 @@ +from eos.configuration.entities.parameters import ( + AllowedParameterTypes, +) + + +def is_parameter_reference(parameter: AllowedParameterTypes) -> bool: + return ( + isinstance(parameter, str) + and parameter.count(".") == 1 + and all(component.strip() for component in parameter.split(".")) + ) + + +def is_dynamic_parameter(parameter: AllowedParameterTypes) -> bool: + return isinstance(parameter, str) and parameter.lower() == "eos_dynamic" + + +def is_dynamic_container(container_id: str) -> bool: + """ + Check if the container ID is a dynamic container ID (eos_dynamic). + """ + return isinstance(container_id, str) and container_id.lower() == "eos_dynamic" + + +def is_container_reference(container_id: str) -> bool: + """ + Check if the container ID is a reference. + """ + return ( + isinstance(container_id, str) + and container_id.count(".") == 1 + and all(component.strip() for component in container_id.split(".")) + ) diff --git a/eos/containers/__init__.py b/eos/containers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/containers/container_manager.py b/eos/containers/container_manager.py new file mode 100644 index 0000000..b4e990f --- /dev/null +++ b/eos/containers/container_manager.py @@ -0,0 +1,159 @@ +import threading +from collections import defaultdict +from typing import Any + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.containers.entities.container import Container +from eos.containers.exceptions import EosContainerStateError +from eos.containers.repositories.container_repository import ContainerRepository +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager + + +class ContainerManager: + """ + The container manager provides methods for interacting with containers in a lab. + """ + + def __init__(self, configuration_manager: ConfigurationManager, db_manager: DbManager): + self._configuration_manager = configuration_manager + + self._containers = ContainerRepository("containers", db_manager) + self._containers.create_indices([("id", 1)], unique=True) + self._locks = defaultdict(threading.RLock) + + self._create_containers() + log.debug("Container manager initialized.") + + def get_container(self, container_id: str) -> Container: + """ + Get a copy of the container with the specified ID. + """ + container = self._containers.get_one(id=container_id) + + if container: + return Container(**container) + + raise EosContainerStateError(f"Container '{container_id}' does not exist.") + + def get_containers(self, **query: dict[str, Any]) -> list[Container]: + """ + Query containers with arbitrary parameters. + + :param query: Dictionary of query parameters. + """ + containers = self._containers.get_all(**query) + return [Container(**container) for container in containers] + + def set_location(self, container_id: str, location: str) -> None: + """ + Set the location of a container. + """ + with self._get_lock(container_id): + self._containers.update({"location": location}, id=container_id) + + def set_lab(self, container_id: str, lab: str) -> None: + """ + Set the lab of a container. + """ + with self._get_lock(container_id): + self._containers.update({"lab": lab}, id=container_id) + + def set_metadata(self, container_id: str, metadata: dict[str, Any]) -> None: + """ + Set metadata for a container. + """ + with self._get_lock(container_id): + self._containers.update({"metadata": metadata}, id=container_id) + + def add_metadata(self, container_id: str, metadata: dict[str, Any]) -> None: + """ + Add metadata to a container. + """ + container = self.get_container(container_id) + container.metadata.update(metadata) + + with self._get_lock(container_id): + self._containers.update({"metadata": container.metadata}, id=container_id) + + def remove_metadata(self, container_id: str, metadata_keys: list[str]) -> None: + """ + Remove metadata from a container. + """ + container = self.get_container(container_id) + for key in metadata_keys: + container.metadata.pop(key, None) + + with self._get_lock(container_id): + self._containers.update({"metadata": container.metadata}, id=container_id) + + def update_container(self, container: Container) -> None: + """ + Update a container in the database. + """ + self._containers.update(container.model_dump(), id=container.id) + + def update_containers(self, loaded_labs: set[str] | None = None, unloaded_labs: set[str] | None = None) -> None: + """ + Update containers based on loaded and unloaded labs. + """ + if unloaded_labs: + for lab_id in unloaded_labs: + self._remove_containers_for_lab(lab_id) + + if loaded_labs: + for lab_id in loaded_labs: + self._create_containers_for_lab(lab_id) + + log.debug("Containers have been updated.") + + def _remove_containers_for_lab(self, lab_id: str) -> None: + """ + Remove containers associated with an unloaded lab. + """ + containers_to_remove = self.get_containers(lab=lab_id) + for container in containers_to_remove: + self._containers.delete(id=container.id) + log.debug(f"Removed containers for lab '{lab_id}'") + + def _create_containers_for_lab(self, lab_id: str) -> None: + """ + Create containers for a loaded lab. + """ + lab_config = self._configuration_manager.labs[lab_id] + for container_config in lab_config.containers: + for container_id in container_config.ids: + existing_container = self._containers.get_one(id=container_id) + if not existing_container: + container = Container( + id=container_id, + type=container_config.type, + lab=lab_id, + location=container_config.location, + metadata=container_config.metadata, + ) + self._containers.update(container.model_dump(), id=container_id) + log.debug(f"Created containers for lab '{lab_id}'") + + def _create_containers(self) -> None: + """ + Create containers from the lab configuration and add them to the database. + """ + for lab_name, lab_config in self._configuration_manager.labs.items(): + for container_config in lab_config.containers: + for container_id in container_config.ids: + container = Container( + id=container_id, + type=container_config.type, + lab=lab_name, + location=container_config.location, + metadata=container_config.metadata, + ) + self._containers.update(container.model_dump(), id=container_id) + log.debug("Created containers") + + def _get_lock(self, container_id: str) -> threading.RLock: + """ + Get the lock for a specific container. + """ + return self._locks[container_id] diff --git a/eos/containers/entities/__init__.py b/eos/containers/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/containers/entities/container.py b/eos/containers/entities/container.py new file mode 100644 index 0000000..8e30d0d --- /dev/null +++ b/eos/containers/entities/container.py @@ -0,0 +1,16 @@ +from typing import Any + +from pydantic import BaseModel + + +class Container(BaseModel): + id: str + type: str + lab: str + + location: str + + metadata: dict[str, Any] = {} + + class Config: + arbitrary_types_allowed = True diff --git a/eos/containers/exceptions.py b/eos/containers/exceptions.py new file mode 100644 index 0000000..6745a5f --- /dev/null +++ b/eos/containers/exceptions.py @@ -0,0 +1,6 @@ +class EosContainerError(Exception): + pass + + +class EosContainerStateError(EosContainerError): + pass diff --git a/eos/containers/repositories/__init__.py b/eos/containers/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/containers/repositories/container_repository.py b/eos/containers/repositories/container_repository.py new file mode 100644 index 0000000..cf3bb3a --- /dev/null +++ b/eos/containers/repositories/container_repository.py @@ -0,0 +1,5 @@ +from eos.persistence.mongo_repository import MongoRepository + + +class ContainerRepository(MongoRepository): + pass diff --git a/eos/devices/__init__.py b/eos/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/devices/base_device.py b/eos/devices/base_device.py new file mode 100644 index 0000000..ce9f48f --- /dev/null +++ b/eos/devices/base_device.py @@ -0,0 +1,167 @@ +import threading +from abc import ABC, abstractmethod, ABCMeta +from typing import Any + +from eos.devices.exceptions import ( + EosDeviceInitializationError, + EosDeviceCleanupError, + EosDeviceError, +) + + +class DeviceStatus: + DISABLED = "DISABLED" + IDLE = "IDLE" + BUSY = "BUSY" + ERROR = "ERROR" + + +def capture_exceptions(func: callable) -> callable: + def wrapper(self, *args, **kwargs) -> Any: + try: + return func(self, *args, **kwargs) + except Exception as e: + self._status = DeviceStatus.ERROR + raise EosDeviceError(f"Error in {func.__name__} in device {self._device_id}") from e + + return wrapper + + +class DeviceMeta(ABCMeta): + def __new__(cls, name: str, bases: tuple, dct: dict): + cls._add_exception_capture_to_child_methods(bases, dct) + return super().__new__(cls, name, bases, dct) + + @staticmethod + def _add_exception_capture_to_child_methods(bases: tuple, dct: dict) -> None: + base_methods = set() + for base in bases: + if isinstance(base, DeviceMeta): + base_methods.update(base.__dict__.keys()) + + for attr, value in dct.items(): + if callable(value) and not attr.startswith("__") and attr not in base_methods: + dct[attr] = capture_exceptions(value) + + +class BaseDevice(ABC, metaclass=DeviceMeta): + """ + The base class for all devices in EOS. + """ + + def __init__( + self, + device_id: str, + lab_id: str, + device_type: str, + initialization_parameters: dict[str, Any], + ): + self._device_id = device_id + self._lab_id = lab_id + self._device_type = device_type + self._status = DeviceStatus.DISABLED + self._initialization_parameters = initialization_parameters + + self._lock = threading.Lock() + + self.initialize(initialization_parameters) + + def __del__(self): + if "_status" not in self.__dict__: + return + if self._status and self._status != DeviceStatus.DISABLED: + self.cleanup() + + def initialize(self, initialization_parameters: dict[str, Any]) -> None: + """ + Initialize the device. After calling this method, the device is ready to be used for tasks + and the status is IDLE. + """ + with self._lock: + if self._status != DeviceStatus.DISABLED: + raise EosDeviceInitializationError(f"Device {self._device_id} is already initialized.") + + try: + self._initialize(initialization_parameters) + self._status = DeviceStatus.IDLE + except Exception as e: + self._status = DeviceStatus.ERROR + raise EosDeviceInitializationError( + f"Error initializing device {self._device_id}: {e!s}", + ) from e + + def cleanup(self) -> None: + """ + Clean up the device. After calling this method, the device can no longer be used for tasks and the status is + DISABLED. + """ + with self._lock: + if self._status == DeviceStatus.BUSY: + raise EosDeviceCleanupError( + f"Device {self._device_id} is busy. Cannot perform cleanup.", + ) + + try: + self._cleanup() + self._status = DeviceStatus.DISABLED + except Exception as e: + self._status = DeviceStatus.ERROR + raise EosDeviceCleanupError(f"Error cleaning up device {self._device_id}: {e!s}") from e + + def enable(self) -> None: + """ + Enable the device. The status should be IDLE after calling this method. + """ + with self._lock: + if self._status == DeviceStatus.DISABLED: + self.initialize(self._initialization_parameters) + + def disable(self) -> None: + """ + Disable the device. The status should be DISABLED after calling this method. + """ + with self._lock: + if self._status != DeviceStatus.DISABLED: + self.cleanup() + + def report(self) -> dict[str, Any]: + """ + Return a dictionary with any member variables needed for logging purposes and progress tracking. + """ + return self._report() + + def report_status(self) -> dict[str, Any]: + """ + Return a dictionary with the id and status of the task handler. + """ + return { + "id": self._device_id, + "status": self._status, + } + + def get_id(self) -> str: + return self._device_id + + def get_type(self) -> str: + return self._device_type + + def get_status(self) -> str: + return self._status + + @abstractmethod + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + """ + Implementation for the initialization of the device. + """ + + @abstractmethod + def _cleanup(self) -> None: + """ + Implementation for the cleanup of the device. + """ + + @abstractmethod + def _report(self) -> dict[str, Any]: + """ + Implementation for the report method. + """ diff --git a/eos/devices/device_actor_references.py b/eos/devices/device_actor_references.py new file mode 100644 index 0000000..67156fa --- /dev/null +++ b/eos/devices/device_actor_references.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass + +from ray.actor import ActorHandle + +from eos.utils.ray_utils import RayActorWrapper + + +@dataclass(frozen=True) +class DeviceRayActorReference: + id: str + lab_id: str + type: str + actor_handle: ActorHandle + + +@dataclass(frozen=True) +class DeviceRayActorWrapperReference: + id: str + lab_id: str + type: str + ray_actor_wrapper: RayActorWrapper + + +class DeviceRayActorWrapperReferences: + def __init__(self, devices: list[DeviceRayActorReference]): + self._devices_by_lab_and_id: dict[tuple[str, str], DeviceRayActorWrapperReference] = {} + self._devices_by_lab_id: dict[str, list[DeviceRayActorWrapperReference]] = {} + self._devices_by_type: dict[str, list[DeviceRayActorWrapperReference]] = {} + + for device in devices: + device_actor_wrapper_reference = DeviceRayActorWrapperReference( + id=device.id, + lab_id=device.lab_id, + type=device.type, + ray_actor_wrapper=RayActorWrapper(device.actor_handle), + ) + self._devices_by_lab_and_id[(device.lab_id, device.id)] = device_actor_wrapper_reference + + if device.lab_id not in self._devices_by_lab_id: + self._devices_by_lab_id[device.lab_id] = [] + self._devices_by_lab_id[device.lab_id].append(device_actor_wrapper_reference) + + if device.type not in self._devices_by_type: + self._devices_by_type[device.type] = [] + self._devices_by_type[device.type].append(device_actor_wrapper_reference) + + def get(self, lab_id: str, device_id: str) -> RayActorWrapper | None: + device = self._devices_by_lab_and_id.get((lab_id, device_id)) + return device.ray_actor_wrapper if device else None + + def get_all_by_lab_id(self, lab_id: str) -> list[RayActorWrapper]: + devices = self._devices_by_lab_id.get(lab_id, []) + return [device.ray_actor_wrapper for device in devices] + + def get_all_by_type(self, device_type: str) -> list[RayActorWrapper]: + devices = self._devices_by_type.get(device_type, []) + return [device.ray_actor_wrapper for device in devices] diff --git a/eos/devices/device_manager.py b/eos/devices/device_manager.py new file mode 100644 index 0000000..2d0f717 --- /dev/null +++ b/eos/devices/device_manager.py @@ -0,0 +1,198 @@ +from typing import Any + +import ray +from omegaconf import OmegaConf +from ray.actor import ActorHandle + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.configuration.constants import EOS_COMPUTER_NAME +from eos.configuration.plugin_registries.device_plugin_registry import DevicePluginRegistry +from eos.devices.entities.device import Device, DeviceStatus +from eos.devices.exceptions import EosDeviceStateError, EosDeviceInitializationError +from eos.logging.batch_error_logger import batch_error, raise_batched_errors +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.persistence.mongo_repository import MongoRepository + + +class DeviceManager: + """ + Provides methods for interacting with the devices in a lab. + """ + + def __init__(self, configuration_manager: ConfigurationManager, db_manager: DbManager): + self._configuration_manager = configuration_manager + + self._devices = MongoRepository("devices", db_manager) + self._devices.create_indices([("lab_id", 1), ("id", 1)], unique=True) + + self._device_plugin_registry = DevicePluginRegistry() + self._device_actor_handles: dict[str, ActorHandle] = {} + self._device_actor_computer_ips: dict[str, str] = {} + + log.debug("Device manager initialized.") + + def get_device(self, lab_id: str, device_id: str) -> Device | None: + """ + Get a device by its ID. + """ + device = self._devices.get_one(lab_id=lab_id, id=device_id) + if not device: + return None + return Device(**device) + + def get_devices(self, **query: dict[str, Any]) -> list[Device]: + """ + Query devices with arbitrary parameters. + + :param query: Dictionary of query parameters. + """ + devices = self._devices.get_all(**query) + return [Device(**device) for device in devices] + + def set_device_status(self, lab_id: str, device_id: str, status: DeviceStatus) -> None: + """ + Set the status of a device. + """ + if not self._devices.exists(lab_id=lab_id, id=device_id): + raise EosDeviceStateError(f"Device '{device_id}' in lab '{lab_id}' does not exist.") + + self._devices.update({"status": status.value}, lab_id=lab_id, id=device_id) + + def get_device_actor(self, lab_id: str, device_id: str) -> ActorHandle: + """ + Get the actor handle of a device. + """ + actor_id = f"{lab_id}.{device_id}" + if actor_id not in self._device_actor_handles: + raise EosDeviceInitializationError(f"Device actor '{actor_id}' does not exist.") + + return self._device_actor_handles.get(actor_id) + + def update_devices(self, loaded_labs: set[str] | None = None, unloaded_labs: set[str] | None = None) -> None: + if unloaded_labs: + for lab_id in unloaded_labs: + self._remove_devices_for_lab(lab_id) + + if loaded_labs: + for lab_id in loaded_labs: + self._create_devices_for_lab(lab_id) + + self._check_device_actors_healthy() + log.debug("Devices have been updated.") + + def cleanup_device_actors(self) -> None: + for actor in self._device_actor_handles.values(): + ray.kill(actor) + self._device_actor_handles.clear() + self._device_actor_computer_ips.clear() + self._devices.delete() + log.info("All device actors have been cleaned up.") + + def _remove_devices_for_lab(self, lab_id: str) -> None: + devices_to_remove = self.get_devices(lab_id=lab_id) + for device in devices_to_remove: + actor_id = device.get_actor_id() + if actor_id in self._device_actor_handles: + ray.kill(self._device_actor_handles[actor_id]) + del self._device_actor_handles[actor_id] + del self._device_actor_computer_ips[actor_id] + self._devices.delete(lab_id=lab_id) + log.debug(f"Removed devices for lab '{lab_id}'") + + def _create_devices_for_lab(self, lab_id: str) -> None: + lab_config = self._configuration_manager.labs[lab_id] + for device_id, device_config in lab_config.devices.items(): + device = self.get_device(lab_id, device_id) + + if device and device.get_actor_id() in self._device_actor_handles: + continue + + if device and device.actor_handle: + self._restore_device_actor(device) + else: + device = Device( + lab_id=lab_id, + id=device_id, + type=device_config.type, + location=device_config.location, + computer=device_config.computer, + ) + self._devices.update(device.model_dump(), lab_id=lab_id, id=device_id) + self._create_device_actor(device) + + log.debug(f"Created devices for lab '{lab_id}'") + + def _restore_device_actor(self, device: Device) -> None: + """ + Restore a device actor registered in the database by looking up its actor in the Ray cluster. + """ + device_actor_id = device.get_actor_id() + device_config = self._configuration_manager.labs[device.lab_id].devices[device.id] + self._device_actor_handles[device_actor_id] = ray.get_actor(device_actor_id) + self._device_actor_computer_ips[device_actor_id] = ( + self._configuration_manager.labs[device.lab_id].computers[device_config.computer].ip + ) + log.debug(f"Restored device actor {device_actor_id}") + + def _create_device_actor(self, device: Device) -> None: + lab_config = self._configuration_manager.labs[device.lab_id] + device_config = lab_config.devices[device.id] + computer_name = device_config.computer.lower() + + computer_ip = "127.0.0.1" if computer_name == EOS_COMPUTER_NAME else lab_config.computers[computer_name].ip + + device_actor_id = device.get_actor_id() + self._device_actor_computer_ips[device_actor_id] = computer_ip + + spec_initialization_parameters = ( + self._configuration_manager.device_specs.get_spec_by_type(device.type).initialization_parameters or {} + ) + if spec_initialization_parameters: + spec_initialization_parameters = OmegaConf.to_object(spec_initialization_parameters) + + device_config_initialization_parameters = device_config.initialization_parameters or {} + if device_config_initialization_parameters: + device_config_initialization_parameters = OmegaConf.to_object(device_config_initialization_parameters) + + initialization_parameters: dict[str, Any] = { + **spec_initialization_parameters, + **device_config_initialization_parameters, + } + + resources = ( + {"eos-core": 0.0001} if computer_ip in ["localhost", "127.0.0.1"] else {f"node:{computer_ip}": 0.0001} + ) + + device_class = ray.remote(self._device_plugin_registry.get_device_class_type(device.type)) + self._device_actor_handles[device_actor_id] = device_class.options( + name=device_actor_id, + num_cpus=0, + resources=resources, + ).remote(device.id, device.lab_id, device.type, initialization_parameters) + + def _check_device_actors_healthy(self) -> None: + status_reports = [actor_handle.report_status.remote() for actor_handle in self._device_actor_handles.values()] + status_report_to_device_actor_id = { + status_report: device_actor_id + for device_actor_id, status_report in zip(self._device_actor_handles.keys(), status_reports, strict=False) + } + + ready_status_reports, not_ready_status_reports = ray.wait( + status_reports, + num_returns=len(self._device_actor_handles), + timeout=5, + ) + + for not_ready_ref in not_ready_status_reports: + device_actor_id = status_report_to_device_actor_id[not_ready_ref] + actor_handle = self._device_actor_handles[device_actor_id] + computer_ip = self._device_actor_computer_ips[device_actor_id] + + ray.kill(actor_handle) + + batch_error( + f"Device actor '{device_actor_id}' could not be reached on the computer {computer_ip}", + EosDeviceInitializationError, + ) + raise_batched_errors(EosDeviceInitializationError) diff --git a/eos/devices/entities/__init__.py b/eos/devices/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/devices/entities/device.py b/eos/devices/entities/device.py new file mode 100644 index 0000000..409da9c --- /dev/null +++ b/eos/devices/entities/device.py @@ -0,0 +1,32 @@ +from enum import Enum +from typing import Any + +from pydantic import BaseModel, field_serializer, Field +from ray.actor import ActorHandle + + +class DeviceStatus(Enum): + ACTIVE = "ACTIVE" + INACTIVE = "INACTIVE" + + +class Device(BaseModel): + id: str + lab_id: str + type: str + computer: str + location: str | None = None + status: DeviceStatus = DeviceStatus.ACTIVE + metadata: dict[str, Any] = {} + + actor_handle: ActorHandle | None = Field(exclude=True, default=None) + + class Config: + arbitrary_types_allowed = True + + def get_actor_id(self) -> str: + return f"{self.lab_id}.{self.id}" + + @field_serializer("status") + def status_enum_to_string(self, v: DeviceStatus) -> str: + return v.value diff --git a/eos/devices/exceptions.py b/eos/devices/exceptions.py new file mode 100644 index 0000000..8ccff45 --- /dev/null +++ b/eos/devices/exceptions.py @@ -0,0 +1,18 @@ +class EosDeviceError(Exception): + pass + + +class EosDeviceStateError(EosDeviceError): + pass + + +class EosDeviceClassNotFoundError(EosDeviceError): + pass + + +class EosDeviceInitializationError(EosDeviceError): + pass + + +class EosDeviceCleanupError(EosDeviceError): + pass diff --git a/eos/eos.py b/eos/eos.py new file mode 100755 index 0000000..280be90 --- /dev/null +++ b/eos/eos.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import typer + +from eos.cli.orchestrator_cli import start_orchestrator +from eos.cli.pkg_cli import pkg_app +from eos.cli.web_api_cli import start_web_api + +eos_app = typer.Typer(pretty_exceptions_show_locals=False) +eos_app.command(name="orchestrator", help="Start the EOS orchestrator")(start_orchestrator) +eos_app.command(name="api", help="Start the EOS web API")(start_web_api) +eos_app.add_typer(pkg_app, name="pkg", help="Manage EOS packages") + +if __name__ == "__main__": + eos_app() diff --git a/eos/experiments/__init__.py b/eos/experiments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/experiments/entities/__init__.py b/eos/experiments/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/experiments/entities/experiment.py b/eos/experiments/entities/experiment.py new file mode 100644 index 0000000..a73f800 --- /dev/null +++ b/eos/experiments/entities/experiment.py @@ -0,0 +1,48 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import Any + +from pydantic import BaseModel, field_serializer + + +class ExperimentStatus(Enum): + CREATED = "CREATED" + RUNNING = "RUNNING" + COMPLETED = "COMPLETED" + SUSPENDED = "SUSPENDED" + CANCELLED = "CANCELLED" + FAILED = "FAILED" + + +class ExperimentExecutionParameters(BaseModel): + resume: bool = False + + +class Experiment(BaseModel): + id: str + type: str + + execution_parameters: ExperimentExecutionParameters + + status: ExperimentStatus = ExperimentStatus.CREATED + + labs: list[str] = [] + + running_tasks: list[str] = [] + completed_tasks: list[str] = [] + + dynamic_parameters: dict[str, dict[str, Any]] = {} + + metadata: dict[str, Any] = {} + + start_time: datetime | None = None + end_time: datetime | None = None + + created_at: datetime = datetime.now(tz=timezone.utc) + + class Config: + arbitrary_types_allowed = True + + @field_serializer("status") + def status_enum_to_string(self, v: ExperimentStatus) -> str: + return v.value diff --git a/eos/experiments/exceptions.py b/eos/experiments/exceptions.py new file mode 100644 index 0000000..ae4890f --- /dev/null +++ b/eos/experiments/exceptions.py @@ -0,0 +1,18 @@ +class EosExperimentError(Exception): + pass + + +class EosExperimentStateError(EosExperimentError): + pass + + +class EosExperimentTaskExecutionError(EosExperimentError): + pass + + +class EosExperimentExecutionError(EosExperimentError): + pass + + +class EosExperimentCancellationError(EosExperimentError): + pass diff --git a/eos/experiments/experiment_executor.py b/eos/experiments/experiment_executor.py new file mode 100644 index 0000000..2c33050 --- /dev/null +++ b/eos/experiments/experiment_executor.py @@ -0,0 +1,304 @@ +import asyncio +from typing import Any + +from eos.configuration.experiment_graph.experiment_graph import ExperimentGraph +from eos.configuration.validation import validation_utils +from eos.containers.container_manager import ContainerManager +from eos.experiments.entities.experiment import ExperimentStatus, ExperimentExecutionParameters, Experiment +from eos.experiments.exceptions import ( + EosExperimentExecutionError, + EosExperimentTaskExecutionError, + EosExperimentCancellationError, +) +from eos.experiments.experiment_manager import ExperimentManager +from eos.logging.logger import log +from eos.scheduling.abstract_scheduler import AbstractScheduler +from eos.scheduling.entities.scheduled_task import ScheduledTask +from eos.tasks.entities.task import TaskOutput +from eos.tasks.entities.task_execution_parameters import TaskExecutionParameters +from eos.tasks.exceptions import EosTaskExecutionError +from eos.tasks.task_executor import TaskExecutor +from eos.tasks.task_input_resolver import TaskInputResolver +from eos.tasks.task_manager import TaskManager + + +class ExperimentExecutor: + """Responsible for executing all the tasks of a single experiment.""" + + def __init__( + self, + experiment_id: str, + experiment_type: str, + execution_parameters: ExperimentExecutionParameters, + experiment_graph: ExperimentGraph, + experiment_manager: ExperimentManager, + task_manager: TaskManager, + container_manager: ContainerManager, + task_executor: TaskExecutor, + scheduler: AbstractScheduler, + ): + self._experiment_id = experiment_id + self._experiment_type = experiment_type + self._execution_parameters = execution_parameters + self._experiment_graph = experiment_graph + self._experiment_manager = experiment_manager + self._task_manager = task_manager + self._container_manager = container_manager + self._task_executor = task_executor + self._scheduler = scheduler + self._task_input_resolver = TaskInputResolver(task_manager, experiment_manager) + + self._current_task_execution_parameters: dict[str, TaskExecutionParameters] = {} + self._task_output_futures: dict[str, asyncio.Task] = {} + self._experiment_status = None + + def start_experiment( + self, + dynamic_parameters: dict[str, dict[str, Any]] | None = None, + metadata: dict[str, Any] | None = None, + ) -> None: + """ + Start the experiment and register the executor with the scheduler. + """ + experiment = self._experiment_manager.get_experiment(self._experiment_id) + if experiment: + self._handle_existing_experiment(experiment) + else: + self._create_new_experiment(dynamic_parameters, metadata) + + self._scheduler.register_experiment( + experiment_id=self._experiment_id, + experiment_type=self._experiment_type, + experiment_graph=self._experiment_graph, + ) + + self._experiment_manager.start_experiment(self._experiment_id) + self._experiment_status = ExperimentStatus.RUNNING + + log.info(f"{'Resumed' if self._execution_parameters.resume else 'Started'} experiment '{self._experiment_id}'.") + + def _handle_existing_experiment(self, experiment: Experiment) -> None: + """ + Handle cases when the experiment already exists. + """ + self._experiment_status = experiment.status + + if not self._execution_parameters.resume: + def _raise_error(status: str) -> None: + raise EosExperimentExecutionError( + f"Cannot start experiment '{self._experiment_id}' as it already exists and is '{status}'. " + f"Please create a new experiment or re-submit with 'resume=True'." + ) + + status_handlers = { + ExperimentStatus.COMPLETED: lambda: _raise_error("completed"), + ExperimentStatus.SUSPENDED: lambda: _raise_error("suspended"), + ExperimentStatus.CANCELLED: lambda: _raise_error("cancelled"), + ExperimentStatus.FAILED: lambda: _raise_error("failed"), + } + status_handlers.get(self._experiment_status, lambda: None)() + + self._resume_experiment() + + async def cancel_experiment(self) -> None: + """ + Cancel the experiment. + """ + experiment = self._experiment_manager.get_experiment(self._experiment_id) + if not experiment or experiment.status != ExperimentStatus.RUNNING: + raise EosExperimentCancellationError( + f"Cannot cancel experiment '{self._experiment_id}' with status '{experiment.status}'. " + f"It must be running." + ) + + log.warning(f"Cancelling experiment '{self._experiment_id}'...") + self._experiment_status = ExperimentStatus.CANCELLED + self._experiment_manager.cancel_experiment(self._experiment_id) + self._scheduler.unregister_experiment(self._experiment_id) + await self._cancel_running_tasks() + + log.warning(f"Cancelled experiment '{self._experiment_id}'.") + + async def progress_experiment(self) -> bool: + """ + Try to progress the experiment by executing tasks. + + :return: True if the experiment has been completed, False otherwise. + """ + try: + if self._experiment_status != ExperimentStatus.RUNNING: + return self._experiment_status == ExperimentStatus.CANCELLED + + if self._scheduler.is_experiment_completed(self._experiment_id): + self._complete_experiment() + return True + + self._process_completed_tasks() + await self._execute_tasks() + + return False + except Exception as e: + self._fail_experiment() + raise EosExperimentExecutionError(f"Error executing experiment '{self._experiment_id}'") from e + + def _resume_experiment(self) -> None: + """ + Resume an existing experiment. + """ + self._experiment_manager.delete_non_completed_tasks(self._experiment_id) + log.info(f"Experiment '{self._experiment_id}' resumed.") + + def _create_new_experiment(self, dynamic_parameters: dict[str, dict[str, Any]], metadata: dict[str, Any]) -> None: + """ + Create a new experiment with the given parameters. + """ + dynamic_parameters = dynamic_parameters or {} + self._validate_dynamic_parameters(dynamic_parameters) + self._experiment_manager.create_experiment( + experiment_id=self._experiment_id, + experiment_type=self._experiment_type, + execution_parameters=self._execution_parameters, + dynamic_parameters=dynamic_parameters, + metadata=metadata, + ) + + async def _cancel_running_tasks(self) -> None: + """ + Cancel all running tasks in the experiment. + """ + cancellation_futures = [ + self._task_executor.request_task_cancellation(params.experiment_id, params.task_config.id) + for params in self._current_task_execution_parameters.values() + ] + try: + await asyncio.wait_for(asyncio.gather(*cancellation_futures), timeout=30) + except asyncio.TimeoutError as e: + raise EosExperimentExecutionError( + f"Timeout while cancelling experiment {self._experiment_id}. Some tasks may not have been cancelled." + ) from e + + def _complete_experiment(self) -> None: + """ + Complete the experiment and clean up. + """ + self._scheduler.unregister_experiment(self._experiment_id) + self._experiment_manager.complete_experiment(self._experiment_id) + self._experiment_status = ExperimentStatus.COMPLETED + + def _fail_experiment(self) -> None: + """ + Fail the experiment. + """ + self._scheduler.unregister_experiment(self._experiment_id) + self._experiment_manager.fail_experiment(self._experiment_id) + self._experiment_status = ExperimentStatus.FAILED + + def _process_completed_tasks(self) -> None: + """ + Process the output of completed tasks. + """ + completed_tasks = [task_id for task_id, future in self._task_output_futures.items() if future.done()] + for task_id in completed_tasks: + self._process_task_output(task_id) + + def _process_task_output(self, task_id: str) -> None: + """ + Process the output of a single completed task. + """ + try: + result = self._task_output_futures[task_id].result() + if result: + output_parameters, output_containers, output_files = result + self._update_containers(output_containers) + self._add_task_output(task_id, output_parameters, output_containers, output_files) + self._task_manager.complete_task(self._experiment_id, task_id) + log.info(f"EXP '{self._experiment_id}' - Completed task '{task_id}'.") + except EosTaskExecutionError as e: + raise EosExperimentTaskExecutionError( + f"Error executing task '{task_id}' of experiment '{self._experiment_id}'" + ) from e + finally: + del self._task_output_futures[task_id] + del self._current_task_execution_parameters[task_id] + + def _update_containers(self, output_containers: dict[str, Any]) -> None: + """ + Update containers with task output. + """ + for container in output_containers.values(): + self._container_manager.update_container(container) + + def _add_task_output( + self, + task_id: str, + output_parameters: dict[str, Any], + output_containers: dict[str, Any], + output_files: dict[str, Any], + ) -> None: + """ + Add task output to the task manager. + """ + task_output = TaskOutput( + experiment_id=self._experiment_id, + task_id=task_id, + parameters=output_parameters, + containers=output_containers, + file_names=list(output_files.keys()), + ) + for file_name, file_data in output_files.items(): + self._task_manager.add_task_output_file(self._experiment_id, task_id, file_name, file_data) + self._task_manager.add_task_output(self._experiment_id, task_id, task_output) + + async def _execute_tasks(self) -> None: + """ + Request and execute new tasks from the scheduler. + """ + new_scheduled_tasks = await self._scheduler.request_tasks(self._experiment_id) + for scheduled_task in new_scheduled_tasks: + if scheduled_task.id not in self._current_task_execution_parameters: + await self._execute_task(scheduled_task) + + async def _execute_task(self, scheduled_task: ScheduledTask) -> None: + """ + Execute a single task. + """ + task_config = self._experiment_graph.get_task_config(scheduled_task.id) + task_config = self._task_input_resolver.resolve_task_inputs(self._experiment_id, task_config) + task_execution_parameters = TaskExecutionParameters( + task_id=scheduled_task.id, + experiment_id=self._experiment_id, + devices=scheduled_task.devices, + task_config=task_config, + ) + self._task_output_futures[scheduled_task.id] = asyncio.create_task( + self._task_executor.request_task_execution(task_execution_parameters, scheduled_task) + ) + self._current_task_execution_parameters[scheduled_task.id] = task_execution_parameters + + def _validate_dynamic_parameters(self, dynamic_parameters: dict[str, dict[str, Any]]) -> None: + """ + Validate that all required dynamic parameters are provided and there are no surplus parameters. + """ + required_params = self._get_required_dynamic_parameters() + provided_params = { + f"{task_id}.{param_name}" for task_id, params in dynamic_parameters.items() for param_name in params + } + + missing_params = required_params - provided_params + unexpected_params = provided_params - required_params + + if missing_params: + raise EosExperimentExecutionError(f"Missing values for dynamic parameters: {missing_params}") + if unexpected_params: + raise EosExperimentExecutionError(f"Unexpected dynamic parameters provided: {unexpected_params}") + + def _get_required_dynamic_parameters(self) -> set[str]: + """ + Get a set of all required dynamic parameters in the experiment graph. + """ + return { + f"{task_id}.{param_name}" + for task_id in self._experiment_graph.get_tasks() + for param_name, param_value in self._experiment_graph.get_task_config(task_id).parameters.items() + if validation_utils.is_dynamic_parameter(param_value) + } diff --git a/eos/experiments/experiment_executor_factory.py b/eos/experiments/experiment_executor_factory.py new file mode 100644 index 0000000..c2f3f37 --- /dev/null +++ b/eos/experiments/experiment_executor_factory.py @@ -0,0 +1,49 @@ +from eos.configuration.configuration_manager import ConfigurationManager +from eos.configuration.experiment_graph.experiment_graph import ExperimentGraph +from eos.containers.container_manager import ContainerManager +from eos.experiments.entities.experiment import ExperimentExecutionParameters +from eos.experiments.experiment_executor import ExperimentExecutor +from eos.experiments.experiment_manager import ExperimentManager +from eos.scheduling.abstract_scheduler import AbstractScheduler +from eos.tasks.task_executor import TaskExecutor +from eos.tasks.task_manager import TaskManager + + +class ExperimentExecutorFactory: + """ + Factory class to create ExperimentExecutor instances. + """ + + def __init__( + self, + configuration_manager: ConfigurationManager, + experiment_manager: ExperimentManager, + task_manager: TaskManager, + container_manager: ContainerManager, + task_executor: TaskExecutor, + scheduler: AbstractScheduler, + ): + self._configuration_manager = configuration_manager + self._experiment_manager = experiment_manager + self._task_manager = task_manager + self._container_manager = container_manager + self._task_executor = task_executor + self._scheduler = scheduler + + def create( + self, experiment_id: str, experiment_type: str, execution_parameters: ExperimentExecutionParameters + ) -> ExperimentExecutor: + experiment_config = self._configuration_manager.experiments.get(experiment_type) + experiment_graph = ExperimentGraph(experiment_config) + + return ExperimentExecutor( + experiment_id=experiment_id, + experiment_type=experiment_type, + execution_parameters=execution_parameters, + experiment_graph=experiment_graph, + experiment_manager=self._experiment_manager, + task_manager=self._task_manager, + container_manager=self._container_manager, + task_executor=self._task_executor, + scheduler=self._scheduler, + ) diff --git a/eos/experiments/experiment_manager.py b/eos/experiments/experiment_manager.py new file mode 100644 index 0000000..8e2b5d1 --- /dev/null +++ b/eos/experiments/experiment_manager.py @@ -0,0 +1,174 @@ +from datetime import datetime, timezone +from typing import Any + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.experiments.entities.experiment import Experiment, ExperimentStatus, ExperimentExecutionParameters +from eos.experiments.exceptions import EosExperimentStateError +from eos.experiments.repositories.experiment_repository import ExperimentRepository +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.tasks.entities.task import TaskStatus +from eos.tasks.repositories.task_repository import TaskRepository + + +class ExperimentManager: + """ + Responsible for managing the state of all experiments in EOS and tracking their execution. + """ + + def __init__(self, configuration_manager: ConfigurationManager, db_manager: DbManager): + self._configuration_manager = configuration_manager + self._experiments = ExperimentRepository("experiments", db_manager) + self._experiments.create_indices([("id", 1)], unique=True) + self._tasks = TaskRepository("tasks", db_manager) + + log.debug("Experiment manager initialized.") + + def create_experiment( + self, + experiment_id: str, + experiment_type: str, + execution_parameters: ExperimentExecutionParameters | None = None, + dynamic_parameters: dict[str, dict[str, Any]] | None = None, + metadata: dict[str, Any] | None = None, + ) -> None: + """ + Create a new experiment of a given type with a unique id. + + :param experiment_id: A unique id for the experiment. + :param experiment_type: The type of the experiment as defined in the configuration. + :param dynamic_parameters: Dictionary of the dynamic parameters per task and their provided values. + :param execution_parameters: Parameters for the execution of the experiment. + :param metadata: Additional metadata to be stored with the experiment. + """ + if self._experiments.get_one(id=experiment_id): + raise EosExperimentStateError(f"Experiment '{experiment_id}' already exists.") + + experiment_config = self._configuration_manager.experiments.get(experiment_type) + if not experiment_config: + raise EosExperimentStateError(f"Experiment type '{experiment_type}' not found in the configuration.") + + labs = experiment_config.labs + + experiment = Experiment( + id=experiment_id, + type=experiment_type, + execution_parameters=execution_parameters or ExperimentExecutionParameters(), + labs=labs, + dynamic_parameters=dynamic_parameters or {}, + metadata=metadata or {}, + ) + self._experiments.create(experiment.model_dump()) + + log.info(f"Created experiment '{experiment_id}'.") + + def delete_experiment(self, experiment_id: str) -> None: + """ + Delete an experiment. + """ + if not self._experiments.exists(id=experiment_id): + raise EosExperimentStateError(f"Experiment '{experiment_id}' does not exist.") + + self._experiments.delete(id=experiment_id) + self._tasks.delete(experiment_id=experiment_id) + + log.info(f"Deleted experiment '{experiment_id}'.") + + def start_experiment(self, experiment_id: str) -> None: + """ + Start an experiment. + """ + self._set_experiment_status(experiment_id, ExperimentStatus.RUNNING) + + def complete_experiment(self, experiment_id: str) -> None: + """ + Complete an experiment. + """ + self._set_experiment_status(experiment_id, ExperimentStatus.COMPLETED) + + def cancel_experiment(self, experiment_id: str) -> None: + """ + Cancel an experiment. + """ + self._set_experiment_status(experiment_id, ExperimentStatus.CANCELLED) + + def suspend_experiment(self, experiment_id: str) -> None: + """ + Suspend an experiment. + """ + self._set_experiment_status(experiment_id, ExperimentStatus.SUSPENDED) + + def fail_experiment(self, experiment_id: str) -> None: + """ + Fail an experiment. + """ + self._set_experiment_status(experiment_id, ExperimentStatus.FAILED) + + def get_experiment(self, experiment_id: str) -> Experiment | None: + """ + Get an experiment. + """ + experiment = self._experiments.get_one(id=experiment_id) + return Experiment(**experiment) if experiment else None + + def get_experiments(self, **query: dict[str, Any]) -> list[Experiment]: + """ + Get experiments with a custom query. + + :param query: Dictionary of query parameters. + """ + experiments = self._experiments.get_all(**query) + return [Experiment(**experiment) for experiment in experiments] + + def get_lab_experiments(self, lab: str) -> list[Experiment]: + """ + Get all experiments associated with a lab. + """ + experiments = self._experiments.get_experiments_by_lab(lab) + return [Experiment(**experiment) for experiment in experiments] + + def get_running_tasks(self, experiment_id: str | None) -> set[str]: + """ + Get the list of currently running tasks constrained by experiment ID. + """ + experiment = self._experiments.get_one(id=experiment_id) + return set(experiment.get("running_tasks", {})) if experiment else {} + + def get_completed_tasks(self, experiment_id: str) -> set[str]: + """ + Get the list of completed tasks constrained by experiment ID. + """ + experiment = self._experiments.get_one(id=experiment_id) + return set(experiment.get("completed_tasks", {})) if experiment else {} + + def delete_non_completed_tasks(self, experiment_id: str) -> None: + """ + Delete all tasks that are not completed in the given experiment. + """ + experiment = self.get_experiment(experiment_id) + + for task_id in experiment.running_tasks: + self._tasks.delete(experiment_id=experiment_id, id=task_id) + self._experiments.clear_running_tasks(experiment_id) + + self._tasks.delete(experiment_id=experiment_id, status=TaskStatus.FAILED.value) + self._tasks.delete(experiment_id=experiment_id, status=TaskStatus.CANCELLED.value) + + def _set_experiment_status(self, experiment_id: str, new_status: ExperimentStatus) -> None: + """ + Set the status of an experiment. + """ + if not self._experiments.exists(id=experiment_id): + raise EosExperimentStateError(f"Experiment '{experiment_id}' does not exist.") + + update_fields = {"status": new_status.value} + if new_status == ExperimentStatus.RUNNING: + update_fields["start_time"] = datetime.now(tz=timezone.utc) + elif new_status in [ + ExperimentStatus.COMPLETED, + ExperimentStatus.CANCELLED, + ExperimentStatus.FAILED, + ]: + update_fields["end_time"] = datetime.now(tz=timezone.utc) + + self._experiments.update(update_fields, id=experiment_id) diff --git a/eos/experiments/repositories/__init__.py b/eos/experiments/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/experiments/repositories/experiment_repository.py b/eos/experiments/repositories/experiment_repository.py new file mode 100644 index 0000000..d925d52 --- /dev/null +++ b/eos/experiments/repositories/experiment_repository.py @@ -0,0 +1,45 @@ +from eos.experiments.entities.experiment import ExperimentStatus +from eos.persistence.mongo_repository import MongoRepository + + +class ExperimentRepository(MongoRepository): + def get_experiments_by_lab(self, lab_type: str) -> list[dict]: + return self._collection.find({"labs": {"$in": [lab_type]}}) + + def add_running_task(self, experiment_id: str, task_id: str) -> None: + self._collection.update_one( + {"id": experiment_id}, + {"$addToSet": {"running_tasks": task_id}}, + ) + + def delete_running_task(self, experiment_id: str, task_id: str) -> None: + self._collection.update_one( + {"id": experiment_id}, + {"$pull": {"running_tasks": task_id}}, + ) + + def clear_running_tasks(self, experiment_id: str) -> None: + self._collection.update_one( + {"id": experiment_id}, + {"$set": {"running_tasks": []}}, + ) + + def move_task_queue(self, experiment_id: str, task_id: str, source: str, target: str) -> None: + self._collection.update_one( + {"id": experiment_id}, + {"$pull": {source: task_id}, "$addToSet": {target: task_id}}, + ) + + def get_experiment_ids_by_campaign(self, campaign_id: str, status: ExperimentStatus | None = None) -> list[str]: + """ + Get all experiment IDs of a campaign with an optional status filter. + + :param campaign_id: The ID of the campaign. + :param status: Optional status to filter experiments. + :return: A list of experiment IDs. + """ + query = {"id": {"$regex": f"^{campaign_id}"}} + if status: + query["status"] = status.value + + return [doc["id"] for doc in self._collection.find(query, {"id": 1})] diff --git a/eos/logging/__init__.py b/eos/logging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/logging/batch_error_logger.py b/eos/logging/batch_error_logger.py new file mode 100644 index 0000000..c418520 --- /dev/null +++ b/eos/logging/batch_error_logger.py @@ -0,0 +1,30 @@ +class BatchErrorLogger: + """ + The BatchErrorLogger class is used to batch-log errors together. Instead of printing + errors as they occur, they are stored in a list and can be printed all at once. + """ + + def __init__(self): + self.errors: list[tuple[str, type[Exception]]] = [] + + def batch_error(self, message: str, exception_type: type[Exception]) -> None: + self.errors.append((message, exception_type)) + + def raise_batched_errors(self, root_exception_type: type[Exception] = Exception) -> None: + if self.errors: + error_messages = "\n\n".join( + f"{message} ({exception_type.__name__})" for message, exception_type in self.errors + ) + self.errors.clear() + raise root_exception_type(error_messages) + + +def batch_error(message: str, exception_type: type[Exception]) -> None: + batch_logger.batch_error(message, exception_type) + + +def raise_batched_errors(root_exception_type: type[Exception] = Exception) -> None: + batch_logger.raise_batched_errors(root_exception_type) + + +batch_logger = BatchErrorLogger() diff --git a/eos/logging/logger.py b/eos/logging/logger.py new file mode 100644 index 0000000..fc8398e --- /dev/null +++ b/eos/logging/logger.py @@ -0,0 +1,48 @@ +import logging +from enum import Enum + +from eos.logging.rich_console_handler import RichConsoleHandler + + +class LogLevel(Enum): + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + + +class Logger: + """ + The Logger class is used to log all kinds of messages in EOS. It provides a simple interface + for logging messages at different levels. + """ + + def __init__(self): + self.logger = logging.getLogger("rich") + self.logger.name = "eos" + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(RichConsoleHandler()) + + def set_level(self, level: LogLevel | str) -> None: + if isinstance(level, str): + level = LogLevel(level) + self.logger.setLevel(level.value) + + def debug(self, message: str, *args, **kwargs) -> None: + stacklevel = kwargs.pop("stacklevel", 2) + self.logger.debug(message, *args, **kwargs, stacklevel=stacklevel) + + def info(self, message: str, *args, **kwargs) -> None: + stacklevel = kwargs.pop("stacklevel", 2) + self.logger.info(message, *args, **kwargs, stacklevel=stacklevel) + + def warning(self, message: str, *args, **kwargs) -> None: + stacklevel = kwargs.pop("stacklevel", 2) + self.logger.warning(message, *args, **kwargs, stacklevel=stacklevel) + + def error(self, message: str, *args, **kwargs) -> None: + stacklevel = kwargs.pop("stacklevel", 2) + self.logger.error(message, *args, **kwargs, stacklevel=stacklevel) + + +log = Logger() diff --git a/eos/logging/rich_console_handler.py b/eos/logging/rich_console_handler.py new file mode 100644 index 0000000..555d737 --- /dev/null +++ b/eos/logging/rich_console_handler.py @@ -0,0 +1,32 @@ +from datetime import datetime, timezone +from logging import Handler, LogRecord +from pathlib import Path +from typing import ClassVar + +from rich.console import Console + + +class RichConsoleHandler(Handler): + """ + A logging handler that uses the Rich library to print logs to the console. + """ + + _LOG_COLORS: ClassVar = { + "DEBUG": "[cyan]", + "INFO": "[green]", + "WARNING": "[yellow]", + "ERROR": "[bold red]", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.console = Console() + + def emit(self, record: LogRecord) -> None: + time = datetime.now(tz=timezone.utc).strftime("%m/%d/%Y %H:%M:%S") + level = record.levelname + filename = Path(record.pathname).name + line_no = record.lineno + + log_prefix = f"{self._LOG_COLORS.get(level, '[white]')}{level}[/] {time} {filename}:{line_no} -" + self.console.print(f"{log_prefix} {record.getMessage()}") diff --git a/eos/monitoring/__init__.py b/eos/monitoring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/monitoring/graceful_termination_monitor.py b/eos/monitoring/graceful_termination_monitor.py new file mode 100644 index 0000000..549ece0 --- /dev/null +++ b/eos/monitoring/graceful_termination_monitor.py @@ -0,0 +1,34 @@ +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.persistence.mongo_repository import MongoRepository +from eos.utils.singleton import Singleton + + +class GracefulTerminationMonitor(metaclass=Singleton): + """ + The graceful termination monitor is responsible for tracking whether EOS has been terminated gracefully. + """ + + def __init__(self, db_manager: DbManager): + self._globals = MongoRepository("globals", db_manager) + self._globals.create_indices([("key", 1)], unique=True) + + graceful_termination = self._globals.get_one(key="graceful_termination") + if not graceful_termination: + self._globals.create({"key": "graceful_termination", "terminated_gracefully": False}) + self._terminated_gracefully = False + else: + self._terminated_gracefully = graceful_termination["terminated_gracefully"] + if not self._terminated_gracefully: + log.warning("EOS did not terminate gracefully!") + + def previously_terminated_gracefully(self) -> bool: + return self._terminated_gracefully + + def terminated_gracefully(self) -> None: + self._set_terminated_gracefully(True) + log.debug("EOS terminated gracefully.") + + def _set_terminated_gracefully(self, value: bool) -> None: + self._terminated_gracefully = value + self._globals.update({"terminated_gracefully": value}, key="graceful_termination") diff --git a/eos/optimization/__init__.py b/eos/optimization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/optimization/abstract_sequential_optimizer.py b/eos/optimization/abstract_sequential_optimizer.py new file mode 100644 index 0000000..d85870a --- /dev/null +++ b/eos/optimization/abstract_sequential_optimizer.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod + +import pandas as pd + + +class AbstractSequentialOptimizer(ABC): + """ + Abstract interface for a sequential optimizer. + At a minimum, the optimizer should give new parameters to clients, receive results from clients, and + report the best parameters found so far. + """ + + @abstractmethod + def sample(self, num_experiments: int = 1) -> pd.DataFrame: + """ + Ask the optimizer for new experimental parameters. The experimental parameters are provided as a DataFrame, + with one row per experiment and one column per dynamic parameter in flat format (task_name/param_name). + + :param num_experiments: The number of experiments for which to request new parameters. + """ + + @abstractmethod + def report(self, inputs_df: pd.DataFrame, outputs_df: pd.DataFrame) -> None: + """ + Report the results of experiments to the optimizer. + + :param inputs_df: A DataFrame with the input parameters for the experiments. + :param outputs_df: A DataFrame with the output parameters for the experiments. + """ + + @abstractmethod + def get_optimal_solutions(self) -> pd.DataFrame: + """ + Get the set of best outputs found so far and the parameters that produced them. + This is the Pareto front. + + :return: A dataframe with the best parameters and outputs found so far. + """ + + @abstractmethod + def get_input_names(self) -> list[str]: + """ + Get the names of the input parameters. + + :return: A list of the names of the input parameters. + """ + + @abstractmethod + def get_output_names(self) -> list[str]: + """ + Get the names of the output values. + + :return: A list of the names of the output parameter values. + """ diff --git a/eos/optimization/exceptions.py b/eos/optimization/exceptions.py new file mode 100644 index 0000000..0edf105 --- /dev/null +++ b/eos/optimization/exceptions.py @@ -0,0 +1,2 @@ +class EosCampaignOptimizerDomainError(Exception): + pass diff --git a/eos/optimization/sequential_bayesian_optimizer.py b/eos/optimization/sequential_bayesian_optimizer.py new file mode 100644 index 0000000..85afc20 --- /dev/null +++ b/eos/optimization/sequential_bayesian_optimizer.py @@ -0,0 +1,164 @@ +import bofire.strategies.api as strategies +import pandas as pd +from bofire.data_models.acquisition_functions.acquisition_function import ( + AcquisitionFunction, +) +from bofire.data_models.constraints.constraint import Constraint +from bofire.data_models.domain.constraints import Constraints +from bofire.data_models.domain.domain import Domain +from bofire.data_models.domain.features import Inputs, Outputs +from bofire.data_models.enum import SamplingMethodEnum +from bofire.data_models.features.categorical import CategoricalInput, CategoricalOutput +from bofire.data_models.features.continuous import ContinuousInput, ContinuousOutput +from bofire.data_models.features.discrete import DiscreteInput +from bofire.data_models.objectives.identity import MaximizeObjective, MinimizeObjective +from bofire.data_models.objectives.target import CloseToTargetObjective +from bofire.data_models.strategies.predictives.mobo import MoboStrategy +from bofire.data_models.strategies.predictives.sobo import SoboStrategy +from pandas import Series + +from eos.optimization.exceptions import EosCampaignOptimizerDomainError +from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + +class BayesianSequentialOptimizer(AbstractSequentialOptimizer): + """ + Uses BoFire's Bayesian optimization to optimize the parameters of a series of experiments. + """ + + InputType = ContinuousInput | DiscreteInput | CategoricalInput + OutputType = ContinuousOutput | CategoricalOutput + + def __init__( + self, + inputs: list[InputType], + outputs: list[OutputType], + constraints: list[Constraint], + acquisition_function: AcquisitionFunction, + num_initial_samples: int, + initial_sampling_method: SamplingMethodEnum = SamplingMethodEnum.SOBOL, + ): + self._acquisition_function: AcquisitionFunction = acquisition_function + self._num_initial_samples: int = num_initial_samples + self._initial_sampling_method: SamplingMethodEnum = initial_sampling_method + self._domain: Domain = Domain( + inputs=Inputs(features=inputs), + outputs=Outputs(features=outputs), + constraints=Constraints(constraints=constraints), + ) + self._input_names = [input_feature.key for input_feature in self._domain.inputs.features] + self._output_names = [output_feature.key for output_feature in self._domain.outputs.features] + + self._generate_initial_samples: bool = self._num_initial_samples > 0 + self._initial_samples_df: pd.DataFrame | None = None + self._results_reported: int = 0 + + self._optimizer_data_model = ( + SoboStrategy(domain=self._domain, acquisition_function=acquisition_function) + if len(outputs) == 1 + else MoboStrategy(domain=self._domain, acquisition_function=acquisition_function) + ) + self._optimizer = strategies.map(data_model=self._optimizer_data_model) + + def sample(self, num_experiments: int = 1) -> pd.DataFrame: + if self._generate_initial_samples and self._results_reported < self._num_initial_samples: + if self._initial_samples_df is None: + self._generate_initial_samples_df() + + if self._initial_samples_df is not None and not self._initial_samples_df.empty: + return self._fetch_and_remove_initial_samples(num_experiments) + + self._initial_samples_df = None + + new_parameters_df = self._optimizer.ask(candidate_count=num_experiments) + + return new_parameters_df[self._input_names] + + def _generate_initial_samples_df(self) -> None: + self._initial_samples_df = self._domain.inputs.sample( + n=self._num_initial_samples, method=self._initial_sampling_method + ) + + def _fetch_and_remove_initial_samples(self, num_experiments: int) -> pd.DataFrame: + num_experiments = min(num_experiments, len(self._initial_samples_df)) + new_parameters_df = self._initial_samples_df.iloc[:num_experiments] + self._initial_samples_df = self._initial_samples_df.iloc[num_experiments:] + return new_parameters_df + + def report(self, inputs_df: pd.DataFrame, outputs_df: pd.DataFrame) -> None: + self._validate_sample(inputs_df, outputs_df) + results_df = pd.concat([inputs_df, outputs_df], axis=1) + self._optimizer.tell(results_df) + self._results_reported += len(results_df) + + def get_optimal_solutions(self) -> pd.DataFrame: + experiments = self._optimizer.experiments + outputs = self._domain.outputs.get_by_objective( + includes=[MaximizeObjective, MinimizeObjective, CloseToTargetObjective] + ).features + + def is_dominated(exp: Series, other_exp: Series) -> bool: + at_least_one_worse = False + for output in outputs: + if isinstance(output.objective, MaximizeObjective): + if exp[output.key] > other_exp[output.key]: + return False + if exp[output.key] < other_exp[output.key]: + at_least_one_worse = True + elif isinstance(output.objective, MinimizeObjective): + if exp[output.key] < other_exp[output.key]: + return False + if exp[output.key] > other_exp[output.key]: + at_least_one_worse = True + elif isinstance(output.objective, CloseToTargetObjective): + target = output.objective.target + if abs(exp[output.key] - target) < abs(other_exp[output.key] - target): + return False + if abs(exp[output.key] - target) > abs(other_exp[output.key] - target): + at_least_one_worse = True + return at_least_one_worse + + pareto_solutions = [ + exp + for i, exp in experiments.iterrows() + if not any(is_dominated(exp, other_exp) for j, other_exp in experiments.iterrows() if i != j) + ] + + result_df = pd.DataFrame(pareto_solutions) + + # 'valid_' columns are generated by BoFire + filtered_columns = [col for col in result_df.columns if not col.startswith("valid_")] + + return result_df[filtered_columns] + + def get_input_names(self) -> list[str]: + return self._input_names + + def get_output_names(self) -> list[str]: + return self._output_names + + def _get_output(self, output_name: str) -> OutputType: + for output in self._domain.outputs.features: + if output.key == output_name: + return output + + raise EosCampaignOptimizerDomainError(f"Output {output_name} not found in the optimization domain.") + + def _validate_sample(self, inputs_df: pd.DataFrame, outputs_df: pd.DataFrame) -> None: + """ + Validate that all expected input and output columns are present in their respective DataFrames. + + :param inputs_df: DataFrame with input parameters for the experiments. + :param outputs_df: DataFrame with output parameters for the experiments. + :raises EosCampaignOptimizerDomainError: If any expected input or output columns are missing. + """ + missing_inputs = set(self._input_names) - set(inputs_df.columns) + missing_outputs = set(self._output_names) - set(outputs_df.columns) + + if missing_inputs or missing_outputs: + error_message = [] + if missing_inputs: + error_message.append(f"Missing input columns: {', '.join(missing_inputs)}") + if missing_outputs: + error_message.append(f"Missing output columns: {', '.join(missing_outputs)}") + raise EosCampaignOptimizerDomainError(". ".join(error_message)) diff --git a/eos/optimization/sequential_optimizer_actor.py b/eos/optimization/sequential_optimizer_actor.py new file mode 100644 index 0000000..f97f964 --- /dev/null +++ b/eos/optimization/sequential_optimizer_actor.py @@ -0,0 +1,27 @@ +from typing import Any + +import pandas as pd +import ray + +from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + +@ray.remote +class SequentialOptimizerActor(AbstractSequentialOptimizer): + def __init__(self, constructor_args: dict[str, Any], optimizer_type: type[AbstractSequentialOptimizer]): + self.optimizer = optimizer_type(**constructor_args) + + def sample(self, num_experiments: int = 1) -> pd.DataFrame: + return self.optimizer.sample(num_experiments) + + def report(self, input_df: pd.DataFrame, output_df: pd.DataFrame) -> None: + self.optimizer.report(input_df, output_df) + + def get_optimal_solutions(self) -> pd.DataFrame: + return self.optimizer.get_optimal_solutions() + + def get_input_names(self) -> list[str]: + return self.optimizer.get_input_names() + + def get_output_names(self) -> list[str]: + return self.optimizer.get_output_names() diff --git a/eos/orchestration/__init__.py b/eos/orchestration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/orchestration/exceptions.py b/eos/orchestration/exceptions.py new file mode 100644 index 0000000..7841fe4 --- /dev/null +++ b/eos/orchestration/exceptions.py @@ -0,0 +1,18 @@ +class EosExperimentTypeInUseError(Exception): + pass + + +class EosFailedExperimentRecoveryError(Exception): + pass + + +class EosFailedCampaignRecoveryError(Exception): + pass + + +class EosExperimentDoesNotExistError(Exception): + pass + + +class EosError(Exception): + pass diff --git a/eos/orchestration/orchestrator.py b/eos/orchestration/orchestrator.py new file mode 100644 index 0000000..91969fc --- /dev/null +++ b/eos/orchestration/orchestrator.py @@ -0,0 +1,721 @@ +import asyncio +import atexit +import traceback +from asyncio import Lock as AsyncLock +from collections.abc import AsyncIterable +from typing import Any, TYPE_CHECKING + +import ray + +from eos.campaigns.campaign_executor_factory import CampaignExecutorFactory +from eos.campaigns.campaign_manager import CampaignManager +from eos.campaigns.campaign_optimizer_manager import CampaignOptimizerManager +from eos.campaigns.entities.campaign import CampaignStatus, CampaignExecutionParameters, Campaign +from eos.campaigns.exceptions import EosCampaignExecutionError +from eos.configuration.configuration_manager import ConfigurationManager +from eos.configuration.entities.lab import LabDeviceConfig +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.configuration.exceptions import EosConfigurationError +from eos.configuration.validation import validation_utils +from eos.containers.container_manager import ContainerManager +from eos.devices.device_manager import DeviceManager +from eos.experiments.entities.experiment import ExperimentStatus, Experiment, ExperimentExecutionParameters +from eos.experiments.exceptions import EosExperimentExecutionError +from eos.experiments.experiment_executor_factory import ExperimentExecutorFactory +from eos.experiments.experiment_manager import ExperimentManager +from eos.logging.logger import log +from eos.monitoring.graceful_termination_monitor import GracefulTerminationMonitor +from eos.orchestration.exceptions import ( + EosExperimentTypeInUseError, + EosExperimentDoesNotExistError, + EosError, +) +from eos.persistence.db_manager import DbManager +from eos.persistence.file_db_manager import FileDbManager +from eos.persistence.service_credentials import ServiceCredentials +from eos.resource_allocation.resource_allocation_manager import ( + ResourceAllocationManager, +) +from eos.scheduling.basic_scheduler import BasicScheduler +from eos.tasks.entities.task import Task, TaskStatus +from eos.tasks.on_demand_task_executor import OnDemandTaskExecutor +from eos.tasks.task_executor import TaskExecutor +from eos.tasks.task_manager import TaskManager +from eos.utils.singleton import Singleton + +if TYPE_CHECKING: + from eos.campaigns.campaign_executor import CampaignExecutor + from eos.experiments.experiment_executor import ExperimentExecutor + + +class Orchestrator(metaclass=Singleton): + """ + The top-level orchestrator that initializes and manages all EOS components. + """ + + def __init__( + self, + user_dir: str, + db_credentials: ServiceCredentials, + file_db_credentials: ServiceCredentials, + ): + self._user_dir = user_dir + self._db_credentials = db_credentials + self._file_db_credentials = file_db_credentials + self._initialized = False + + self.initialize() + atexit.register(self.terminate) + + def initialize(self) -> None: + """ + Prepare the orchestrator. This is required before any other operations can be performed. + """ + if self._initialized: + return + + log.info("Initializing EOS...") + log.info("Initializing Ray cluster...") + ray.init(namespace="eos", resources={"eos-core": 1000}) + log.info("Ray initialized.") + + # Configuration ########################################### + self._configuration_manager = ConfigurationManager(self._user_dir) + + # Persistence ############################################# + self._db_manager = DbManager(self._db_credentials) + self._file_db_manager = FileDbManager(self._file_db_credentials) + + # Monitoring ############################################## + self._graceful_termination_monitor = GracefulTerminationMonitor(self._db_manager) + + # State management ######################################## + self._device_manager = DeviceManager(self._configuration_manager, self._db_manager) + self._container_manager = ContainerManager(self._configuration_manager, self._db_manager) + self._resource_allocation_manager = ResourceAllocationManager(self._configuration_manager, self._db_manager) + self._task_manager = TaskManager(self._configuration_manager, self._db_manager, self._file_db_manager) + self._experiment_manager = ExperimentManager(self._configuration_manager, self._db_manager) + self._campaign_manager = CampaignManager(self._configuration_manager, self._db_manager) + self._campaign_optimizer_manager = CampaignOptimizerManager(self._db_manager) + + # Execution ############################################### + self._task_executor = TaskExecutor( + self._task_manager, + self._device_manager, + self._container_manager, + self._resource_allocation_manager, + self._configuration_manager, + ) + self._on_demand_task_executor = OnDemandTaskExecutor( + self._task_executor, self._task_manager, self._container_manager + ) + self._scheduler = BasicScheduler( + self._configuration_manager, + self._experiment_manager, + self._task_manager, + self._device_manager, + self._resource_allocation_manager, + ) + self._experiment_executor_factory = ExperimentExecutorFactory( + self._configuration_manager, + self._experiment_manager, + self._task_manager, + self._container_manager, + self._task_executor, + self._scheduler, + ) + self._campaign_executor_factory = CampaignExecutorFactory( + self._configuration_manager, + self._campaign_manager, + self._campaign_optimizer_manager, + self._task_manager, + self._experiment_executor_factory, + ) + + self._campaign_submission_lock = AsyncLock() + self._submitted_campaigns: dict[str, CampaignExecutor] = {} + self._experiment_submission_lock = AsyncLock() + self._submitted_experiments: dict[str, ExperimentExecutor] = {} + + self._campaign_cancellation_queue = asyncio.Queue(maxsize=100) + self._experiment_cancellation_queue = asyncio.Queue(maxsize=100) + + self._loading_lock = AsyncLock() + + self._fail_all_running_work() + + self._initialized = True + + def _fail_all_running_work(self) -> None: + """ + When the orchestrator starts, fail all running tasks, experiments, and campaigns. + This is for safety, as if the orchestrator was terminated while there was running work then the state of the + system may be unknown. We want to force manual review of the state of the system and explicitly require + re-submission of any work that was running. + """ + running_tasks = self._task_manager.get_tasks(status=TaskStatus.RUNNING.value) + for task in running_tasks: + self._task_manager.fail_task(task.experiment_id, task.id) + log.warning(f"EXP '{task.experiment_id}' - Failed task '{task.id}'.") + + running_experiments = self._experiment_manager.get_experiments(status=ExperimentStatus.RUNNING.value) + for experiment in running_experiments: + self._experiment_manager.fail_experiment(experiment.id) + + running_campaigns = self._campaign_manager.get_campaigns(status=CampaignStatus.RUNNING.value) + for campaign in running_campaigns: + self._campaign_manager.fail_campaign(campaign.id) + + if running_tasks: + log.warning("All running tasks have been marked as failed. Please review the state of the system.") + + if running_experiments: + log.warning( + "All running experiments have been marked as failed. Please review the state of the system and " + "re-submit with resume=True." + ) + + if running_campaigns: + log.warning( + "All running campaigns have been marked as failed. Please review the state of the system and re-submit " + "with resume=True." + ) + + def terminate(self) -> None: + """ + Terminate the orchestrator. After this, no other operations can be performed. + This should be called before the program exits. + """ + if not self._initialized: + return + log.info("Cleaning up device actors...") + self._device_manager.cleanup_device_actors() + log.info("Shutting down Ray cluster...") + ray.shutdown() + self._graceful_termination_monitor.terminated_gracefully() + self._initialized = False + + def load_labs(self, labs: set[str]) -> None: + """ + Load one or more labs into the orchestrator. + """ + self._configuration_manager.load_labs(labs) + self._device_manager.update_devices(loaded_labs=labs) + self._container_manager.update_containers(loaded_labs=labs) + + def unload_labs(self, labs: set[str]) -> None: + """ + Unload one or more labs from the orchestrator. + """ + self._configuration_manager.unload_labs(labs) + self._device_manager.update_devices(unloaded_labs=labs) + self._container_manager.update_containers(unloaded_labs=labs) + + async def reload_labs(self, lab_types: set[str]) -> None: + """ + Reload one or more labs in the orchestrator. + """ + async with self._loading_lock: + experiments_to_reload = set() + for lab_type in lab_types: + existing_experiments = self._experiment_manager.get_experiments(status=ExperimentStatus.RUNNING.value) + + for experiment in existing_experiments: + experiment_config = self._configuration_manager.experiments[experiment.type] + if lab_type in experiment_config.labs: + raise EosExperimentTypeInUseError( + f"Cannot reload lab type '{lab_type}' as there are running experiments that use it." + ) + + # Determine experiments to reload for this lab type + for experiment_type, experiment_config in self._configuration_manager.experiments.items(): + if lab_type in experiment_config.labs: + experiments_to_reload.add(experiment_type) + try: + self.unload_labs(lab_types) + self.load_labs(lab_types) + self.load_experiments(experiments_to_reload) + except EosConfigurationError: + log.error(f"Error reloading labs: {traceback.format_exc()}") + raise + + async def update_loaded_labs(self, lab_types: set[str]) -> None: + """ + Update the loaded labs with new configurations. + """ + async with self._loading_lock: + currently_loaded = set(self._configuration_manager.labs.keys()) + + if currently_loaded == lab_types: + return + + to_unload = currently_loaded - lab_types + to_load = lab_types - currently_loaded + + for lab_type in to_unload: + existing_experiments = self._experiment_manager.get_experiments(status=ExperimentStatus.RUNNING.value) + + for experiment in existing_experiments: + experiment_config = self._configuration_manager.experiments[experiment.type] + if lab_type in experiment_config.labs: + raise EosExperimentTypeInUseError( + f"Cannot unload lab type '{lab_type}' as there are running experiments that use it." + ) + + try: + self.unload_labs(to_unload) + self.load_labs(to_load) + except EosConfigurationError: + log.error(f"Error updating loaded labs: {traceback.format_exc()}") + raise + + async def get_lab_loaded_statuses(self) -> dict[str, bool]: + """ + Return a dictionary of lab types and a boolean indicating whether they are loaded. + """ + return self._configuration_manager.get_lab_loaded_statuses() + + def load_experiments(self, experiment_types: set[str]) -> None: + """ + Load one or more experiments into the orchestrator. + """ + self._configuration_manager.load_experiments(experiment_types) + + def unload_experiments(self, experiment_types: set[str]) -> None: + """ + Unload one or more experiments from the orchestrator. + """ + self._configuration_manager.unload_experiments(experiment_types) + + async def reload_experiments(self, experiment_types: set[str]) -> None: + """ + Reload one or more experiments in the orchestrator. + """ + async with self._loading_lock: + for experiment_type in experiment_types: + existing_experiments = self._experiment_manager.get_experiments( + status=ExperimentStatus.RUNNING.value, type=experiment_type + ) + if existing_experiments: + raise EosExperimentTypeInUseError( + f"Cannot reload experiment type '{experiment_type}' as there are running experiments of this " + f"type." + ) + try: + self.unload_experiments(experiment_types) + self.load_experiments(experiment_types) + except EosConfigurationError: + log.error(f"Error reloading experiments: {traceback.format_exc()}") + raise + + async def update_loaded_experiments(self, experiment_types: set[str]) -> None: + """ + Update the loaded experiments with new configurations. + """ + async with self._loading_lock: + currently_loaded = set(self._configuration_manager.experiments.keys()) + + if currently_loaded == experiment_types: + return + + to_unload = currently_loaded - experiment_types + to_load = experiment_types - currently_loaded + + for experiment_type in to_unload: + existing_experiments = self._experiment_manager.get_experiments( + status=ExperimentStatus.RUNNING.value, type=experiment_type + ) + if existing_experiments: + raise EosExperimentTypeInUseError( + f"Cannot unload experiment type '{experiment_type}' as there are running experiments of this " + f"type." + ) + + try: + self.unload_experiments(to_unload) + self.load_experiments(to_load) + except EosConfigurationError: + log.error(f"Error updating loaded experiments: {traceback.format_exc()}") + raise + + async def get_experiment_loaded_statuses(self) -> dict[str, bool]: + """ + Return a dictionary of experiment types and a boolean indicating whether they are loaded. + """ + return self._configuration_manager.get_experiment_loaded_statuses() + + async def get_lab_devices( + self, lab_types: set[str] | None = None, task_type: str | None = None + ) -> dict[str, dict[str, LabDeviceConfig]]: + """ + Get the devices that are available in the given labs or for a specific task type. + + :param lab_types: The lab types. If None, all labs will be considered. + :param task_type: The task type. If provided, only devices supporting this task type will be returned. + :return: A dictionary of lab types and the devices available in each lab. + """ + lab_devices = {} + + if not lab_types or not any(lab_type.strip() for lab_type in lab_types): + lab_types = set(self._configuration_manager.labs.keys()) + + task_device_types = set() + if task_type: + task_spec = self._configuration_manager.task_specs.get_spec_by_type(task_type) + task_device_types = set(task_spec.device_types) if task_spec.device_types else set() + + for lab_type in lab_types: + lab = self._configuration_manager.labs.get(lab_type) + if not lab: + continue + + if task_device_types: + devices = {name: device for name, device in lab.devices.items() if device.type in task_device_types} + else: + devices = lab.devices + + if devices: + lab_devices[lab_type] = devices + + return lab_devices + + async def get_task(self, experiment_id: str, task_id: str) -> Task: + """ + Get a task by its unique identifier. + + :param experiment_id: The unique identifier of the experiment. + :param task_id: The unique identifier of the task. + :return: The task entity. + """ + return self._task_manager.get_task(experiment_id, task_id) + + async def submit_task( + self, + task_config: TaskConfig, + resource_allocation_priority: int = 1, + resource_allocation_timeout: int = 180, + ) -> None: + """ + Submit a new task for execution. By default, tasks submitted in this way have maximum resource allocation + priority and a timeout of 180 seconds. + + :param task_config: The task configuration. This is the same data as defined in an experiment configuration. + :param resource_allocation_priority: The priority of the task in acquiring resources. + :param resource_allocation_timeout: The maximum seconds to wait for resources to be allocated before raising an + error. + :return: The output of the task. + """ + await self._on_demand_task_executor.submit_task( + task_config, resource_allocation_priority, resource_allocation_timeout + ) + + async def cancel_task(self, task_id: str, experiment_id: str = "on_demand") -> None: + """ + Cancel a task that is currently being executed. + + :param task_id: The unique identifier of the task. + :param experiment_id: The unique identifier of the experiment. + """ + if experiment_id == "on_demand": + await self._on_demand_task_executor.cancel_task(task_id) + else: + await self._task_executor.request_task_cancellation(experiment_id, task_id) + + async def get_task_types(self) -> list[str]: + """ + Get a list of all task types that are defined in the configuration. + """ + return [task.type for task in self._configuration_manager.task_specs.get_all_specs().values()] + + async def get_task_spec(self, task_type: str) -> TaskSpecification | None: + """ + Get the task specification for a given task type. + """ + task_spec = self._configuration_manager.task_specs.get_spec_by_type(task_type) + if not task_spec: + raise EosError(f"Task type '{task_type}' does not exist.") + + return task_spec + + def stream_task_output_file( + self, experiment_id: str, task_id: str, file_name: str, chunk_size: int = 3 * 1024 * 1024 + ) -> AsyncIterable[bytes]: + """ + Stream the contents of a task output file in chunks. + """ + return self._task_manager.stream_task_output_file(experiment_id, task_id, file_name, chunk_size) + + async def list_task_output_files(self, experiment_id: str, task_id: str) -> list[str]: + """ + Get a list of all output files for a given task. + """ + return self._task_manager.list_task_output_files(experiment_id, task_id) + + async def get_experiment(self, experiment_id: str) -> Experiment | None: + """ + Get an experiment by its unique identifier. + + :param experiment_id: The unique identifier of the experiment. + :return: The experiment entity. + """ + return self._experiment_manager.get_experiment(experiment_id) + + async def submit_experiment( + self, + experiment_id: str, + experiment_type: str, + execution_parameters: ExperimentExecutionParameters, + dynamic_parameters: dict[str, dict[str, Any]], + metadata: dict[str, Any] | None = None, + ) -> None: + """ + Submit a new experiment for execution. The experiment will be executed asynchronously. + + :param experiment_id: The unique identifier of the experiment. + :param experiment_type: The type of the experiment. Must have a configuration defined in the + configuration manager. + :param execution_parameters: The execution parameters for the experiment. + :param dynamic_parameters: The dynamic parameters for the experiment. + :param metadata: Any additional metadata. + """ + self._validate_experiment_type_exists(experiment_type) + + async with self._experiment_submission_lock: + if experiment_id in self._submitted_experiments: + log.warning(f"Experiment '{experiment_id}' is already submitted. Ignoring new submission.") + return + + experiment_executor = self._experiment_executor_factory.create( + experiment_id, experiment_type, execution_parameters + ) + + try: + experiment_executor.start_experiment(dynamic_parameters, metadata) + self._submitted_experiments[experiment_id] = experiment_executor + except EosExperimentExecutionError: + log.error(f"Failed to submit experiment '{experiment_id}': {traceback.format_exc()}") + del self._submitted_experiments[experiment_id] + return + + log.info(f"Submitted experiment '{experiment_id}'.") + + async def cancel_experiment(self, experiment_id: str) -> None: + """ + Cancel an experiment that is currently being executed. + + :param experiment_id: The unique identifier of the experiment. + """ + if experiment_id in self._submitted_experiments: + await self._experiment_cancellation_queue.put(experiment_id) + + async def get_experiment_types(self) -> list[str]: + """ + Get a list of all experiment types that are defined in the configuration. + """ + return list(self._configuration_manager.experiments.keys()) + + async def get_experiment_dynamic_params_template(self, experiment_type: str) -> dict[str, Any]: + """ + Get the dynamic parameters template for a given experiment type. + + :param experiment_type: The type of the experiment. + :return: The dynamic parameter template. + """ + experiment_config = self._configuration_manager.experiments[experiment_type] + dynamic_parameters = {} + + for task in experiment_config.tasks: + task_dynamic_parameters = {} + for parameter_name, parameter_value in task.parameters.items(): + if validation_utils.is_dynamic_parameter(parameter_value): + task_dynamic_parameters[parameter_name] = "PLACEHOLDER" + if task_dynamic_parameters: + dynamic_parameters[task.id] = task_dynamic_parameters + + return dynamic_parameters + + async def get_campaign(self, campaign_id: str) -> Campaign | None: + """ + Get a campaign by its unique identifier. + + :param campaign_id: The unique identifier of the campaign. + :return: The campaign entity. + """ + return self._campaign_manager.get_campaign(campaign_id) + + async def submit_campaign( + self, + campaign_id: str, + experiment_type: str, + campaign_execution_parameters: CampaignExecutionParameters, + ) -> None: + self._validate_experiment_type_exists(experiment_type) + + async with self._campaign_submission_lock: + if campaign_id in self._submitted_campaigns: + log.warning(f"Campaign '{campaign_id}' is already submitted. Ignoring new submission.") + return + + campaign_executor = self._campaign_executor_factory.create( + campaign_id, experiment_type, campaign_execution_parameters + ) + + try: + await campaign_executor.start_campaign() + self._submitted_campaigns[campaign_id] = campaign_executor + except EosCampaignExecutionError: + log.error(f"Failed to submit campaign '{campaign_id}': {traceback.format_exc()}") + del self._submitted_campaigns[campaign_id] + return + + log.info(f"Submitted campaign '{campaign_id}'.") + + async def cancel_campaign(self, campaign_id: str) -> None: + """ + Cancel a campaign that is currently being executed. + + :param campaign_id: The unique identifier of the campaign. + """ + if campaign_id in self._submitted_campaigns: + await self._campaign_cancellation_queue.put(campaign_id) + + async def spin(self, rate_hz: int = 10) -> None: + """ + Spin the orchestrator at a given rate in Hz. + + :param rate_hz: The processing rate in Hz. This is the rate in which the orchestrator will check for progress in + submitted experiments and campaigns. + """ + while True: + await self._process_experiment_and_campaign_cancellations() + + await asyncio.gather( + self._process_on_demand_tasks(), + self._process_experiments(), + self._process_campaigns(), + ) + self._resource_allocation_manager.process_active_requests() + + await asyncio.sleep(1 / rate_hz) + + async def _process_experiment_and_campaign_cancellations(self) -> None: + while not self._experiment_cancellation_queue.empty(): + experiment_id = await self._experiment_cancellation_queue.get() + + log.warning(f"Attempting to cancel experiment '{experiment_id}'.") + try: + await self._submitted_experiments[experiment_id].cancel_experiment() + finally: + del self._submitted_experiments[experiment_id] + log.warning(f"Cancelled experiment '{experiment_id}'.") + + while not self._campaign_cancellation_queue.empty(): + campaign_id = await self._campaign_cancellation_queue.get() + + log.warning(f"Attempting to cancel campaign '{campaign_id}'.") + try: + await self._submitted_campaigns[campaign_id].cancel_campaign() + finally: + self._submitted_campaigns[campaign_id].cleanup() + del self._submitted_campaigns[campaign_id] + log.warning(f"Cancelled campaign '{campaign_id}'.") + + async def _process_experiments(self) -> None: + to_remove_completed = [] + to_remove_failed = [] + + for experiment_id, experiment_executor in self._submitted_experiments.items(): + try: + completed = await experiment_executor.progress_experiment() + + if completed: + to_remove_completed.append(experiment_id) + except EosExperimentExecutionError: + log.error(f"Error in experiment '{experiment_id}': {traceback.format_exc()}") + to_remove_failed.append(experiment_id) + + for experiment_id in to_remove_completed: + log.info(f"Completed experiment '{experiment_id}'.") + del self._submitted_experiments[experiment_id] + + for experiment_id in to_remove_failed: + log.error(f"Failed experiment '{experiment_id}'.") + del self._submitted_experiments[experiment_id] + + async def _process_campaigns(self) -> None: + async def process_single_campaign(campaign_id: str, campaign_executor) -> tuple[str, bool, bool]: + try: + completed = await campaign_executor.progress_campaign() + return campaign_id, completed, False + except EosCampaignExecutionError: + log.error(f"Error in campaign '{campaign_id}': {traceback.format_exc()}") + return campaign_id, False, True + + results = await asyncio.gather( + *(process_single_campaign(cid, executor) for cid, executor in self._submitted_campaigns.items()), + ) + + to_remove_completed: list[str] = [] + to_remove_failed: list[str] = [] + + for campaign_id, completed, failed in results: + if completed: + to_remove_completed.append(campaign_id) + elif failed: + to_remove_failed.append(campaign_id) + + for campaign_id in to_remove_completed: + log.info(f"Completed campaign '{campaign_id}'.") + self._submitted_campaigns[campaign_id].cleanup() + del self._submitted_campaigns[campaign_id] + + for campaign_id in to_remove_failed: + log.error(f"Failed campaign '{campaign_id}'.") + self._submitted_campaigns[campaign_id].cleanup() + del self._submitted_campaigns[campaign_id] + + async def _process_on_demand_tasks(self) -> None: + await self._on_demand_task_executor.process_tasks() + + def _validate_experiment_type_exists(self, experiment_type: str) -> None: + if experiment_type not in self._configuration_manager.experiments: + raise EosExperimentDoesNotExistError( + f"Cannot submit experiment of type '{experiment_type}' as it does not exist." + ) + + @property + def configuration_manager(self) -> ConfigurationManager: + return self._configuration_manager + + @property + def db_manager(self) -> DbManager: + return self._db_manager + + @property + def device_manager(self) -> DeviceManager: + return self._device_manager + + @property + def container_manager(self) -> ContainerManager: + return self._container_manager + + @property + def resource_allocation_manager(self) -> ResourceAllocationManager: + return self._resource_allocation_manager + + @property + def task_manager(self) -> TaskManager: + return self._task_manager + + @property + def experiment_manager(self) -> ExperimentManager: + return self._experiment_manager + + @property + def campaign_manager(self) -> CampaignManager: + return self._campaign_manager + + @property + def task_executor(self) -> TaskExecutor: + return self._task_executor diff --git a/eos/persistence/__init__.py b/eos/persistence/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/persistence/abstract_repository.py b/eos/persistence/abstract_repository.py new file mode 100644 index 0000000..c9ffa7f --- /dev/null +++ b/eos/persistence/abstract_repository.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod + + +class AbstractRepository(ABC): + """ + Abstract class for a repository that provides CRUD operations for a collection of entities. + """ + + @abstractmethod + def create(self, entity: dict) -> None: + pass + + @abstractmethod + def count(self, **query: dict) -> int: + pass + + @abstractmethod + def exists(self, count: int = 1, **query: dict) -> bool: + pass + + @abstractmethod + def get_one(self, **query: dict) -> dict: + pass + + @abstractmethod + def get_all(self, **query: dict) -> list[dict]: + pass + + @abstractmethod + def update(self, entity_id: str, entity: dict) -> None: + pass + + @abstractmethod + def delete(self, entity_id: str) -> None: + pass diff --git a/eos/persistence/db_manager.py b/eos/persistence/db_manager.py new file mode 100644 index 0000000..f2dbbd4 --- /dev/null +++ b/eos/persistence/db_manager.py @@ -0,0 +1,55 @@ +from pymongo import MongoClient +from pymongo.client_session import ClientSession +from pymongo.database import Database + +from eos.logging.logger import log +from eos.persistence.service_credentials import ServiceCredentials + + +class DbManager: + """ + Responsible for giving access to a MongoDB database. + """ + + def __init__( + self, + db_credentials: ServiceCredentials, + db_name: str = "eos", + ): + self._db_credentials = db_credentials + + self._db_client = MongoClient( + host=self._db_credentials.host, + port=self._db_credentials.port, + username=self._db_credentials.username, + password=self._db_credentials.password, + serverSelectionTimeoutMS=10000, + ) + + self._db: Database = self._db_client[db_name] + + log.debug(f"Db manager initialized with database '{db_name}'.") + + def get_db(self) -> Database: + """Get the database.""" + return self._db + + def create_collection_index(self, collection: str, index: list[tuple[str, int]], unique: bool = False) -> None: + """ + Create an index for a collection in the database if it doesn't already exist. + :param collection: The collection name. + :param index: The index to create. A list of tuples of the field names and index orders. + :param unique: Whether the index should be unique. + """ + index_name = "_".join(f"{field}_{order}" for field, order in index) + if index_name not in self._db[collection].index_information(): + self._db[collection].create_index(index, unique=unique, name=index_name) + + def start_session(self) -> ClientSession: + """Start a new client session.""" + return self._db_client.start_session() + + def clean_db(self) -> None: + """Clean the database.""" + for collection in self._db.list_collection_names(): + self._db[collection].drop() diff --git a/eos/persistence/exceptions.py b/eos/persistence/exceptions.py new file mode 100644 index 0000000..3b43bc6 --- /dev/null +++ b/eos/persistence/exceptions.py @@ -0,0 +1,2 @@ +class EosFileDbError(Exception): + pass diff --git a/eos/persistence/file_db_manager.py b/eos/persistence/file_db_manager.py new file mode 100644 index 0000000..f848aab --- /dev/null +++ b/eos/persistence/file_db_manager.py @@ -0,0 +1,91 @@ +import io +from collections.abc import AsyncIterable + +from minio import Minio, S3Error + +from eos.logging.logger import log +from eos.persistence.exceptions import EosFileDbError +from eos.persistence.service_credentials import ServiceCredentials + + +class FileDbManager: + """ + Responsible for storing and retrieving files from a MinIO server. + """ + + def __init__(self, file_db_credentials: ServiceCredentials, bucket_name: str = "eos"): + endpoint = f"{file_db_credentials.host}:{file_db_credentials.port}" + + self._client = Minio( + endpoint, + access_key=file_db_credentials.username, + secret_key=file_db_credentials.password, + secure=False, + ) + self._bucket_name = bucket_name + + if not self._client.bucket_exists(self._bucket_name): + self._client.make_bucket(self._bucket_name) + + log.debug("File database manager initialized.") + + def store_file(self, path: str, file_data: bytes) -> None: + """ + Store a file at the specified path. + """ + try: + self._client.put_object(self._bucket_name, path, io.BytesIO(file_data), len(file_data)) + log.debug(f"File at path '{path}' uploaded successfully.") + except S3Error as e: + raise EosFileDbError(f"Error uploading file at path '{path}': {e!s}") from e + + def delete_file(self, path: str) -> None: + """ + Delete a file at the specified path. + """ + try: + self._client.remove_object(self._bucket_name, path) + log.debug(f"File at path '{path}' deleted successfully.") + except S3Error as e: + raise EosFileDbError(f"Error deleting file at path '{path}': {e!s}") from e + + def get_file(self, path: str) -> bytes: + """ + Retrieve an entire file at the specified path. + """ + response = None + try: + response = self._client.get_object(self._bucket_name, path) + return response.read() + except S3Error as e: + raise EosFileDbError(f"Error retrieving file at path '{path}': {e!s}") from e + finally: + if response: + response.close() + response.release_conn() + + async def stream_file(self, path: str, chunk_size: int = 3 * 1024 * 1024) -> AsyncIterable[bytes]: + """ + Stream a file at the specified path. More memory efficient than get_file. + """ + response = None + try: + response = self._client.get_object(self._bucket_name, path) + while True: + data = response.read(chunk_size) + if not data: + break + yield data + except S3Error as e: + raise EosFileDbError(f"Error streaming file at path '{path}': {e!s}") from e + finally: + if response: + response.close() + response.release_conn() + + def list_files(self, prefix: str = "") -> list[str]: + """ + List files with the specified prefix. + """ + objects = self._client.list_objects(self._bucket_name, prefix=prefix, recursive=True) + return [obj.object_name for obj in objects] diff --git a/eos/persistence/mongo_repository.py b/eos/persistence/mongo_repository.py new file mode 100644 index 0000000..e2b0bcc --- /dev/null +++ b/eos/persistence/mongo_repository.py @@ -0,0 +1,91 @@ +from typing import Any + +from pymongo.results import DeleteResult, UpdateResult, InsertOneResult + +from eos.persistence.abstract_repository import AbstractRepository +from eos.persistence.db_manager import DbManager + + +class MongoRepository(AbstractRepository): + """ + Provides CRUD operations for a MongoDB collection. + """ + + def __init__(self, collection_name: str, db_manager: DbManager): + self._collection = db_manager.get_db().get_collection(collection_name) + + def create_indices(self, indices: list[tuple[str, int]], unique: bool = False) -> None: + """ + Create indices on the collection. + + :param indices: List of tuples of field names and order (1 for ascending, -1 for descending). + :param unique: Whether the index should be unique. + """ + index_name = "_".join(f"{field}_{order}" for field, order in indices) + if index_name not in self._collection.index_information(): + self._collection.create_index(indices, unique=unique, name=index_name) + + def create(self, entity: dict[str, Any]) -> InsertOneResult: + """ + Create a new entity in the collection. + + :param entity: The entity to create. + :return: The result of the insert operation. + """ + return self._collection.insert_one(entity) + + def count(self, **kwargs) -> int: + """ + Count the number of entities that match the query in the collection. + + :param kwargs: Query parameters. + :return: The number of entities. + """ + return self._collection.count_documents(kwargs) + + def exists(self, count: int = 1, **kwargs) -> bool: + """ + Check if the number of entities that match the query exist in the collection. + + :param count: The number of entities to check for. + :param kwargs: Query parameters. + :return: Whether the entity exists. + """ + return self.count(**kwargs) >= count + + def get_one(self, **kwargs) -> dict[str, Any]: + """ + Get a single entity from the collection. + + :param kwargs: Query parameters. + :return: The entity as a dictionary. + """ + return self._collection.find_one(kwargs) + + def get_all(self, **kwargs) -> list[dict[str, Any]]: + """ + Get all entities from the collection. + + :param kwargs: Query parameters. + :return: List of entities as dictionaries. + """ + return list(self._collection.find(kwargs)) + + def update(self, entity: dict[str, Any], **kwargs) -> UpdateResult: + """ + Update an entity in the collection. + + :param entity: The updated entity (or some of its fields). + :param kwargs: Query parameters. + :return: The result of the update operation. + """ + return self._collection.update_one(kwargs, {"$set": entity}, upsert=True) + + def delete(self, **kwargs) -> DeleteResult: + """ + Delete entities from the collection. + + :param kwargs: Query parameters. + :return: The result of the delete operation. + """ + return self._collection.delete_many(kwargs) diff --git a/eos/persistence/service_credentials.py b/eos/persistence/service_credentials.py new file mode 100644 index 0000000..97c0687 --- /dev/null +++ b/eos/persistence/service_credentials.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class ServiceCredentials: + host: str + port: int + username: str + password: str diff --git a/eos/resource_allocation/__init__.py b/eos/resource_allocation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/resource_allocation/container_allocation_manager.py b/eos/resource_allocation/container_allocation_manager.py new file mode 100644 index 0000000..fd0645a --- /dev/null +++ b/eos/resource_allocation/container_allocation_manager.py @@ -0,0 +1,121 @@ +from typing import Any + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.persistence.mongo_repository import MongoRepository +from eos.resource_allocation.entities.container_allocation import ( + ContainerAllocation, +) +from eos.resource_allocation.exceptions import ( + EosContainerAllocatedError, + EosContainerNotFoundError, +) + + +class ContainerAllocationManager: + """ + Responsible for allocating containers to "owners". + An owner may be an experiment task, a human, etc. A container can only be held by one owner at a time. + """ + + def __init__( + self, + configuration_manager: ConfigurationManager, + db_manager: DbManager, + ): + self._configuration_manager = configuration_manager + self._allocations = MongoRepository("container_allocations", db_manager) + self._allocations.create_indices([("id", 1)], unique=True) + + log.debug("Container allocator initialized.") + + def allocate(self, container_id: str, owner: str, experiment_id: str | None = None) -> None: + """ + Allocate a container to an owner. + """ + if self.is_allocated(container_id): + raise EosContainerAllocatedError(f"Container '{container_id}' is already allocated.") + + container_config = self._get_container_config(container_id) + allocation = ContainerAllocation( + id=container_id, + owner=owner, + container_type=container_config["type"], + lab=container_config["lab"], + experiment_id=experiment_id, + ) + self._allocations.create(allocation.model_dump()) + + def deallocate(self, container_id: str) -> None: + """ + Deallocate a container. + """ + result = self._allocations.delete(id=container_id) + if result.deleted_count == 0: + log.warning(f"Container '{container_id}' is not allocated. No action taken.") + else: + log.debug(f"Deallocated container '{container_id}'.") + + def is_allocated(self, container_id: str) -> bool: + """ + Check if a container is allocated. + """ + self._get_container_config(container_id) + return self._allocations.get_one(id=container_id) is not None + + def get_allocation(self, container_id: str) -> ContainerAllocation | None: + """ + Get the allocation details of a container. + """ + self._get_container_config(container_id) + allocation = self._allocations.get_one(id=container_id) + return ContainerAllocation(**allocation) if allocation else None + + def get_allocations(self, **query: dict[str, Any]) -> list[ContainerAllocation]: + """ + Query allocations with arbitrary parameters. + """ + allocations = self._allocations.get_all(**query) + return [ContainerAllocation(**allocation) for allocation in allocations] + + def get_all_unallocated(self) -> list[str]: + """ + Get all unallocated containers. + """ + allocated_containers = [allocation.id for allocation in self.get_allocations()] + all_containers = [ + container_id + for lab_config in self._configuration_manager.labs.values() + for container_config in lab_config.containers + for container_id in container_config.ids + ] + return list(set(all_containers) - set(allocated_containers)) + + def deallocate_all(self) -> None: + """ + Deallocate all containers. + """ + result = self._allocations.delete() + log.debug(f"Deallocated all {result.deleted_count} containers.") + + def deallocate_all_by_owner(self, owner: str) -> None: + """ + Deallocate all containers allocated to an owner. + """ + result = self._allocations.delete(owner=owner) + if result.deleted_count == 0: + log.warning(f"Owner '{owner}' has no containers allocated. No action taken.") + else: + log.debug(f"Deallocated {result.deleted_count} containers for owner '{owner}'.") + + def _get_container_config(self, container_id: str) -> dict: + for lab_config in self._configuration_manager.labs.values(): + for container_config in lab_config.containers: + if container_id in container_config.ids: + return { + "type": container_config.type, + "lab": lab_config.type, + } + + raise EosContainerNotFoundError(f"Container '{container_id}' not found in the configuration.") diff --git a/eos/resource_allocation/device_allocation_manager.py b/eos/resource_allocation/device_allocation_manager.py new file mode 100644 index 0000000..3e6c75c --- /dev/null +++ b/eos/resource_allocation/device_allocation_manager.py @@ -0,0 +1,118 @@ +from typing import Any + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.persistence.mongo_repository import MongoRepository +from eos.resource_allocation.entities.device_allocation import ( + DeviceAllocation, +) +from eos.resource_allocation.exceptions import ( + EosDeviceAllocatedError, + EosDeviceNotFoundError, +) + + +class DeviceAllocationManager: + """ + Responsible for allocating devices to "owners". + An owner may be an experiment task, a human, etc. A device can only be held by one owner at a time. + """ + + def __init__( + self, + configuration_manager: ConfigurationManager, + db_manager: DbManager, + ): + self._configuration_manager = configuration_manager + self._allocations = MongoRepository("device_allocations", db_manager) + self._allocations.create_indices([("lab_id", 1), ("id", 1)], unique=True) + + log.debug("Device allocator initialized.") + + def allocate(self, lab_id: str, device_id: str, owner: str, experiment_id: str | None = None) -> None: + """ + Allocate a device to an owner. + """ + if self.is_allocated(lab_id, device_id): + raise EosDeviceAllocatedError(f"Device '{device_id}' in lab '{lab_id}' is already allocated.") + + device_config = self._get_device_config(lab_id, device_id) + allocation = DeviceAllocation( + id=device_id, + lab_id=device_config["lab_id"], + owner=owner, + device_type=device_config["type"], + experiment_id=experiment_id, + ) + self._allocations.create(allocation.model_dump()) + + def deallocate(self, lab_id: str, device_id: str) -> None: + """ + Deallocate a device. + """ + result = self._allocations.delete(lab_id=lab_id, id=device_id) + if result.deleted_count == 0: + log.warning(f"Device '{device_id}' in lab '{lab_id}' is not allocated. No action taken.") + else: + log.debug(f"Deallocated device '{device_id}' in lab '{lab_id}'.") + + def is_allocated(self, lab_id: str, device_id: str) -> bool: + """ + Check if a device is allocated. + """ + self._get_device_config(lab_id, device_id) + return self._allocations.get_one(lab_id=lab_id, id=device_id) is not None + + def get_allocation(self, lab_id: str, device_id: str) -> DeviceAllocation | None: + """ + Get the allocation details of a device. + """ + self._get_device_config(lab_id, device_id) + allocation = self._allocations.get_one(lab_id=lab_id, id=device_id) + return DeviceAllocation(**allocation) if allocation else None + + def get_allocations(self, **query: dict[str, Any]) -> list[DeviceAllocation]: + """ + Query device allocations with arbitrary parameters. + """ + allocations = self._allocations.get_all(**query) + return [DeviceAllocation(**allocation) for allocation in allocations] + + def get_all_unallocated(self) -> list[str]: + """ + Get all unallocated devices. + """ + allocated_devices = [allocation.id for allocation in self.get_allocations()] + all_devices = [ + device_id for lab_config in self._configuration_manager.labs.values() for device_id in lab_config.devices + ] + return list(set(all_devices) - set(allocated_devices)) + + def deallocate_all_by_owner(self, owner: str) -> None: + """ + Deallocate all devices allocated to an owner. + """ + result = self._allocations.delete(owner=owner) + if result.deleted_count == 0: + log.warning(f"Owner '{owner}' has no devices allocated. No action taken.") + else: + log.debug(f"Deallocated {result.deleted_count} devices for owner '{owner}'.") + + def deallocate_all(self) -> None: + """ + Deallocate all devices. + """ + result = self._allocations.delete() + log.debug(f"Deallocated all {result.deleted_count} devices.") + + def _get_device_config(self, lab_id: str, device_id: str) -> dict[str, Any]: + lab = self._configuration_manager.labs.get(lab_id) + for dev_id, device_config in lab.devices.items(): + if dev_id == device_id: + return { + "lab_id": lab.type, + "type": device_config.type, + } + + raise EosDeviceNotFoundError(f"Device '{device_id}' in lab '{lab_id}' not found in the configuration.") diff --git a/eos/resource_allocation/entities/__init__.py b/eos/resource_allocation/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/resource_allocation/entities/container_allocation.py b/eos/resource_allocation/entities/container_allocation.py new file mode 100644 index 0000000..91e90f3 --- /dev/null +++ b/eos/resource_allocation/entities/container_allocation.py @@ -0,0 +1,7 @@ +from eos.resource_allocation.entities.resource_allocation import ( + ResourceAllocation, +) + + +class ContainerAllocation(ResourceAllocation): + container_type: str diff --git a/eos/resource_allocation/entities/device_allocation.py b/eos/resource_allocation/entities/device_allocation.py new file mode 100644 index 0000000..fab32ca --- /dev/null +++ b/eos/resource_allocation/entities/device_allocation.py @@ -0,0 +1,8 @@ +from eos.resource_allocation.entities.resource_allocation import ( + ResourceAllocation, +) + + +class DeviceAllocation(ResourceAllocation): + lab_id: str + device_type: str diff --git a/eos/resource_allocation/entities/resource_allocation.py b/eos/resource_allocation/entities/resource_allocation.py new file mode 100644 index 0000000..3c32f98 --- /dev/null +++ b/eos/resource_allocation/entities/resource_allocation.py @@ -0,0 +1,14 @@ +from datetime import datetime, timezone + +from pydantic import BaseModel + + +class ResourceAllocation(BaseModel): + id: str + owner: str + experiment_id: str | None = None + start_time: datetime | None = None + created_at: datetime = datetime.now(tz=timezone.utc) + + class Config: + arbitrary_types_allowed = True diff --git a/eos/resource_allocation/entities/resource_request.py b/eos/resource_allocation/entities/resource_request.py new file mode 100644 index 0000000..b29e823 --- /dev/null +++ b/eos/resource_allocation/entities/resource_request.py @@ -0,0 +1,61 @@ +from datetime import datetime +from enum import Enum + +from bson import ObjectId +from pydantic import BaseModel, field_serializer, Field + + +class ResourceType(Enum): + CONTAINER = "CONTAINER" + DEVICE = "DEVICE" + + +class Resource(BaseModel): + id: str + lab_id: str + resource_type: ResourceType + + @field_serializer("resource_type") + def resource_type_enum_to_string(self, v: ResourceType) -> str: + return v.value + + +class ResourceAllocationRequest(BaseModel): + requester: str + resources: list[Resource] = [] + experiment_id: str | None = None + reason: str | None = None + priority: int = Field(default=100, gt=0) + + def add_resource(self, resource_id: str, lab_id: str, resource_type: ResourceType) -> None: + self.resources.append(Resource(id=resource_id, lab_id=lab_id, resource_type=resource_type)) + + def remove_resource(self, resource_id: str, lab_id: str, resource_type: ResourceType) -> None: + self.resources = [ + r + for r in self.resources + if not (r.id == resource_id and r.lab_id == lab_id and r.resource_type == resource_type) + ] + + +class ResourceRequestAllocationStatus(Enum): + PENDING = "PENDING" + ALLOCATED = "ALLOCATED" + COMPLETED = "COMPLETED" + ABORTED = "ABORTED" + + +class ActiveResourceAllocationRequest(BaseModel): + id: ObjectId = Field(default_factory=ObjectId, alias="_id") + request: ResourceAllocationRequest + status: ResourceRequestAllocationStatus = ResourceRequestAllocationStatus.PENDING + created_at: datetime = Field(default_factory=datetime.utcnow) + allocated_at: datetime | None = None + + class Config: + arbitrary_types_allowed = True + populate_by_name = True + + @field_serializer("status") + def status_enum_to_string(self, v: ResourceRequestAllocationStatus) -> str: + return v.value diff --git a/eos/resource_allocation/exceptions.py b/eos/resource_allocation/exceptions.py new file mode 100644 index 0000000..551a807 --- /dev/null +++ b/eos/resource_allocation/exceptions.py @@ -0,0 +1,18 @@ +class EosResourceRequestError(Exception): + pass + + +class EosDeviceAllocatedError(EosResourceRequestError): + pass + + +class EosDeviceNotFoundError(EosResourceRequestError): + pass + + +class EosContainerAllocatedError(EosResourceRequestError): + pass + + +class EosContainerNotFoundError(EosResourceRequestError): + pass diff --git a/eos/resource_allocation/repositories/__init__.py b/eos/resource_allocation/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/resource_allocation/repositories/resource_request_repository.py b/eos/resource_allocation/repositories/resource_request_repository.py new file mode 100644 index 0000000..9998349 --- /dev/null +++ b/eos/resource_allocation/repositories/resource_request_repository.py @@ -0,0 +1,36 @@ +from eos.persistence.mongo_repository import MongoRepository +from eos.resource_allocation.entities.resource_request import ( + ResourceAllocationRequest, + ResourceRequestAllocationStatus, +) + + +class ResourceRequestRepository(MongoRepository): + def get_requests_prioritized(self, status: ResourceRequestAllocationStatus) -> list[dict]: + return self._collection.find({"status": status.value}).sort("request.priority", 1) + + def get_existing_request(self, request: ResourceAllocationRequest) -> dict: + query = { + "request.resources": [r.model_dump() for r in request.resources], + "request.requester": request.requester, + "status": { + "$in": [ + ResourceRequestAllocationStatus.PENDING.value, + ResourceRequestAllocationStatus.ALLOCATED.value, + ] + }, + } + + return self._collection.find_one(query) + + def clean_requests(self) -> None: + self._collection.delete_many( + { + "status": { + "$in": [ + ResourceRequestAllocationStatus.COMPLETED.value, + ResourceRequestAllocationStatus.ABORTED.value, + ] + } + } + ) diff --git a/eos/resource_allocation/resource_allocation_manager.py b/eos/resource_allocation/resource_allocation_manager.py new file mode 100644 index 0000000..882c7ad --- /dev/null +++ b/eos/resource_allocation/resource_allocation_manager.py @@ -0,0 +1,261 @@ +from collections.abc import Callable +from datetime import datetime, timezone +from threading import Lock + +from bson import ObjectId + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.resource_allocation.container_allocation_manager import ContainerAllocationManager +from eos.resource_allocation.device_allocation_manager import DeviceAllocationManager +from eos.resource_allocation.entities.resource_request import ( + ResourceAllocationRequest, + ActiveResourceAllocationRequest, + ResourceRequestAllocationStatus, + ResourceType, +) +from eos.resource_allocation.exceptions import EosResourceRequestError +from eos.resource_allocation.repositories.resource_request_repository import ( + ResourceRequestRepository, +) + + +class ResourceAllocationManager: + """ + Provides facilities to request allocation of resources. + """ + + def __init__( + self, + configuration_manager: ConfigurationManager, + db_manager: DbManager, + ): + self._device_allocation_manager = DeviceAllocationManager(configuration_manager, db_manager) + self._container_allocation_manager = ContainerAllocationManager(configuration_manager, db_manager) + self._active_requests = ResourceRequestRepository("resource_requests", db_manager) + + # Callbacks for when resource allocation requests are processed + self._request_callbacks: dict[ObjectId, Callable[[ActiveResourceAllocationRequest], None]] = {} + + self._lock = Lock() + + self._delete_all_requests() + self._delete_all_allocations() + + log.debug("Resource allocation manager initialized.") + + def request_resources( + self, + request: ResourceAllocationRequest, + callback: Callable[[ActiveResourceAllocationRequest], None], + ) -> ActiveResourceAllocationRequest: + """ + Request allocation of resources. A callback function is called when the resource allocation requests are + processed. If a resource allocation request already exists, the existing request is used instead of creating + a new one. + + :param request: The resource allocation request. + :param callback: Callback function to be called when the resource allocation request is processed. + :return: List of active resource allocation requests. + """ + with self._lock: + existing_request = self._find_existing_request(request) + if existing_request: + if existing_request.status in [ + ResourceRequestAllocationStatus.PENDING, + ResourceRequestAllocationStatus.ALLOCATED, + ]: + self._request_callbacks[existing_request.id] = callback + return existing_request + + active_request = ActiveResourceAllocationRequest(request=request) + result = self._active_requests.create(active_request.model_dump(by_alias=True)) + active_request.id = result.inserted_id + self._request_callbacks[active_request.id] = callback + return active_request + + def release_resources(self, active_request: ActiveResourceAllocationRequest) -> None: + """ + Release the resources allocated for an active resource allocation request. + + :param active_request: The active resource allocation request. + """ + with self._lock: + for resource in active_request.request.resources: + if resource.resource_type == ResourceType.DEVICE: + self._device_allocation_manager.deallocate(resource.lab_id, resource.id) + elif resource.resource_type == ResourceType.CONTAINER: + self._container_allocation_manager.deallocate(resource.id) + else: + raise EosResourceRequestError(f"Unknown resource type: {resource.resource_type}") + + self._update_request_status(active_request.id, ResourceRequestAllocationStatus.COMPLETED) + + def process_active_requests(self) -> None: + with self._lock: + self._clean_completed_and_aborted_requests() + + active_requests = self._get_all_active_requests_prioritized() + + for active_request in active_requests: + if active_request.status != ResourceRequestAllocationStatus.PENDING: + continue + + allocation_success = self._try_allocate(active_request) + + if allocation_success: + self._invoke_request_callback(active_request) + + def abort_active_request(self, request_id: ObjectId) -> None: + """ + Abort an active resource allocation request. + """ + with self._lock: + request = self.get_active_request(request_id) + for resource in request.request.resources: + if resource.resource_type == ResourceType.DEVICE: + self._device_allocation_manager.deallocate(resource.lab_id, resource.id) + elif resource.resource_type == ResourceType.CONTAINER: + self._container_allocation_manager.deallocate(resource.id) + self._update_request_status(request_id, ResourceRequestAllocationStatus.ABORTED) + active_request = self.get_active_request(request_id) + self._invoke_request_callback(active_request) + + def _get_all_active_requests_prioritized(self) -> list[ActiveResourceAllocationRequest]: + """ + Get all active resource allocation requests prioritized by the request priority in ascending order. + """ + active_requests = [] + active_requests_count = self._active_requests.count(status=ResourceRequestAllocationStatus.PENDING.value) + + if active_requests_count > 0: + active_requests = self._active_requests.get_requests_prioritized(ResourceRequestAllocationStatus.PENDING) + + return [ActiveResourceAllocationRequest(**request) for request in active_requests] + + def get_all_active_requests( + self, + requester: str | None = None, + lab_id: str | None = None, + experiment_id: str | None = None, + status: ResourceRequestAllocationStatus | None = None, + ) -> list[ActiveResourceAllocationRequest]: + """ + Get all active resource allocation requests. + + :param requester: Filter by the requester. + :param lab_id: Filter by the lab ID. + :param experiment_id: Filter by the experiment ID. + :param status: Filter by the status. + """ + query = {"requester": requester} + if lab_id: + query["request.lab_id"] = lab_id + if experiment_id: + query["request.experiment_id"] = experiment_id + if status: + query["status"] = status.value + active_requests = self._active_requests.get_all(**query) + return [ActiveResourceAllocationRequest(**request) for request in active_requests] + + def get_active_request(self, request_id: ObjectId) -> ActiveResourceAllocationRequest | None: + """ + Get an active resource allocation request by ID. If the request does not exist, returns None. + """ + request = self._active_requests.get_one(_id=request_id) + return ActiveResourceAllocationRequest(**request) if request else None + + @property + def device_allocation_manager(self) -> DeviceAllocationManager: + return self._device_allocation_manager + + @property + def container_allocation_manager(self) -> ContainerAllocationManager: + return self._container_allocation_manager + + def _update_request_status(self, request_id: ObjectId, status: ResourceRequestAllocationStatus) -> None: + """ + Update the status of an active resource allocation request. + """ + update_data = {"status": status.value} + if status == ResourceRequestAllocationStatus.ALLOCATED: + update_data["allocated_at"] = datetime.now(tz=timezone.utc) + + self._active_requests.update(update_data, _id=request_id) + + def _find_existing_request(self, request: ResourceAllocationRequest) -> ActiveResourceAllocationRequest | None: + """ + Find an existing active resource allocation request that matches the given request. + """ + existing_request = self._active_requests.get_existing_request(request) + return ActiveResourceAllocationRequest(**existing_request) if existing_request else None + + def _invoke_request_callback(self, active_request: ActiveResourceAllocationRequest) -> None: + """ + Invoke the allocation callback for an active resource allocation request. + """ + callback = self._request_callbacks.pop(active_request.id, None) + if callback: + callback(active_request) + + def _try_allocate(self, active_request: ActiveResourceAllocationRequest) -> bool: + temp_allocations = [] + all_available = True + + for resource in active_request.request.resources: + if resource.resource_type == ResourceType.DEVICE: + if not self._device_allocation_manager.is_allocated(resource.lab_id, resource.id): + temp_allocations.append(("device", resource.lab_id, resource.id)) + else: + all_available = False + break + elif resource.resource_type == ResourceType.CONTAINER: + if not self._container_allocation_manager.is_allocated(resource.id): + temp_allocations.append(("container", resource.id)) + else: + all_available = False + break + else: + raise EosResourceRequestError(f"Unknown resource type: {resource.resource_type}") + + if all_available: + for allocation in temp_allocations: + if allocation[0] == "device": + self._device_allocation_manager.allocate( + allocation[1], + allocation[2], + active_request.request.requester, + experiment_id=active_request.request.experiment_id, + ) + else: # container + self._container_allocation_manager.allocate( + allocation[1], + active_request.request.requester, + experiment_id=active_request.request.experiment_id, + ) + + self._update_request_status(active_request.id, ResourceRequestAllocationStatus.ALLOCATED) + active_request.status = ResourceRequestAllocationStatus.ALLOCATED + return True + + return False + + def _clean_completed_and_aborted_requests(self) -> None: + """ + Remove completed or aborted active resource allocation requests. + """ + self._active_requests.clean_requests() + + def _delete_all_requests(self) -> None: + """ + Delete all active resource allocation requests. + """ + self._active_requests.delete() + + def _delete_all_allocations(self) -> None: + """ + Delete all device and container allocations. + """ + self._device_allocation_manager.deallocate_all() + self._container_allocation_manager.deallocate_all() diff --git a/eos/scheduling/__init__.py b/eos/scheduling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/scheduling/abstract_scheduler.py b/eos/scheduling/abstract_scheduler.py new file mode 100644 index 0000000..b253140 --- /dev/null +++ b/eos/scheduling/abstract_scheduler.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod + +from eos.configuration.experiment_graph.experiment_graph import ExperimentGraph +from eos.scheduling.entities.scheduled_task import ScheduledTask + + +class AbstractScheduler(ABC): + @abstractmethod + def register_experiment(self, experiment_id: str, experiment_type: str, experiment_graph: ExperimentGraph) -> None: + """ + Register an experiment with the scheduler. + + :param experiment_id: The ID of the experiment. + :param experiment_type: The type of the experiment. + :param experiment_graph: The task graph of the experiment's task sequence. + """ + + @abstractmethod + def unregister_experiment(self, experiment_id: str) -> None: + """ + Unregister an experiment from the scheduler. + + :param experiment_id: The ID of the experiment. + """ + + @abstractmethod + async def request_tasks(self, experiment_id: str) -> list[ScheduledTask]: + """ + Request the next tasks to be executed for a specific experiment. + + :param experiment_id: The ID of the experiment. + :return: A list of tasks to be executed next. Returns an empty list if no new tasks are available. + """ + + @abstractmethod + def is_experiment_completed(self, experiment_id: str) -> bool: + """ + Check if an experiment has been completed. + + :param experiment_id: The ID of the experiment. + :return: True if the experiment has been completed, False otherwise. + """ diff --git a/eos/scheduling/basic_scheduler.py b/eos/scheduling/basic_scheduler.py new file mode 100644 index 0000000..030232e --- /dev/null +++ b/eos/scheduling/basic_scheduler.py @@ -0,0 +1,280 @@ +import asyncio +import threading + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.configuration.entities.task import TaskDeviceConfig, TaskConfig +from eos.configuration.experiment_graph.experiment_graph import ExperimentGraph +from eos.devices.device_manager import DeviceManager +from eos.devices.entities.device import DeviceStatus +from eos.experiments.experiment_manager import ExperimentManager +from eos.logging.logger import log +from eos.resource_allocation.entities.resource_request import ( + ActiveResourceAllocationRequest, + ResourceAllocationRequest, + ResourceType, + ResourceRequestAllocationStatus, +) +from eos.resource_allocation.exceptions import EosResourceRequestError +from eos.resource_allocation.resource_allocation_manager import ResourceAllocationManager +from eos.scheduling.abstract_scheduler import AbstractScheduler +from eos.scheduling.entities.scheduled_task import ScheduledTask +from eos.scheduling.exceptions import EosSchedulerRegistrationError, EosSchedulerResourceAllocationError +from eos.tasks.task_input_resolver import TaskInputResolver +from eos.tasks.task_manager import TaskManager + + +class BasicScheduler(AbstractScheduler): + """ + The basic scheduler is responsible for scheduling experimental tasks based on their precedence constraints. + The task graph is a DAG. Each task in the experiment has dependencies to other tasks, as well as a device in + the lab. Each device can only be used by one task at a time. In addition, each task may have dependencies to certain + containers and output parameters generated from previous tasks. The precedence scheduler is responsible for + scheduling tasks based on their dependencies, device availability, and container and output parameter availability. + + The scheduler should be able to schedule tasks across multiple experiments simultaneously and dynamically add and + remove experiments from task scheduling consideration. + """ + + def __init__( + self, + configuration_manager: ConfigurationManager, + experiment_manager: ExperimentManager, + task_manager: TaskManager, + device_manager: DeviceManager, + resource_allocation_manager: ResourceAllocationManager, + ): + self._configuration_manager = configuration_manager + self._experiment_manager = experiment_manager + self._task_input_resolver = TaskInputResolver(task_manager, experiment_manager) + self._device_manager = device_manager + + self._resource_allocation_manager = resource_allocation_manager + self._device_allocation_manager = self._resource_allocation_manager.device_allocation_manager + self._container_allocation_manager = self._resource_allocation_manager.container_allocation_manager + + self._registered_experiments = {} + self._allocated_resources: dict[str, dict[str, ActiveResourceAllocationRequest]] = {} + self._lock = threading.Lock() + + log.debug("Basic scheduler initialized.") + + def register_experiment(self, experiment_id: str, experiment_type: str, experiment_graph: ExperimentGraph) -> None: + """ + Register an experiment for execution. The scheduler will also consider this experiment when tasks are requested. + The scheduler records the experiment's ID, type, and task graph. + """ + with self._lock: + if experiment_type not in self._configuration_manager.experiments: + raise EosSchedulerRegistrationError( + f"Cannot register an experiment with the scheduler. Experiment '{experiment_type}' does not exist." + ) + self._registered_experiments[experiment_id] = (experiment_type, experiment_graph) + log.debug("Experiment '%s' registered for scheduling.", experiment_id) + + def unregister_experiment(self, experiment_id: str) -> None: + """ + Unregister an experiment from the scheduler. The scheduler will no longer consider this experiment when tasks + are requested. + """ + with self._lock: + if experiment_id in self._registered_experiments: + del self._registered_experiments[experiment_id] + self._release_experiment_resources(experiment_id) + else: + raise EosSchedulerRegistrationError( + f"Cannot unregister experiment {experiment_id} from the scheduler as it is not registered." + ) + + async def request_tasks(self, experiment_id: str) -> list[ScheduledTask]: + """ + Request the next tasks to be executed for a specific experiment. Resources such as devices are + allocated for the tasks. The scheduler will only consider tasks that have all their dependencies met and have + available resources. + + :param experiment_id: The ID of the experiment for which to request tasks. + :return: A list of tasks that are ready to be executed. + """ + with self._lock: + if experiment_id not in self._registered_experiments: + raise EosSchedulerRegistrationError( + f"Cannot request tasks from the scheduler for unregistered experiment {experiment_id}." + ) + experiment_type, experiment_graph = self._registered_experiments[experiment_id] + + all_tasks = experiment_graph.get_topologically_sorted_tasks() + completed_tasks = self._experiment_manager.get_completed_tasks(experiment_id) + pending_tasks = [task_id for task_id in all_tasks if task_id not in completed_tasks] + + # Release resources for completed tasks + for task_id in completed_tasks: + if task_id in self._allocated_resources.get(experiment_id, {}): + self._release_task_resources(experiment_id, task_id) + + scheduled_tasks = [] + for task_id in pending_tasks: + if not self._check_task_dependencies_met(task_id, completed_tasks, experiment_graph): + continue + + task_config = experiment_graph.get_task_config(task_id) + task_config = self._task_input_resolver.resolve_input_container_references(experiment_id, task_config) + + if not all(self._check_device_available(device) for device in task_config.devices): + continue + if not all( + self._check_container_available(container_id) for container_id in task_config.containers.values() + ): + continue + + try: + resource_request = self._create_resource_request(experiment_id, task_id, task_config) + allocated_resources = await self._request_resources(resource_request) + self._allocated_resources.setdefault(experiment_id, {})[task_id] = allocated_resources + scheduled_tasks.append( + ScheduledTask( + id=task_id, + experiment_id=experiment_id, + devices=[ + TaskDeviceConfig(lab_id=device.lab_id, id=device.id) for device in task_config.devices + ], + allocated_resources=allocated_resources, + ) + ) + except EosSchedulerResourceAllocationError: + log.warning( + f"Timed out in allocating resources for task '{task_id}' in experiment '{experiment_id}. " + f"Will retry.'" + ) + continue + + return scheduled_tasks + + def _create_resource_request( + self, experiment_id: str, task_id: str, task_config: TaskConfig + ) -> ResourceAllocationRequest: + """ + Create a single resource allocation request for all devices and containers required by a task. + """ + request = ResourceAllocationRequest( + requester=task_id, + experiment_id=experiment_id, + reason=f"Resources required for task '{task_id}'", + ) + + for device in task_config.devices: + request.add_resource(device.id, device.lab_id, ResourceType.DEVICE) + + for container_id in task_config.containers.values(): + request.add_resource(container_id, "", ResourceType.CONTAINER) + + return request + + async def _request_resources( + self, resource_request: ResourceAllocationRequest, timeout: int = 15 + ) -> ActiveResourceAllocationRequest: + """ + Request resources from the resource allocation manager for a single resource allocation request. This method + will block until all resources are allocated or until the timeout is reached. If the timeout is reached, the + resource allocation will be aborted and an error will be raised. + + :param resource_request: A resource allocation request to be allocated. + :param timeout: The maximum time to wait for resource allocation in seconds. + + :return: An active resource allocation request that has been allocated. + """ + allocation_event = asyncio.Event() + active_request = None + + def resource_request_callback(request: ActiveResourceAllocationRequest) -> None: + nonlocal active_request + active_request = request + allocation_event.set() + + active_resource_request = self._resource_allocation_manager.request_resources( + resource_request, resource_request_callback + ) + + if active_resource_request.status == ResourceRequestAllocationStatus.ALLOCATED: + return active_resource_request + + self._resource_allocation_manager.process_active_requests() + + try: + await asyncio.wait_for(allocation_event.wait(), timeout) + except asyncio.TimeoutError as e: + self._resource_allocation_manager.abort_active_request(active_resource_request.id) + raise EosSchedulerResourceAllocationError( + f"Resource allocation timed out after {timeout} seconds for task '{resource_request.requester}' " + f"while trying to schedule it. " + f"Aborting resource allocation for this task. Will retry again." + ) from e + + if not active_request: + raise EosSchedulerResourceAllocationError( + f"Failed to allocate resources for task '{resource_request.requester}'." + ) + + return active_request + + def _release_task_resources(self, experiment_id: str, task_id: str) -> None: + active_request = self._allocated_resources[experiment_id].pop(task_id, None) + if active_request: + try: + self._resource_allocation_manager.release_resources(active_request) + self._resource_allocation_manager.process_active_requests() + except EosResourceRequestError as e: + log.error(f"Error releasing resources for task '{task_id}' in experiment '{experiment_id}': {e}") + + def _release_experiment_resources(self, experiment_id: str) -> None: + task_ids = list(self._allocated_resources.get(experiment_id, {}).keys()) + for task_id in task_ids: + self._release_task_resources(experiment_id, task_id) + + if experiment_id in self._allocated_resources: + del self._allocated_resources[experiment_id] + + @staticmethod + def _check_task_dependencies_met( + task_id: str, completed_tasks: set[str], experiment_graph: ExperimentGraph + ) -> bool: + """ + Return True if all dependencies of a task have been completed, False otherwise. + """ + dependencies = experiment_graph.get_task_dependencies(task_id) + return all(dep in completed_tasks for dep in dependencies) + + def _check_device_available(self, task_device: TaskDeviceConfig) -> bool: + """ + Check if a device is available for a task. A device is available if it is active, not allocated by the device + allocation manager. + """ + if self._device_manager.get_device(task_device.lab_id, task_device.id).status == DeviceStatus.INACTIVE: + log.warning( + f"Device {task_device.id} in lab {task_device.lab_id} is inactive but is requested by task " + f"{task_device.id}." + ) + return False + + return not self._device_allocation_manager.is_allocated(task_device.lab_id, task_device.id) + + def _check_container_available(self, container_id: str) -> bool: + """ + Check if a container is available for a task. A device is available if not allocated by the container + allocation manager. + """ + return not self._container_allocation_manager.is_allocated(container_id) + + def is_experiment_completed(self, experiment_id: str) -> bool: + """ + Check if an experiment has been completed. The scheduler should consider the completed tasks from the task + manager to determine if the experiment has been completed. + """ + if experiment_id not in self._registered_experiments: + raise EosSchedulerRegistrationError( + f"Cannot check if experiment {experiment_id} is completed as it is not registered." + ) + + experiment_type, experiment_graph = self._registered_experiments[experiment_id] + all_tasks = experiment_graph.get_task_graph().nodes + completed_tasks = self._experiment_manager.get_completed_tasks(experiment_id) + + return all(task in completed_tasks for task in all_tasks) diff --git a/eos/scheduling/entities/__init__.py b/eos/scheduling/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/scheduling/entities/scheduled_task.py b/eos/scheduling/entities/scheduled_task.py new file mode 100644 index 0000000..06adcf4 --- /dev/null +++ b/eos/scheduling/entities/scheduled_task.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + +from eos.configuration.entities.task import TaskDeviceConfig +from eos.resource_allocation.entities.resource_request import ActiveResourceAllocationRequest + + +class ScheduledTask(BaseModel): + id: str + experiment_id: str + devices: list[TaskDeviceConfig] + allocated_resources: ActiveResourceAllocationRequest diff --git a/eos/scheduling/exceptions.py b/eos/scheduling/exceptions.py new file mode 100644 index 0000000..6f9fef1 --- /dev/null +++ b/eos/scheduling/exceptions.py @@ -0,0 +1,10 @@ +class EosSchedulerError(Exception): + pass + + +class EosSchedulerRegistrationError(EosSchedulerError): + pass + + +class EosSchedulerResourceAllocationError(EosSchedulerError): + pass diff --git a/eos/tasks/__init__.py b/eos/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/tasks/base_task.py b/eos/tasks/base_task.py new file mode 100644 index 0000000..bf1d970 --- /dev/null +++ b/eos/tasks/base_task.py @@ -0,0 +1,47 @@ +from abc import ABC, abstractmethod +from typing import Any + +from eos.containers.entities.container import Container +from eos.devices.device_actor_references import DeviceRayActorWrapperReferences +from eos.tasks.exceptions import EosTaskExecutionError + + +class BaseTask(ABC): + """Base class for all tasks in EOS.""" + + DevicesType = dict[str, DeviceRayActorWrapperReferences] + ParametersType = dict[str, Any] + ContainersType = dict[str, Container] + FilesType = dict[str, bytes] + OutputType = tuple[ParametersType, ContainersType, FilesType] + + def __init__(self, experiment_id: str, task_id: str) -> None: + self._experiment_id = experiment_id + self._task_id = task_id + + def execute( + self, devices: DevicesType, parameters: ParametersType, containers: ContainersType + ) -> OutputType | None: + """Execute a task with the given input and return the output.""" + try: + output = self._execute(devices, parameters, containers) + + output_parameters, output_containers, output_files = ({}, {}, {}) + + if output: + output_parameters = output[0] if len(output) > 0 and output[0] is not None else {} + output_containers = output[1] if len(output) > 1 and output[1] is not None else {} + output_files = output[2] if len(output) == 3 and output[2] is not None else {} + + if containers: + output_containers = {**containers, **output_containers} + + return output_parameters, output_containers, output_files + except Exception as e: + raise EosTaskExecutionError(f"Error executing task {self._task_id}") from e + + @abstractmethod + def _execute( + self, devices: DevicesType, parameters: ParametersType, containers: ContainersType + ) -> OutputType | None: + """Implementation for the execution of a task.""" diff --git a/eos/tasks/entities/__init__.py b/eos/tasks/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/tasks/entities/task.py b/eos/tasks/entities/task.py new file mode 100644 index 0000000..095f489 --- /dev/null +++ b/eos/tasks/entities/task.py @@ -0,0 +1,86 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import Any, ClassVar + +from omegaconf import ListConfig, DictConfig, OmegaConf +from pydantic import BaseModel, field_serializer + +from eos.configuration.entities.task import TaskDeviceConfig +from eos.containers.entities.container import Container + + +class TaskStatus(Enum): + CREATED = "CREATED" + RUNNING = "RUNNING" + COMPLETED = "COMPLETED" + FAILED = "FAILED" + CANCELLED = "CANCELLED" + + +class TaskContainer(BaseModel): + id: str + + +class TaskInput(BaseModel): + parameters: dict[str, Any] | None = None + containers: dict[str, Container] | None = None + + class Config: + arbitrary_types_allowed = True + + @field_serializer("parameters") + def serialize_parameters(self, parameters: dict[str, Any] | None, _info) -> Any: + if parameters is None: + return None + return omegaconf_serializer(parameters) + + +class TaskOutput(BaseModel): + parameters: dict[str, Any] | None = None + containers: dict[str, Container] | None = None + file_names: list[str] | None = None + + @field_serializer("parameters") + def serialize_parameters(self, parameters: dict[str, Any] | None, _info) -> Any: + if parameters is None: + return None + return omegaconf_serializer(parameters) + + +def omegaconf_serializer(obj: Any) -> Any: + if isinstance(obj, ListConfig | DictConfig): + return OmegaConf.to_object(obj) + if isinstance(obj, dict): + return {k: omegaconf_serializer(v) for k, v in obj.items()} + if isinstance(obj, list): + return [omegaconf_serializer(v) for v in obj] + return obj + + +class Task(BaseModel): + id: str + type: str + experiment_id: str + + devices: list[TaskDeviceConfig] = [] + input: TaskInput = TaskInput() + output: TaskOutput = TaskInput() + + status: TaskStatus = TaskStatus.CREATED + + metadata: dict[str, Any] = {} + start_time: datetime | None = None + end_time: datetime | None = None + + created_at: datetime = datetime.now(tz=timezone.utc) + + class Config: + arbitrary_types_allowed = True + json_encoders: ClassVar = { + ListConfig: lambda v: omegaconf_serializer(v), + DictConfig: lambda v: omegaconf_serializer(v), + } + + @field_serializer("status") + def status_enum_to_string(self, v: TaskStatus) -> str: + return v.value diff --git a/eos/tasks/entities/task_execution_parameters.py b/eos/tasks/entities/task_execution_parameters.py new file mode 100644 index 0000000..7d148c6 --- /dev/null +++ b/eos/tasks/entities/task_execution_parameters.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field + +from eos.configuration.entities.task import TaskConfig + + +class TaskExecutionParameters(BaseModel): + experiment_id: str + task_config: TaskConfig + resource_allocation_priority: int = Field(120, ge=0) + resource_allocation_timeout: int = Field(30, ge=0) diff --git a/eos/tasks/exceptions.py b/eos/tasks/exceptions.py new file mode 100644 index 0000000..bdee178 --- /dev/null +++ b/eos/tasks/exceptions.py @@ -0,0 +1,26 @@ +class EosTaskError(Exception): + pass + + +class EosTaskValidationError(EosTaskError): + pass + + +class EosTaskInputResolutionError(EosTaskError): + pass + + +class EosTaskStateError(EosTaskError): + pass + + +class EosTaskExistsError(EosTaskError): + pass + + +class EosTaskExecutionError(EosTaskError): + pass + + +class EosTaskResourceAllocationError(EosTaskError): + pass diff --git a/eos/tasks/on_demand_task_executor.py b/eos/tasks/on_demand_task_executor.py new file mode 100644 index 0000000..23cb45b --- /dev/null +++ b/eos/tasks/on_demand_task_executor.py @@ -0,0 +1,102 @@ +import asyncio +import traceback +from typing import Any + +from eos.configuration.entities.task import TaskConfig +from eos.containers.container_manager import ContainerManager +from eos.containers.entities.container import Container +from eos.logging.logger import log +from eos.tasks.entities.task import TaskOutput +from eos.tasks.entities.task_execution_parameters import TaskExecutionParameters +from eos.tasks.exceptions import EosTaskExecutionError, EosTaskValidationError, EosTaskStateError +from eos.tasks.task_executor import TaskExecutor +from eos.tasks.task_manager import TaskManager + + +class OnDemandTaskExecutor: + """ + Executor for on-demand tasks (not part of an experiment or campaign). + """ + + EXPERIMENT_ID = "on_demand" + + def __init__(self, task_executor: TaskExecutor, task_manager: TaskManager, container_manager: ContainerManager): + self._task_executor = task_executor + self._task_manager = task_manager + self._container_manager = container_manager + + self._task_futures: dict[str, asyncio.Task] = {} + + log.debug("On-demand task executor initialized.") + + async def submit_task( + self, + task_config: TaskConfig, + resource_allocation_priority: int = 90, + resource_allocation_timeout: int = 3600, + ) -> None: + task_id = task_config.id + task_execution_parameters = TaskExecutionParameters( + experiment_id=self.EXPERIMENT_ID, + task_config=task_config, + resource_allocation_priority=resource_allocation_priority, + resource_allocation_timeout=resource_allocation_timeout, + ) + + self._task_futures[task_id] = asyncio.create_task( + self._task_executor.request_task_execution(task_execution_parameters) + ) + log.info(f"Submitted on-demand task '{task_id}'.") + + async def cancel_task(self, task_id: str) -> None: + if task_id not in self._task_futures: + raise EosTaskExecutionError(f"Cannot cancel non-existent on-demand task '{task_id}'.") + + future = self._task_futures[task_id] + future.cancel() + await self._task_executor.request_task_cancellation(self.EXPERIMENT_ID, task_id) + del self._task_futures[task_id] + log.info(f"Cancelled on-demand task '{task_id}'.") + + async def process_tasks(self) -> None: + completed_tasks = [] + + for task_id, future in self._task_futures.items(): + if future.done(): + try: + output = await future + self._process_task_output(task_id, *output) + except asyncio.CancelledError: + log.info(f"On-demand task '{task_id}' was cancelled.") + except (EosTaskExecutionError, EosTaskValidationError, EosTaskStateError): + log.error(f"Failed on-demand task '{task_id}': {traceback.format_exc()}") + finally: + completed_tasks.append(task_id) + + for task_id in completed_tasks: + del self._task_futures[task_id] + + def _process_task_output( + self, + task_id: str, + output_parameters: dict[str, Any], + output_containers: dict[str, Container], + output_files: dict[str, bytes], + ) -> None: + for container in output_containers.values(): + self._container_manager.update_container(container) + + task_output = TaskOutput( + experiment_id=self.EXPERIMENT_ID, + task_id=task_id, + parameters=output_parameters, + containers=output_containers, + file_names=list(output_files.keys()), + ) + + for file_name, file_data in output_files.items(): + self._task_manager.add_task_output_file(self.EXPERIMENT_ID, task_id, file_name, file_data) + + self._task_manager.add_task_output(self.EXPERIMENT_ID, task_id, task_output) + self._task_manager.complete_task(self.EXPERIMENT_ID, task_id) + log.info(f"EXP '{self.EXPERIMENT_ID}' - Completed task '{task_id}'.") diff --git a/eos/tasks/repositories/__init__.py b/eos/tasks/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/tasks/repositories/task_repository.py b/eos/tasks/repositories/task_repository.py new file mode 100644 index 0000000..dce767b --- /dev/null +++ b/eos/tasks/repositories/task_repository.py @@ -0,0 +1,5 @@ +from eos.persistence.mongo_repository import MongoRepository + + +class TaskRepository(MongoRepository): + pass diff --git a/eos/tasks/task_executor.py b/eos/tasks/task_executor.py new file mode 100644 index 0000000..f025803 --- /dev/null +++ b/eos/tasks/task_executor.py @@ -0,0 +1,278 @@ +import asyncio +from dataclasses import dataclass +from typing import Any + +import ray +from omegaconf import OmegaConf +from ray import ObjectRef + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.configuration.plugin_registries.task_plugin_registry import TaskPluginRegistry +from eos.containers.container_manager import ContainerManager +from eos.containers.entities.container import Container +from eos.devices.device_actor_references import DeviceRayActorReference, DeviceRayActorWrapperReferences +from eos.devices.device_manager import DeviceManager +from eos.logging.logger import log +from eos.resource_allocation.entities.resource_request import ( + ActiveResourceAllocationRequest, + ResourceAllocationRequest, + ResourceType, + ResourceRequestAllocationStatus, +) +from eos.resource_allocation.exceptions import EosResourceRequestError +from eos.resource_allocation.resource_allocation_manager import ResourceAllocationManager +from eos.scheduling.entities.scheduled_task import ScheduledTask +from eos.tasks.base_task import BaseTask +from eos.tasks.entities.task import TaskStatus +from eos.tasks.entities.task_execution_parameters import TaskExecutionParameters +from eos.tasks.exceptions import ( + EosTaskResourceAllocationError, + EosTaskExecutionError, + EosTaskValidationError, + EosTaskExistsError, +) +from eos.tasks.task_input_parameter_caster import TaskInputParameterCaster +from eos.tasks.task_manager import TaskManager +from eos.tasks.task_validator import TaskValidator + + +@dataclass +class TaskExecutionContext: + experiment_id: str + task_id: str + task_ref: ObjectRef | None = None + active_resource_request: ActiveResourceAllocationRequest = None + + +class TaskExecutor: + def __init__( + self, + task_manager: TaskManager, + device_manager: DeviceManager, + container_manager: ContainerManager, + resource_allocation_manager: ResourceAllocationManager, + configuration_manager: ConfigurationManager, + ): + self._task_manager = task_manager + self._device_manager = device_manager + self._container_manager = container_manager + self._resource_allocation_manager = resource_allocation_manager + self._configuration_manager = configuration_manager + self._task_plugin_registry = TaskPluginRegistry() + self._task_validator = TaskValidator() + self._task_input_parameter_caster = TaskInputParameterCaster() + + self._active_tasks: dict[str, TaskExecutionContext] = {} + + log.debug("Task executor initialized.") + + async def request_task_execution( + self, task_parameters: TaskExecutionParameters, scheduled_task: ScheduledTask | None = None + ) -> BaseTask.OutputType | None: + context = TaskExecutionContext(task_parameters.experiment_id, task_parameters.task_config.id) + self._active_tasks[context.task_id] = context + + try: + containers = self._prepare_containers(task_parameters) + await self._initialize_task(task_parameters, containers) + + self._task_validator.validate(task_parameters.task_config) + + context.active_resource_request = ( + scheduled_task.allocated_resources + if scheduled_task + else await self._allocate_resources(task_parameters) + ) + + context.task_ref = self._execute_task(task_parameters, containers) + return await context.task_ref + except EosTaskExistsError as e: + raise EosTaskExecutionError( + f"Error executing task '{context.task_id}' in experiment '{context.experiment_id}'" + ) from e + except EosTaskValidationError as e: + self._task_manager.fail_task(context.experiment_id, context.task_id) + log.warning(f"EXP '{context.experiment_id}' - Failed task '{context.task_id}'.") + raise EosTaskValidationError( + f"Validation error for task '{context.task_id}' in experiment '{context.experiment_id}'" + ) from e + except EosTaskResourceAllocationError as e: + self._task_manager.fail_task(context.experiment_id, context.task_id) + log.warning(f"EXP '{context.experiment_id}' - Failed task '{context.task_id}'.") + raise EosTaskResourceAllocationError( + f"Failed to allocate resources for task '{context.task_id}' in experiment '{context.experiment_id}'" + ) from e + except Exception as e: + self._task_manager.fail_task(context.experiment_id, context.task_id) + log.warning(f"EXP '{context.experiment_id}' - Failed task '{context.task_id}'.") + raise EosTaskExecutionError( + f"Error executing task '{context.task_id}' in experiment '{context.experiment_id}'" + ) from e + finally: + if context.active_resource_request and not scheduled_task: + self._release_resources(context.active_resource_request) + + if context.task_id in self._active_tasks: + del self._active_tasks[context.task_id] + + async def request_task_cancellation(self, experiment_id: str, task_id: str) -> None: + context = self._active_tasks.get(task_id) + if not context: + return + + if context.task_ref: + ray.cancel(context.task_ref, recursive=True) + + if context.active_resource_request: + self._resource_allocation_manager.abort_active_request(context.active_resource_request.id) + self._resource_allocation_manager.process_active_requests() + + self._task_manager.cancel_task(experiment_id, task_id) + log.warning(f"EXP '{experiment_id}' - Cancelled task '{task_id}'.") + del self._active_tasks[task_id] + + def _prepare_containers(self, execution_parameters: TaskExecutionParameters) -> dict[str, Container]: + return { + container_name: self._container_manager.get_container(container_id) + for container_name, container_id in execution_parameters.task_config.containers.items() + } + + async def _initialize_task( + self, execution_parameters: TaskExecutionParameters, containers: dict[str, Container] + ) -> None: + experiment_id, task_id = execution_parameters.experiment_id, execution_parameters.task_config.id + log.debug(f"Execution of task '{task_id}' for experiment '{experiment_id}' has been requested") + + task = self._task_manager.get_task(experiment_id, task_id) + if task and task.status == TaskStatus.RUNNING: + log.warning(f"Found running task '{task_id}' for experiment '{experiment_id}'. Restarting it.") + await self.request_task_cancellation(experiment_id, task_id) + self._task_manager.delete_task(experiment_id, task_id) + + self._task_manager.create_task( + experiment_id=experiment_id, + task_id=task_id, + task_type=execution_parameters.task_config.type, + devices=execution_parameters.task_config.devices, + parameters=execution_parameters.task_config.parameters, + containers=containers, + ) + + async def _allocate_resources( + self, execution_parameters: TaskExecutionParameters + ) -> ActiveResourceAllocationRequest: + resource_request = self._create_resource_request(execution_parameters) + return await self._request_resources(resource_request, execution_parameters.resource_allocation_timeout) + + def _get_device_actor_references(self, task_parameters: TaskExecutionParameters) -> list[DeviceRayActorReference]: + return [ + DeviceRayActorReference( + id=device.id, + lab_id=device.lab_id, + type=self._configuration_manager.labs[device.lab_id].devices[device.id].type, + actor_handle=self._device_manager.get_device_actor(device.lab_id, device.id), + ) + for device in task_parameters.task_config.devices + ] + + def _execute_task( + self, + task_execution_parameters: TaskExecutionParameters, + containers: dict[str, Container], + ) -> ObjectRef: + experiment_id, task_id = task_execution_parameters.experiment_id, task_execution_parameters.task_config.id + device_actor_references = self._get_device_actor_references(task_execution_parameters) + task_class_type = self._task_plugin_registry.get_task_class_type(task_execution_parameters.task_config.type) + parameters = task_execution_parameters.task_config.parameters + if not isinstance(parameters, dict): + parameters = OmegaConf.to_object(parameters) + + parameters = self._task_input_parameter_caster.cast_input_parameters( + task_id, task_execution_parameters.task_config.type, parameters + ) + + @ray.remote(num_cpus=0) + def _ray_execute_task( + _experiment_id: str, + _task_id: str, + _devices_actor_references: list[DeviceRayActorReference], + _parameters: dict[str, Any], + _containers: dict[str, Container], + ) -> tuple: + task = task_class_type(_experiment_id, _task_id) + devices = DeviceRayActorWrapperReferences(_devices_actor_references) + return task.execute(devices, _parameters, _containers) + + self._task_manager.start_task(experiment_id, task_id) + log.info(f"EXP '{experiment_id}' - Started task '{task_id}'.") + + return _ray_execute_task.options(name=f"{experiment_id}.{task_id}").remote( + experiment_id, + task_id, + device_actor_references, + parameters, + containers, + ) + + @staticmethod + def _create_resource_request( + task_parameters: TaskExecutionParameters, + ) -> ResourceAllocationRequest: + task_id, experiment_id = task_parameters.task_config.id, task_parameters.experiment_id + resource_allocation_priority = task_parameters.resource_allocation_priority + + request = ResourceAllocationRequest( + requester=task_id, + experiment_id=experiment_id, + reason=f"Resources required for task '{task_id}'", + priority=resource_allocation_priority, + ) + + for device in task_parameters.task_config.devices: + request.add_resource(device.id, device.lab_id, ResourceType.DEVICE) + + for container_id in task_parameters.task_config.containers.values(): + request.add_resource(container_id, "", ResourceType.CONTAINER) + + return request + + async def _request_resources( + self, resource_request: ResourceAllocationRequest, timeout: int = 30 + ) -> ActiveResourceAllocationRequest: + allocation_event = asyncio.Event() + active_request = None + + def resource_request_callback(request: ActiveResourceAllocationRequest) -> None: + nonlocal active_request + active_request = request + allocation_event.set() + + active_resource_request = self._resource_allocation_manager.request_resources( + resource_request, resource_request_callback + ) + + if active_resource_request.status == ResourceRequestAllocationStatus.ALLOCATED: + return active_resource_request + + self._resource_allocation_manager.process_active_requests() + + try: + await asyncio.wait_for(allocation_event.wait(), timeout) + except asyncio.TimeoutError as e: + self._resource_allocation_manager.abort_active_request(active_resource_request.id) + raise EosTaskResourceAllocationError( + f"Resource allocation timed out after {timeout} seconds for task '{resource_request.requester}'. " + f"Aborting all resource allocations for this task." + ) from e + + if not active_request: + raise EosTaskResourceAllocationError(f"Error allocating resources for task '{resource_request.requester}'") + + return active_request + + def _release_resources(self, active_request: ActiveResourceAllocationRequest) -> None: + try: + self._resource_allocation_manager.release_resources(active_request) + self._resource_allocation_manager.process_active_requests() + except EosResourceRequestError as e: + raise EosTaskExecutionError(f"Error releasing task '{active_request.request.requester}' resources") from e diff --git a/eos/tasks/task_input_parameter_caster.py b/eos/tasks/task_input_parameter_caster.py new file mode 100644 index 0000000..008e092 --- /dev/null +++ b/eos/tasks/task_input_parameter_caster.py @@ -0,0 +1,33 @@ +from typing import Any + +from eos.configuration.entities.parameters import ParameterType +from eos.configuration.exceptions import EosTaskValidationError +from eos.configuration.spec_registries.task_specification_registry import TaskSpecificationRegistry + + +class TaskInputParameterCaster: + def __init__(self): + self.task_spec_registry = TaskSpecificationRegistry() + + def cast_input_parameters(self, task_id: str, task_type: str, input_parameters: dict[str, Any]) -> dict[str, Any]: + """ + Cast input parameters of a task to the expected Python types. + + :param task_id: The ID of the task. + :param task_type: The type of the task. + :param input_parameters: The input parameters of the task. + :return: The input parameters cast to the expected Python types. + """ + task_spec = self.task_spec_registry.get_spec_by_type(task_type) + + for parameter_name, parameter in input_parameters.items(): + try: + parameter_type = ParameterType(task_spec.input_parameters[parameter_name].type) + input_parameters[parameter_name] = parameter_type.python_type()(parameter) + except TypeError as e: + raise EosTaskValidationError( + f"Failed to cast input parameter '{parameter_name}' of task '{task_id}' of type \ + f'{type(parameter)}' to the expected type '{task_spec.input_parameters[parameter_name].type}'." + ) from e + + return input_parameters diff --git a/eos/tasks/task_input_parameter_validator.py b/eos/tasks/task_input_parameter_validator.py new file mode 100644 index 0000000..cb62f98 --- /dev/null +++ b/eos/tasks/task_input_parameter_validator.py @@ -0,0 +1,156 @@ +import copy +from typing import Any + +from omegaconf import ListConfig, OmegaConf, DictConfig + +from eos.configuration.entities.parameters import ParameterType, ParameterFactory +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.configuration.exceptions import EosConfigurationError +from eos.configuration.validation import validation_utils +from eos.logging.batch_error_logger import batch_error, raise_batched_errors +from eos.tasks.exceptions import EosTaskValidationError + + +class TaskInputParameterValidator: + """ + Validates that the input parameters of a task conform to the task's specification. + """ + + def __init__(self, task: TaskConfig, task_spec: TaskSpecification): + self._task_id = task.id + self._input_parameters = task.parameters + self._task_spec = task_spec + + def validate_input_parameters(self) -> None: + """ + Validate the input parameters of a task. + Ensure that all required parameters are provided and that the provided parameters conform to the task's + specification. + """ + for parameter_name in self._input_parameters: + self._validate_parameter_in_task_spec(parameter_name) + raise_batched_errors(root_exception_type=EosTaskValidationError) + + self._validate_all_required_parameters_provided() + + for parameter_name, parameter in self._input_parameters.items(): + self._validate_parameter(parameter_name, parameter) + raise_batched_errors(root_exception_type=EosTaskValidationError) + + def _validate_parameter_in_task_spec(self, parameter_name: str) -> None: + """ + Check that the parameter exists in the task specification. + """ + if parameter_name not in self._task_spec.input_parameters: + batch_error( + f"Parameter '{parameter_name}' in task '{self._task_id}' is invalid. " + f"Expected a parameter found in the task specification.", + EosTaskValidationError, + ) + + def _validate_parameter(self, parameter_name: str, parameter: Any) -> None: + """ + Validate a parameter according to the task specification. Expect that the parameter is concrete. + """ + if validation_utils.is_dynamic_parameter(parameter): + batch_error( + f"Input parameter '{parameter_name}' in task '{self._task_id}' is 'eos_dynamic', which is not " + f"allowed.", + EosTaskValidationError, + ) + else: + self._validate_parameter_spec(parameter_name, parameter) + + def _validate_parameter_spec(self, parameter_name: str, parameter: Any) -> None: + """ + Validate a parameter to make sure it conforms to its task specification. + """ + parameter_spec = copy.deepcopy(self._task_spec.input_parameters[parameter_name]) + + try: + parameter = self._convert_value_type(parameter, ParameterType(parameter_spec.type)) + except Exception: + batch_error( + f"Parameter '{parameter_name}' in task '{self._task_id}' has incorrect type {type(parameter)}. " + f"Expected type: '{parameter_spec.type}'.", + EosTaskValidationError, + ) + return + + parameter_spec["value"] = parameter + + try: + parameter_type = ParameterType(parameter_spec.type) + ParameterFactory.create_parameter(parameter_type, **parameter_spec) + except EosConfigurationError as e: + batch_error( + f"Parameter '{parameter_name}' in task '{self._task_id}' validation error: {e}", + EosTaskValidationError, + ) + + def _convert_value_type(self, value: Any, expected_type: ParameterType) -> Any: + if isinstance(value, expected_type.python_type()): + return value + + if isinstance(value, ListConfig | DictConfig): + value = OmegaConf.to_object(value) + + conversion_map = { + ParameterType.integer: int, + ParameterType.decimal: float, + ParameterType.string: str, + ParameterType.choice: str, + } + + if expected_type in conversion_map: + return conversion_map[expected_type](value) + + if expected_type == ParameterType.boolean: + if isinstance(value, bool): + return value + if isinstance(value, str): + v = value.strip().lower() + if v == "true": + return True + if v == "false": + return False + raise ValueError(f"Cannot convert {value} to boolean") + + if expected_type == ParameterType.list: + if isinstance(value, list | tuple): + return list(value) + raise ValueError(f"Cannot convert {value} to list") + + if expected_type == ParameterType.dictionary: + if isinstance(value, dict): + return value + raise ValueError(f"Cannot convert {value} to dictionary") + + raise ValueError(f"Unsupported parameter type: {expected_type}") + + def _validate_all_required_parameters_provided(self) -> None: + """ + Validate that all required parameters are provided in the parameter dictionary. + """ + missing_parameters = self._get_missing_required_task_parameters() + + if missing_parameters: + raise EosTaskValidationError( + f"Task '{self._task_id}' is missing required input parameters: {missing_parameters}" + ) + + def _get_missing_required_task_parameters(self) -> list[str]: + """ + Get all the missing required parameters in the parameter dictionary. + """ + required_parameters = self._get_required_input_parameters() + return [ + parameter_name for parameter_name in required_parameters if parameter_name not in self._input_parameters + ] + + def _get_required_input_parameters(self) -> list[str]: + """ + Get all the required input parameters for the task. + """ + return [param for param, spec in self._task_spec.input_parameters.items() if "value" not in spec] diff --git a/eos/tasks/task_input_resolver.py b/eos/tasks/task_input_resolver.py new file mode 100644 index 0000000..c24508a --- /dev/null +++ b/eos/tasks/task_input_resolver.py @@ -0,0 +1,130 @@ +import copy +import functools +from typing import Protocol + +from eos.configuration.entities.task import TaskConfig +from eos.configuration.validation import validation_utils +from eos.experiments.experiment_manager import ExperimentManager +from eos.tasks.exceptions import EosTaskInputResolutionError +from eos.tasks.task_manager import TaskManager + + +class Resolver(Protocol): + def __call__(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: ... + + +class TaskInputResolver: + """ + Resolves dynamic parameters, input parameter references, and input container references for a task that is + part of an experiment. + """ + + def __init__(self, task_manager: TaskManager, experiment_manager: ExperimentManager): + self._task_manager = task_manager + self._experiment_manager = experiment_manager + + def resolve_task_inputs(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: + """ + Resolve all input references for a task. + """ + return self._apply_resolvers( + experiment_id, + task_config, + [ + self._resolve_dynamic_parameters, + self._resolve_input_parameter_references, + self._resolve_input_container_references, + ], + ) + + def resolve_dynamic_parameters(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: + """ + Resolve dynamic parameters for a task. + """ + return self._apply_resolvers(experiment_id, task_config, [self._resolve_dynamic_parameters]) + + def resolve_input_parameter_references(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: + """ + Resolve input parameter references for a task. + """ + return self._apply_resolvers(experiment_id, task_config, [self._resolve_input_parameter_references]) + + def resolve_input_container_references(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: + """ + Resolve input container references for a task. + """ + return self._apply_resolvers(experiment_id, task_config, [self._resolve_input_container_references]) + + def _apply_resolvers(self, experiment_id: str, task_config: TaskConfig, resolvers: list[Resolver]) -> TaskConfig: + """ + Apply a list of resolver functions to the task config. + """ + return functools.reduce( + lambda config, resolver: resolver(experiment_id, config), resolvers, copy.deepcopy(task_config) + ) + + def _resolve_dynamic_parameters(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: + experiment = self._experiment_manager.get_experiment(experiment_id) + task_dynamic_parameters = experiment.dynamic_parameters.get(task_config.id, {}) + + task_config.parameters.update(task_dynamic_parameters) + + unresolved_parameters = [ + param for param, value in task_config.parameters.items() if validation_utils.is_dynamic_parameter(value) + ] + + if unresolved_parameters: + raise EosTaskInputResolutionError( + f"Unresolved input dynamic parameters in task '{task_config.id}': {unresolved_parameters}" + ) + + return task_config + + def _resolve_input_parameter_references(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: + for param_name, param_value in task_config.parameters.items(): + if not validation_utils.is_parameter_reference(param_value): + continue + + ref_task_id, ref_param_name = param_value.split(".") + resolved_value = self._resolve_reference(experiment_id, ref_task_id, ref_param_name, "parameter") + + if resolved_value is not None: + task_config.parameters[param_name] = resolved_value + else: + raise EosTaskInputResolutionError( + f"Unresolved input parameter reference '{param_value}' in task '{task_config.id}'" + ) + + return task_config + + def _resolve_input_container_references(self, experiment_id: str, task_config: TaskConfig) -> TaskConfig: + for container_name, container_id in task_config.containers.items(): + if not validation_utils.is_container_reference(container_id): + continue + + ref_task_id, ref_container_name = container_id.split(".") + resolved_value = self._resolve_reference(experiment_id, ref_task_id, ref_container_name, "container") + + if resolved_value is not None: + task_config.containers[container_name] = resolved_value + else: + raise EosTaskInputResolutionError( + f"Unresolved input container reference '{container_id}' in task '{task_config.id}'" + ) + + return task_config + + def _resolve_reference(self, experiment_id: str, ref_task_id: str, ref_name: str, ref_type: str) -> str | None: + ref_task_output = self._task_manager.get_task_output(experiment_id, ref_task_id) + + if ref_type == "parameter": + if ref_name in (ref_task_output.parameters or {}): + return ref_task_output.parameters[ref_name] + ref_task = self._task_manager.get_task(experiment_id, ref_task_id) + if ref_name in (ref_task.input.parameters or {}): + return ref_task.input.parameters[ref_name] + elif ref_type == "container": + if ref_name in (ref_task_output.containers or {}): + return ref_task_output.containers[ref_name].id + + return None diff --git a/eos/tasks/task_manager.py b/eos/tasks/task_manager.py new file mode 100644 index 0000000..ad10117 --- /dev/null +++ b/eos/tasks/task_manager.py @@ -0,0 +1,215 @@ +from collections.abc import AsyncIterable +from datetime import datetime, timezone +from typing import Any + +from eos.configuration.configuration_manager import ConfigurationManager +from eos.configuration.entities.task import TaskDeviceConfig +from eos.containers.entities.container import Container +from eos.experiments.repositories.experiment_repository import ExperimentRepository +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.persistence.file_db_manager import FileDbManager +from eos.tasks.entities.task import Task, TaskStatus, TaskInput, TaskOutput +from eos.tasks.exceptions import EosTaskStateError, EosTaskExistsError +from eos.tasks.repositories.task_repository import TaskRepository + + +class TaskManager: + """ + Manages the state of all tasks in EOS. + """ + + def __init__( + self, + configuration_manager: ConfigurationManager, + db_manager: DbManager, + file_db_manager: FileDbManager, + ): + self._configuration_manager = configuration_manager + self._db_manager = db_manager + self._file_db_manager = file_db_manager + self._tasks = TaskRepository("tasks", db_manager) + self._tasks.create_indices([("experiment_id", 1), ("id", 1)], unique=True) + self._experiments = ExperimentRepository("experiments", db_manager) + + log.debug("Task manager initialized.") + + def create_task( + self, + experiment_id: str, + task_id: str, + task_type: str, + devices: list[TaskDeviceConfig], + parameters: dict[str, Any] | None = None, + containers: dict[str, Container] | None = None, + metadata: dict[str, Any] | None = None, + ) -> None: + """ + Create a new task instance for a specific task type that is associated with an experiment. + + :param experiment_id: The id of the experiment. + :param task_id: The id of the task in the experiment task sequence. + :param task_type: The type of the task as defined in the configuration. + :param devices: The devices required for the task. + :param parameters: The input parameters for the task. + :param containers: The input containers for the task. + :param metadata: Additional metadata to be stored with the task. + """ + if self._tasks.get_one(experiment_id=experiment_id, id=task_id): + raise EosTaskExistsError(f"Cannot create task '{task_id}' as a task with that ID already exists.") + + task_spec = self._configuration_manager.task_specs.get_spec_by_type(task_type) + if not task_spec: + raise EosTaskStateError(f"Task type '{task_type}' does not exist.") + + task_input = TaskInput(parameters=parameters or {}, containers=containers or {}) + + task = Task( + id=task_id, + type=task_type, + experiment_id=experiment_id, + devices=[TaskDeviceConfig(id=device.id, lab_id=device.lab_id) for device in devices], + input=task_input, + metadata=metadata or {}, + ) + self._tasks.create(task.model_dump()) + + def delete_task(self, experiment_id: str, task_id: str) -> None: + """ + Delete an experiment task instance. + """ + self._validate_task_exists(experiment_id, task_id) + + self._tasks.delete(experiment_id=experiment_id, id=task_id) + + self._experiments.delete_running_task(experiment_id, task_id) + log.info(f"Deleted task '{task_id}' from experiment '{experiment_id}'.") + + def start_task(self, experiment_id: str, task_id: str) -> None: + """ + Add a task to the running tasks list and update its status to running. + """ + self._validate_task_exists(experiment_id, task_id) + self._experiments.add_running_task(experiment_id, task_id) + self._set_task_status(experiment_id, task_id, TaskStatus.RUNNING) + + def complete_task(self, experiment_id: str, task_id: str) -> None: + """ + Remove a task from the running tasks list and add it to the completed tasks list. + """ + self._validate_task_exists(experiment_id, task_id) + self._experiments.move_task_queue(experiment_id, task_id, "running_tasks", "completed_tasks") + self._set_task_status(experiment_id, task_id, TaskStatus.COMPLETED) + + def fail_task(self, experiment_id: str, task_id: str) -> None: + """ + Remove a task from the running tasks list and do not add it to the executed tasks list. Update the task status + to failed. + """ + self._validate_task_exists(experiment_id, task_id) + self._experiments.delete_running_task(experiment_id, task_id) + self._set_task_status(experiment_id, task_id, TaskStatus.FAILED) + + def cancel_task(self, experiment_id: str, task_id: str) -> None: + """ + Remove a task from the running tasks list and do not add it to the executed tasks list. Update the task status + to cancelled. + """ + self._validate_task_exists(experiment_id, task_id) + self._experiments.delete_running_task(experiment_id, task_id) + self._set_task_status(experiment_id, task_id, TaskStatus.CANCELLED) + log.warning(f"EXP '{experiment_id}' - Cancelled task '{task_id}'.") + + def get_task(self, experiment_id: str, task_id: str) -> Task | None: + """ + Get a task by its ID and experiment ID. + """ + task = self._tasks.get_one(experiment_id=experiment_id, id=task_id) + return Task(**task) if task else None + + def get_tasks(self, **query: dict[str, Any]) -> list[Task]: + """ + Query tasks with arbitrary parameters. + + :param query: Dictionary of query parameters. + """ + tasks = self._tasks.get_all(**query) + return [Task(**task) for task in tasks] + + def add_task_output(self, experiment_id: str, task_id: str, task_output: TaskOutput) -> None: + """ + Add the output of a task to the database. + """ + self._tasks.update({"output": task_output.model_dump()}, experiment_id=experiment_id, id=task_id) + + def get_task_output(self, experiment_id: str, task_id: str) -> TaskOutput | None: + """ + Get the output of a task by its ID and experiment ID. + """ + result = self._tasks.get_one(experiment_id=experiment_id, id=task_id) + if not result: + return None + + task = Task(**result) + if not task.output: + return None + + return task.output + + def add_task_output_file(self, experiment_id: str, task_id: str, file_name: str, file_data: bytes) -> None: + """ + Add a file output from a task to the file database. + """ + path = f"{experiment_id}/{task_id}/{file_name}" + self._file_db_manager.store_file(path, file_data) + + def get_task_output_file(self, experiment_id: str, task_id: str, file_name: str) -> bytes: + """ + Get a file output from a task from the file database. + """ + path = f"{experiment_id}/{task_id}/{file_name}" + return self._file_db_manager.get_file(path) + + def stream_task_output_file( + self, experiment_id: str, task_id: str, file_name: str, chunk_size: int = 3 * 1024 * 1024 + ) -> AsyncIterable[bytes]: + """ + Stream a file output from a task from the file database. + """ + path = f"{experiment_id}/{task_id}/{file_name}" + return self._file_db_manager.stream_file(path, chunk_size) + + def list_task_output_files(self, experiment_id: str, task_id: str) -> list[str]: + """ + List all file outputs from a task in the file database. + """ + prefix = f"{experiment_id}/{task_id}/" + return self._file_db_manager.list_files(prefix) + + def delete_task_output_file(self, experiment_id: str, task_id: str, file_name: str) -> None: + """ + Delete a file output from a task in the file database. + """ + path = f"{experiment_id}/{task_id}/{file_name}" + self._file_db_manager.delete_file(path) + + def _set_task_status(self, experiment_id: str, task_id: str, new_status: TaskStatus) -> None: + """ + Update the status of a task. + """ + self._validate_task_exists(experiment_id, task_id) + + update_fields = {"status": new_status.value} + if new_status == TaskStatus.RUNNING: + update_fields["start_time"] = datetime.now(tz=timezone.utc) + elif new_status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]: + update_fields["end_time"] = datetime.now(tz=timezone.utc) + + self._tasks.update(update_fields, experiment_id=experiment_id, id=task_id) + + def _validate_task_exists(self, experiment_id: str, task_id: str) -> None: + """ + Check if a task exists in an experiment. + """ + if not self._tasks.exists(experiment_id=experiment_id, id=task_id): + raise EosTaskStateError(f"Task '{task_id}' does not exist in experiment '{experiment_id}'.") diff --git a/eos/tasks/task_validator.py b/eos/tasks/task_validator.py new file mode 100644 index 0000000..0e34b97 --- /dev/null +++ b/eos/tasks/task_validator.py @@ -0,0 +1,18 @@ +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.configuration.spec_registries.task_specification_registry import TaskSpecificationRegistry +from eos.tasks.task_input_parameter_validator import TaskInputParameterValidator + + +class TaskValidator: + def __init__(self): + self.task_spec_registry = TaskSpecificationRegistry() + + def validate(self, task_config: TaskConfig) -> None: + task_spec = self.task_spec_registry.get_spec_by_config(task_config) + self._validate_parameters(task_config, task_spec) + + @staticmethod + def _validate_parameters(task_config: TaskConfig, task_spec: TaskSpecification) -> None: + validator = TaskInputParameterValidator(task_config, task_spec) + validator.validate_input_parameters() diff --git a/eos/utils/__init__.py b/eos/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/utils/dict_utils.py b/eos/utils/dict_utils.py new file mode 100644 index 0000000..50f38b8 --- /dev/null +++ b/eos/utils/dict_utils.py @@ -0,0 +1,53 @@ +from typing import Any + +import pandas as pd + + +def flatten_dict(d: dict[str, Any], sep: str = ".") -> dict[str, Any]: + """ + Flatten a nested dictionary, concatenating keys with a separator. + Works for arbitrary levels of nesting. + """ + + def _flatten(current: dict[str, Any], parent_key: str = "") -> dict[str, Any]: + items = {} + for k, v in current.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + if isinstance(v, dict): + items.update(_flatten(v, new_key)) + else: + items[new_key] = v + return items + + return _flatten(d) + + +def unflatten_dict(d: dict[str, Any], sep: str = ".") -> dict[str, Any]: + """ + Unflatten a dictionary with concatenated keys into a nested dictionary. + Works for arbitrary levels of nesting. + """ + unflattened = {} + for key, value in d.items(): + parts = key.split(sep) + current = unflattened + for part in parts[:-1]: + current = current.setdefault(part, {}) + current[parts[-1]] = value + return unflattened + + +def dicts_to_dfs(dicts: list[dict[str, Any]], sep: str = ".") -> pd.DataFrame: + """ + Convert a list of nested dictionaries to a pandas DataFrame. + Each nested dictionary is flattened, and the resulting DataFrame is created. + """ + flattened_dicts = [flatten_dict(d, sep) for d in dicts] + return pd.DataFrame(flattened_dicts) + + +def df_to_dicts(df: pd.DataFrame, sep: str = ".") -> list[dict[str, Any]]: + """ + Convert a pandas DataFrame back to a list of nested dictionaries. + """ + return [unflatten_dict(row.dropna().to_dict(), sep) for _, row in df.iterrows()] diff --git a/eos/utils/file_utils.py b/eos/utils/file_utils.py new file mode 100644 index 0000000..5e3ad38 --- /dev/null +++ b/eos/utils/file_utils.py @@ -0,0 +1,134 @@ +import dataclasses +import os +import re +import tempfile +import zipfile +from pathlib import Path + + +@dataclasses.dataclass +class FileData: + filename: str + content: bytes + + +@dataclasses.dataclass +class FolderData: + folder_name: str + content: bytes + + +def read_file(filename: str) -> FileData: + """ + Reads a file and returns its content along with its name in a FileData object. + + :param filename: The name of the file to be read. + :return: FileData object containing the filename and binary buffer. + """ + with Path(filename).open("rb") as file: + buffer = file.read() + return FileData(Path(filename).name, buffer) + + +def write_file(file_data: FileData) -> None: + """ + Writes the binary buffer from a FileData object to a file. + + :param file_data: The FileData object containing the filename and binary buffer. + """ + with Path(file_data.filename).open("wb") as file: + file.write(file_data.content) + + +def find_files_with_pattern(directory: str, pattern: str) -> list: + """ + Search for files in the specified directory that match the given regex pattern. + + :param directory: The directory path to search in. + :param pattern: The regex pattern to match filenames against. + :return: A list of filenames that match the pattern. + """ + matched_files = [] + regex = re.compile(pattern) + + if not Path(directory).is_dir(): + raise FileNotFoundError(f"Directory '{directory}' does not exist.") + + for _, _, filenames in os.walk(directory): + for filename in filenames: + if regex.match(filename): + matched_files.append(filename) + + return matched_files + + +def find_highest_numbered_files(file_list: list) -> list: + """ + Find all files with the greatest number following the dash or underscore in their names. + All files returned will have the same number. + Works with files formatted as "*-NUM.*" + + :param file_list: List of filenames. + :return: List of filenames with the greatest common number. + """ + number_pattern = re.compile(r"[\-_]([0-9]+)\.") + + numbers = {} + for file in file_list: + match = number_pattern.search(file) + if match: + number = int(match.group(1)) + if number in numbers: + numbers[number].append(file) + else: + numbers[number] = [file] + + if not numbers: + return [] + + max_number = max(numbers.keys()) + + return numbers[max_number] + + +def read_folder(folder_name: str) -> FolderData: + """ + Zips a folder and reads it as a binary buffer. + + :param folder_name: The name of the folder to be zipped and read. + :return: FolderData object containing the folder name and binary buffer of the zipped folder. + """ + with tempfile.TemporaryFile() as temp_zip: + with zipfile.ZipFile(temp_zip, "w", zipfile.ZIP_DEFLATED) as zipf: + for root, _, files in os.walk(folder_name): + for file in files: + zipf.write( + Path(root) / file, + os.path.relpath(Path(root) / file, Path(folder_name).parent), + ) + + temp_zip.seek(0) + buffer = temp_zip.read() + + return FolderData(Path(folder_name).name, buffer) + + +def write_folder(folder_data: FolderData) -> None: + """ + Decompresses the zip binary buffer and recreates the folder with a new name. + + :param folder_data: The FolderData object containing the new folder name and binary buffer. + """ + with tempfile.TemporaryDirectory() as temp_dir: + zip_filename = Path(temp_dir) / "temp.zip" + + with Path(zip_filename).open("wb") as file: + file.write(folder_data.content) + + with zipfile.ZipFile(zip_filename, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + root_folder = next(os.walk(temp_dir))[1][0] + extracted_folder_path = Path(temp_dir) / root_folder + + Path(extracted_folder_path).rename(folder_data.folder_name) diff --git a/eos/utils/ray_utils.py b/eos/utils/ray_utils.py new file mode 100644 index 0000000..da2031e --- /dev/null +++ b/eos/utils/ray_utils.py @@ -0,0 +1,40 @@ +from typing import Any + +import ray +from ray.actor import ActorHandle + + +class RayActorWrapper: + """ + Wrapper for Ray actors to allow for easy synchronous calls to actor methods. + """ + + def __init__(self, actor: ActorHandle): + self.actor = actor + + def __getattr__(self, name: str) -> Any: + if not name.startswith("__"): + async_func = getattr(self.actor, name) + + def wrapper(*args, **kwargs) -> Any: + return ray.get(async_func.remote(*args, **kwargs)) + + return wrapper + + return super().__getattr__(name) + + +def ray_run(ray_remote_method: callable, *args, **kwargs) -> Any: + """ + A helper function to simplify calling Ray remote functions. + + Args: + ray_remote_method: The Ray remote method to be invoked. + *args: Arguments to be passed to the remote method. + **kwargs: Keyword arguments to be passed to the remote method. + + Returns: + The result of the Ray remote method call. + """ + # Invoke the remote method and get the result + return ray.get(ray_remote_method.remote(*args, **kwargs)) diff --git a/eos/utils/singleton.py b/eos/utils/singleton.py new file mode 100644 index 0000000..5d30505 --- /dev/null +++ b/eos/utils/singleton.py @@ -0,0 +1,10 @@ +from typing import ClassVar + + +class Singleton(type): + _instances: ClassVar = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/eos/web_api/__init__.py b/eos/web_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/web_api/common/__init__.py b/eos/web_api/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/web_api/common/entities.py b/eos/web_api/common/entities.py new file mode 100644 index 0000000..fe3328e --- /dev/null +++ b/eos/web_api/common/entities.py @@ -0,0 +1,55 @@ +from typing import Any + +from pydantic import BaseModel + +from eos.campaigns.entities.campaign import CampaignExecutionParameters +from eos.configuration.entities.task import TaskConfig +from eos.experiments.entities.experiment import ExperimentExecutionParameters + + +class SubmitTaskRequest(BaseModel): + task_config: TaskConfig + resource_allocation_priority: int = 1 + resource_allocation_timeout: int = 180 + + +class TaskTypesResponse(BaseModel): + task_types: list[str] | str + + +class SubmitExperimentRequest(BaseModel): + experiment_id: str + experiment_type: str + experiment_execution_parameters: ExperimentExecutionParameters + dynamic_parameters: dict[str, dict[str, Any]] + metadata: dict[str, Any] = {} + + +class ExperimentTypes(BaseModel): + experiment_types: list[str] | str + + +class ExperimentLoadedStatusesResponse(BaseModel): + experiment_loaded_statuses: dict[str, bool] + + +class ExperimentTypesResponse(BaseModel): + experiment_types: list[str] + + +class ExperimentDynamicParamsTemplateResponse(BaseModel): + dynamic_params_template: str + + +class SubmitCampaignRequest(BaseModel): + campaign_id: str + experiment_type: str + campaign_execution_parameters: CampaignExecutionParameters + + +class LabTypes(BaseModel): + lab_types: list[str] | str + + +class LabLoadedStatusesResponse(BaseModel): + lab_loaded_statuses: dict[str, bool] diff --git a/eos/web_api/orchestrator/__init__.py b/eos/web_api/orchestrator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/web_api/orchestrator/controllers/__init__.py b/eos/web_api/orchestrator/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/web_api/orchestrator/controllers/campaign_controller.py b/eos/web_api/orchestrator/controllers/campaign_controller.py new file mode 100644 index 0000000..20312d4 --- /dev/null +++ b/eos/web_api/orchestrator/controllers/campaign_controller.py @@ -0,0 +1,33 @@ +from litestar import Controller, Response, get +from litestar.handlers import post +from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_201_CREATED + +from eos.orchestration.orchestrator import Orchestrator +from eos.web_api.common.entities import SubmitCampaignRequest +from eos.web_api.public.exception_handling import handle_exceptions + + +class CampaignController(Controller): + path = "/campaigns" + + @get("/{campaign_id:str}") + @handle_exceptions("Failed to get campaign") + async def get_campaign(self, campaign_id: str, orchestrator: Orchestrator) -> Response: + campaign = await orchestrator.get_campaign(campaign_id) + + if campaign is None: + return Response(content={"error": "Campaign not found"}, status_code=HTTP_404_NOT_FOUND) + + return Response(content=campaign.model_dump_json(), status_code=HTTP_200_OK) + + @post("/submit") + @handle_exceptions("Failed to submit campaign") + async def submit_campaign(self, data: SubmitCampaignRequest, orchestrator: Orchestrator) -> Response: + await orchestrator.submit_campaign(data.campaign_id, data.experiment_type, data.campaign_execution_parameters) + return Response(content=None, status_code=HTTP_201_CREATED) + + @post("/{campaign_id:str}/cancel") + @handle_exceptions("Failed to cancel campaign") + async def cancel_campaign(self, campaign_id: str, orchestrator: Orchestrator) -> Response: + await orchestrator.cancel_campaign(campaign_id) + return Response(content=None, status_code=HTTP_200_OK) diff --git a/eos/web_api/orchestrator/controllers/experiment_controller.py b/eos/web_api/orchestrator/controllers/experiment_controller.py new file mode 100644 index 0000000..b0cca83 --- /dev/null +++ b/eos/web_api/orchestrator/controllers/experiment_controller.py @@ -0,0 +1,75 @@ +from litestar import Controller, get, put, Response +from litestar.handlers import post +from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_404_NOT_FOUND + +from eos.orchestration.orchestrator import Orchestrator +from eos.web_api.common.entities import ( + SubmitExperimentRequest, + ExperimentTypesResponse, + ExperimentLoadedStatusesResponse, + ExperimentTypes, +) +from eos.web_api.public.exception_handling import handle_exceptions + + +class ExperimentController(Controller): + path = "/experiments" + + @get("/{experiment_id:str}") + async def get_experiment(self, experiment_id: str, orchestrator: Orchestrator) -> Response: + experiment = await orchestrator.get_experiment(experiment_id) + + if experiment is None: + return Response(content={"error": "Experiment not found"}, status_code=HTTP_404_NOT_FOUND) + + return Response(content=experiment.model_dump_json(), status_code=HTTP_200_OK) + + @post("/submit") + @handle_exceptions("Failed to submit experiment") + async def submit_experiment(self, data: SubmitExperimentRequest, orchestrator: Orchestrator) -> Response: + await orchestrator.submit_experiment( + data.experiment_id, + data.experiment_type, + data.experiment_execution_parameters, + data.dynamic_parameters, + data.metadata, + ) + return Response(content=None, status_code=HTTP_201_CREATED) + + @post("/{experiment_id:str}/cancel") + @handle_exceptions("Failed to cancel experiment") + async def cancel_experiment(self, experiment_id: str, orchestrator: Orchestrator) -> Response: + await orchestrator.cancel_experiment(experiment_id) + return Response(content=None, status_code=HTTP_200_OK) + + @put("/update_loaded") + @handle_exceptions("Failed to update loaded experiments") + async def update_loaded_experiments(self, data: ExperimentTypes, orchestrator: Orchestrator) -> Response: + await orchestrator.update_loaded_experiments(set(data.experiment_types)) + return Response(content=None, status_code=HTTP_200_OK) + + @put("/reload") + @handle_exceptions("Failed to reload experiments") + async def reload_experiments(self, data: ExperimentTypes, orchestrator: Orchestrator) -> Response: + await orchestrator.reload_experiments(set(data.experiment_types)) + return Response(content=None, status_code=HTTP_200_OK) + + @get("/types") + @handle_exceptions("Failed to get experiment types") + async def get_experiment_types(self, orchestrator: Orchestrator) -> ExperimentTypesResponse: + experiment_types = await orchestrator.get_experiment_types() + return ExperimentTypesResponse(experiment_types=experiment_types) + + @get("/loaded_statuses") + @handle_exceptions("Failed to get experiment loaded statuses") + async def get_experiment_loaded_statuses(self, orchestrator: Orchestrator) -> ExperimentLoadedStatusesResponse: + experiment_loaded_statuses = await orchestrator.get_experiment_loaded_statuses() + return ExperimentLoadedStatusesResponse(experiment_loaded_statuses=experiment_loaded_statuses) + + @get("/{experiment_type:str}/dynamic_params_template") + @handle_exceptions("Failed to get dynamic parameters template") + async def get_experiment_dynamic_params_template( + self, experiment_type: str, orchestrator: Orchestrator + ) -> Response: + dynamic_params_template = await orchestrator.get_experiment_dynamic_params_template(experiment_type) + return Response(content=dynamic_params_template, status_code=HTTP_200_OK) diff --git a/eos/web_api/orchestrator/controllers/file_controller.py b/eos/web_api/orchestrator/controllers/file_controller.py new file mode 100644 index 0000000..48fc445 --- /dev/null +++ b/eos/web_api/orchestrator/controllers/file_controller.py @@ -0,0 +1,74 @@ +import io +import zipfile +from collections.abc import AsyncIterable +from pathlib import Path + +from litestar import Controller, get +from litestar.exceptions import HTTPException +from litestar.response import Stream + +from eos.orchestration.orchestrator import Orchestrator +from eos.web_api.public.exception_handling import handle_exceptions + +_CHUNK_SIZE = 3 * 1024 * 1024 # 3MB + + +class FileController(Controller): + path = "/files" + + @get("/download/{experiment_id:str}/{task_id:str}/{file_name:str}") + @handle_exceptions("Failed to download file") + async def download_task_output_file( + self, experiment_id: str, task_id: str, file_name: str, orchestrator: Orchestrator + ) -> Stream: + async def file_stream() -> AsyncIterable: + try: + async for chunk in orchestrator.stream_task_output_file( + experiment_id, task_id, file_name, chunk_size=_CHUNK_SIZE + ): + yield chunk + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + return Stream(file_stream(), headers={"Content-Disposition": f"attachment; filename={file_name}"}) + + @get("/download/{experiment_id:str}/{task_id:str}") + @handle_exceptions("Failed to download zipped task output files") + async def download_task_output_files_zipped( + self, experiment_id: str, task_id: str, orchestrator: Orchestrator + ) -> Stream: + async def zip_stream() -> AsyncIterable: + try: + file_list = await orchestrator.list_task_output_files(experiment_id, task_id) + + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + for file_path in file_list: + file_name = Path(file_path).name + + zip_info = zipfile.ZipInfo(file_name) + zip_info.compress_type = zipfile.ZIP_DEFLATED + + with zip_file.open(zip_info, mode="w") as file_in_zip: + async for chunk in orchestrator.stream_task_output_file(experiment_id, task_id, file_name): + file_in_zip.write(chunk) + + if buffer.tell() > _CHUNK_SIZE: + buffer.seek(0) + yield buffer.read(_CHUNK_SIZE) + buffer.seek(0) + buffer.truncate() + + buffer.seek(0) + while True: + chunk = buffer.read(_CHUNK_SIZE) + if not chunk: + break + yield chunk + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + return Stream( + zip_stream(), headers={"Content-Disposition": f"attachment; filename={experiment_id}_{task_id}_output.zip"} + ) diff --git a/eos/web_api/orchestrator/controllers/lab_controller.py b/eos/web_api/orchestrator/controllers/lab_controller.py new file mode 100644 index 0000000..a1bc80a --- /dev/null +++ b/eos/web_api/orchestrator/controllers/lab_controller.py @@ -0,0 +1,43 @@ +from litestar import Controller, put, get, Response +from litestar.status_codes import HTTP_200_OK +from omegaconf import OmegaConf + +from eos.orchestration.orchestrator import Orchestrator +from eos.web_api.common.entities import LabLoadedStatusesResponse, LabTypes +from eos.web_api.public.exception_handling import handle_exceptions + + +class LabController(Controller): + path = "/labs" + + @get("/devices") + @handle_exceptions("Failed to get lab devices") + async def get_lab_devices( + self, lab_types: list[str] | None, task_type: str | None, orchestrator: Orchestrator + ) -> Response: + lab_devices = await orchestrator.get_lab_devices(lab_types, task_type) + + # Convert LabDeviceConfig objects to plain dictionaries + dict_lab_devices = {} + for lab_type, devices in lab_devices.items(): + dict_lab_devices[lab_type] = {name: OmegaConf.to_object(device) for name, device in devices.items()} + + return Response(content=dict_lab_devices, status_code=HTTP_200_OK) + + @put("/update_loaded") + @handle_exceptions("Failed to update loaded labs") + async def update_loaded_labs(self, data: LabTypes, orchestrator: Orchestrator) -> Response: + await orchestrator.update_loaded_labs(set(data.lab_types)) + return Response(content=None, status_code=HTTP_200_OK) + + @put("/reload") + @handle_exceptions("Failed to reload labs") + async def reload_labs(self, data: LabTypes, orchestrator: Orchestrator) -> Response: + await orchestrator.reload_labs(set(data.lab_types)) + return Response(content=None, status_code=HTTP_200_OK) + + @get("/loaded_statuses") + @handle_exceptions("Failed to get lab loaded statuses") + async def get_lab_loaded_statuses(self, orchestrator: Orchestrator) -> LabLoadedStatusesResponse: + lab_loaded_statuses = await orchestrator.get_lab_loaded_statuses() + return LabLoadedStatusesResponse(lab_loaded_statuses=lab_loaded_statuses) diff --git a/eos/web_api/orchestrator/controllers/task_controller.py b/eos/web_api/orchestrator/controllers/task_controller.py new file mode 100644 index 0000000..023f84d --- /dev/null +++ b/eos/web_api/orchestrator/controllers/task_controller.py @@ -0,0 +1,45 @@ +from litestar import Controller, Response, get +from litestar.handlers import post +from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED +from omegaconf import OmegaConf + +from eos.orchestration.orchestrator import Orchestrator +from eos.web_api.common.entities import SubmitTaskRequest, TaskTypesResponse +from eos.web_api.public.exception_handling import handle_exceptions + + +class TaskController(Controller): + path = "/tasks" + + @get("/{experiment_id:str}/{task_id:str}") + @handle_exceptions("Failed to get task") + async def get_task(self, experiment_id: str, task_id: str, orchestrator: Orchestrator) -> Response: + task = await orchestrator.get_task(experiment_id, task_id) + return Response(content=task.model_dump_json(), status_code=HTTP_200_OK) + + @post("/submit") + @handle_exceptions("Failed to submit task") + async def submit_task(self, data: SubmitTaskRequest, orchestrator: Orchestrator) -> Response: + await orchestrator.submit_task( + data.task_config, data.resource_allocation_priority, data.resource_allocation_timeout + ) + return Response(content=None, status_code=HTTP_201_CREATED) + + @post("/{task_id:str}/cancel") + @handle_exceptions("Failed to cancel task") + async def cancel_task(self, task_id: str, orchestrator: Orchestrator) -> Response: + await orchestrator.cancel_task(task_id) + return Response(content=None, status_code=HTTP_200_OK) + + @get("/types") + @handle_exceptions("Failed to get task types") + async def get_task_types(self, orchestrator: Orchestrator) -> TaskTypesResponse: + task_types = await orchestrator.get_task_types() + return TaskTypesResponse(task_types=task_types) + + @get("/{task_type:str}/spec") + @handle_exceptions("Failed to get task spec") + async def get_task_spec(self, task_type: str, orchestrator: Orchestrator) -> Response: + task_spec = await orchestrator.get_task_spec(task_type) + task_spec = OmegaConf.to_object(task_spec) + return Response(content=task_spec, status_code=HTTP_200_OK) diff --git a/eos/web_api/orchestrator/exception_handling.py b/eos/web_api/orchestrator/exception_handling.py new file mode 100644 index 0000000..b7cdd08 --- /dev/null +++ b/eos/web_api/orchestrator/exception_handling.py @@ -0,0 +1,40 @@ +from functools import wraps + +from litestar import Response, status_codes, Request + +from eos.logging.logger import log + + +class AppError(Exception): + def __init__( + self, message: str, status_code: int = status_codes.HTTP_500_INTERNAL_SERVER_ERROR, expose_message: bool = False + ): + self.message = message + self.status_code = status_code + self.expose_message = expose_message + + +def handle_exceptions(error_msg: str) -> callable: + def decorator(func: callable) -> callable: + @wraps(func) + async def wrapper(*args, **kwargs) -> Response: + try: + return await func(*args, **kwargs) + except Exception as e: + raise AppError(f"{error_msg}: {e!s}", expose_message=True) from e + + return wrapper + + return decorator + + +def global_exception_handler(request: Request, exc: Exception) -> Response: + log.error(f"Web API error: {exc!s}") + if isinstance(exc, AppError): + content = {"message": exc.message} if exc.expose_message else {"message": "An error occurred"} + return Response(content=content, status_code=exc.status_code) + + # For any other exception, return a generic error message + return Response( + content={"message": "An unexpected error occurred"}, status_code=status_codes.HTTP_500_INTERNAL_SERVER_ERROR + ) diff --git a/eos/web_api/public/__init__.py b/eos/web_api/public/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/web_api/public/controllers/__init__.py b/eos/web_api/public/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/web_api/public/controllers/campaign_controller.py b/eos/web_api/public/controllers/campaign_controller.py new file mode 100644 index 0000000..c1bf2ab --- /dev/null +++ b/eos/web_api/public/controllers/campaign_controller.py @@ -0,0 +1,47 @@ +from litestar import Controller, Response, get +from litestar.datastructures import State +from litestar.exceptions import HTTPException +from litestar.handlers import post +from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_201_CREATED + +from eos.web_api.common.entities import SubmitCampaignRequest +from eos.web_api.public.exception_handling import handle_exceptions + + +class CampaignController(Controller): + path = "/campaigns" + + @get("/{campaign_id:str}") + @handle_exceptions("Failed to get campaign") + async def get_campaign(self, campaign_id: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get(f"/api/campaigns/{campaign_id}") as response: + if response.status == HTTP_200_OK: + campaign = await response.json() + return Response(content=campaign, status_code=HTTP_200_OK) + if response.status == HTTP_404_NOT_FOUND: + return Response(content={"error": "Campaign not found"}, status_code=HTTP_404_NOT_FOUND) + + raise HTTPException(status_code=response.status, detail="Error fetching campaign") + + @post("/submit") + @handle_exceptions("Failed to submit campaign") + async def submit_campaign(self, data: SubmitCampaignRequest, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.post("/api/campaigns/submit", json=data.model_dump()) as response: + if response.status == HTTP_201_CREATED: + return Response(content=None, status_code=HTTP_201_CREATED) + + raise HTTPException(status_code=response.status, detail="Error submitting campaign") + + @post("/{campaign_id:str}/cancel") + @handle_exceptions("Failed to cancel campaign") + async def cancel_campaign(self, campaign_id: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.post(f"/api/campaigns/{campaign_id}/cancel") as response: + if response.status == HTTP_200_OK: + return Response(content=None, status_code=HTTP_200_OK) + if response.status == HTTP_404_NOT_FOUND: + return Response(content={"error": "Campaign not found"}, status_code=HTTP_404_NOT_FOUND) + + raise HTTPException(status_code=response.status, detail="Error cancelling campaign") diff --git a/eos/web_api/public/controllers/experiment_controller.py b/eos/web_api/public/controllers/experiment_controller.py new file mode 100644 index 0000000..3049040 --- /dev/null +++ b/eos/web_api/public/controllers/experiment_controller.py @@ -0,0 +1,116 @@ +from litestar import Controller, get, put, Response +from litestar.datastructures import State +from litestar.exceptions import HTTPException +from litestar.handlers import post +from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_404_NOT_FOUND + +from eos.web_api.common.entities import ( + SubmitExperimentRequest, + ExperimentTypesResponse, + ExperimentLoadedStatusesResponse, + ExperimentTypes, +) +from eos.web_api.public.exception_handling import handle_exceptions + + +class ExperimentController(Controller): + path = "/experiments" + + @get("/{experiment_id:str}") + async def get_experiment(self, experiment_id: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get(f"/api/experiments/{experiment_id}") as response: + if response.status == HTTP_200_OK: + experiment = await response.json() + return Response(content=experiment, status_code=HTTP_200_OK) + if response.status == HTTP_404_NOT_FOUND: + return Response(content={"error": "Experiment not found"}, status_code=HTTP_404_NOT_FOUND) + + raise HTTPException(status_code=response.status, detail="Error fetching experiment") + + @post("/submit") + @handle_exceptions("Failed to submit experiment") + async def submit_experiment(self, data: SubmitExperimentRequest, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.post("/api/experiments/submit", json=data.model_dump()) as response: + if response.status == HTTP_201_CREATED: + return Response(content=None, status_code=HTTP_201_CREATED) + + raise HTTPException(status_code=response.status, detail="Error submitting experiment") + + @post("/{experiment_id:str}/cancel") + @handle_exceptions("Failed to cancel experiment") + async def cancel_experiment(self, experiment_id: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.post(f"/api/experiments/{experiment_id}/cancel") as response: + if response.status == HTTP_200_OK: + return Response(content={"message": "Experiment cancelled successfully"}, status_code=HTTP_200_OK) + if response.status == HTTP_404_NOT_FOUND: + return Response(content={"error": "Experiment not found"}, status_code=HTTP_404_NOT_FOUND) + + raise HTTPException(status_code=response.status, detail="Error cancelling experiment") + + @put("/update_loaded") + @handle_exceptions("Failed to update loaded experiments") + async def update_loaded_experiments(self, data: ExperimentTypes, state: State) -> Response: + orchestrator_client = state.orchestrator_client + if isinstance(data.experiment_types, str): + if data.experiment_types in ["", "[]"]: + data.experiment_types = [] + else: + data.experiment_types = [data.experiment_types] + async with orchestrator_client.put( + "/api/experiments/update_loaded", json={"experiment_types": data.experiment_types} + ) as response: + if response.status == HTTP_200_OK: + return Response(content={"message": "Experiments updated successfully"}, status_code=HTTP_200_OK) + + raise HTTPException(status_code=response.status, detail="Error updating loaded experiments") + + @put("/reload") + @handle_exceptions("Failed to reload experiments") + async def reload_experiments(self, data: ExperimentTypes, state: State) -> Response: + orchestrator_client = state.orchestrator_client + if isinstance(data.experiment_types, str): + if data.experiment_types in ["", "[]"]: + data.experiment_types = [] + else: + data.experiment_types = [data.experiment_types] + async with orchestrator_client.put( + "/api/experiments/reload", json={"experiment_types": data.experiment_types} + ) as response: + if response.status == HTTP_200_OK: + return Response(content={"message": "Experiments reloaded successfully"}, status_code=HTTP_200_OK) + + raise HTTPException(status_code=response.status, detail="Error reloading experiments") + + @get("/types") + @handle_exceptions("Failed to get experiment types") + async def get_experiment_types(self, state: State) -> ExperimentTypesResponse: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get("/api/experiments/types") as response: + if response.status == HTTP_200_OK: + return ExperimentTypesResponse(**await response.json()) + + raise HTTPException(status_code=response.status, detail="Error fetching experiment types") + + @get("/loaded_statuses") + @handle_exceptions("Failed to get experiment loaded statuses") + async def get_experiment_loaded_statuses(self, state: State) -> ExperimentLoadedStatusesResponse: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get("/api/experiments/loaded_statuses") as response: + if response.status == HTTP_200_OK: + return ExperimentLoadedStatusesResponse(**await response.json()) + + raise HTTPException(status_code=response.status, detail="Error fetching experiment loaded statuses") + + @get("/{experiment_type:str}/dynamic_params_template") + @handle_exceptions("Failed to get dynamic parameters template") + async def get_experiment_dynamic_params_template(self, experiment_type: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get(f"/api/experiments/{experiment_type}/dynamic_params_template") as response: + if response.status == HTTP_200_OK: + dynamic_params_template = await response.json() + return Response(content=dynamic_params_template, status_code=HTTP_200_OK) + + raise HTTPException(status_code=response.status, detail="Error fetching dynamic parameters template") diff --git a/eos/web_api/public/controllers/file_controller.py b/eos/web_api/public/controllers/file_controller.py new file mode 100644 index 0000000..c226b11 --- /dev/null +++ b/eos/web_api/public/controllers/file_controller.py @@ -0,0 +1,49 @@ +from collections.abc import AsyncIterable + +from litestar import Controller, get +from litestar.datastructures import State +from litestar.exceptions import HTTPException +from litestar.response import Stream +from litestar.status_codes import HTTP_200_OK + +from eos.web_api.public.exception_handling import handle_exceptions + + +class FileController(Controller): + path = "/files" + + @get("/download/{experiment_id:str}/{task_id:str}/{file_name:str}") + @handle_exceptions("Failed to download file") + async def download_task_output_file(self, experiment_id: str, task_id: str, file_name: str, state: State) -> Stream: + orchestrator_client = state.orchestrator_client + + async def file_stream() -> AsyncIterable: + async with orchestrator_client.get( + f"/api/files/download/{experiment_id}/{task_id}/{file_name}", chunked=True + ) as response: + if response.status == HTTP_200_OK: + async for chunk in response.content.iter_any(): + yield chunk + else: + raise HTTPException(status_code=response.status, detail="Error downloading file") + + return Stream(file_stream(), headers={"Content-Disposition": f"attachment; filename={file_name}"}) + + @get("/download/{experiment_id:str}/{task_id:str}") + @handle_exceptions("Failed to download zipped task output files") + async def download_task_output_files_zipped(self, experiment_id: str, task_id: str, state: State) -> Stream: + orchestrator_client = state.orchestrator_client + + async def zip_stream() -> AsyncIterable: + async with orchestrator_client.get( + f"/api/files/download/{experiment_id}/{task_id}", chunked=True + ) as response: + if response.status == HTTP_200_OK: + async for chunk in response.content.iter_any(): + yield chunk + else: + raise HTTPException(status_code=response.status, detail="Error downloading zipped files") + + return Stream( + zip_stream(), headers={"Content-Disposition": f"attachment; filename={experiment_id}_{task_id}_output.zip"} + ) diff --git a/eos/web_api/public/controllers/lab_controller.py b/eos/web_api/public/controllers/lab_controller.py new file mode 100644 index 0000000..ce3cb22 --- /dev/null +++ b/eos/web_api/public/controllers/lab_controller.py @@ -0,0 +1,73 @@ +from litestar import Controller, put, get, Response +from litestar.datastructures import State +from litestar.exceptions import HTTPException +from litestar.status_codes import HTTP_200_OK + +from eos.web_api.common.entities import LabLoadedStatusesResponse, LabTypes +from eos.web_api.public.exception_handling import handle_exceptions + + +class LabController(Controller): + path = "/labs" + + @get("/devices") + @handle_exceptions("Failed to get lab devices") + async def get_lab_devices(self, lab_types: list[str] | None, task_type: str | None, state: State) -> Response: + orchestrator_client = state.orchestrator_client + + params = {} + if lab_types: + params["lab_types"] = ",".join(lab_types) + if task_type: + params["task_type"] = task_type + + async with orchestrator_client.get("/api/labs/devices", params=params) as response: + if response.status == HTTP_200_OK: + lab_devices = await response.json() + return Response(content=lab_devices, status_code=HTTP_200_OK) + + raise HTTPException(status_code=response.status, detail="Error fetching lab devices") + + @put("/update_loaded") + @handle_exceptions("Failed to update loaded labs") + async def update_loaded_labs(self, data: LabTypes, state: State) -> Response: + orchestrator_client = state.orchestrator_client + + if isinstance(data.lab_types, str): + if data.lab_types in ["", "[]"]: + data.lab_types = [] + else: + data.lab_types = [data.lab_types] + + async with orchestrator_client.put("/api/labs/update_loaded", json={"lab_types": data.lab_types}) as response: + if response.status == HTTP_200_OK: + return Response(content={"message": "Labs updated successfully"}, status_code=HTTP_200_OK) + + raise HTTPException(status_code=response.status, detail="Error updating loaded labs") + + @put("/reload") + @handle_exceptions("Failed to reload labs") + async def reload_labs(self, data: LabTypes, state: State) -> Response: + orchestrator_client = state.orchestrator_client + + if isinstance(data.lab_types, str): + if data.lab_types in ["", "[]"]: + data.lab_types = [] + else: + data.lab_types = [data.lab_types] + + async with orchestrator_client.put("/api/labs/reload", json={"lab_types": data.lab_types}) as response: + if response.status == HTTP_200_OK: + return Response(content={"message": "Labs reloaded successfully"}, status_code=HTTP_200_OK) + + raise HTTPException(status_code=response.status, detail="Error reloading labs") + + @get("/loaded_statuses") + @handle_exceptions("Failed to get lab loaded statuses") + async def get_lab_loaded_statuses(self, state: State) -> LabLoadedStatusesResponse: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get("/api/labs/loaded_statuses") as response: + if response.status == HTTP_200_OK: + return LabLoadedStatusesResponse(**await response.json()) + + raise HTTPException(status_code=response.status, detail="Error fetching lab loaded statuses") diff --git a/eos/web_api/public/controllers/task_controller.py b/eos/web_api/public/controllers/task_controller.py new file mode 100644 index 0000000..8fab1e3 --- /dev/null +++ b/eos/web_api/public/controllers/task_controller.py @@ -0,0 +1,71 @@ +from litestar import Controller, Response, get +from litestar.datastructures import State +from litestar.exceptions import HTTPException +from litestar.handlers import post +from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_404_NOT_FOUND + +from eos.web_api.common.entities import SubmitTaskRequest, TaskTypesResponse +from eos.web_api.public.exception_handling import handle_exceptions + + +class TaskController(Controller): + path = "/tasks" + + @get("/{experiment_id:str}/{task_id:str}") + @handle_exceptions("Failed to get task") + async def get_task(self, experiment_id: str, task_id: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get(f"/api/tasks/{experiment_id}/{task_id}") as response: + if response.status == HTTP_200_OK: + task = await response.json() + return Response(content=task, status_code=HTTP_200_OK) + if response.status == HTTP_404_NOT_FOUND: + return Response(content={"error": "Task not found"}, status_code=HTTP_404_NOT_FOUND) + + raise HTTPException(status_code=response.status, detail="Error fetching task") + + @post("/submit") + @handle_exceptions("Failed to submit task") + async def submit_task(self, data: SubmitTaskRequest, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.post("/api/tasks/submit", json=data.model_dump()) as response: + if response.status == HTTP_201_CREATED: + result = await response.json() + return Response(content=result, status_code=HTTP_201_CREATED) + + raise HTTPException(status_code=response.status, detail="Error submitting task") + + @post("/{task_id:str}/cancel") + @handle_exceptions("Failed to cancel task") + async def cancel_task(self, task_id: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.post(f"/api/tasks/{task_id}/cancel") as response: + if response.status == HTTP_200_OK: + return Response(content={"message": "Task cancelled successfully"}, status_code=HTTP_200_OK) + if response.status == HTTP_404_NOT_FOUND: + return Response(content={"error": "Task not found"}, status_code=HTTP_404_NOT_FOUND) + + raise HTTPException(status_code=response.status, detail="Error cancelling task") + + @get("/types") + @handle_exceptions("Failed to get task types") + async def get_task_types(self, state: State) -> TaskTypesResponse: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get("/api/tasks/types") as response: + if response.status == HTTP_200_OK: + return TaskTypesResponse(**await response.json()) + + raise HTTPException(status_code=response.status, detail="Error fetching task types") + + @get("/{task_type:str}/spec") + @handle_exceptions("Failed to get task spec") + async def get_task_spec(self, task_type: str, state: State) -> Response: + orchestrator_client = state.orchestrator_client + async with orchestrator_client.get(f"/api/tasks/{task_type}/spec") as response: + if response.status == HTTP_200_OK: + task_spec = await response.json() + return Response(content=task_spec, status_code=HTTP_200_OK) + if response.status == HTTP_404_NOT_FOUND: + return Response(content={"error": "Task specification not found"}, status_code=HTTP_404_NOT_FOUND) + + raise HTTPException(status_code=response.status, detail="Error fetching task specification") diff --git a/eos/web_api/public/exception_handling.py b/eos/web_api/public/exception_handling.py new file mode 100644 index 0000000..a727d2e --- /dev/null +++ b/eos/web_api/public/exception_handling.py @@ -0,0 +1,40 @@ +from functools import wraps + +from litestar import Response, status_codes, Request + +from eos.logging.logger import log + + +class AppError(Exception): + def __init__( + self, message: str, status_code: int = status_codes.HTTP_500_INTERNAL_SERVER_ERROR, expose_message: bool = False + ) -> None: + self.message = message + self.status_code = status_code + self.expose_message = expose_message + + +def handle_exceptions(error_msg: str) -> callable: + def decorator(func: callable) -> callable: + @wraps(func) + async def wrapper(*args, **kwargs) -> Response: + try: + return await func(*args, **kwargs) + except Exception as e: + raise AppError(f"{error_msg}: {e!s}", expose_message=True) from e + + return wrapper + + return decorator + + +def global_exception_handler(request: Request, exc: Exception) -> Response: + log.error(f"Error: {exc!s}") + if isinstance(exc, AppError): + content = {"message": exc.message} if exc.expose_message else {"message": "An error occurred"} + return Response(content=content, status_code=exc.status_code) + + # For any other exception, return a generic error message + return Response( + content={"message": "An unexpected error occurred"}, status_code=status_codes.HTTP_500_INTERNAL_SERVER_ERROR + ) diff --git a/eos/web_api/public/server.py b/eos/web_api/public/server.py new file mode 100644 index 0000000..2e3e848 --- /dev/null +++ b/eos/web_api/public/server.py @@ -0,0 +1,50 @@ +import os +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +import aiohttp +from litestar import Litestar, Router +from litestar.logging import LoggingConfig +from litestar.openapi import OpenAPIConfig + +from eos.web_api.public.controllers.campaign_controller import CampaignController +from eos.web_api.public.controllers.experiment_controller import ExperimentController +from eos.web_api.public.controllers.file_controller import FileController +from eos.web_api.public.controllers.lab_controller import LabController +from eos.web_api.public.controllers.task_controller import TaskController +from eos.web_api.public.exception_handling import global_exception_handler + +orchestrator_host = os.getenv("EOS_ORCHESTRATOR_HOST", "localhost") +orchestrator_port = os.getenv("EOS_ORCHESTRATOR_PORT", "8070") + + +@asynccontextmanager +async def orchestrator_client(app: Litestar) -> AsyncGenerator[None, None]: + client = getattr(app.state, "client", None) + if client is None: + client = aiohttp.ClientSession(base_url=f"http://{orchestrator_host}:{orchestrator_port}") + app.state.orchestrator_client = client + try: + yield + finally: + await client.close() + + +api_router = Router( + path="/api", + route_handlers=[TaskController, ExperimentController, CampaignController, LabController, FileController], + exception_handlers={Exception: global_exception_handler}, +) + +logging_config = LoggingConfig(configure_root_logger=False) +app = Litestar( + route_handlers=[api_router], + logging_config=logging_config, + lifespan=[orchestrator_client], + openapi_config=OpenAPIConfig( + title="EOS REST API", + description="Documentation for the EOS REST API", + version="0.5.0", + path="/docs", + ), +) diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..0d6439e --- /dev/null +++ b/pdm.lock @@ -0,0 +1,3701 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev", "docs"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:8c7e0b866c8de9954f521425fe4a268163c441bfce9aea93f717e5393ae11116" + +[[metadata.targets]] +requires_python = ">=3.10" + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +requires_python = ">=3.9" +summary = "A collection of accessible pygments styles" +groups = ["docs"] +dependencies = [ + "pygments>=1.5", +] +files = [ + {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, + {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.3.5" +requires_python = ">=3.8" +summary = "Happy Eyeballs for asyncio" +groups = ["default"] +files = [ + {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, + {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, +] + +[[package]] +name = "aiohttp" +version = "3.10.3" +requires_python = ">=3.8" +summary = "Async http client/server framework (asyncio)" +groups = ["default"] +dependencies = [ + "aiohappyeyeballs>=2.3.0", + "aiosignal>=1.1.2", + "async-timeout<5.0,>=4.0; python_version < \"3.11\"", + "attrs>=17.3.0", + "frozenlist>=1.1.1", + "multidict<7.0,>=4.5", + "yarl<2.0,>=1.0", +] +files = [ + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc36cbdedf6f259371dbbbcaae5bb0e95b879bc501668ab6306af867577eb5db"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85466b5a695c2a7db13eb2c200af552d13e6a9313d7fa92e4ffe04a2c0ea74c1"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71bb1d97bfe7e6726267cea169fdf5df7658831bb68ec02c9c6b9f3511e108bb"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baec1eb274f78b2de54471fc4c69ecbea4275965eab4b556ef7a7698dee18bf2"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13031e7ec1188274bad243255c328cc3019e36a5a907978501256000d57a7201"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bbc55a964b8eecb341e492ae91c3bd0848324d313e1e71a27e3d96e6ee7e8e8"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8cc0564b286b625e673a2615ede60a1704d0cbbf1b24604e28c31ed37dc62aa"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f817a54059a4cfbc385a7f51696359c642088710e731e8df80d0607193ed2b73"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8542c9e5bcb2bd3115acdf5adc41cda394e7360916197805e7e32b93d821ef93"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:671efce3a4a0281060edf9a07a2f7e6230dca3a1cbc61d110eee7753d28405f7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0974f3b5b0132edcec92c3306f858ad4356a63d26b18021d859c9927616ebf27"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:44bb159b55926b57812dca1b21c34528e800963ffe130d08b049b2d6b994ada7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6ae9ae382d1c9617a91647575255ad55a48bfdde34cc2185dd558ce476bf16e9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win32.whl", hash = "sha256:aed12a54d4e1ee647376fa541e1b7621505001f9f939debf51397b9329fd88b9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b51aef59370baf7444de1572f7830f59ddbabd04e5292fa4218d02f085f8d299"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e021c4c778644e8cdc09487d65564265e6b149896a17d7c0f52e9a088cc44e1b"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24fade6dae446b183e2410a8628b80df9b7a42205c6bfc2eff783cbeedc224a2"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bc8e9f15939dacb0e1f2d15f9c41b786051c10472c7a926f5771e99b49a5957f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a9ec959b5381271c8ec9310aae1713b2aec29efa32e232e5ef7dcca0df0279"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a5d0ea8a6467b15d53b00c4e8ea8811e47c3cc1bdbc62b1aceb3076403d551f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9ed607dbbdd0d4d39b597e5bf6b0d40d844dfb0ac6a123ed79042ef08c1f87e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e66d5b506832e56add66af88c288c1d5ba0c38b535a1a59e436b300b57b23e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fda91ad797e4914cca0afa8b6cccd5d2b3569ccc88731be202f6adce39503189"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:61ccb867b2f2f53df6598eb2a93329b5eee0b00646ee79ea67d68844747a418e"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d881353264e6156f215b3cb778c9ac3184f5465c2ece5e6fce82e68946868ef"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b031ce229114825f49cec4434fa844ccb5225e266c3e146cb4bdd025a6da52f1"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5337cc742a03f9e3213b097abff8781f79de7190bbfaa987bd2b7ceb5bb0bdec"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab3361159fd3dcd0e48bbe804006d5cfb074b382666e6c064112056eb234f1a9"}, + {file = "aiohttp-3.10.3-cp311-cp311-win32.whl", hash = "sha256:05d66203a530209cbe40f102ebaac0b2214aba2a33c075d0bf825987c36f1f0b"}, + {file = "aiohttp-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:70b4a4984a70a2322b70e088d654528129783ac1ebbf7dd76627b3bd22db2f17"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:166de65e2e4e63357cfa8417cf952a519ac42f1654cb2d43ed76899e2319b1ee"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7084876352ba3833d5d214e02b32d794e3fd9cf21fdba99cff5acabeb90d9806"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d98c604c93403288591d7d6d7d6cc8a63459168f8846aeffd5b3a7f3b3e5e09"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d73b073a25a0bb8bf014345374fe2d0f63681ab5da4c22f9d2025ca3e3ea54fc"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8da6b48c20ce78f5721068f383e0e113dde034e868f1b2f5ee7cb1e95f91db57"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a9dcdccf50284b1b0dc72bc57e5bbd3cc9bf019060dfa0668f63241ccc16aa7"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56fb94bae2be58f68d000d046172d8b8e6b1b571eb02ceee5535e9633dcd559c"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf75716377aad2c718cdf66451c5cf02042085d84522aec1f9246d3e4b8641a6"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c51ed03e19c885c8e91f574e4bbe7381793f56f93229731597e4a499ffef2a5"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b84857b66fa6510a163bb083c1199d1ee091a40163cfcbbd0642495fed096204"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c124b9206b1befe0491f48185fd30a0dd51b0f4e0e7e43ac1236066215aff272"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3461d9294941937f07bbbaa6227ba799bc71cc3b22c40222568dc1cca5118f68"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08bd0754d257b2db27d6bab208c74601df6f21bfe4cb2ec7b258ba691aac64b3"}, + {file = "aiohttp-3.10.3-cp312-cp312-win32.whl", hash = "sha256:7f9159ae530297f61a00116771e57516f89a3de6ba33f314402e41560872b50a"}, + {file = "aiohttp-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:e1128c5d3a466279cb23c4aa32a0f6cb0e7d2961e74e9e421f90e74f75ec1edf"}, + {file = "aiohttp-3.10.3.tar.gz", hash = "sha256:21650e7032cc2d31fc23d353d7123e771354f2a3d5b05a5647fc30fea214e696"}, +] + +[[package]] +name = "aiohttp-cors" +version = "0.7.0" +summary = "CORS support for aiohttp" +groups = ["default"] +dependencies = [ + "aiohttp>=1.1", + "typing; python_version < \"3.5\"", +] +files = [ + {file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"}, + {file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"}, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +requires_python = ">=3.7" +summary = "aiosignal: a list of registered asynchronous callbacks" +groups = ["default"] +dependencies = [ + "frozenlist>=1.1.0", +] +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +requires_python = ">=3.10" +summary = "A light, configurable Sphinx theme" +groups = ["docs"] +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] +files = [ + {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 = "antlr4-python3-runtime" +version = "4.9.3" +summary = "ANTLR 4.9.3 runtime for Python 3.7" +groups = ["default"] +dependencies = [ + "typing; python_version < \"3.5\"", +] +files = [ + {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, +] + +[[package]] +name = "anyio" +version = "4.4.0" +requires_python = ">=3.8" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +groups = ["default", "docs"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.1; python_version < \"3.11\"", +] +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +requires_python = ">=3.7" +summary = "Argon2 for Python" +groups = ["default"] +dependencies = [ + "argon2-cffi-bindings", + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +requires_python = ">=3.6" +summary = "Low-level CFFI bindings for Argon2" +groups = ["default"] +dependencies = [ + "cffi>=1.0.1", +] +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +requires_python = ">=3.7" +summary = "Timeout context manager for asyncio programs" +groups = ["default"] +marker = "python_version < \"3.11\"" +dependencies = [ + "typing-extensions>=3.6.5; python_version < \"3.8\"", +] +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "attrs" +version = "24.2.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +groups = ["default"] +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[[package]] +name = "babel" +version = "2.16.0" +requires_python = ">=3.8" +summary = "Internationalization utilities" +groups = ["docs"] +dependencies = [ + "pytz>=2015.7; python_version < \"3.9\"", +] +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +requires_python = ">=3.6.0" +summary = "Screen-scraping library" +groups = ["docs"] +dependencies = [ + "soupsieve>1.2", +] +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[[package]] +name = "black" +version = "24.8.0" +requires_python = ">=3.8" +summary = "The uncompromising code formatter." +groups = ["dev"] +dependencies = [ + "click>=8.0.0", + "mypy-extensions>=0.4.3", + "packaging>=22.0", + "pathspec>=0.9.0", + "platformdirs>=2", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.0.1; python_version < \"3.11\"", +] +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[[package]] +name = "bofire" +version = "0.0.13" +requires_python = ">=3.9.0" +summary = "" +groups = ["default"] +dependencies = [ + "numpy", + "pandas", + "pydantic>=2.5", + "scipy>=1.7", + "typing-extensions", +] +files = [ + {file = "bofire-0.0.13-py3-none-any.whl", hash = "sha256:a1b47732e70770a591d74bb24cdcd1ad1048b60e624df52a1529c6c54f8822c8"}, + {file = "bofire-0.0.13.tar.gz", hash = "sha256:d1d83e781c63992c1fc9157587189251b4c06f6c9196f0e000e1cd329ca0fe6a"}, +] + +[[package]] +name = "bofire" +version = "0.0.13" +extras = ["optimization"] +requires_python = ">=3.9.0" +summary = "" +groups = ["default"] +dependencies = [ + "bofire==0.0.13", + "botorch>=0.10.0", + "cloudpickle>=2.0.0", + "cvxpy[clarabel]", + "formulaic>=1.0.1", + "multiprocess", + "plotly", + "scikit-learn>=1.0.0", + "sympy>=1.12", +] +files = [ + {file = "bofire-0.0.13-py3-none-any.whl", hash = "sha256:a1b47732e70770a591d74bb24cdcd1ad1048b60e624df52a1529c6c54f8822c8"}, + {file = "bofire-0.0.13.tar.gz", hash = "sha256:d1d83e781c63992c1fc9157587189251b4c06f6c9196f0e000e1cd329ca0fe6a"}, +] + +[[package]] +name = "botorch" +version = "0.11.3" +requires_python = ">=3.10" +summary = "Bayesian Optimization in PyTorch" +groups = ["default"] +dependencies = [ + "gpytorch==1.12", + "linear-operator==0.5.2", + "mpmath<=1.3,>=0.19", + "multipledispatch", + "numpy<2.0", + "pyro-ppl>=1.8.4", + "scipy", + "torch>=1.13.1", +] +files = [ + {file = "botorch-0.11.3-py3-none-any.whl", hash = "sha256:5ea3e95b82b9e7b36e1b04ed40c5d928fb4fb60f3ff1ef7f2fdd410979101e4d"}, + {file = "botorch-0.11.3.tar.gz", hash = "sha256:600ab08f8007a94adbc5acf35073e7a25f55b58e85b2d895c101dabef74121ef"}, +] + +[[package]] +name = "cachetools" +version = "5.4.0" +requires_python = ">=3.7" +summary = "Extensible memoizing collections and decorators" +groups = ["default"] +marker = "python_version >= \"3.6\"" +files = [ + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default", "docs"] +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "cffi" +version = "1.17.0" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default", "docs"] +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "clarabel" +version = "0.9.0" +requires_python = ">=3.7" +summary = "Clarabel Conic Interior Point Solver for Rust / Python" +groups = ["default"] +dependencies = [ + "numpy", + "scipy", +] +files = [ + {file = "clarabel-0.9.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:702cc4666c0ccf893c936f9f1f55cbb3233ae2d5fa05f67b370ac3e7ec50f222"}, + {file = "clarabel-0.9.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8ea616757b460153ead375b3dd3ce763d46fc3717248077bbfa7b2c844b1775f"}, + {file = "clarabel-0.9.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b5ae16d7dd87aabf72260cf9590ba0d037c52d48555bcf3a86b1f0d9cf88dd4"}, + {file = "clarabel-0.9.0-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85cb560a5c4cdfb079e3437e21f0b62b69ba766ae082aeb96ced0b5763214077"}, + {file = "clarabel-0.9.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eaeb3fbb5a90b598700d5435c7f102592a1a79ee25df5a097e0af575838786b"}, + {file = "clarabel-0.9.0-cp37-abi3-win32.whl", hash = "sha256:759c2fa0ccc61ae1a02691c43753638a0ae793bf1de81c6f6763c346789a7e25"}, + {file = "clarabel-0.9.0-cp37-abi3-win_amd64.whl", hash = "sha256:d24e4ed1b686eb2fe2a1b6e77935af6ad62a2c044131e70801ec1d3ef3d33280"}, + {file = "clarabel-0.9.0.tar.gz", hash = "sha256:0d6d3fe8800be5b4b5d40a8e14bd492667b3e46cc5dbe37677ce5ed25f0719d4"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default", "dev", "docs"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "cloudpickle" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Pickler class to extend the standard pickle.Pickler functionality" +groups = ["default"] +files = [ + {file = "cloudpickle-3.0.0-py3-none-any.whl", hash = "sha256:246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7"}, + {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default", "dev", "docs"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "colorful" +version = "0.5.6" +summary = "Terminal string styling done right, in Python." +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e"}, + {file = "colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +requires_python = ">=3.8" +summary = "Code coverage measurement for Python" +groups = ["dev"] +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +extras = ["toml"] +requires_python = ">=3.8" +summary = "Code coverage measurement for Python" +groups = ["dev"] +dependencies = [ + "coverage==7.6.1", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[[package]] +name = "cvxpy" +version = "1.5.2" +requires_python = ">=3.8" +summary = "A domain-specific language for modeling convex optimization problems in Python." +groups = ["default"] +dependencies = [ + "clarabel>=0.5.0", + "ecos>=2", + "numpy>=1.15", + "osqp>=0.6.2", + "scipy>=1.1.0", + "scs>=3.2.4.post1", +] +files = [ + {file = "cvxpy-1.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:60d24c3d656fa71b49790cc389313c8fdc9a2d17a97f530168eb93c46333c958"}, + {file = "cvxpy-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9cbec71b6abca6c6d98f13e36a0daa95fecfbf54ef83b3f51c555ec0700b9b2f"}, + {file = "cvxpy-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78e1fef846dec415f74783439bd66e9f232c885dee3ad5f9a050f83e3a433ac5"}, + {file = "cvxpy-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42bca823c3f7f63f82caff7163833dba1c4c8c5368b0435fa3417f70b7f0841"}, + {file = "cvxpy-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:acbdd6c6f2e5e7a506429878a4d835eaf7efd45fdb4409c59fdfb8a157ef76c4"}, + {file = "cvxpy-1.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b9cf1437327f84f78d4efdd1baada049de3a749a3548e24ec3502ef35e663c0b"}, + {file = "cvxpy-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24c3156fb49252ea994d4629cbcecff1d1f1951ae76f6c225451d25d79dee923"}, + {file = "cvxpy-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbb74e2eecd1b7ee5dfdcbf61a4916d12f90444df55c2377aa02935932d13421"}, + {file = "cvxpy-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515886f6760a017354674b8f045e096ba20e7641241bd6557d04d0a01bfefbaa"}, + {file = "cvxpy-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:73e6917cd6754bef63a70fc93a83d80e2713a67c9b26157aa049d0e4588ee3c7"}, + {file = "cvxpy-1.5.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:484f1a1f687c18cda6c382918a7c44f891d4901b4456d927da3c8ce9208c3e97"}, + {file = "cvxpy-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d51b5e56dcd93a6efdd83ea0b39df83808691706db21c11496d59dc66dca108"}, + {file = "cvxpy-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae4820f285a8547c5fac197073572ed9c750978651c6499e3ec30a92b6be26a8"}, + {file = "cvxpy-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2189a959eb4bc1e81d8993e7fe780791d14fa486d558bd49adb8561b1df510"}, + {file = "cvxpy-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:b8c1c9a302229ded2bc9bc5c7263e4a24bcb645f3cfedb29072b0b49d77af7fb"}, + {file = "cvxpy-1.5.2.tar.gz", hash = "sha256:8231f006f6b55da141758282aecb788b3b5742448765dba6a9440b6336080ce3"}, +] + +[[package]] +name = "cvxpy" +version = "1.5.2" +extras = ["clarabel"] +requires_python = ">=3.8" +summary = "A domain-specific language for modeling convex optimization problems in Python." +groups = ["default"] +dependencies = [ + "cvxpy==1.5.2", +] +files = [ + {file = "cvxpy-1.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:60d24c3d656fa71b49790cc389313c8fdc9a2d17a97f530168eb93c46333c958"}, + {file = "cvxpy-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9cbec71b6abca6c6d98f13e36a0daa95fecfbf54ef83b3f51c555ec0700b9b2f"}, + {file = "cvxpy-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78e1fef846dec415f74783439bd66e9f232c885dee3ad5f9a050f83e3a433ac5"}, + {file = "cvxpy-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42bca823c3f7f63f82caff7163833dba1c4c8c5368b0435fa3417f70b7f0841"}, + {file = "cvxpy-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:acbdd6c6f2e5e7a506429878a4d835eaf7efd45fdb4409c59fdfb8a157ef76c4"}, + {file = "cvxpy-1.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b9cf1437327f84f78d4efdd1baada049de3a749a3548e24ec3502ef35e663c0b"}, + {file = "cvxpy-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24c3156fb49252ea994d4629cbcecff1d1f1951ae76f6c225451d25d79dee923"}, + {file = "cvxpy-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbb74e2eecd1b7ee5dfdcbf61a4916d12f90444df55c2377aa02935932d13421"}, + {file = "cvxpy-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515886f6760a017354674b8f045e096ba20e7641241bd6557d04d0a01bfefbaa"}, + {file = "cvxpy-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:73e6917cd6754bef63a70fc93a83d80e2713a67c9b26157aa049d0e4588ee3c7"}, + {file = "cvxpy-1.5.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:484f1a1f687c18cda6c382918a7c44f891d4901b4456d927da3c8ce9208c3e97"}, + {file = "cvxpy-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d51b5e56dcd93a6efdd83ea0b39df83808691706db21c11496d59dc66dca108"}, + {file = "cvxpy-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae4820f285a8547c5fac197073572ed9c750978651c6499e3ec30a92b6be26a8"}, + {file = "cvxpy-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2189a959eb4bc1e81d8993e7fe780791d14fa486d558bd49adb8561b1df510"}, + {file = "cvxpy-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:b8c1c9a302229ded2bc9bc5c7263e4a24bcb645f3cfedb29072b0b49d77af7fb"}, + {file = "cvxpy-1.5.2.tar.gz", hash = "sha256:8231f006f6b55da141758282aecb788b3b5742448765dba6a9440b6336080ce3"}, +] + +[[package]] +name = "dill" +version = "0.3.8" +requires_python = ">=3.8" +summary = "serialize all of Python" +groups = ["default"] +files = [ + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +summary = "Distribution utilities" +groups = ["default"] +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +requires_python = ">=3.8" +summary = "DNS toolkit" +groups = ["default"] +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[[package]] +name = "docutils" +version = "0.21.2" +requires_python = ">=3.9" +summary = "Docutils -- Python Documentation Utilities" +groups = ["docs"] +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "ecos" +version = "2.0.14" +summary = "This is the Python package for ECOS: Embedded Cone Solver. See Github page for more information." +groups = ["default"] +dependencies = [ + "numpy>=1.6", + "scipy>=0.9", +] +files = [ + {file = "ecos-2.0.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d16f8c97c42a18be77530b4d0090d8dd38105ae311518fc58a66c5c403d79672"}, + {file = "ecos-2.0.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a977976ec618261456d6c9cd4ec7b7745607e448e78cd0c851190c6cc515ef"}, + {file = "ecos-2.0.14-cp310-cp310-win_amd64.whl", hash = "sha256:f2e8ab314609117f7e96bb83db7458f011ab0496c61078e146a8f5c8244e70b2"}, + {file = "ecos-2.0.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dc90b54eaae16ead128bfdd95e04bf808b73578bdf40ed652c55aa36a6d02e42"}, + {file = "ecos-2.0.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8be3b4856838ae351fec40fb3589181d52b41cf75bf4d35342686a508c37a6"}, + {file = "ecos-2.0.14-cp311-cp311-win_amd64.whl", hash = "sha256:7495b3031ccc2d4cec72cdb40aed8a2d1fdd734fe40519b7e6047aead5e811cf"}, + {file = "ecos-2.0.14-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4a7e2704a3ef9acfb8146d594deff9942d3a0f0d0399de8fe2e0bd95e8b0855c"}, + {file = "ecos-2.0.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3cbb1a66ecf10955a1a4bcd6b99db55148000cb79fd176bfac26d98b21a4814"}, + {file = "ecos-2.0.14-cp312-cp312-win_amd64.whl", hash = "sha256:718eb62afb8e45426bcc365ebaf3ca9f610afcbb754de6073ef5f104da8fca1f"}, + {file = "ecos-2.0.14.tar.gz", hash = "sha256:64b3201c0e0a7f0129050557c4ac50b00031e80a10534506dba1200c8dc1efe4"}, +] + +[[package]] +name = "editorconfig" +version = "0.12.4" +summary = "EditorConfig File Locator and Interpreter for Python" +groups = ["default"] +files = [ + {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "dev", "docs"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[[package]] +name = "faker" +version = "27.0.0" +requires_python = ">=3.8" +summary = "Faker is a Python package that generates fake data for you." +groups = ["default"] +dependencies = [ + "python-dateutil>=2.4", +] +files = [ + {file = "Faker-27.0.0-py3-none-any.whl", hash = "sha256:55ed0c4ed7bf16800c64823805f6fbbe6d4823db4b7c0903f6f890b8e4d6c34b"}, + {file = "faker-27.0.0.tar.gz", hash = "sha256:32c78b68d2ba97aaad78422e4035785de2b4bb46b81e428190fc11978da9036c"}, +] + +[[package]] +name = "fast-query-parsers" +version = "1.0.3" +requires_python = ">=3.8" +summary = "Ultra-fast query string and url-encoded form-data parsers" +groups = ["default"] +files = [ + {file = "fast_query_parsers-1.0.3-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:afbf71c1b4398dacfb9d84755eb026f8e759f68a066f1f3cc19e471fc342e74f"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:42f26875311d1b151c3406adfa39ec2db98df111a369d75f6fa243ec8462f147"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66630ad423b5b1f5709f82a4d8482cd6aa2f3fa73d2c779ff1877f25dee08d55"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6e3d816c572a6fad1ae9b93713b2db0d3db6e8f594e035ad52361d668dd94a8"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0bdcc0ddb4cc69d823c2c0dedd8f5affc71042db39908ad2ca06261bf388cac6"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6720505f2d2a764c76bcc4f3730a9dff69d9871740e46264f6605d73f9ce3794"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e947e7251769593da93832a10861f59565a46149fa117ebdf25377e7b2853936"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55a30b7cee0a53cddf9016b86fdad87221980d5a02a6126c491bd309755e6de9"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc2b457caa38371df1a30cfdfc57bd9bfdf348367abdaf6f36533416a0b0e93"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5736d3c32d6ba23995fa569fe572feabcfcfc30ac9e4709e94cff6f2c456a3d1"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a6377eb0c5b172fbc77c3f96deaf1e51708b4b96d27ce173658bf11c1c00b20"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:7ca6be04f443a1b055e910ccad01b1d72212f269a530415df99a87c5f1e9c927"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a70d4d8852606f2dd5b798ab628b9d8dc6970ddfdd9e96f4543eb0cc89a74fb5"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-win32.whl", hash = "sha256:14b3fab7e9a6ac1c1efaf66c3fd2a3fd1e25ede03ed14118035e530433830a11"}, + {file = "fast_query_parsers-1.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:21ae5f3a209aee7d3b84bdcdb33dd79f39fc8cb608b3ae8cfcb78123758c1a16"}, + {file = "fast_query_parsers-1.0.3.tar.gz", hash = "sha256:5200a9e02997ad51d4d76a60ea1b256a68a184b04359540eb6310a15013df68f"}, +] + +[[package]] +name = "filelock" +version = "3.15.4" +requires_python = ">=3.8" +summary = "A platform independent file lock." +groups = ["default"] +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[[package]] +name = "formulaic" +version = "1.0.2" +requires_python = ">=3.7.2" +summary = "An implementation of Wilkinson formulas." +groups = ["default"] +dependencies = [ + "astor>=0.8; python_version < \"3.9\"", + "cached-property>=1.3.0; python_version < \"3.8\"", + "graphlib-backport>=1.0.0; python_version < \"3.9\"", + "interface-meta>=1.2.0", + "numpy>=1.16.5", + "pandas>=1.0", + "scipy>=1.6", + "typing-extensions>=4.2.0", + "wrapt>=1.0", +] +files = [ + {file = "formulaic-1.0.2-py3-none-any.whl", hash = "sha256:663328b038a0eb7644f59400615da7abf2672b0e11124b3bef3307afc441d97c"}, + {file = "formulaic-1.0.2.tar.gz", hash = "sha256:6eb65bedd1903c5381d8f2ae7a55b6ba13cb77d57bbaf6e4278f3b2c38e3660e"}, +] + +[[package]] +name = "frozenlist" +version = "1.4.1" +requires_python = ">=3.8" +summary = "A list-like structure which implements collections.abc.MutableSequence" +groups = ["default"] +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "fsspec" +version = "2024.6.1" +requires_python = ">=3.8" +summary = "File-system specification" +groups = ["default"] +files = [ + {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, + {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, +] + +[[package]] +name = "google-api-core" +version = "2.19.1" +requires_python = ">=3.7" +summary = "Google API client core library" +groups = ["default"] +marker = "python_version >= \"3.6\"" +dependencies = [ + "google-auth<3.0.dev0,>=2.14.1", + "googleapis-common-protos<2.0.dev0,>=1.56.2", + "proto-plus<2.0.0dev,>=1.22.3", + "protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.19.5", + "requests<3.0.0.dev0,>=2.18.0", +] +files = [ + {file = "google-api-core-2.19.1.tar.gz", hash = "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd"}, + {file = "google_api_core-2.19.1-py3-none-any.whl", hash = "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125"}, +] + +[[package]] +name = "google-auth" +version = "2.33.0" +requires_python = ">=3.7" +summary = "Google Authentication Library" +groups = ["default"] +marker = "python_version >= \"3.6\"" +dependencies = [ + "cachetools<6.0,>=2.0.0", + "pyasn1-modules>=0.2.1", + "rsa<5,>=3.1.4", +] +files = [ + {file = "google_auth-2.33.0-py2.py3-none-any.whl", hash = "sha256:8eff47d0d4a34ab6265c50a106a3362de6a9975bb08998700e389f857e4d39df"}, + {file = "google_auth-2.33.0.tar.gz", hash = "sha256:d6a52342160d7290e334b4d47ba390767e4438ad0d45b7630774533e82655b95"}, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.63.2" +requires_python = ">=3.7" +summary = "Common protobufs used in Google APIs" +groups = ["default"] +marker = "python_version >= \"3.6\"" +dependencies = [ + "protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2", +] +files = [ + {file = "googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87"}, + {file = "googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945"}, +] + +[[package]] +name = "gpytorch" +version = "1.12" +requires_python = ">=3.8" +summary = "An implementation of Gaussian Processes in Pytorch" +groups = ["default"] +dependencies = [ + "linear-operator>=0.5.2", + "mpmath<=1.3,>=0.19", + "scikit-learn", + "scipy", +] +files = [ + {file = "gpytorch-1.12-py3-none-any.whl", hash = "sha256:cee9da2dc53642a7aaba3da443b14f762dd07655a926920f5c8a4e5c54d39a8a"}, + {file = "gpytorch-1.12.tar.gz", hash = "sha256:dc2c160af72364189f5b1fd7c804f69bdbcd8c65bfd3da5c9c2fc34029639adf"}, +] + +[[package]] +name = "grpcio" +version = "1.65.4" +requires_python = ">=3.8" +summary = "HTTP/2-based RPC framework" +groups = ["default"] +marker = "python_version >= \"3.10\"" +files = [ + {file = "grpcio-1.65.4-cp310-cp310-linux_armv7l.whl", hash = "sha256:0e85c8766cf7f004ab01aff6a0393935a30d84388fa3c58d77849fcf27f3e98c"}, + {file = "grpcio-1.65.4-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e4a795c02405c7dfa8affd98c14d980f4acea16ea3b539e7404c645329460e5a"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:d7b984a8dd975d949c2042b9b5ebcf297d6d5af57dcd47f946849ee15d3c2fb8"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644a783ce604a7d7c91412bd51cf9418b942cf71896344b6dc8d55713c71ce82"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5764237d751d3031a36fafd57eb7d36fd2c10c658d2b4057c516ccf114849a3e"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ee40d058cf20e1dd4cacec9c39e9bce13fedd38ce32f9ba00f639464fcb757de"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4482a44ce7cf577a1f8082e807a5b909236bce35b3e3897f839f2fbd9ae6982d"}, + {file = "grpcio-1.65.4-cp310-cp310-win32.whl", hash = "sha256:66bb051881c84aa82e4f22d8ebc9d1704b2e35d7867757f0740c6ef7b902f9b1"}, + {file = "grpcio-1.65.4-cp310-cp310-win_amd64.whl", hash = "sha256:870370524eff3144304da4d1bbe901d39bdd24f858ce849b7197e530c8c8f2ec"}, + {file = "grpcio-1.65.4-cp311-cp311-linux_armv7l.whl", hash = "sha256:85e9c69378af02e483bc626fc19a218451b24a402bdf44c7531e4c9253fb49ef"}, + {file = "grpcio-1.65.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2bd672e005afab8bf0d6aad5ad659e72a06dd713020554182a66d7c0c8f47e18"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:abccc5d73f5988e8f512eb29341ed9ced923b586bb72e785f265131c160231d8"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:886b45b29f3793b0c2576201947258782d7e54a218fe15d4a0468d9a6e00ce17"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be952436571dacc93ccc7796db06b7daf37b3b56bb97e3420e6503dccfe2f1b4"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8dc9ddc4603ec43f6238a5c95400c9a901b6d079feb824e890623da7194ff11e"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ade1256c98cba5a333ef54636095f2c09e6882c35f76acb04412f3b1aa3c29a5"}, + {file = "grpcio-1.65.4-cp311-cp311-win32.whl", hash = "sha256:280e93356fba6058cbbfc6f91a18e958062ef1bdaf5b1caf46c615ba1ae71b5b"}, + {file = "grpcio-1.65.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2b819f9ee27ed4e3e737a4f3920e337e00bc53f9e254377dd26fc7027c4d558"}, + {file = "grpcio-1.65.4-cp312-cp312-linux_armv7l.whl", hash = "sha256:926a0750a5e6fb002542e80f7fa6cab8b1a2ce5513a1c24641da33e088ca4c56"}, + {file = "grpcio-1.65.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2a1d4c84d9e657f72bfbab8bedf31bdfc6bfc4a1efb10b8f2d28241efabfaaf2"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:17de4fda50967679677712eec0a5c13e8904b76ec90ac845d83386b65da0ae1e"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dee50c1b69754a4228e933696408ea87f7e896e8d9797a3ed2aeed8dbd04b74"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c34fc7562bdd169b77966068434a93040bfca990e235f7a67cdf26e1bd5c63"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:24a2246e80a059b9eb981e4c2a6d8111b1b5e03a44421adbf2736cc1d4988a8a"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18c10f0d054d2dce34dd15855fcca7cc44ec3b811139437543226776730c0f28"}, + {file = "grpcio-1.65.4-cp312-cp312-win32.whl", hash = "sha256:d72962788b6c22ddbcdb70b10c11fbb37d60ae598c51eb47ec019db66ccfdff0"}, + {file = "grpcio-1.65.4-cp312-cp312-win_amd64.whl", hash = "sha256:7656376821fed8c89e68206a522522317787a3d9ed66fb5110b1dff736a5e416"}, + {file = "grpcio-1.65.4.tar.gz", hash = "sha256:2a4f476209acffec056360d3e647ae0e14ae13dcf3dfb130c227ae1c594cbe39"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default", "docs"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["default"] +dependencies = [ + "certifi", + "h11<0.15,>=0.13", +] +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[[package]] +name = "httptools" +version = "0.6.1" +requires_python = ">=3.8.0" +summary = "A collection of framework independent HTTP protocol utils." +groups = ["default"] +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[[package]] +name = "httpx" +version = "0.27.0" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", + "sniffio", +] +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[[package]] +name = "idna" +version = "3.7" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default", "docs"] +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Getting image size from png/jpeg/jpeg2000/gif file" +groups = ["docs"] +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "interface-meta" +version = "1.3.0" +requires_python = ">=3.7,<4.0" +summary = "`interface_meta` provides a convenient way to expose an extensible API with enforced method signatures and consistent documentation." +groups = ["default"] +files = [ + {file = "interface_meta-1.3.0-py3-none-any.whl", hash = "sha256:de35dc5241431886e709e20a14d6597ed07c9f1e8b4bfcffde2190ca5b700ee8"}, + {file = "interface_meta-1.3.0.tar.gz", hash = "sha256:8a4493f8bdb73fb9655dcd5115bc897e207319e36c8835f39c516a2d7e9d79a1"}, +] + +[[package]] +name = "jaxtyping" +version = "0.2.33" +requires_python = "~=3.9" +summary = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees." +groups = ["default"] +dependencies = [ + "typeguard==2.13.3", +] +files = [ + {file = "jaxtyping-0.2.33-py3-none-any.whl", hash = "sha256:918d6094c73f28d3196185ef55d1832cbcd2804d1d388f180060c4366a9e2107"}, + {file = "jaxtyping-0.2.33.tar.gz", hash = "sha256:9a9cfccae4fe05114b9fb27a5ea5440be4971a5a075bbd0526f6dd7d2730f83e"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["default", "docs"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[[package]] +name = "joblib" +version = "1.4.2" +requires_python = ">=3.8" +summary = "Lightweight pipelining with Python functions" +groups = ["default"] +files = [ + {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, + {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, +] + +[[package]] +name = "jsbeautifier" +version = "1.15.1" +summary = "JavaScript unobfuscator and beautifier." +groups = ["default"] +dependencies = [ + "editorconfig>=0.12.2", + "six>=1.13.0", +] +files = [ + {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +requires_python = ">=3.8" +summary = "An implementation of JSON Schema validation for Python" +groups = ["default"] +dependencies = [ + "attrs>=22.2.0", + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "jsonschema-specifications>=2023.03.6", + "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", + "referencing>=0.28.4", + "rpds-py>=0.7.1", +] +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +requires_python = ">=3.8" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +groups = ["default"] +dependencies = [ + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[[package]] +name = "linear-operator" +version = "0.5.2" +requires_python = ">=3.8" +summary = "A linear operator implementation, primarily designed for finite-dimensional positive definite operators (i.e. kernel matrices)." +groups = ["default"] +dependencies = [ + "jaxtyping>=0.2.9", + "scipy", + "torch>=1.11", + "typeguard~=2.13.3", +] +files = [ + {file = "linear_operator-0.5.2-py3-none-any.whl", hash = "sha256:26defe85e3c924f24d49117bf78afaf0207f6847877903309dc9bf40a46d08a7"}, + {file = "linear_operator-0.5.2.tar.gz", hash = "sha256:5cd9099bca5b9f1e57017a4153526df7410561a46aedd47096c8da642159b90d"}, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +requires_python = ">=3.7" +summary = "Links recognition library with FULL unicode support." +groups = ["default"] +marker = "sys_platform != \"win32\"" +dependencies = [ + "uc-micro-py", +] +files = [ + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, +] + +[[package]] +name = "litestar" +version = "2.11.0" +requires_python = "<4.0,>=3.8" +summary = "Litestar - A production-ready, highly performant, extensible ASGI API Framework" +groups = ["default"] +dependencies = [ + "anyio>=3", + "click", + "exceptiongroup; python_version < \"3.11\"", + "httpx>=0.22", + "importlib-metadata; python_version < \"3.10\"", + "importlib-resources>=5.12.0; python_version < \"3.9\"", + "msgspec>=0.18.2", + "multidict>=6.0.2", + "polyfactory>=2.6.3", + "pyyaml", + "rich-click", + "rich>=13.0.0", + "typing-extensions", +] +files = [ + {file = "litestar-2.11.0-py3-none-any.whl", hash = "sha256:6d677ccdc00a0b4ce54cff5172531890358a27d6da1a054c8cab6a7e2119823e"}, + {file = "litestar-2.11.0.tar.gz", hash = "sha256:6c8cf2b60c352e6b8e08e6a995d2a66ddc26ec53bc2f1df7214d26abcc1d00c2"}, +] + +[[package]] +name = "litestar" +version = "2.11.0" +extras = ["standard"] +requires_python = "<4.0,>=3.8" +summary = "Litestar - A production-ready, highly performant, extensible ASGI API Framework" +groups = ["default"] +dependencies = [ + "fast-query-parsers>=1.0.2", + "jinja2", + "jsbeautifier", + "litestar==2.11.0", + "uvicorn[standard]", + "uvloop>=0.18.0; sys_platform != \"win32\"", +] +files = [ + {file = "litestar-2.11.0-py3-none-any.whl", hash = "sha256:6d677ccdc00a0b4ce54cff5172531890358a27d6da1a054c8cab6a7e2119823e"}, + {file = "litestar-2.11.0.tar.gz", hash = "sha256:6c8cf2b60c352e6b8e08e6a995d2a66ddc26ec53bc2f1df7214d26abcc1d00c2"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +extras = ["linkify", "plugins"] +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +marker = "sys_platform != \"win32\"" +dependencies = [ + "linkify-it-py<3,>=1", + "markdown-it-py==3.0.0", + "mdit-py-plugins", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["default", "docs"] +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.1" +requires_python = ">=3.8" +summary = "Collection of plugins for markdown-it-py" +groups = ["default"] +marker = "sys_platform != \"win32\"" +dependencies = [ + "markdown-it-py<4.0.0,>=1.0.0", +] +files = [ + {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"}, + {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "memray" +version = "1.13.4" +requires_python = ">=3.7.0" +summary = "A memory profiler for Python applications" +groups = ["default"] +marker = "sys_platform != \"win32\"" +dependencies = [ + "jinja2>=2.9", + "rich>=11.2.0", + "textual>=0.41.0", + "typing-extensions; python_version < \"3.8.0\"", +] +files = [ + {file = "memray-1.13.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ed0bfcffbd857cbf78a4db942019e9e153019b754048b0522065844d1c538e8c"}, + {file = "memray-1.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fcf71802b2c6d68c5336b1e4ae341eab64dcccd0dcf67687af53f18bc020237b"}, + {file = "memray-1.13.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c9ae675131492bdfafcc44e86d0b81401ea8d052a9cab7793b1dab642cd58e6"}, + {file = "memray-1.13.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bac9d30ce39aaee40601087d09c1639a071293f414b5e726a152ed3581d25e50"}, + {file = "memray-1.13.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a437c7e28734028a2f43f942c3146e9737033718cea092ea910f6de3cf46221d"}, + {file = "memray-1.13.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3cae161d5b6769cc3af574cfa0c7ea77f98d6ae714ba5ec508f6f05b84800801"}, + {file = "memray-1.13.4-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:bf407123e175de4f5a7264886eb64ea514f4b388b617f05dfcd857d99ecadd1c"}, + {file = "memray-1.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6f1bd3d0adf84f864e24f74552c1533224e64283dfee33641011acf384fc138"}, + {file = "memray-1.13.4-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ba5bb9a3b7c3c08752f3b55a3b5b360963c9f666e2220eb388ab6f7d1271d843"}, + {file = "memray-1.13.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1e8cec70e51e81c0e9448e62a5366914b74a3dbb60826cdec8f0e7559e58e74"}, + {file = "memray-1.13.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81497e578017feb57a46e19c349450888e57ff7fb8f0f5134d3e07605c435500"}, + {file = "memray-1.13.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e585d866c82ce92060fa1c925298aa8b89936ca22df9698a25a5f0cf7ca81fa2"}, + {file = "memray-1.13.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d048da01dc138711a2c9c70ba693d186690c98fb0ca26fdc3483486d4849238"}, + {file = "memray-1.13.4-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:b6459761046ab46638d2c62d7f3f55eaaf45a947bd1d36dcfb5e860047280557"}, + {file = "memray-1.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:637651f5ca2870e9156f189c337e8c6d0002e3f6f7d44d6486ff5baf12a6115e"}, + {file = "memray-1.13.4-cp312-cp312-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5b9e10fde6f652ea176cbc0d4d4c563d2831faec4434d3e03c4c0aff8ddc6c0"}, + {file = "memray-1.13.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1f3ab803b703b9be29259039caf43803ad5abf37f04e77cd9e8373054dd91f6"}, + {file = "memray-1.13.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfdc070da2df9241f78b7429d44f6ee16e924d43eddc587f6ed7218c4cb792d3"}, + {file = "memray-1.13.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:523a63dee71cd4d55eddca866244a045e7549ca5137ec906c62893b87a2161ce"}, + {file = "memray-1.13.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3bf06f8883a26b779cc828addad97a2d39d7587263e348655dae3ec90b6ee079"}, + {file = "memray-1.13.4.tar.gz", hash = "sha256:48f8f9b89b3a84028668244151eb7248189fb3f4f2a761ec1211439adcbb2ad1"}, +] + +[[package]] +name = "minio" +version = "7.2.8" +requires_python = ">3.8" +summary = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" +groups = ["default"] +dependencies = [ + "argon2-cffi", + "certifi", + "pycryptodome", + "typing-extensions", + "urllib3", +] +files = [ + {file = "minio-7.2.8-py3-none-any.whl", hash = "sha256:aa3b485788b63b12406a5798465d12a57e4be2ac2a58a8380959b6b748e64ddd"}, + {file = "minio-7.2.8.tar.gz", hash = "sha256:f8af2dafc22ebe1aef3ac181b8e217037011c430aa6da276ed627e55aaf7c815"}, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +summary = "Python library for arbitrary-precision floating-point arithmetic" +groups = ["default"] +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[[package]] +name = "msgpack" +version = "1.0.8" +requires_python = ">=3.8" +summary = "MessagePack serializer" +groups = ["default"] +files = [ + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, +] + +[[package]] +name = "msgspec" +version = "0.18.6" +requires_python = ">=3.8" +summary = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +groups = ["default"] +files = [ + {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, + {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, + {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, + {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, + {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, +] + +[[package]] +name = "multidict" +version = "6.0.5" +requires_python = ">=3.7" +summary = "multidict implementation" +groups = ["default"] +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "multipledispatch" +version = "1.0.0" +summary = "Multiple dispatch" +groups = ["default"] +files = [ + {file = "multipledispatch-1.0.0-py3-none-any.whl", hash = "sha256:0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4"}, + {file = "multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0"}, +] + +[[package]] +name = "multiprocess" +version = "0.70.16" +requires_python = ">=3.8" +summary = "better multiprocessing and multithreading in Python" +groups = ["default"] +dependencies = [ + "dill>=0.3.8", +] +files = [ + {file = "multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee"}, + {file = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}, + {file = "multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}, + {file = "multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a"}, + {file = "multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e"}, + {file = "multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "networkx" +version = "3.3" +requires_python = ">=3.10" +summary = "Python package for creating and manipulating graphs and networks" +groups = ["default"] +files = [ + {file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"}, + {file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +requires_python = ">=3.9" +summary = "Fundamental package for array computing in Python" +groups = ["default"] +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +requires_python = ">=3" +summary = "CUBLAS native runtime libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"}, + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"}, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +requires_python = ">=3" +summary = "CUDA profiling tools runtime libs." +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"}, + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"}, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +requires_python = ">=3" +summary = "NVRTC native runtime libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"}, + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"}, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +requires_python = ">=3" +summary = "CUDA Runtime native Libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"}, + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"}, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.1.0.70" +requires_python = ">=3" +summary = "cuDNN runtime libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +dependencies = [ + "nvidia-cublas-cu12", +] +files = [ + {file = "nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f"}, + {file = "nvidia_cudnn_cu12-9.1.0.70-py3-none-win_amd64.whl", hash = "sha256:6278562929433d68365a07a4a1546c237ba2849852c0d4b2262a486e805b977a"}, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +requires_python = ">=3" +summary = "CUFFT native runtime libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"}, + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"}, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +requires_python = ">=3" +summary = "CURAND native runtime libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"}, + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"}, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +requires_python = ">=3" +summary = "CUDA solver native runtime libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +dependencies = [ + "nvidia-cublas-cu12", + "nvidia-cusparse-cu12", + "nvidia-nvjitlink-cu12", +] +files = [ + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"}, + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"}, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +requires_python = ">=3" +summary = "CUSPARSE native runtime libraries" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +dependencies = [ + "nvidia-nvjitlink-cu12", +] +files = [ + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"}, + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"}, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.20.5" +requires_python = ">=3" +summary = "NVIDIA Collective Communication Library (NCCL) Runtime" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1fc150d5c3250b170b29410ba682384b14581db722b2531b0d8d33c595f33d01"}, + {file = "nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:057f6bf9685f75215d0c53bf3ac4a10b3e6578351de307abad9e18a99182af56"}, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.6.20" +requires_python = ">=3" +summary = "Nvidia JIT LTO Library" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_nvjitlink_cu12-12.6.20-py3-none-manylinux2014_aarch64.whl", hash = "sha256:84fb38465a5bc7c70cbc320cfd0963eb302ee25a5e939e9f512bbba55b6072fb"}, + {file = "nvidia_nvjitlink_cu12-12.6.20-py3-none-manylinux2014_x86_64.whl", hash = "sha256:562ab97ea2c23164823b2a89cb328d01d45cb99634b8c65fe7cd60d14562bd79"}, + {file = "nvidia_nvjitlink_cu12-12.6.20-py3-none-win_amd64.whl", hash = "sha256:ed3c43a17f37b0c922a919203d2d36cbef24d41cc3e6b625182f8b58203644f6"}, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +requires_python = ">=3" +summary = "NVIDIA Tools Extension" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" +files = [ + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"}, + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"}, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +requires_python = ">=3.6" +summary = "A flexible configuration library" +groups = ["default"] +dependencies = [ + "PyYAML>=5.1.0", + "antlr4-python3-runtime==4.9.*", + "dataclasses; python_version == \"3.6\"", +] +files = [ + {file = "omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b"}, + {file = "omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7"}, +] + +[[package]] +name = "opencensus" +version = "0.11.4" +summary = "A stats collection and distributed tracing framework" +groups = ["default"] +dependencies = [ + "google-api-core<2.0.0,>=1.0.0; python_version < \"3.6\"", + "google-api-core<3.0.0,>=1.0.0; python_version >= \"3.6\"", + "opencensus-context>=0.1.3", + "six~=1.16", +] +files = [ + {file = "opencensus-0.11.4-py2.py3-none-any.whl", hash = "sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864"}, + {file = "opencensus-0.11.4.tar.gz", hash = "sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2"}, +] + +[[package]] +name = "opencensus-context" +version = "0.1.3" +summary = "OpenCensus Runtime Context" +groups = ["default"] +dependencies = [ + "contextvars; python_version >= \"3.6\" and python_version < \"3.7\"", +] +files = [ + {file = "opencensus-context-0.1.3.tar.gz", hash = "sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c"}, + {file = "opencensus_context-0.1.3-py2.py3-none-any.whl", hash = "sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039"}, +] + +[[package]] +name = "opt-einsum" +version = "3.3.0" +requires_python = ">=3.5" +summary = "Optimizing numpys einsum function" +groups = ["default"] +dependencies = [ + "numpy>=1.7", +] +files = [ + {file = "opt_einsum-3.3.0-py3-none-any.whl", hash = "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147"}, + {file = "opt_einsum-3.3.0.tar.gz", hash = "sha256:59f6475f77bbc37dcf7cd748519c0ec60722e91e63ca114e68821c0c54a46549"}, +] + +[[package]] +name = "osqp" +version = "0.6.7.post1" +summary = "OSQP: The Operator Splitting QP Solver" +groups = ["default"] +dependencies = [ + "numpy>=1.7", + "qdldl", + "scipy>=0.13.2", +] +files = [ + {file = "osqp-0.6.7.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:14d221c049c2f1495a91d6683a3b0319f23d0c3e81b3aa5102e4b377ca002980"}, + {file = "osqp-0.6.7.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b0bd72d95d6b97ab8273cdd08c1304dfeb6071e038a0b2d34fa2aebd16cfbec5"}, + {file = "osqp-0.6.7.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4980f2ad0814898396a3ea522f46d199a3412bd3b191065d4ba6837e9cc4c1"}, + {file = "osqp-0.6.7.post1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e99469b7986f9042925d3082cd6d02cdf012a32483603b64a713f0275de413bb"}, + {file = "osqp-0.6.7.post1-cp310-cp310-win_amd64.whl", hash = "sha256:ab6e42c8af7c82f5b4b4b989a623151dca98e7bd6c131454edc8cf5cde2b3aa9"}, + {file = "osqp-0.6.7.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d01d6b03628b851107671d7c785df147acea6865f090290a04e38ed250d8b829"}, + {file = "osqp-0.6.7.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7082c852edc9afc63ba7b073bb2e559093b4df4eb24efff7b2f898241a83071c"}, + {file = "osqp-0.6.7.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9f104c9710d8e51cded15ac9b2b9bc77bc265e70c891c671c1935e4b85b0810"}, + {file = "osqp-0.6.7.post1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ae75ed6a5c6aba415b8d11963cec2b9ac4d7f1897067e9e095b60e81136022"}, + {file = "osqp-0.6.7.post1-cp311-cp311-win_amd64.whl", hash = "sha256:117c30affdab60f5872d758c5ad82f5deb029b4fa84fea54bd04b8e7d884c5f6"}, + {file = "osqp-0.6.7.post1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ed3c98cb31296368a72145875ceab3fb3e3497fdb820a185bf6b9ee39a3d5762"}, + {file = "osqp-0.6.7.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:98fdee4065b9f65c37f63532ba8e11e7efddce3eb9a8b62961bf1a9b62105e0a"}, + {file = "osqp-0.6.7.post1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20bf96fbf7d51abb95ab75e20508e37b1217cb467fc9cc9f73a584fbf1d5fc88"}, + {file = "osqp-0.6.7.post1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a41a9765be0ef1817ebfdcff10018f792e5452a5af3f2864ea44d7cb22e663"}, + {file = "osqp-0.6.7.post1-cp312-cp312-win_amd64.whl", hash = "sha256:fd56c7e82a6af11d96a549bb07d224359bcd148d4aae9180b8944d20eecd461b"}, + {file = "osqp-0.6.7.post1.tar.gz", hash = "sha256:554aa10dca8481978b4d334e28201f24ed18f294c5a84350ce2022a8b78f4d72"}, +] + +[[package]] +name = "packaging" +version = "24.1" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default", "dev", "docs"] +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pandas" +version = "2.2.2" +requires_python = ">=3.9" +summary = "Powerful data structures for data analysis, time series, and statistics" +groups = ["default"] +dependencies = [ + "numpy>=1.22.4; python_version < \"3.11\"", + "numpy>=1.23.2; python_version == \"3.11\"", + "numpy>=1.26.0; python_version >= \"3.12\"", + "python-dateutil>=2.8.2", + "pytz>=2020.1", + "tzdata>=2022.7", +] +files = [ + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default", "dev"] +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[[package]] +name = "plotly" +version = "5.23.0" +requires_python = ">=3.8" +summary = "An open-source, interactive data visualization library for Python" +groups = ["default"] +dependencies = [ + "packaging", + "tenacity>=6.2.0", +] +files = [ + {file = "plotly-5.23.0-py3-none-any.whl", hash = "sha256:76cbe78f75eddc10c56f5a4ee3e7ccaade7c0a57465546f02098c0caed6c2d1a"}, + {file = "plotly-5.23.0.tar.gz", hash = "sha256:89e57d003a116303a34de6700862391367dd564222ab71f8531df70279fc0193"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "polyfactory" +version = "2.16.2" +requires_python = "<4.0,>=3.8" +summary = "Mock data generation factories" +groups = ["default"] +dependencies = [ + "faker", + "typing-extensions>=4.6.0", +] +files = [ + {file = "polyfactory-2.16.2-py3-none-any.whl", hash = "sha256:e5eaf97358fee07d0d8de86a93e81dc56e3be1e1514d145fea6c5f486cda6ea1"}, + {file = "polyfactory-2.16.2.tar.gz", hash = "sha256:6d0d90deb85e5bb1733ea8744c2d44eea2b31656e11b4fa73832d2e2ab5422da"}, +] + +[[package]] +name = "prometheus-client" +version = "0.20.0" +requires_python = ">=3.8" +summary = "Python client for the Prometheus monitoring system." +groups = ["default"] +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] + +[[package]] +name = "proto-plus" +version = "1.24.0" +requires_python = ">=3.7" +summary = "Beautiful, Pythonic protocol buffers." +groups = ["default"] +marker = "python_version >= \"3.6\"" +dependencies = [ + "protobuf<6.0.0dev,>=3.19.0", +] +files = [ + {file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"}, + {file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"}, +] + +[[package]] +name = "protobuf" +version = "5.27.3" +requires_python = ">=3.8" +summary = "" +groups = ["default"] +files = [ + {file = "protobuf-5.27.3-cp310-abi3-win32.whl", hash = "sha256:dcb307cd4ef8fec0cf52cb9105a03d06fbb5275ce6d84a6ae33bc6cf84e0a07b"}, + {file = "protobuf-5.27.3-cp310-abi3-win_amd64.whl", hash = "sha256:16ddf3f8c6c41e1e803da7abea17b1793a97ef079a912e42351eabb19b2cffe7"}, + {file = "protobuf-5.27.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:68248c60d53f6168f565a8c76dc58ba4fa2ade31c2d1ebdae6d80f969cdc2d4f"}, + {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b8a994fb3d1c11156e7d1e427186662b64694a62b55936b2b9348f0a7c6625ce"}, + {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"}, + {file = "protobuf-5.27.3-py3-none-any.whl", hash = "sha256:8572c6533e544ebf6899c360e91d6bcbbee2549251643d32c52cf8a5de295ba5"}, + {file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"}, +] + +[[package]] +name = "py-spy" +version = "0.3.14" +summary = "Sampling profiler for Python programs " +groups = ["default"] +files = [ + {file = "py_spy-0.3.14-py2.py3-none-macosx_10_7_x86_64.whl", hash = "sha256:5b342cc5feb8d160d57a7ff308de153f6be68dcf506ad02b4d67065f2bae7f45"}, + {file = "py_spy-0.3.14-py2.py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:fe7efe6c91f723442259d428bf1f9ddb9c1679828866b353d539345ca40d9dd2"}, + {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590905447241d789d9de36cff9f52067b6f18d8b5e9fb399242041568d414461"}, + {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6211fe7f587b3532ba9d300784326d9a6f2b890af7bf6fff21a029ebbc812b"}, + {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3e8e48032e71c94c3dd51694c39e762e4bbfec250df5bf514adcdd64e79371e0"}, + {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f59b0b52e56ba9566305236375e6fc68888261d0d36b5addbe3cf85affbefc0e"}, + {file = "py_spy-0.3.14-py2.py3-none-win_amd64.whl", hash = "sha256:8f5b311d09f3a8e33dbd0d44fc6e37b715e8e0c7efefafcda8bfd63b31ab5a31"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.0" +requires_python = ">=3.8" +summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +groups = ["default"] +marker = "python_version >= \"3.6\"" +files = [ + {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, + {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.0" +requires_python = ">=3.8" +summary = "A collection of ASN.1-based protocols modules" +groups = ["default"] +marker = "python_version >= \"3.6\"" +dependencies = [ + "pyasn1<0.7.0,>=0.4.6", +] +files = [ + {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, + {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pycryptodome" +version = "3.20.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "Cryptographic library for Python" +groups = ["default"] +files = [ + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, + {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, +] + +[[package]] +name = "pydantic" +version = "2.9.1" +requires_python = ">=3.8" +summary = "Data validation using Python type hints" +groups = ["default"] +dependencies = [ + "annotated-types>=0.6.0", + "pydantic-core==2.23.3", + "typing-extensions>=4.12.2; python_version >= \"3.13\"", + "typing-extensions>=4.6.1; python_version < \"3.13\"", +] +files = [ + {file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"}, + {file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"}, +] + +[[package]] +name = "pydantic-core" +version = "2.23.3" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["default"] +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"}, + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"}, + {file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"}, + {file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"}, + {file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"}, + {file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"}, + {file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"}, + {file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"}, + {file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"}, + {file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"}, + {file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"}, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.15.4" +requires_python = ">=3.9" +summary = "Bootstrap-based Sphinx theme from the PyData community" +groups = ["docs"] +dependencies = [ + "Babel", + "accessible-pygments", + "beautifulsoup4", + "docutils!=0.17.0", + "packaging", + "pygments>=2.7", + "sphinx>=5", + "typing-extensions", +] +files = [ + {file = "pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6"}, + {file = "pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default", "docs"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[[package]] +name = "pymongo" +version = "4.8.0" +requires_python = ">=3.8" +summary = "Python driver for MongoDB " +groups = ["default"] +dependencies = [ + "dnspython<3.0.0,>=1.16.0", +] +files = [ + {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.tar.gz", hash = "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde"}, +] + +[[package]] +name = "pyro-api" +version = "0.1.2" +summary = "Generic API for dispatch to Pyro backends." +groups = ["default"] +files = [ + {file = "pyro-api-0.1.2.tar.gz", hash = "sha256:a1b900d9580aa1c2fab3b123ab7ff33413744da7c5f440bd4aadc4d40d14d920"}, + {file = "pyro_api-0.1.2-py3-none-any.whl", hash = "sha256:10e0e42e9e4401ce464dab79c870e50dfb4f413d326fa777f3582928ef9caf8f"}, +] + +[[package]] +name = "pyro-ppl" +version = "1.9.1" +requires_python = ">=3.8" +summary = "A Python library for probabilistic modeling and inference" +groups = ["default"] +dependencies = [ + "numpy>=1.7", + "opt-einsum>=2.3.2", + "pyro-api>=0.1.1", + "torch>=2.0", + "tqdm>=4.36", +] +files = [ + {file = "pyro_ppl-1.9.1-py3-none-any.whl", hash = "sha256:91fb2c8740d9d3bd548180ac5ecfa04552ed8c471a1ab66870180663b8f09852"}, + {file = "pyro_ppl-1.9.1.tar.gz", hash = "sha256:5e1596de276c038a3f77d2580a90d0a97126e0104900444a088eee620bb0d65e"}, +] + +[[package]] +name = "pytest" +version = "8.3.2" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +requires_python = ">=3.8" +summary = "Pytest support for asyncio" +groups = ["dev"] +dependencies = [ + "pytest<9,>=7.0.0", +] +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +requires_python = ">=3.8" +summary = "Pytest plugin for measuring coverage." +groups = ["dev"] +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["default"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +requires_python = ">=3.8" +summary = "Read key-value pairs from a .env file and set them as environment variables" +groups = ["default"] +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[[package]] +name = "pytz" +version = "2024.1" +summary = "World timezone definitions, modern and historical" +groups = ["default"] +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +requires_python = ">=3.8" +summary = "YAML parser and emitter for Python" +groups = ["default"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "qdldl" +version = "0.1.7.post4" +summary = "QDLDL, a free LDL factorization routine." +groups = ["default"] +dependencies = [ + "numpy>=1.7", + "scipy>=0.13.2", +] +files = [ + {file = "qdldl-0.1.7.post4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff4a9c5f7fa96e222c767aaaabea9d5df1d099e172c14b322b98d54dac03705d"}, + {file = "qdldl-0.1.7.post4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ad4ecd90c8031e0094fbab0b0bf09520b382177db63ec9568f06b4f16c219"}, + {file = "qdldl-0.1.7.post4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:490b52049c4cd794cb9bb2a8b26d69e74bbb71e55b5f0cac1480de971970d79c"}, + {file = "qdldl-0.1.7.post4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf39433b467d2b33872e96fd05ed4a74d701eb94cd14cb010d5980fbdc02954"}, + {file = "qdldl-0.1.7.post4-cp310-cp310-win_amd64.whl", hash = "sha256:5227ace6741618aa9aa2b0162740e806040f3a69e88204911e74b5d220d5bfce"}, + {file = "qdldl-0.1.7.post4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6639d63c3bf9abbfdffafd3c99b7c603359ca748ab62117ec7fc0948a1c5e77"}, + {file = "qdldl-0.1.7.post4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87f31e7f2a2708def201b6dc507a48ada7e0c37efd0afda7ef6ef94ae3487c2c"}, + {file = "qdldl-0.1.7.post4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47c1b27712444d7b1030c562ed79af18320b4a910454716c9d88114e181eddec"}, + {file = "qdldl-0.1.7.post4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1496a820ffb0c1a5bb18392b44052b83b5442745b15f62bbf2d22eec1f506afe"}, + {file = "qdldl-0.1.7.post4-cp311-cp311-win_amd64.whl", hash = "sha256:b6f8d59c01fa5c9dc3b6463fc7e1de7601dcb1aa16b6e14a6d5d283169dc629f"}, + {file = "qdldl-0.1.7.post4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2b9e92bb52d3bc49cfc9fd9a761adb692f049c46e68c0535ed07df2de8292f5"}, + {file = "qdldl-0.1.7.post4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f6710b0c1013292697262803ddd549a81cdfdbdbbbcfa5b56aad04ac9cebbb4a"}, + {file = "qdldl-0.1.7.post4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a40429f5c0d0edb28d22c4e52c2459fd9a64892ba7d8a39ba51a1a37b3581927"}, + {file = "qdldl-0.1.7.post4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b85beb51096100dcdea575acedbafb5bac2b7f44485a1d7090bb68a47c8f9928"}, + {file = "qdldl-0.1.7.post4-cp312-cp312-win_amd64.whl", hash = "sha256:684306b37a2f06f72c18edd2d6fa45a832e99071ebd87b875d172719e09a322d"}, + {file = "qdldl-0.1.7.post4.tar.gz", hash = "sha256:0c163b9afb92c4b69d446387b1d4295094438b041ec4e8510271b6c4ff1f86fd"}, +] + +[[package]] +name = "ray" +version = "2.35.0" +requires_python = ">=3.8" +summary = "Ray provides a simple, universal API for building distributed applications." +groups = ["default"] +dependencies = [ + "aiosignal", + "click>=7.0", + "filelock", + "frozenlist", + "jsonschema", + "msgpack<2.0.0,>=1.0.0", + "packaging", + "protobuf!=3.19.5,>=3.15.3", + "pyyaml", + "requests", +] +files = [ + {file = "ray-2.35.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1e7e2d2e987be728a81821b6fd2bccb23e4d8a6cca8417db08b24f06a08d8476"}, + {file = "ray-2.35.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bd48be4c362004d31e5df072fd58b929efc67adfefc0adece41483b15f84539"}, + {file = "ray-2.35.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ef41e9254f3e18a90a8cf13fac9e35ac086eb778079ab6c76a37d3a6059186c5"}, + {file = "ray-2.35.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:1994aaf9996ffc45019856545e817d527ad572762f1af76ad669ae4e786fcfd6"}, + {file = "ray-2.35.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3b7a7d73f818e249064460ffa95402ebd852bf97d9ec6167b8b0d95be03da9f"}, + {file = "ray-2.35.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:e29754fac4b69a9cb0d089841af59ec6fb10b5d4a248b7c579d319ca2ed1c96f"}, + {file = "ray-2.35.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7a606c8ca53c64fc496703e9fd15d1a1ffb50e6b457a33d3622be2f13fc30a5"}, + {file = "ray-2.35.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:ac561e20a62ce941b74d02a0b92b7765c6ba87cc22e24f34f64ded2c454ba64e"}, + {file = "ray-2.35.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:587af570cbe5f6cedca854f15107740e63c67207bee900713cb2ee38f6ebf20f"}, + {file = "ray-2.35.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e406cce41679790146d4d2b1b0cb0b413ca35276e43b68ee796366169c1dbde"}, + {file = "ray-2.35.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:eb86355a3a0e794e2f1dbd5a84805dddfca64921ad0999b7fa5276e40d243692"}, + {file = "ray-2.35.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b746913268d5ea5e19bff0eb6bdc7e0538036892a8b57c08411787481195df2"}, + {file = "ray-2.35.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:e2ccfd144180f03d38b02a81afdac2b437f27e46736bf2653a1f0e8d67ea56cd"}, + {file = "ray-2.35.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:2ca1a0de41d4462fd764598a5981cf55fc955599f38f9a1ae10868e94c6dd80d"}, + {file = "ray-2.35.0-cp312-cp312-win_amd64.whl", hash = "sha256:c5600f745bb0e4df840a5cd51e82b1acf517f73505df9869fe3e369966956129"}, +] + +[[package]] +name = "ray" +version = "2.35.0" +extras = ["default"] +requires_python = ">=3.8" +summary = "Ray provides a simple, universal API for building distributed applications." +groups = ["default"] +dependencies = [ + "aiohttp-cors", + "aiohttp>=3.7", + "colorful", + "grpcio>=1.32.0; python_version < \"3.10\"", + "grpcio>=1.42.0; python_version >= \"3.10\"", + "memray; sys_platform != \"win32\"", + "opencensus", + "prometheus-client>=0.7.1", + "py-spy>=0.2.0", + "pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3", + "ray==2.35.0", + "requests", + "smart-open", + "virtualenv!=20.21.1,>=20.0.24", +] +files = [ + {file = "ray-2.35.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1e7e2d2e987be728a81821b6fd2bccb23e4d8a6cca8417db08b24f06a08d8476"}, + {file = "ray-2.35.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bd48be4c362004d31e5df072fd58b929efc67adfefc0adece41483b15f84539"}, + {file = "ray-2.35.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ef41e9254f3e18a90a8cf13fac9e35ac086eb778079ab6c76a37d3a6059186c5"}, + {file = "ray-2.35.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:1994aaf9996ffc45019856545e817d527ad572762f1af76ad669ae4e786fcfd6"}, + {file = "ray-2.35.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3b7a7d73f818e249064460ffa95402ebd852bf97d9ec6167b8b0d95be03da9f"}, + {file = "ray-2.35.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:e29754fac4b69a9cb0d089841af59ec6fb10b5d4a248b7c579d319ca2ed1c96f"}, + {file = "ray-2.35.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7a606c8ca53c64fc496703e9fd15d1a1ffb50e6b457a33d3622be2f13fc30a5"}, + {file = "ray-2.35.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:ac561e20a62ce941b74d02a0b92b7765c6ba87cc22e24f34f64ded2c454ba64e"}, + {file = "ray-2.35.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:587af570cbe5f6cedca854f15107740e63c67207bee900713cb2ee38f6ebf20f"}, + {file = "ray-2.35.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e406cce41679790146d4d2b1b0cb0b413ca35276e43b68ee796366169c1dbde"}, + {file = "ray-2.35.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:eb86355a3a0e794e2f1dbd5a84805dddfca64921ad0999b7fa5276e40d243692"}, + {file = "ray-2.35.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b746913268d5ea5e19bff0eb6bdc7e0538036892a8b57c08411787481195df2"}, + {file = "ray-2.35.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:e2ccfd144180f03d38b02a81afdac2b437f27e46736bf2653a1f0e8d67ea56cd"}, + {file = "ray-2.35.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:2ca1a0de41d4462fd764598a5981cf55fc955599f38f9a1ae10868e94c6dd80d"}, + {file = "ray-2.35.0-cp312-cp312-win_amd64.whl", hash = "sha256:c5600f745bb0e4df840a5cd51e82b1acf517f73505df9869fe3e369966956129"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +requires_python = ">=3.8" +summary = "JSON Referencing + Python" +groups = ["default"] +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", +] +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["default", "docs"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[[package]] +name = "rich" +version = "13.8.1" +requires_python = ">=3.7.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, +] + +[[package]] +name = "rich-click" +version = "1.8.3" +requires_python = ">=3.7" +summary = "Format click help output nicely with rich" +groups = ["default"] +dependencies = [ + "click>=7", + "importlib-metadata; python_version < \"3.8\"", + "rich>=10.7", + "typing-extensions", +] +files = [ + {file = "rich_click-1.8.3-py3-none-any.whl", hash = "sha256:636d9c040d31c5eee242201b5bf4f2d358bfae4db14bb22ec1cafa717cfd02cd"}, + {file = "rich_click-1.8.3.tar.gz", hash = "sha256:6d75bdfa7aa9ed2c467789a0688bc6da23fbe3a143e19aa6ad3f8bac113d2ab3"}, +] + +[[package]] +name = "rpds-py" +version = "0.20.0" +requires_python = ">=3.8" +summary = "Python bindings to Rust's persistent data structures (rpds)" +groups = ["default"] +files = [ + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, +] + +[[package]] +name = "rsa" +version = "4.9" +requires_python = ">=3.6,<4" +summary = "Pure-Python RSA implementation" +groups = ["default"] +marker = "python_version >= \"3.6\"" +dependencies = [ + "pyasn1>=0.1.3", +] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[[package]] +name = "ruff" +version = "0.5.7" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["dev"] +files = [ + {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, + {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, + {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, + {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, + {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, + {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, + {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, +] + +[[package]] +name = "scikit-learn" +version = "1.5.1" +requires_python = ">=3.9" +summary = "A set of python modules for machine learning and data mining" +groups = ["default"] +dependencies = [ + "joblib>=1.2.0", + "numpy>=1.19.5", + "scipy>=1.6.0", + "threadpoolctl>=3.1.0", +] +files = [ + {file = "scikit_learn-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:781586c414f8cc58e71da4f3d7af311e0505a683e112f2f62919e3019abd3745"}, + {file = "scikit_learn-1.5.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5b213bc29cc30a89a3130393b0e39c847a15d769d6e59539cd86b75d276b1a7"}, + {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ff4ba34c2abff5ec59c803ed1d97d61b036f659a17f55be102679e88f926fac"}, + {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:161808750c267b77b4a9603cf9c93579c7a74ba8486b1336034c2f1579546d21"}, + {file = "scikit_learn-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:10e49170691514a94bb2e03787aa921b82dbc507a4ea1f20fd95557862c98dc1"}, + {file = "scikit_learn-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:154297ee43c0b83af12464adeab378dee2d0a700ccd03979e2b821e7dd7cc1c2"}, + {file = "scikit_learn-1.5.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b5e865e9bd59396220de49cb4a57b17016256637c61b4c5cc81aaf16bc123bbe"}, + {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909144d50f367a513cee6090873ae582dba019cb3fca063b38054fa42704c3a4"}, + {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689b6f74b2c880276e365fe84fe4f1befd6a774f016339c65655eaff12e10cbf"}, + {file = "scikit_learn-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:9a07f90846313a7639af6a019d849ff72baadfa4c74c778821ae0fad07b7275b"}, + {file = "scikit_learn-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5944ce1faada31c55fb2ba20a5346b88e36811aab504ccafb9f0339e9f780395"}, + {file = "scikit_learn-1.5.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0828673c5b520e879f2af6a9e99eee0eefea69a2188be1ca68a6121b809055c1"}, + {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508907e5f81390e16d754e8815f7497e52139162fd69c4fdbd2dfa5d6cc88915"}, + {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97625f217c5c0c5d0505fa2af28ae424bd37949bb2f16ace3ff5f2f81fb4498b"}, + {file = "scikit_learn-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:da3f404e9e284d2b0a157e1b56b6566a34eb2798205cba35a211df3296ab7a74"}, + {file = "scikit_learn-1.5.1.tar.gz", hash = "sha256:0ea5d40c0e3951df445721927448755d3fe1d80833b0b7308ebff5d2a45e6414"}, +] + +[[package]] +name = "scipy" +version = "1.14.0" +requires_python = ">=3.10" +summary = "Fundamental algorithms for scientific computing in Python" +groups = ["default"] +dependencies = [ + "numpy<2.3,>=1.23.5", +] +files = [ + {file = "scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0"}, + {file = "scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d"}, + {file = "scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74"}, + {file = "scipy-1.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb"}, + {file = "scipy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14"}, + {file = "scipy-1.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159"}, + {file = "scipy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20"}, + {file = "scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b"}, +] + +[[package]] +name = "scs" +version = "3.2.6" +requires_python = ">=3.7" +summary = "Splitting conic solver" +groups = ["default"] +dependencies = [ + "numpy", + "scipy", +] +files = [ + {file = "scs-3.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df520880f456e94cb10a4a380d69ccf74d20f8e1576e3e70b4508d8bb897f62"}, + {file = "scs-3.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecff69a2e300eed03159059da5bf431f3077c7a147c56ec6e52605b35f0dba57"}, + {file = "scs-3.2.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:4716b670c1f29e75bd4ce993d5ed2b76a71d3bc04fd82696e4e6cdd0c8529580"}, + {file = "scs-3.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:23cf3f783a9ad88b42a0dd6ca37b46d2a0a7776b49c851adc810bb0b8669865c"}, + {file = "scs-3.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b2cb93492305dc17961602cc3d2d81d8918ce7f2cdd15e4d5958566a4bdfe5e1"}, + {file = "scs-3.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8b34bc900cca3b56c7e4ff988eeee84b3dc15667d68f0f8fbbe4fbb2433c29c"}, + {file = "scs-3.2.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bc4f91e0ce27f662d4fa12704212e7142e1cd1bd3db0344730f763e27e9824d8"}, + {file = "scs-3.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:f971008b76272f085c9ffe9416ed4abe9f52cf111c6798491b3a5d4fcaf10476"}, + {file = "scs-3.2.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7a40f42834109e079e2cd03bcb0a99540a04cd837bf8d7edb5407ad78e70fc70"}, + {file = "scs-3.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:65d36db28955569cd56b0ef90aa551e5d9b1c42b9988e38844084b430b795d12"}, + {file = "scs-3.2.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b65f6d5d8b1e3eb8bbbb0e9b791cd6e7ad8205d2c4023d8368762c19fe854f19"}, + {file = "scs-3.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:55e3eb71880baf05fd0cd6671fbfb563210c2579e15cbc18491b5c90e659d258"}, + {file = "scs-3.2.6.tar.gz", hash = "sha256:caf6ef48b86e8d4712a3d7b586ffb7a2b413c2a9664ac4da2c8de81dec6a1020"}, +] + +[[package]] +name = "setuptools" +version = "72.2.0" +requires_python = ">=3.8" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["default"] +files = [ + {file = "setuptools-72.2.0-py3-none-any.whl", hash = "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4"}, + {file = "setuptools-72.2.0.tar.gz", hash = "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +requires_python = ">=3.7" +summary = "Tool to Detect Surrounding Shell" +groups = ["default"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smart-open" +version = "7.0.4" +requires_python = "<4.0,>=3.7" +summary = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)" +groups = ["default"] +dependencies = [ + "wrapt", +] +files = [ + {file = "smart_open-7.0.4-py3-none-any.whl", hash = "sha256:4e98489932b3372595cddc075e6033194775165702887216b65eba760dfd8d47"}, + {file = "smart_open-7.0.4.tar.gz", hash = "sha256:62b65852bdd1d1d516839fcb1f6bc50cd0f16e05b4ec44b52f43d38bcb838524"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default", "docs"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +groups = ["docs"] +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +requires_python = ">=3.8" +summary = "A modern CSS selector implementation for Beautiful Soup." +groups = ["docs"] +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "sphinx" +version = "8.0.2" +requires_python = ">=3.10" +summary = "Python documentation generator" +groups = ["docs"] +dependencies = [ + "Jinja2>=3.1", + "Pygments>=2.17", + "alabaster>=0.7.14", + "babel>=2.13", + "colorama>=0.4.6; sys_platform == \"win32\"", + "docutils<0.22,>=0.20", + "imagesize>=1.3", + "packaging>=23.0", + "requests>=2.30.0", + "snowballstemmer>=2.2", + "sphinxcontrib-applehelp", + "sphinxcontrib-devhelp", + "sphinxcontrib-htmlhelp>=2.0.0", + "sphinxcontrib-jsmath", + "sphinxcontrib-qthelp", + "sphinxcontrib-serializinghtml>=1.1.9", + "tomli>=2; python_version < \"3.11\"", +] +files = [ + {file = "sphinx-8.0.2-py3-none-any.whl", hash = "sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d"}, + {file = "sphinx-8.0.2.tar.gz", hash = "sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b"}, +] + +[[package]] +name = "sphinx-autobuild" +version = "2024.4.16" +requires_python = ">=3.9" +summary = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." +groups = ["docs"] +dependencies = [ + "colorama", + "sphinx", + "starlette>=0.35", + "uvicorn>=0.25", + "watchfiles>=0.20", + "websockets>=11", +] +files = [ + {file = "sphinx_autobuild-2024.4.16-py3-none-any.whl", hash = "sha256:f2522779d30fcbf0253e09714f274ce8c608cb6ebcd67922b1c54de59faba702"}, + {file = "sphinx_autobuild-2024.4.16.tar.gz", hash = "sha256:1c0ed37a1970eed197f9c5a66d65759e7c4e4cba7b5a5d77940752bf1a59f2c7"}, +] + +[[package]] +name = "sphinx-click" +version = "6.0.0" +requires_python = ">=3.8" +summary = "Sphinx extension that automatically documents click applications" +groups = ["docs"] +dependencies = [ + "click>=8.0", + "docutils", + "sphinx>=4.0", +] +files = [ + {file = "sphinx_click-6.0.0-py3-none-any.whl", hash = "sha256:1e0a3c83bcb7c55497751b19d07ebe56b5d7b85eb76dd399cf9061b497adc317"}, + {file = "sphinx_click-6.0.0.tar.gz", hash = "sha256:f5d664321dc0c6622ff019f1e1c84e58ce0cecfddeb510e004cf60c2a3ab465b"}, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +requires_python = ">=3.7" +summary = "Add a copy button to each of your code cells." +groups = ["docs"] +dependencies = [ + "sphinx>=1.8", +] +files = [ + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, +] + +[[package]] +name = "sphinx-design" +version = "0.6.1" +requires_python = ">=3.9" +summary = "A sphinx extension for designing beautiful, view size responsive web components." +groups = ["docs"] +dependencies = [ + "sphinx<9,>=6", +] +files = [ + {file = "sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c"}, + {file = "sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632"}, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +requires_python = ">=3.9" +summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +groups = ["docs"] +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +requires_python = ">=3.9" +summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +groups = ["docs"] +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +requires_python = ">=3.9" +summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +groups = ["docs"] +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +requires_python = ">=3.5" +summary = "A sphinx extension which renders display math in HTML via JavaScript" +groups = ["docs"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +requires_python = ">=3.9" +summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +groups = ["docs"] +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +requires_python = ">=3.9" +summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +groups = ["docs"] +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[[package]] +name = "starlette" +version = "0.38.2" +requires_python = ">=3.8" +summary = "The little ASGI library that shines." +groups = ["docs"] +dependencies = [ + "anyio<5,>=3.4.0", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff"}, + {file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"}, +] + +[[package]] +name = "sympy" +version = "1.13.2" +requires_python = ">=3.8" +summary = "Computer algebra system (CAS) in Python" +groups = ["default"] +dependencies = [ + "mpmath<1.4,>=1.1.0", +] +files = [ + {file = "sympy-1.13.2-py3-none-any.whl", hash = "sha256:c51d75517712f1aed280d4ce58506a4a88d635d6b5dd48b39102a7ae1f3fcfe9"}, + {file = "sympy-1.13.2.tar.gz", hash = "sha256:401449d84d07be9d0c7a46a64bd54fe097667d5e7181bfe67ec777be9e01cb13"}, +] + +[[package]] +name = "tenacity" +version = "9.0.0" +requires_python = ">=3.8" +summary = "Retry code until it succeeds" +groups = ["default"] +files = [ + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, +] + +[[package]] +name = "textual" +version = "0.76.0" +requires_python = "<4.0.0,>=3.8.1" +summary = "Modern Text User Interface framework" +groups = ["default"] +marker = "sys_platform != \"win32\"" +dependencies = [ + "markdown-it-py[linkify,plugins]>=2.1.0", + "rich>=13.3.3", + "typing-extensions<5.0.0,>=4.4.0", +] +files = [ + {file = "textual-0.76.0-py3-none-any.whl", hash = "sha256:e2035609c889dba507d34a5d7b333f1c8c53a29fb170962cb92101507663517a"}, + {file = "textual-0.76.0.tar.gz", hash = "sha256:b12e8879d591090c0901b5cb8121d086e28e677353b368292d3865ec99b83b70"}, +] + +[[package]] +name = "threadpoolctl" +version = "3.5.0" +requires_python = ">=3.8" +summary = "threadpoolctl" +groups = ["default"] +files = [ + {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, + {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["dev", "docs"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "torch" +version = "2.4.0" +requires_python = ">=3.8.0" +summary = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +groups = ["default"] +dependencies = [ + "filelock", + "fsspec", + "jinja2", + "networkx", + "nvidia-cublas-cu12==12.1.3.1; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-cuda-cupti-cu12==12.1.105; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-cuda-nvrtc-cu12==12.1.105; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-cuda-runtime-cu12==12.1.105; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-cudnn-cu12==9.1.0.70; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-cufft-cu12==11.0.2.54; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-curand-cu12==10.3.2.106; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-cusolver-cu12==11.4.5.107; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-cusparse-cu12==12.1.0.106; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-nccl-cu12==2.20.5; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "nvidia-nvtx-cu12==12.1.105; platform_system == \"Linux\" and platform_machine == \"x86_64\"", + "setuptools", + "sympy", + "triton==3.0.0; platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"", + "typing-extensions>=4.8.0", +] +files = [ + {file = "torch-2.4.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:4ed94583e244af51d6a8d28701ca5a9e02d1219e782f5a01dd401f90af17d8ac"}, + {file = "torch-2.4.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c4ca297b7bd58b506bfd6e78ffd14eb97c0e7797dcd7965df62f50bb575d8954"}, + {file = "torch-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:2497cbc7b3c951d69b276ca51fe01c2865db67040ac67f5fc20b03e41d16ea4a"}, + {file = "torch-2.4.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:685418ab93730efbee71528821ff54005596970dd497bf03c89204fb7e3f71de"}, + {file = "torch-2.4.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e743adadd8c8152bb8373543964551a7cb7cc20ba898dc8f9c0cdbe47c283de0"}, + {file = "torch-2.4.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:7334325c0292cbd5c2eac085f449bf57d3690932eac37027e193ba775703c9e6"}, + {file = "torch-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:97730014da4c57ffacb3c09298c6ce05400606e890bd7a05008d13dd086e46b1"}, + {file = "torch-2.4.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:f169b4ea6dc93b3a33319611fcc47dc1406e4dd539844dcbd2dec4c1b96e166d"}, + {file = "torch-2.4.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:997084a0f9784d2a89095a6dc67c7925e21bf25dea0b3d069b41195016ccfcbb"}, + {file = "torch-2.4.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:bc3988e8b36d1e8b998d143255d9408d8c75da4ab6dd0dcfd23b623dfb0f0f57"}, + {file = "torch-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3374128bbf7e62cdaed6c237bfd39809fbcfaa576bee91e904706840c3f2195c"}, + {file = "torch-2.4.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:91aaf00bfe1ffa44dc5b52809d9a95129fca10212eca3ac26420eb11727c6288"}, +] + +[[package]] +name = "tqdm" +version = "4.66.5" +requires_python = ">=3.7" +summary = "Fast, Extensible Progress Meter" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, + {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, +] + +[[package]] +name = "triton" +version = "3.0.0" +summary = "A language and compiler for custom Deep Learning operations" +groups = ["default"] +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" +dependencies = [ + "filelock", +] +files = [ + {file = "triton-3.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1efef76935b2febc365bfadf74bcb65a6f959a9872e5bddf44cc9e0adce1e1a"}, + {file = "triton-3.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ce8520437c602fb633f1324cc3871c47bee3b67acf9756c1a66309b60e3216c"}, + {file = "triton-3.0.0-1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e509deb77f1c067d8640725ef00c5cbfcb2052a1a3cb6a6d343841f92624eb"}, + {file = "triton-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b052da883351fdf6be3d93cedae6db3b8e3988d3b09ed221bccecfa9612230"}, + {file = "triton-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd34f19a8582af96e6291d4afce25dac08cb2a5d218c599163761e8e0827208e"}, + {file = "triton-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d5e10de8c011adeb7c878c6ce0dd6073b14367749e34467f1cff2bde1b78253"}, +] + +[[package]] +name = "typeguard" +version = "2.13.3" +requires_python = ">=3.5.3" +summary = "Run-time type checker for Python" +groups = ["default"] +files = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] + +[[package]] +name = "typer" +version = "0.12.5" +requires_python = ">=3.7" +summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." +groups = ["default"] +dependencies = [ + "click>=8.0.0", + "rich>=10.11.0", + "shellingham>=1.3.0", + "typing-extensions>=3.7.4.3", +] +files = [ + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default", "dev", "docs"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +groups = ["default"] +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +requires_python = ">=3.7" +summary = "Micro subset of unicode data files for linkify-it-py projects." +groups = ["default"] +marker = "sys_platform != \"win32\"" +files = [ + {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, + {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default", "docs"] +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[[package]] +name = "uvicorn" +version = "0.30.6" +requires_python = ">=3.8" +summary = "The lightning-fast ASGI server." +groups = ["default", "docs"] +dependencies = [ + "click>=7.0", + "h11>=0.8", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, +] + +[[package]] +name = "uvicorn" +version = "0.30.6" +extras = ["standard"] +requires_python = ">=3.8" +summary = "The lightning-fast ASGI server." +groups = ["default"] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "httptools>=0.5.0", + "python-dotenv>=0.13", + "pyyaml>=5.1", + "uvicorn==0.30.6", + "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", + "watchfiles>=0.13", + "websockets>=10.4", +] +files = [ + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, +] + +[[package]] +name = "uvloop" +version = "0.19.0" +requires_python = ">=3.8.0" +summary = "Fast implementation of asyncio event loop on top of libuv" +groups = ["default"] +marker = "sys_platform != \"win32\"" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.3" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +groups = ["default"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", +] +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[[package]] +name = "watchfiles" +version = "0.23.0" +requires_python = ">=3.8" +summary = "Simple, modern and high performance file watching and code reload in python." +groups = ["default", "docs"] +dependencies = [ + "anyio>=3.0.0", +] +files = [ + {file = "watchfiles-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4"}, + {file = "watchfiles-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb02d41c33be667e6135e6686f1bb76104c88a312a18faa0ef0262b5bf7f1a0f"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf12ac34c444362f3261fb3ff548f0037ddd4c5bb85f66c4be30d2936beb3c5"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0b2c25040a3c0ce0e66c7779cc045fdfbbb8d59e5aabfe033000b42fe44b53e"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf2be4b9eece4f3da8ba5f244b9e51932ebc441c0867bd6af46a3d97eb068d6"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40cb8fa00028908211eb9f8d47744dca21a4be6766672e1ff3280bee320436f1"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f48c917ffd36ff9a5212614c2d0d585fa8b064ca7e66206fb5c095015bc8207"}, + {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d183e3888ada88185ab17064079c0db8c17e32023f5c278d7bf8014713b1b5b"}, + {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9837edf328b2805346f91209b7e660f65fb0e9ca18b7459d075d58db082bf981"}, + {file = "watchfiles-0.23.0-cp310-none-win32.whl", hash = "sha256:296e0b29ab0276ca59d82d2da22cbbdb39a23eed94cca69aed274595fb3dfe42"}, + {file = "watchfiles-0.23.0-cp310-none-win_amd64.whl", hash = "sha256:4ea756e425ab2dfc8ef2a0cb87af8aa7ef7dfc6fc46c6f89bcf382121d4fff75"}, + {file = "watchfiles-0.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e397b64f7aaf26915bf2ad0f1190f75c855d11eb111cc00f12f97430153c2eab"}, + {file = "watchfiles-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4ac73b02ca1824ec0a7351588241fd3953748d3774694aa7ddb5e8e46aef3e3"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a896d53b48a1cecccfa903f37a1d87dbb74295305f865a3e816452f6e49e4"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5e7803a65eb2d563c73230e9d693c6539e3c975ccfe62526cadde69f3fda0cf"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1aa4cc85202956d1a65c88d18c7b687b8319dbe6b1aec8969784ef7a10e7d1a"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87f889f6e58849ddb7c5d2cb19e2e074917ed1c6e3ceca50405775166492cca8"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37fd826dac84c6441615aa3f04077adcc5cac7194a021c9f0d69af20fb9fa788"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7db6e36e7a2c15923072e41ea24d9a0cf39658cb0637ecc9307b09d28827e1"}, + {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2368c5371c17fdcb5a2ea71c5c9d49f9b128821bfee69503cc38eae00feb3220"}, + {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:857af85d445b9ba9178db95658c219dbd77b71b8264e66836a6eba4fbf49c320"}, + {file = "watchfiles-0.23.0-cp311-none-win32.whl", hash = "sha256:1d636c8aeb28cdd04a4aa89030c4b48f8b2954d8483e5f989774fa441c0ed57b"}, + {file = "watchfiles-0.23.0-cp311-none-win_amd64.whl", hash = "sha256:46f1d8069a95885ca529645cdbb05aea5837d799965676e1b2b1f95a4206313e"}, + {file = "watchfiles-0.23.0-cp311-none-win_arm64.whl", hash = "sha256:e495ed2a7943503766c5d1ff05ae9212dc2ce1c0e30a80d4f0d84889298fa304"}, + {file = "watchfiles-0.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1db691bad0243aed27c8354b12d60e8e266b75216ae99d33e927ff5238d270b5"}, + {file = "watchfiles-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62d2b18cb1edaba311fbbfe83fb5e53a858ba37cacb01e69bc20553bb70911b8"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e087e8fdf1270d000913c12e6eca44edd02aad3559b3e6b8ef00f0ce76e0636f"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd41d5c72417b87c00b1b635738f3c283e737d75c5fa5c3e1c60cd03eac3af77"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5f3ca0ff47940ce0a389457b35d6df601c317c1e1a9615981c474452f98de1"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6991e3a78f642368b8b1b669327eb6751439f9f7eaaa625fae67dd6070ecfa0b"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f7252f52a09f8fa5435dc82b6af79483118ce6bd51eb74e6269f05ee22a7b9f"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e01bcb8d767c58865207a6c2f2792ad763a0fe1119fb0a430f444f5b02a5ea0"}, + {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8e56fbcdd27fce061854ddec99e015dd779cae186eb36b14471fc9ae713b118c"}, + {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd3e2d64500a6cad28bcd710ee6269fbeb2e5320525acd0cfab5f269ade68581"}, + {file = "watchfiles-0.23.0-cp312-none-win32.whl", hash = "sha256:eb99c954291b2fad0eff98b490aa641e128fbc4a03b11c8a0086de8b7077fb75"}, + {file = "watchfiles-0.23.0-cp312-none-win_amd64.whl", hash = "sha256:dccc858372a56080332ea89b78cfb18efb945da858fabeb67f5a44fa0bcb4ebb"}, + {file = "watchfiles-0.23.0-cp312-none-win_arm64.whl", hash = "sha256:6c21a5467f35c61eafb4e394303720893066897fca937bade5b4f5877d350ff8"}, + {file = "watchfiles-0.23.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ba31c32f6b4dceeb2be04f717811565159617e28d61a60bb616b6442027fd4b9"}, + {file = "watchfiles-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85042ab91814fca99cec4678fc063fb46df4cbb57b4835a1cc2cb7a51e10250e"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24655e8c1c9c114005c3868a3d432c8aa595a786b8493500071e6a52f3d09217"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1a950ab299a4a78fd6369a97b8763732bfb154fdb433356ec55a5bce9515c1"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8d3c5cd327dd6ce0edfc94374fb5883d254fe78a5e9d9dfc237a1897dc73cd1"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ff785af8bacdf0be863ec0c428e3288b817e82f3d0c1d652cd9c6d509020dd0"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02b7ba9d4557149410747353e7325010d48edcfe9d609a85cb450f17fd50dc3d"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a1b05c0afb2cd2f48c1ed2ae5487b116e34b93b13074ed3c22ad5c743109f0"}, + {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:109a61763e7318d9f821b878589e71229f97366fa6a5c7720687d367f3ab9eef"}, + {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9f8e6bb5ac007d4a4027b25f09827ed78cbbd5b9700fd6c54429278dacce05d1"}, + {file = "watchfiles-0.23.0-cp313-none-win32.whl", hash = "sha256:f46c6f0aec8d02a52d97a583782d9af38c19a29900747eb048af358a9c1d8e5b"}, + {file = "watchfiles-0.23.0-cp313-none-win_amd64.whl", hash = "sha256:f449afbb971df5c6faeb0a27bca0427d7b600dd8f4a068492faec18023f0dcff"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9265cf87a5b70147bfb2fec14770ed5b11a5bb83353f0eee1c25a81af5abfe"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f02a259fcbbb5fcfe7a0805b1097ead5ba7a043e318eef1db59f93067f0b49b"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebaebb53b34690da0936c256c1cdb0914f24fb0e03da76d185806df9328abed"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd257f98cff9c6cb39eee1a83c7c3183970d8a8d23e8cf4f47d9a21329285cee"}, + {file = "watchfiles-0.23.0.tar.gz", hash = "sha256:9338ade39ff24f8086bb005d16c29f8e9f19e55b18dcb04dfa26fcbc09da497b"}, +] + +[[package]] +name = "websockets" +version = "12.0" +requires_python = ">=3.8" +summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +groups = ["default", "docs"] +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +requires_python = ">=3.6" +summary = "Module for decorators, wrappers and monkey patching." +groups = ["default"] +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[[package]] +name = "yarl" +version = "1.9.4" +requires_python = ">=3.7" +summary = "Yet another URL library" +groups = ["default"] +dependencies = [ + "idna>=2.0", + "multidict>=4.0", + "typing-extensions>=3.7.4; python_version < \"3.8\"", +] +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e342404 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,151 @@ +[project] +name = "eos" +version = "0.3.0" +description = "The Experiment Orchestration System (EOS) is a comprehensive software framework and runtime for laboratory automation." +keywords = ["automation", "science", "lab", "experiment", "orchestration", "distributed", "infrastructure"] +authors = [ + { name = "Angelos Angelopoulos", email = "aangelos@cs.unc.edu" } +] +license = { text = "BSD 3-Clause" } +classifiers = [ + "Development Status :: 3 - Alpha", + "Natural Language :: English", + "Operating System :: OS Independent", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python", + "Typing :: Typed", + "Intended Audience :: Science/Research", +] +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "ray[default]~=2.35.0", + "typer~=0.12.5", + "rich~=13.8.1", + "omegaconf~=2.3.0", + "jinja2~=3.1.4", + "PyYAML~=6.0.2", + "networkx~=3.3.0", + "pymongo~=4.8.0", + "pydantic~=2.9.1", + "bofire[optimization]~=0.0.13", + "pandas~=2.2.2", + "numpy~=1.26.2", + "litestar[standard]~=2.11.0", + "minio~=7.2.8", +] + +[project.optional-dependencies] +dev = [ + "ruff", + "pytest", + "pytest-cov", + "pytest-asyncio", + "black", +] +docs = [ + "sphinx", + "sphinx-autobuild", + "sphinx-copybutton", + "sphinx-design", + "sphinx-click", + "pydata-sphinx-theme", +] + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[tool.pdm.build] +includes = ["eos"] + +[tool.pdm.scripts] +test = "pytest" +test-with-cov = "pytest --cov=eos" +cov-report = "coverage html" +lint = "ruff check eos tests" +format = "black ." +docs-build = "sphinx-build docs docs/_build" +docs-build-gh = { shell = "sphinx-build docs docs/_build && touch docs/_build/.nojekyll" } +docs-serve = "sphinx-autobuild docs docs/_build/ -j auto --watch eos --watch docs --port 8002" + +[project.scripts] +eos = "eos.eos:eos_app" + +[tool.black] +line-length = 120 + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] + +[tool.ruff] +include = [ + "{eos,tests}/**/*.py", + "pyproject.toml" +] +target-version = "py310" +line-length = 120 + +lint.mccabe.max-complexity = 14 +lint.isort.known-first-party = ["eos", "tests"] + +lint.select = [ + "A", # flake8-builtins + "ANN", # flake8-annotations + "ASYNC", # flake8-async + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "C90", # mccabe + "DTZ", # flake8-datetimez + "E", # pycodestyle errors + "ERA", # eradicate + "EXE", # flake8-executable + "F", # pyflakes + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "PIE", # flake8-pie + "PLC", # pylint - convention + "PT", # flake8-pytest + "PLE", # pylint - error + "PLR", # pylint - refactor + "PLW", # pylint - warning + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "T10", # flake8-debugger + "T20", # flake8-print + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle - warning + "YTT", # flake8-2020 +] +lint.ignore = ["I001", "ANN001", "ANN002", "ANN003", "ANN101", "ANN204", "ANN401"] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.*" = [ + "S", + "S101", + "I001", + "F405", + "F403", + "T201", + "D", + "ANN", + "PT001", + "PT004", + "PT023", + "PLR0913", + "PLR2004", +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..95d0aad --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,292 @@ +import os +from pathlib import Path + +import pytest +import ray +import yaml + +from eos.campaigns.campaign_executor import CampaignExecutor +from eos.campaigns.campaign_manager import CampaignManager +from eos.campaigns.campaign_optimizer_manager import CampaignOptimizerManager +from eos.campaigns.entities.campaign import CampaignExecutionParameters +from eos.configuration.configuration_manager import ConfigurationManager +from eos.configuration.experiment_graph.experiment_graph import ExperimentGraph +from eos.containers.container_manager import ContainerManager +from eos.devices.device_manager import DeviceManager +from eos.experiments.entities.experiment import ExperimentExecutionParameters +from eos.experiments.experiment_executor import ExperimentExecutor +from eos.experiments.experiment_executor_factory import ExperimentExecutorFactory +from eos.experiments.experiment_manager import ExperimentManager +from eos.logging.logger import log +from eos.persistence.db_manager import DbManager +from eos.persistence.file_db_manager import FileDbManager +from eos.persistence.service_credentials import ServiceCredentials +from eos.resource_allocation.container_allocation_manager import ContainerAllocationManager +from eos.resource_allocation.device_allocation_manager import DeviceAllocationManager +from eos.resource_allocation.resource_allocation_manager import ( + ResourceAllocationManager, +) +from eos.scheduling.basic_scheduler import BasicScheduler +from eos.tasks.task_executor import TaskExecutor +from eos.tasks.task_manager import TaskManager + +log.set_level("INFO") + + +def load_test_config(config_name): + config_path = Path(__file__).resolve().parent / "test_config.yaml" + + if not config_path.exists(): + raise FileNotFoundError(f"Test config file not found at {config_path}") + + with Path(config_path).open("r") as file: + config = yaml.safe_load(file) + + if config_name not in config: + raise KeyError(f"Config key {config_name} not found in test config file") + + return config.get(config_name) + + +@pytest.fixture(scope="session") +def configuration_manager(): + config = load_test_config("configuration_manager") + root_dir = Path(__file__).resolve().parent.parent + user_dir = root_dir / config["user_dir"] + os.chdir(root_dir) + return ConfigurationManager(user_dir=str(user_dir)) + + +@pytest.fixture(scope="session") +def task_specification_registry(configuration_manager): + return configuration_manager.task_specs + + +@pytest.fixture +def user_dir(): + config = load_test_config("configuration_manager") + root_dir = Path(__file__).resolve().parent.parent + return root_dir / config["user_dir"] + + +@pytest.fixture(scope="session") +def db_manager(): + config = load_test_config("db_manager") + + db_credentials_config = config["db_credentials"] + db_credentials = ServiceCredentials( + host=db_credentials_config["host"], + port=db_credentials_config["port"], + username=db_credentials_config["username"], + password=db_credentials_config["password"], + ) + + return DbManager(db_credentials, "test-eos") + + +@pytest.fixture(scope="session") +def file_db_manager(db_manager): + config = load_test_config("file_db_manager") + + file_db_credentials_config = config["file_db_credentials"] + file_db_credentials = ServiceCredentials( + host=file_db_credentials_config["host"], + port=file_db_credentials_config["port"], + username=file_db_credentials_config["username"], + password=file_db_credentials_config["password"], + ) + + return FileDbManager(file_db_credentials, bucket_name="test-eos") + + +@pytest.fixture +def setup_lab_experiment(request, configuration_manager, db_manager): + lab_name, experiment_name = request.param + + if lab_name not in configuration_manager.labs: + configuration_manager.load_lab(lab_name) + lab_config = configuration_manager.labs[lab_name] + + if experiment_name not in configuration_manager.experiments: + configuration_manager.load_experiment(experiment_name) + experiment_config = configuration_manager.experiments[experiment_name] + + return lab_config, experiment_config + + +@pytest.fixture +def experiment_graph(setup_lab_experiment): + _, experiment_config = setup_lab_experiment + + return ExperimentGraph( + experiment_config, + ) + + +@pytest.fixture +def clean_db(db_manager): + print("Cleaned up DB.") + db_manager.clean_db() + + +@pytest.fixture +def container_manager(setup_lab_experiment, configuration_manager, db_manager, clean_db): + return ContainerManager(configuration_manager, db_manager) + + +@pytest.fixture +def device_manager(setup_lab_experiment, configuration_manager, db_manager, ray_cluster, clean_db): + device_manager = DeviceManager(configuration_manager, db_manager) + device_manager.update_devices(loaded_labs=set(configuration_manager.labs.keys())) + yield device_manager + device_manager.cleanup_device_actors() + + +@pytest.fixture +def experiment_manager(setup_lab_experiment, configuration_manager, db_manager, clean_db): + return ExperimentManager(configuration_manager, db_manager) + + +@pytest.fixture +def container_allocator(setup_lab_experiment, configuration_manager, db_manager, clean_db): + return ContainerAllocationManager(configuration_manager, db_manager) + + +@pytest.fixture +def device_allocator(setup_lab_experiment, configuration_manager, db_manager, clean_db): + return DeviceAllocationManager(configuration_manager, db_manager) + + +@pytest.fixture +def resource_allocation_manager(setup_lab_experiment, configuration_manager, db_manager, clean_db): + return ResourceAllocationManager(configuration_manager, db_manager) + + +@pytest.fixture +def task_manager(setup_lab_experiment, configuration_manager, db_manager, file_db_manager, clean_db): + return TaskManager(configuration_manager, db_manager, file_db_manager) + + +@pytest.fixture(scope="module") +def ray_cluster(): + ray.init(namespace="test-eos", ignore_reinit_error=True, resources={"eos-core": 1}) + yield + ray.shutdown() + + +@pytest.fixture +def task_executor( + setup_lab_experiment, + task_manager, + device_manager, + container_manager, + resource_allocation_manager, + configuration_manager, +): + return TaskExecutor( + task_manager, device_manager, container_manager, resource_allocation_manager, configuration_manager + ) + + +@pytest.fixture +def basic_scheduler( + setup_lab_experiment, + configuration_manager, + experiment_manager, + task_manager, + device_manager, + resource_allocation_manager, +): + return BasicScheduler( + configuration_manager, experiment_manager, task_manager, device_manager, resource_allocation_manager + ) + + +@pytest.fixture +def experiment_executor( + request, + experiment_manager, + task_manager, + container_manager, + task_executor, + basic_scheduler, + experiment_graph, +): + experiment_id, experiment_type = request.param + + return ExperimentExecutor( + experiment_id=experiment_id, + experiment_type=experiment_type, + execution_parameters=ExperimentExecutionParameters(), + experiment_graph=experiment_graph, + experiment_manager=experiment_manager, + task_manager=task_manager, + container_manager=container_manager, + task_executor=task_executor, + scheduler=basic_scheduler, + ) + + +@pytest.fixture +def experiment_executor_factory( + configuration_manager, + experiment_manager, + task_manager, + container_manager, + task_executor, + basic_scheduler, +): + return ExperimentExecutorFactory( + configuration_manager=configuration_manager, + experiment_manager=experiment_manager, + task_manager=task_manager, + container_manager=container_manager, + task_executor=task_executor, + scheduler=basic_scheduler, + ) + + +@pytest.fixture +def campaign_manager( + configuration_manager, + db_manager, +): + return CampaignManager(configuration_manager, db_manager) + + +@pytest.fixture +def campaign_optimizer_manager( + db_manager, +): + return CampaignOptimizerManager(db_manager) + + +@pytest.fixture +def campaign_executor( + request, + configuration_manager, + campaign_manager, + campaign_optimizer_manager, + task_manager, + experiment_executor_factory, +): + campaign_id, experiment_type, max_experiments, do_optimization = request.param + + optimizer_computer_ip = "127.0.0.1" + + execution_parameters = CampaignExecutionParameters( + max_experiments=max_experiments, + max_concurrent_experiments=1, + do_optimization=do_optimization, + optimizer_computer_ip=optimizer_computer_ip, + ) + + return CampaignExecutor( + campaign_id=campaign_id, + experiment_type=experiment_type, + campaign_manager=campaign_manager, + campaign_optimizer_manager=campaign_optimizer_manager, + task_manager=task_manager, + experiment_executor_factory=experiment_executor_factory, + execution_parameters=execution_parameters, + ) diff --git a/tests/test_basic_scheduler.py b/tests/test_basic_scheduler.py new file mode 100644 index 0000000..d40b045 --- /dev/null +++ b/tests/test_basic_scheduler.py @@ -0,0 +1,73 @@ +from tests.fixtures import * + + +@pytest.fixture() +def experiment_graph(configuration_manager, basic_scheduler): + experiment = configuration_manager.experiments["abstract_experiment"] + return ExperimentGraph(experiment) + + +@pytest.mark.parametrize("setup_lab_experiment", [("abstract_lab", "abstract_experiment")], indirect=True) +class TestBasicScheduler: + def test_register_experiment(self, basic_scheduler, experiment_graph, configuration_manager): + print(configuration_manager.device_specs) + basic_scheduler.register_experiment("experiment_1", "abstract_experiment", experiment_graph) + assert basic_scheduler._registered_experiments["experiment_1"] == ( + "abstract_experiment", + experiment_graph, + ) + + def test_unregister_experiment(self, basic_scheduler, experiment_graph): + basic_scheduler.register_experiment("experiment_1", "abstract_experiment", experiment_graph) + basic_scheduler.unregister_experiment("experiment_1") + assert "experiment_1" not in basic_scheduler._registered_experiments + + @pytest.mark.asyncio + async def test_correct_schedule(self, basic_scheduler, experiment_graph, experiment_manager, task_manager): + def complete_task(task_id, task_type): + task_manager.create_task("experiment_1", task_id, task_type, []) + task_manager.start_task("experiment_1", task_id) + task_manager.complete_task("experiment_1", task_id) + + def get_task_if_exists(tasks, task_id): + return next((task for task in tasks if task.id == task_id), None) + + def assert_task(task, task_id, device_lab_id, device_id): + assert task.id == task_id + assert task.devices[0].lab_id == device_lab_id + assert task.devices[0].id == device_id + + def process_and_assert(tasks, expected_tasks): + assert len(tasks) == len(expected_tasks) + for task_id, device_lab_id, device_id in expected_tasks: + task = get_task_if_exists(tasks, task_id) + assert_task(task, task_id, device_lab_id, device_id) + complete_task(task_id, "Noop") + + experiment_manager.create_experiment("experiment_1", "abstract_experiment") + experiment_manager.start_experiment("experiment_1") + basic_scheduler.register_experiment("experiment_1", "abstract_experiment", experiment_graph) + + tasks = await basic_scheduler.request_tasks("experiment_1") + process_and_assert(tasks, [("A", "abstract_lab", "D2")]) + + tasks = await basic_scheduler.request_tasks("experiment_1") + process_and_assert(tasks, [("B", "abstract_lab", "D1"), ("C", "abstract_lab", "D3")]) + + tasks = await basic_scheduler.request_tasks("experiment_1") + process_and_assert( + tasks, + [("D", "abstract_lab", "D1"), ("E", "abstract_lab", "D3"), ("F", "abstract_lab", "D2")], + ) + + tasks = await basic_scheduler.request_tasks("experiment_1") + process_and_assert(tasks, [("G", "abstract_lab", "D5")]) + + tasks = await basic_scheduler.request_tasks("experiment_1") + process_and_assert(tasks, [("H", "abstract_lab", "D6")]) + + assert basic_scheduler.is_experiment_completed("experiment_1") + + tasks = await basic_scheduler.request_tasks("experiment_1") + assert len(tasks) == 0 + experiment_manager.complete_experiment("experiment_1") diff --git a/tests/test_bayesian_sequential_optimizer.py b/tests/test_bayesian_sequential_optimizer.py new file mode 100644 index 0000000..73632ea --- /dev/null +++ b/tests/test_bayesian_sequential_optimizer.py @@ -0,0 +1,78 @@ +import pandas as pd +from bofire.data_models.acquisition_functions.acquisition_function import qNEI, qNEHVI +from bofire.data_models.enum import SamplingMethodEnum +from bofire.data_models.features.continuous import ContinuousInput, ContinuousOutput +from bofire.data_models.objectives.identity import MaximizeObjective, MinimizeObjective + +from eos.optimization.sequential_bayesian_optimizer import BayesianSequentialOptimizer + + +class TestCampaignBayesianOptimizer: + def test_single_objective_optimization(self): + optimizer = BayesianSequentialOptimizer( + inputs=[ + ContinuousInput(key="x", bounds=(0, 7)), + ], + outputs=[ContinuousOutput(key="y", objective=MaximizeObjective(w=1.0))], + constraints=[], + acquisition_function=qNEI(), + num_initial_samples=5, + initial_sampling_method=SamplingMethodEnum.SOBOL, + ) + + for _ in range(8): + parameters = optimizer.sample() + results = pd.DataFrame() + results["y"] = -((parameters["x"] - 2) ** 2) + 4 + optimizer.report(parameters, results) + + optimal_solutions = optimizer.get_optimal_solutions() + assert len(optimal_solutions) == 1 + assert abs(optimal_solutions["y"].to_numpy()[0] - 4) < 0.01 + + def test_competing_multi_objective_optimization(self): + optimizer = BayesianSequentialOptimizer( + inputs=[ + ContinuousInput(key="x", bounds=(0, 7)), + ], + outputs=[ + ContinuousOutput(key="y1", objective=MaximizeObjective(w=1.0)), + ContinuousOutput(key="y2", objective=MinimizeObjective(w=1.0)), + ], + constraints=[], + acquisition_function=qNEHVI(), + num_initial_samples=10, + initial_sampling_method=SamplingMethodEnum.SOBOL, + ) + + for _ in range(30): + parameters = optimizer.sample() + results = pd.DataFrame() + results["y1"] = -((parameters["x"] - 2) ** 2) + 4 # Objective 1: Maximize y1 + results["y2"] = (parameters["x"] - 5) ** 2 # Objective 2: Minimize y2 + optimizer.report(parameters, results) + + optimal_solutions = optimizer.get_optimal_solutions() + print() + pd.set_option("display.max_rows", None, "display.max_columns", None) + print(optimal_solutions) + + # Ensure the solutions are non-dominated and belong to the Pareto front + for i, solution_i in optimal_solutions.iterrows(): + for j, solution_j in optimal_solutions.iterrows(): + if i != j: + assert not ( + (solution_i["y1"] <= solution_j["y1"] and solution_i["y2"] >= solution_j["y2"]) + and (solution_i["y1"] < solution_j["y1"] or solution_i["y2"] > solution_j["y2"]) + ) + + # Verify solutions are close to the true Pareto front + true_pareto_front = [{"x": 2, "y1": 4, "y2": 9}, {"x": 5, "y1": -5, "y2": 0}] + + for true_solution in true_pareto_front: + assert any( + abs(solution["x"] - true_solution["x"]) < 0.5 + and abs(solution["y1"] - true_solution["y1"]) < 0.5 + and abs(solution["y2"] - true_solution["y2"]) < 0.5 + for _, solution in optimal_solutions.iterrows() + ) diff --git a/tests/test_campaign_executor.py b/tests/test_campaign_executor.py new file mode 100644 index 0000000..2a2a2a4 --- /dev/null +++ b/tests/test_campaign_executor.py @@ -0,0 +1,45 @@ +import asyncio + +from eos.campaigns.entities.campaign import CampaignStatus +from tests.fixtures import * + +LAB_ID = "multiplication_lab" +CAMPAIGN_ID = "optimize_multiplication_campaign" +EXPERIMENT_TYPE = "optimize_multiplication" +MAX_EXPERIMENTS = 40 +DO_OPTIMIZATION = True + + +@pytest.mark.parametrize( + "setup_lab_experiment", + [(LAB_ID, EXPERIMENT_TYPE)], + indirect=True, +) +@pytest.mark.parametrize( + "campaign_executor", + [(CAMPAIGN_ID, EXPERIMENT_TYPE, MAX_EXPERIMENTS, DO_OPTIMIZATION)], + indirect=True, +) +class TestCampaignExecutor: + @pytest.mark.asyncio + async def test_start_campaign(self, campaign_executor, campaign_manager): + await campaign_executor.start_campaign() + + campaign = campaign_manager.get_campaign(CAMPAIGN_ID) + assert campaign is not None + assert campaign.id == CAMPAIGN_ID + assert campaign.status == CampaignStatus.RUNNING + + @pytest.mark.asyncio + async def test_progress_campaign(self, campaign_executor, campaign_manager, campaign_optimizer_manager): + await campaign_executor.start_campaign() + + campaign_finished = False + while not campaign_finished: + campaign_finished = await campaign_executor.progress_campaign() + await asyncio.sleep(0.1) + + solutions = await campaign_executor.optimizer.get_optimal_solutions.remote() + assert not solutions.empty + assert len(solutions) == 1 + assert solutions["compute_multiplication_objective.objective"].iloc[0] / 100 <= 80 diff --git a/tests/test_config.yaml b/tests/test_config.yaml new file mode 100644 index 0000000..3fe82e7 --- /dev/null +++ b/tests/test_config.yaml @@ -0,0 +1,16 @@ +configuration_manager: + user_dir: tests/user + +db_manager: + db_credentials: + host: localhost + port: 27017 + username: eos-user + password: eos-password + +file_db_manager: + file_db_credentials: + host: localhost + port: 9004 + username: eos-user + password: eos-password diff --git a/tests/test_configuration_manager.py b/tests/test_configuration_manager.py new file mode 100644 index 0000000..701c913 --- /dev/null +++ b/tests/test_configuration_manager.py @@ -0,0 +1,131 @@ +import copy +import shutil +import tempfile + +from eos.configuration.constants import TASK_IMPLEMENTATION_FILE_NAME, LAB_CONFIG_FILE_NAME +from eos.configuration.exceptions import ( + EosMissingConfigurationError, + EosConfigurationError, +) +from tests.fixtures import * + +LAB_1_ID = "small_lab" +LAB_2_ID = "multiplication_lab" + + +class TestConfigurationManager: + + def test_load_lab(self, configuration_manager): + initial_labs = configuration_manager.labs + configuration_manager.load_lab(LAB_1_ID) + + assert LAB_1_ID in configuration_manager.labs + + expected_labs = copy.deepcopy(initial_labs) + expected_labs[LAB_1_ID] = configuration_manager.labs[LAB_1_ID] + + assert configuration_manager.labs == expected_labs + + def test_load_labs(self, configuration_manager): + initial_labs = configuration_manager.labs + configuration_manager.load_labs([LAB_1_ID, LAB_2_ID]) + + assert LAB_1_ID in configuration_manager.labs + assert LAB_2_ID in configuration_manager.labs + + expected_labs = copy.deepcopy(initial_labs) + expected_labs[LAB_1_ID] = configuration_manager.labs[LAB_1_ID] + expected_labs[LAB_2_ID] = configuration_manager.labs[LAB_2_ID] + + assert configuration_manager.labs == expected_labs + + def test_load_nonexistent_lab(self, configuration_manager): + initial_labs = configuration_manager.labs + with pytest.raises(EosMissingConfigurationError): + configuration_manager.load_lab("nonexistent_lab") + + assert configuration_manager.labs == initial_labs + + def test_unload_lab(self, configuration_manager): + configuration_manager.load_lab(LAB_1_ID) + configuration_manager.load_lab(LAB_2_ID) + configuration_manager.load_experiment("water_purification") + + expected_labs = copy.deepcopy(configuration_manager.labs) + expected_experiments = copy.deepcopy(configuration_manager.experiments) + configuration_manager.unload_lab(LAB_1_ID) + + assert LAB_1_ID not in configuration_manager.labs + assert "water_purification" not in configuration_manager.experiments + + expected_labs.pop(LAB_1_ID) + assert configuration_manager.labs == expected_labs + + expected_experiments.pop("water_purification") + assert configuration_manager.experiments == expected_experiments + + def test_unload_nonexistent_lab(self, configuration_manager): + configuration_manager.load_lab(LAB_1_ID) + configuration_manager.load_lab(LAB_2_ID) + + with pytest.raises(EosConfigurationError): + configuration_manager.unload_lab("nonexistent_lab") + + def test_load_experiment(self, configuration_manager): + configuration_manager.load_lab(LAB_1_ID) + + initial_experiments = configuration_manager.experiments + configuration_manager.load_experiment("water_purification") + assert "water_purification" in configuration_manager.experiments + + expected_experiments = copy.deepcopy(initial_experiments) + expected_experiments["water_purification"] = configuration_manager.experiments["water_purification"] + + assert configuration_manager.experiments == expected_experiments + + def test_load_nonexistent_experiment(self, configuration_manager): + configuration_manager.load_lab(LAB_1_ID) + + initial_experiments = configuration_manager.experiments + with pytest.raises(EosMissingConfigurationError): + configuration_manager.load_experiment("nonexistent_experiment") + + assert configuration_manager.experiments == initial_experiments + + def test_unload_experiment(self, configuration_manager): + configuration_manager.load_lab(LAB_1_ID) + + if "water_purification" not in configuration_manager.experiments: + configuration_manager.load_experiment("water_purification") + + expected_experiments = copy.deepcopy(configuration_manager.experiments) + configuration_manager.unload_experiment("water_purification") + + assert "water_purification" not in configuration_manager.experiments + expected_experiments.pop("water_purification") + assert configuration_manager.experiments == expected_experiments + + def test_unload_nonexistent_experiment(self, configuration_manager): + configuration_manager.load_lab(LAB_1_ID) + with pytest.raises(EosConfigurationError): + configuration_manager.unload_experiment("nonexistent_experiment") + + def test_user_dir_lab_file_existence(self, user_dir): + with tempfile.TemporaryDirectory(prefix="eos_test-") as temp_user_dir: + temp_user_dir_path = Path(temp_user_dir) + shutil.copytree(user_dir, temp_user_dir_path, dirs_exist_ok=True) + + (temp_user_dir_path / "testing" / "labs" / LAB_1_ID / LAB_CONFIG_FILE_NAME).unlink() + + with pytest.raises(EosMissingConfigurationError): + ConfigurationManager(user_dir=str(temp_user_dir_path)) + + def test_tasks_dir_task_handler_existence(self, user_dir): + with tempfile.TemporaryDirectory(prefix="eos_test-") as temp_user_dir: + shutil.copytree(user_dir, temp_user_dir, dirs_exist_ok=True) + + temp_tasks_dir_path = Path(temp_user_dir) / "testing" / "tasks" + (temp_tasks_dir_path / "noop" / TASK_IMPLEMENTATION_FILE_NAME).unlink() + + with pytest.raises(EosMissingConfigurationError): + ConfigurationManager(user_dir=str(temp_user_dir)) diff --git a/tests/test_container_allocator.py b/tests/test_container_allocator.py new file mode 100644 index 0000000..acd1bde --- /dev/null +++ b/tests/test_container_allocator.py @@ -0,0 +1,135 @@ +from eos.resource_allocation.exceptions import ( + EosContainerAllocatedError, + EosContainerNotFoundError, +) +from tests.fixtures import * + + +@pytest.mark.parametrize( + "setup_lab_experiment", [("small_lab", "water_purification")], indirect=True +) +class TestContainerAllocator: + def test_allocate_container(self, container_allocator): + container_id = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_allocator.allocate(container_id, "owner", "water_purification_1") + container = container_allocator.get_allocation(container_id) + + assert container.id == container_id + assert container.owner == "owner" + assert container.experiment_id == "water_purification_1" + + def test_allocate_container_already_allocated(self, container_allocator): + container_id = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_allocator.allocate(container_id, "owner", "water_purification_1") + + with pytest.raises(EosContainerAllocatedError): + container_allocator.allocate(container_id, "owner", "water_purification_1") + + def test_allocate_nonexistent_container(self, container_allocator): + container_id = "nonexistent_container_id" + with pytest.raises(EosContainerNotFoundError): + container_allocator.allocate(container_id, "owner", "water_purification_1") + + def test_deallocate_container(self, container_allocator): + container_id = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_allocator.allocate(container_id, "owner", "water_purification_1") + + container_allocator.deallocate(container_id) + container = container_allocator.get_allocation(container_id) + + assert container is None + + def test_deallocate_container_not_allocated(self, container_allocator): + container_id = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_allocator.deallocate(container_id) + assert container_allocator.get_allocation(container_id) is None + + def test_is_allocated(self, container_allocator): + container_id = "ec1ca48cd5d14c0c8cde376476e0d98d" + assert not container_allocator.is_allocated(container_id) + + container_allocator.allocate(container_id, "owner", "water_purification_1") + assert container_allocator.is_allocated(container_id) + + def test_get_allocations_by_owner(self, container_allocator): + container_id_1 = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_id_2 = "84eb17d61e884ffd9d1fdebcbad1532b" + container_id_3 = "a3b958aea8bd435386cdcbab20a2d3ec" + + container_allocator.allocate(container_id_1, "owner", "water_purification_1") + container_allocator.allocate(container_id_2, "owner", "water_purification_1") + container_allocator.allocate(container_id_3, "another_owner", "water_purification_1") + + allocations = container_allocator.get_allocations(owner="owner") + assert allocations[0].id == container_id_1 + assert allocations[1].id == container_id_2 + assert len(allocations) == 2 + + allocations = container_allocator.get_allocations(owner="another_owner") + assert allocations[0].id == container_id_3 + assert len(allocations) == 1 + + def test_get_all_allocations(self, container_allocator): + container_id_1 = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_id_2 = "84eb17d61e884ffd9d1fdebcbad1532b" + container_id_3 = "a3b958aea8bd435386cdcbab20a2d3ec" + + container_allocator.allocate(container_id_1, "owner", "water_purification_1") + container_allocator.allocate(container_id_2, "owner", "water_purification_1") + container_allocator.allocate(container_id_3, "another_owner", "water_purification_1") + + allocations = container_allocator.get_allocations() + assert len(allocations) == 3 + assert {allocation.id for allocation in allocations} == { + container_id_1, + container_id_2, + container_id_3, + } + + def test_get_all_unallocated_containers(self, container_allocator): + container_id_1 = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_id_2 = "84eb17d61e884ffd9d1fdebcbad1532b" + container_id_3 = "a3b958aea8bd435386cdcbab20a2d3ec" + + initial_unallocated_containers = container_allocator.get_all_unallocated() + + container_allocator.allocate(container_id_1, "owner1", "water_purification_1") + container_allocator.allocate(container_id_2, "owner2", "water_purification_1") + + new_unallocated_containers = container_allocator.get_all_unallocated() + assert len(new_unallocated_containers) == len(initial_unallocated_containers) - 2 + assert container_id_1 not in new_unallocated_containers + assert container_id_2 not in new_unallocated_containers + assert container_id_3 in new_unallocated_containers + + def test_deallocate_all_containers(self, container_allocator): + container_id_1 = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_id_2 = "84eb17d61e884ffd9d1fdebcbad1532b" + container_id_3 = "a3b958aea8bd435386cdcbab20a2d3ec" + + container_allocator.allocate(container_id_1, "owner1", "water_purification_1") + container_allocator.allocate(container_id_2, "owner2", "water_purification_1") + container_allocator.allocate(container_id_3, "owner3", "water_purification_1") + + assert container_allocator.get_allocations() != [] + + container_allocator.deallocate_all() + + assert container_allocator.get_allocations() == [] + + def test_deallocate_all_containers_by_owner(self, container_allocator): + container_id_1 = "ec1ca48cd5d14c0c8cde376476e0d98d" + container_id_2 = "84eb17d61e884ffd9d1fdebcbad1532b" + container_id_3 = "a3b958aea8bd435386cdcbab20a2d3ec" + + container_allocator.allocate(container_id_1, "owner1", "water_purification_1") + container_allocator.allocate(container_id_2, "owner2", "water_purification_1") + container_allocator.allocate(container_id_3, "owner2", "water_purification_1") + + container_allocator.deallocate_all_by_owner("owner2") + + owner2_allocations = container_allocator.get_allocations(owner="owner2") + assert owner2_allocations == [] + assert container_allocator.get_allocations() == [ + container_allocator.get_allocation(container_id_1) + ] diff --git a/tests/test_container_manager.py b/tests/test_container_manager.py new file mode 100644 index 0000000..3a7522d --- /dev/null +++ b/tests/test_container_manager.py @@ -0,0 +1,46 @@ +from tests.fixtures import * + + +@pytest.fixture +def container_manager(configuration_manager, setup_lab_experiment, db_manager, clean_db): + return ContainerManager(configuration_manager, db_manager) + + +@pytest.mark.parametrize("setup_lab_experiment", [("small_lab", "water_purification")], indirect=True) +class TestContainerManager: + def test_set_container_location(self, container_manager): + container_id = "acf829f859e04fee80d54a1ee918555d" + container_manager.set_location(container_id, "new_location") + + assert container_manager.get_container(container_id).location == "new_location" + + def test_set_container_lab(self, container_manager): + container_id = "acf829f859e04fee80d54a1ee918555d" + container_manager.set_lab(container_id, "new_lab") + + assert container_manager.get_container(container_id).lab == "new_lab" + + def test_set_container_metadata(self, container_manager): + container_id = "acf829f859e04fee80d54a1ee918555d" + container_manager.set_metadata(container_id, {"substance": "water"}) + container_manager.set_metadata(container_id, {"temperature": "cold"}) + + assert container_manager.get_container(container_id).metadata == {"temperature": "cold"} + + def test_add_container_metadata(self, container_manager): + container_id = "acf829f859e04fee80d54a1ee918555d" + container_manager.add_metadata(container_id, {"substance": "water"}) + container_manager.add_metadata(container_id, {"temperature": "cold"}) + + assert container_manager.get_container(container_id).metadata == { + "capacity": 500, + "substance": "water", + "temperature": "cold", + } + + def test_remove_container_metadata(self, container_manager): + container_id = "acf829f859e04fee80d54a1ee918555d" + container_manager.add_metadata(container_id, {"substance": "water", "temperature": "cold", "color": "blue"}) + container_manager.remove_metadata(container_id, ["color", "temperature"]) + + assert container_manager.get_container(container_id).metadata == {"capacity": 500, "substance": "water"} diff --git a/tests/test_device_allocator.py b/tests/test_device_allocator.py new file mode 100644 index 0000000..9d69da4 --- /dev/null +++ b/tests/test_device_allocator.py @@ -0,0 +1,135 @@ +from eos.resource_allocation.exceptions import ( + EosDeviceAllocatedError, + EosDeviceNotFoundError, +) +from tests.fixtures import * + +LAB_ID = "small_lab" + + +@pytest.mark.parametrize("setup_lab_experiment", [(LAB_ID, "water_purification")], indirect=True) +class TestDeviceAllocator: + def test_allocate_device(self, device_allocator): + device_id = "magnetic_mixer" + device_allocator.allocate(LAB_ID, device_id, "owner", "water_purification_1") + + allocation = device_allocator.get_allocation(LAB_ID, device_id) + + assert allocation.id == device_id + assert allocation.lab_id == LAB_ID + assert allocation.device_type == "magnetic_mixer" + assert allocation.owner == "owner" + assert allocation.experiment_id == "water_purification_1" + + def test_allocate_device_already_allocated(self, device_allocator): + device_id = "magnetic_mixer" + device_allocator.allocate(LAB_ID, device_id, "owner", "water_purification_1") + + with pytest.raises(EosDeviceAllocatedError): + device_allocator.allocate(LAB_ID, device_id, "owner", "water_purification_1") + + def test_allocate_nonexistent_device(self, device_allocator): + device_id = "nonexistent_device_id" + with pytest.raises(EosDeviceNotFoundError): + device_allocator.allocate(LAB_ID, device_id, "owner", "water_purification_1") + + def test_deallocate_device(self, device_allocator): + device_id = "magnetic_mixer" + device_allocator.allocate(LAB_ID, device_id, "owner", "water_purification_1") + + device_allocator.deallocate(LAB_ID, device_id) + allocation = device_allocator.get_allocation(LAB_ID, device_id) + + assert allocation is None + + def test_deallocate_device_not_allocated(self, device_allocator): + device_id = "magnetic_mixer" + device_allocator.deallocate(LAB_ID, device_id) + assert device_allocator.get_allocation(LAB_ID, device_id) is None + + def test_is_allocated(self, device_allocator): + device_id = "magnetic_mixer" + assert not device_allocator.is_allocated(LAB_ID, device_id) + + device_allocator.allocate(LAB_ID, device_id, "owner", "water_purification_1") + assert device_allocator.is_allocated(LAB_ID, device_id) + + def test_get_allocations_by_owner(self, device_allocator): + device_id_1 = "magnetic_mixer" + device_id_2 = "evaporator" + device_id_3 = "substance_fridge" + + device_allocator.allocate(LAB_ID, device_id_1, "owner1", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_2, "owner1", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_3, "owner2", "water_purification_1") + + allocations = device_allocator.get_allocations(owner="owner1") + + assert len(allocations) == 2 + assert device_id_1 in [allocation.id for allocation in allocations] + assert device_id_2 in [allocation.id for allocation in allocations] + + def test_get_all_allocations(self, device_allocator): + device_id_1 = "magnetic_mixer" + device_id_2 = "evaporator" + device_id_3 = "substance_fridge" + + device_allocator.allocate(LAB_ID, device_id_1, "owner", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_2, "owner", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_3, "owner", "water_purification_1") + + allocations = device_allocator.get_allocations() + + assert len(allocations) == 3 + assert device_id_1 in [allocation.id for allocation in allocations] + assert device_id_2 in [allocation.id for allocation in allocations] + assert device_id_3 in [allocation.id for allocation in allocations] + + def test_get_all_unallocated(self, device_allocator): + device_id_1 = "magnetic_mixer" + device_id_2 = "evaporator" + device_id_3 = "substance_fridge" + + initial_unallocated_devices = device_allocator.get_all_unallocated() + + device_allocator.allocate(LAB_ID, device_id_1, "owner", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_2, "owner", "water_purification_1") + + new_unallocated_devices = device_allocator.get_all_unallocated() + + assert len(new_unallocated_devices) == len(initial_unallocated_devices) - 2 + assert device_id_1 not in new_unallocated_devices + assert device_id_2 not in new_unallocated_devices + assert device_id_3 in new_unallocated_devices + + def test_deallocate_all(self, device_allocator): + device_id_1 = "magnetic_mixer" + device_id_2 = "evaporator" + device_id_3 = "substance_fridge" + + device_allocator.allocate(LAB_ID, device_id_1, "owner", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_2, "owner", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_3, "owner", "water_purification_1") + + assert device_allocator.get_allocations() != [] + + device_allocator.deallocate_all() + + assert device_allocator.get_allocations() == [] + + def test_deallocate_all_by_owner(self, device_allocator): + device_id_1 = "magnetic_mixer" + device_id_2 = "evaporator" + device_id_3 = "substance_fridge" + + device_allocator.allocate(LAB_ID, device_id_1, "owner1", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_2, "owner2", "water_purification_1") + device_allocator.allocate(LAB_ID, device_id_3, "owner2", "water_purification_1") + + device_allocator.deallocate_all_by_owner("owner2") + + owner2_allocations = device_allocator.get_allocations(owner="owner2") + assert owner2_allocations == [] + assert device_allocator.get_allocations() == [ + device_allocator.get_allocation(LAB_ID, device_id_1) + ] diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py new file mode 100644 index 0000000..c281257 --- /dev/null +++ b/tests/test_device_manager.py @@ -0,0 +1,37 @@ +from eos.devices.entities.device import DeviceStatus +from eos.devices.exceptions import EosDeviceStateError +from tests.fixtures import * + +LAB_ID = "small_lab" + + +@pytest.mark.parametrize("setup_lab_experiment", [(LAB_ID, "water_purification")], indirect=True) +class TestDeviceManager: + def test_get_device(self, device_manager): + device = device_manager.get_device(LAB_ID, "substance_fridge") + assert device.id == "substance_fridge" + assert device.lab_id == LAB_ID + assert device.type == "fridge" + assert device.location == "substance_fridge" + + def test_get_device_nonexistent(self, device_manager): + device = device_manager.get_device(LAB_ID, "nonexistent_device") + assert device is None + + def test_get_all_devices(self, device_manager): + devices = device_manager.get_devices(lab_id=LAB_ID) + assert len(devices) == 5 + + def test_get_devices_by_type(self, device_manager): + devices = device_manager.get_devices(lab_id=LAB_ID, type="magnetic_mixer") + assert len(devices) == 2 + assert all(device.type == "magnetic_mixer" for device in devices) + + def test_set_device_status(self, device_manager): + device_manager.set_device_status(LAB_ID, "evaporator", DeviceStatus.ACTIVE) + device = device_manager.get_device(LAB_ID, "evaporator") + assert device.status == DeviceStatus.ACTIVE + + def test_set_device_status_nonexistent(self, device_manager): + with pytest.raises(EosDeviceStateError): + device_manager.set_device_status(LAB_ID, "nonexistent_device", DeviceStatus.INACTIVE) diff --git a/tests/test_experiment_executor.py b/tests/test_experiment_executor.py new file mode 100644 index 0000000..0e12f44 --- /dev/null +++ b/tests/test_experiment_executor.py @@ -0,0 +1,103 @@ +import asyncio + +from eos.experiments.entities.experiment import ExperimentStatus +from eos.tasks.entities.task import TaskStatus +from tests.fixtures import * + +LAB_ID = "small_lab" +EXPERIMENT_TYPE = "water_purification" +EXPERIMENT_ID = "water_purification_#1" + +DYNAMIC_PARAMETERS = { + "mixing": { + "time": 120, + }, + "evaporation": { + "evaporation_temperature": 120, + "evaporation_rotation_speed": 200, + "evaporation_sparging_flow": 5, + }, +} + + +@pytest.mark.parametrize( + "setup_lab_experiment", + [(LAB_ID, EXPERIMENT_TYPE)], + indirect=True, +) +@pytest.mark.parametrize( + "experiment_executor", + [(EXPERIMENT_ID, EXPERIMENT_TYPE)], + indirect=True, +) +class TestExperimentExecutor: + def test_start_experiment(self, experiment_executor, experiment_manager): + experiment_executor.start_experiment(DYNAMIC_PARAMETERS) + + experiment = experiment_manager.get_experiment(EXPERIMENT_ID) + assert experiment is not None + assert experiment.id == EXPERIMENT_ID + assert experiment.status == ExperimentStatus.RUNNING + + @pytest.mark.asyncio + async def test_progress_experiment(self, experiment_executor, experiment_manager, task_manager): + experiment_executor.start_experiment(DYNAMIC_PARAMETERS) + + experiment_completed = await experiment_executor.progress_experiment() + assert not experiment_completed + await experiment_executor._task_output_futures["mixing"] + + experiment_completed = await experiment_executor.progress_experiment() + assert not experiment_completed + task = task_manager.get_task(EXPERIMENT_ID, "mixing") + assert task is not None + assert task.status == TaskStatus.COMPLETED + await experiment_executor._task_output_futures["evaporation"] + + experiment_completed = await experiment_executor.progress_experiment() + task = task_manager.get_task(EXPERIMENT_ID, "evaporation") + assert task.status == TaskStatus.COMPLETED + assert not experiment_completed + + # Final progress + experiment_completed = await experiment_executor.progress_experiment() + assert experiment_completed + experiment = experiment_manager.get_experiment(EXPERIMENT_ID) + assert experiment.status == ExperimentStatus.COMPLETED + + @pytest.mark.asyncio + async def test_task_output_registration(self, experiment_executor, task_manager): + experiment_executor.start_experiment(DYNAMIC_PARAMETERS) + + experiment_completed = False + while not experiment_completed: + experiment_completed = await experiment_executor.progress_experiment() + await asyncio.sleep(0.1) + + mixing_output = task_manager.get_task_output(EXPERIMENT_ID, "mixing") + assert mixing_output is not None + assert mixing_output.parameters["mixing_time"] == DYNAMIC_PARAMETERS["mixing"]["time"] + + @pytest.mark.asyncio + async def test_resolve_input_parameter_references_and_dynamic_parameters( + self, experiment_executor, task_manager + ): + experiment_executor.start_experiment(DYNAMIC_PARAMETERS) + + experiment_completed = False + while not experiment_completed: + experiment_completed = await experiment_executor.progress_experiment() + await asyncio.sleep(0.1) + + mixing_task = task_manager.get_task(EXPERIMENT_ID, "mixing") + mixing_result = task_manager.get_task_output(EXPERIMENT_ID, "mixing") + + evaporation_task = task_manager.get_task(EXPERIMENT_ID, "evaporation") + # Check the dynamic parameter for input mixing time + assert mixing_task.input.parameters["time"] == DYNAMIC_PARAMETERS["mixing"]["time"] + + # Check that the output parameter mixing time was assigned to the input parameter evaporation time + assert ( + evaporation_task.input.parameters["evaporation_time"] + == mixing_result.parameters["mixing_time"] + ) diff --git a/tests/test_experiment_graph.py b/tests/test_experiment_graph.py new file mode 100644 index 0000000..fb8c214 --- /dev/null +++ b/tests/test_experiment_graph.py @@ -0,0 +1,41 @@ +from tests.fixtures import * + + +@pytest.mark.parametrize("setup_lab_experiment", [("small_lab", "water_purification")], indirect=True) +class TestExperimentGraph: + def test_get_graph(self, experiment_graph): + graph = experiment_graph.get_graph() + assert graph is not None + + def test_get_task_node(self, experiment_graph): + task_node = experiment_graph.get_task_node("mixing") + assert task_node is not None + assert task_node["node_type"] == "task" + assert task_node["task_config"].type == "Magnetic Mixing" + + def test_get_task_spec(self, experiment_graph): + task_spec = experiment_graph.get_task_spec("mixing") + assert task_spec is not None + assert task_spec.type == "Magnetic Mixing" + + def test_get_container_node(self, experiment_graph): + container_node = experiment_graph.get_container_node("026749f8f40342b38157f9824ae2f512") + assert container_node is not None + assert container_node["node_type"] == "container" + assert container_node["container"]["beaker"] == "026749f8f40342b38157f9824ae2f512" + + def test_get_task_dependencies(self, experiment_graph): + dependencies = experiment_graph.get_task_dependencies("evaporation") + assert dependencies == ["mixing"] + + def test_get_task_inputs(self, experiment_graph): + inputs = experiment_graph.get_task_inputs("mixing") + assert inputs.containers == ["026749f8f40342b38157f9824ae2f512"] + + inputs = experiment_graph.get_task_inputs("evaporation") + assert inputs.parameters == ["mixing.mixing_time"] + + def test_get_task_outputs(self, experiment_graph): + outputs = experiment_graph.get_task_outputs("mixing") + assert outputs.containers == ["026749f8f40342b38157f9824ae2f512_mixing"] + assert outputs.parameters == ["mixing.mixing_time"] diff --git a/tests/test_experiment_manager.py b/tests/test_experiment_manager.py new file mode 100644 index 0000000..2f297c6 --- /dev/null +++ b/tests/test_experiment_manager.py @@ -0,0 +1,93 @@ +from eos.experiments.entities.experiment import ExperimentStatus +from eos.experiments.exceptions import EosExperimentStateError +from tests.fixtures import * + +EXPERIMENT_ID = "water_purification" + + +@pytest.mark.parametrize("setup_lab_experiment", [("small_lab", EXPERIMENT_ID)], indirect=True) +class TestExperimentManager: + def test_create_experiment(self, experiment_manager): + experiment_manager.create_experiment("test_experiment", EXPERIMENT_ID) + experiment_manager.create_experiment("test_experiment_2", EXPERIMENT_ID) + + assert experiment_manager.get_experiment("test_experiment").id == "test_experiment" + assert experiment_manager.get_experiment("test_experiment_2").id == "test_experiment_2" + + def test_create_experiment_nonexistent_type(self, experiment_manager): + with pytest.raises(EosExperimentStateError): + experiment_manager.create_experiment("test_experiment", "nonexistent_type") + + def test_create_existing_experiment(self, experiment_manager): + experiment_manager.create_experiment("test_experiment", EXPERIMENT_ID) + + with pytest.raises(EosExperimentStateError): + experiment_manager.create_experiment("test_experiment", EXPERIMENT_ID) + + def test_delete_experiment(self, experiment_manager): + experiment_manager.create_experiment("test_experiment", EXPERIMENT_ID) + + assert experiment_manager.get_experiment("test_experiment").id == "test_experiment" + + experiment_manager.delete_experiment("test_experiment") + + assert experiment_manager.get_experiment("test_experiment") is None + + def test_delete_nonexisting_experiment(self, experiment_manager): + with pytest.raises(EosExperimentStateError): + experiment_manager.delete_experiment("non_existing_experiment") + + def test_get_experiments_by_status(self, experiment_manager): + experiment_manager.create_experiment("test_experiment", EXPERIMENT_ID) + experiment_manager.create_experiment("test_experiment_2", EXPERIMENT_ID) + experiment_manager.create_experiment("test_experiment_3", EXPERIMENT_ID) + + experiment_manager.start_experiment("test_experiment") + experiment_manager.start_experiment("test_experiment_2") + experiment_manager.complete_experiment("test_experiment_3") + + running_experiments = experiment_manager.get_experiments( + status=ExperimentStatus.RUNNING.value + ) + completed_experiments = experiment_manager.get_experiments( + status=ExperimentStatus.COMPLETED.value + ) + + assert running_experiments == [ + experiment_manager.get_experiment("test_experiment"), + experiment_manager.get_experiment("test_experiment_2"), + ] + + assert completed_experiments == [experiment_manager.get_experiment("test_experiment_3")] + + def test_set_experiment_status(self, experiment_manager): + experiment_manager.create_experiment("test_experiment", EXPERIMENT_ID) + assert ( + experiment_manager.get_experiment("test_experiment").status == ExperimentStatus.CREATED + ) + + experiment_manager.start_experiment("test_experiment") + assert ( + experiment_manager.get_experiment("test_experiment").status == ExperimentStatus.RUNNING + ) + + experiment_manager.complete_experiment("test_experiment") + assert ( + experiment_manager.get_experiment("test_experiment").status + == ExperimentStatus.COMPLETED + ) + + def test_set_experiment_status_nonexistent_experiment(self, experiment_manager): + with pytest.raises(EosExperimentStateError): + experiment_manager.start_experiment("nonexistent_experiment") + + def test_get_all_experiments(self, experiment_manager): + experiment_manager.create_experiment("test_experiment", EXPERIMENT_ID) + experiment_manager.create_experiment("test_experiment_2", EXPERIMENT_ID) + experiment_manager.create_experiment("test_experiment_3", EXPERIMENT_ID) + + assert experiment_manager.get_experiments() == [ + experiment_manager.get_experiment("test_experiment"), + experiment_manager.get_experiment("test_experiment_2"), + experiment_manager.get_experiment("test_experiment_3"), + ] diff --git a/tests/test_lab_validation.py b/tests/test_lab_validation.py new file mode 100644 index 0000000..0f38a93 --- /dev/null +++ b/tests/test_lab_validation.py @@ -0,0 +1,68 @@ +from eos.configuration.entities.lab import LabContainerConfig +from eos.configuration.exceptions import EosLabConfigurationError +from eos.configuration.validation.lab_validator import LabValidator +from tests.fixtures import * + + +@pytest.fixture() +def lab(configuration_manager): + configuration_manager.load_lab("small_lab") + return configuration_manager.labs["small_lab"] + + +class TestLabValidation: + def test_device_locations(self, configuration_manager, lab): + lab.devices.magnetic_mixer.location = "invalid_location" + + with pytest.raises(EosLabConfigurationError): + LabValidator(configuration_manager._user_dir, lab).validate() + + def test_container_locations(self, configuration_manager, lab): + lab.containers[0].location = "invalid_location" + + with pytest.raises(EosLabConfigurationError): + LabValidator(configuration_manager._user_dir, lab).validate() + + def test_device_computers(self, configuration_manager, lab): + lab.devices.magnetic_mixer.computer = "invalid_computer" + + with pytest.raises(EosLabConfigurationError): + LabValidator(configuration_manager._user_dir, lab).validate() + + def test_container_non_unique_type(self, configuration_manager, lab): + lab.containers.extend( + [ + LabContainerConfig( + type="beaker", + location="substance_shelf", + ids=["a", "b"], + ), + LabContainerConfig( + type="beaker", + location="substance_shelf", + ids=["c", "d"], + ), + ] + ) + + with pytest.raises(EosLabConfigurationError): + LabValidator(configuration_manager._user_dir, lab).validate() + + def test_container_duplicate_ids(self, configuration_manager, lab): + lab.containers.extend( + [ + LabContainerConfig( + type="beaker", + location="substance_shelf", + ids=["a", "b"], + ), + LabContainerConfig( + type="flask", + location="substance_shelf", + ids=["a", "b"], + ), + ] + ) + + with pytest.raises(EosLabConfigurationError): + LabValidator(configuration_manager._user_dir, lab).validate() diff --git a/tests/test_multi_lab_validation.py b/tests/test_multi_lab_validation.py new file mode 100644 index 0000000..3bb4045 --- /dev/null +++ b/tests/test_multi_lab_validation.py @@ -0,0 +1,17 @@ +import copy + +from eos.configuration.exceptions import EosLabConfigurationError +from eos.configuration.validation.multi_lab_validator import MultiLabValidator +from tests.fixtures import * + + +class TestMultiLabValidation: + def test_duplicate_container_ids(self, configuration_manager): + configuration_manager.load_lab("small_lab") + lab = configuration_manager.labs["small_lab"] + + # Create a deep copy of the lab to simulate two instances + lab_copy = copy.deepcopy(lab) + + with pytest.raises(EosLabConfigurationError): + MultiLabValidator([lab, lab_copy]).validate() diff --git a/tests/test_resource_allocation_manager.py b/tests/test_resource_allocation_manager.py new file mode 100644 index 0000000..cc4a62a --- /dev/null +++ b/tests/test_resource_allocation_manager.py @@ -0,0 +1,216 @@ +from bson import ObjectId + +from eos.resource_allocation.entities.resource_request import ( + ResourceAllocationRequest, + ActiveResourceAllocationRequest, + ResourceType, + ResourceRequestAllocationStatus, +) +from eos.resource_allocation.exceptions import EosDeviceNotFoundError +from tests.fixtures import * + +LAB_ID = "small_lab" + + +@pytest.mark.parametrize("setup_lab_experiment", [(LAB_ID, "water_purification")], indirect=True) +class TestResourceAllocationManager: + def test_request_resources(self, resource_allocation_manager): + request = ResourceAllocationRequest( + requester="test_requester", + reason="Needed for experiment", + experiment_id="water_purification_1", + ) + request.add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + request.add_resource("026749f8f40342b38157f9824ae2f512", "", ResourceType.CONTAINER) + + def callback(active_request: ActiveResourceAllocationRequest): + assert active_request.status == ResourceRequestAllocationStatus.ALLOCATED + assert len(active_request.request.resources) == 2 + assert any(r.id == "magnetic_mixer" for r in active_request.request.resources) + assert any(r.id == "026749f8f40342b38157f9824ae2f512" for r in active_request.request.resources) + + active_request = resource_allocation_manager.request_resources(request, callback) + + assert active_request.request == request + assert active_request.status == ResourceRequestAllocationStatus.PENDING + + resource_allocation_manager.process_active_requests() + + def test_request_resources_priority(self, resource_allocation_manager): + requests = [ + ResourceAllocationRequest( + requester=f"test_requester{i}", + reason="Needed for experiment", + experiment_id="water_purification_1", + priority=100 + i, + ) + for i in range(1, 4) + ] + for request in requests: + request.add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + + active_requests = [resource_allocation_manager.request_resources(req, lambda x: None) for req in requests] + resource_allocation_manager.process_active_requests() + + # Ensure that requests[0] is allocated and the rest are pending + active_request_3 = resource_allocation_manager.get_active_request(active_requests[2].id) + assert active_request_3.status == ResourceRequestAllocationStatus.PENDING + assert active_request_3.request.requester == "test_requester3" + assert active_request_3.request.priority == 103 + + active_request_2 = resource_allocation_manager.get_active_request(active_requests[1].id) + assert active_request_2.status == ResourceRequestAllocationStatus.PENDING + assert active_request_2.request.requester == "test_requester2" + assert active_request_2.request.priority == 102 + + active_request_1 = resource_allocation_manager.get_active_request(active_requests[0].id) + assert active_request_1.status == ResourceRequestAllocationStatus.ALLOCATED + assert active_request_1.request.requester == "test_requester1" + assert active_request_1.request.priority == 101 + + resource_allocation_manager.release_resources(active_request_1) + + resource_allocation_manager.process_active_requests() + + # Ensure that requests[1] is now allocated and requests[2] is still pending + active_request_3 = resource_allocation_manager.get_active_request(active_requests[2].id) + assert active_request_3.status == ResourceRequestAllocationStatus.PENDING + assert active_request_3.request.requester == "test_requester3" + assert active_request_3.request.priority == 103 + + active_request_2 = resource_allocation_manager.get_active_request(active_requests[1].id) + assert active_request_2.status == ResourceRequestAllocationStatus.ALLOCATED + assert active_request_2.request.requester == "test_requester2" + assert active_request_2.request.priority == 102 + + def test_release_resources(self, resource_allocation_manager): + request = ResourceAllocationRequest( + requester="test_requester", + reason="Needed for experiment", + experiment_id="water_purification_1", + priority=1, + ) + request.add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + request.add_resource("026749f8f40342b38157f9824ae2f512", "", ResourceType.CONTAINER) + + active_request = resource_allocation_manager.request_resources(request, lambda x: None) + + resource_allocation_manager.process_active_requests() + + resource_allocation_manager.release_resources(active_request) + + assert ( + resource_allocation_manager.get_active_request(active_request.id).status + == ResourceRequestAllocationStatus.COMPLETED + ) + + def test_process_active_requests(self, resource_allocation_manager): + requests = [ + ResourceAllocationRequest( + requester=f"test_requester{i}", + reason="Needed for experiment", + experiment_id="water_purification_1", + ) + for i in range(1, 3) + ] + for request in requests: + request.add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + + active_requests = [resource_allocation_manager.request_resources(req, lambda x: None) for req in requests] + + resource_allocation_manager.process_active_requests() + + assert ( + resource_allocation_manager.get_active_request(active_requests[0].id).status + == ResourceRequestAllocationStatus.ALLOCATED + ) + assert ( + resource_allocation_manager.get_active_request(active_requests[1].id).status + == ResourceRequestAllocationStatus.PENDING + ) + + def test_abort_active_request(self, resource_allocation_manager): + request = ResourceAllocationRequest( + requester="test_requester", + reason="Needed for experiment", + experiment_id="water_purification_1", + ) + request.add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + request.add_resource("magnetic_mixer_2", LAB_ID, ResourceType.DEVICE) + + active_request = resource_allocation_manager.request_resources(request, lambda x: None) + + resource_allocation_manager.abort_active_request(active_request.id) + + assert resource_allocation_manager.get_active_request(active_request.id).status == ( + ResourceRequestAllocationStatus.ABORTED + ) + + assert not resource_allocation_manager._device_allocation_manager.is_allocated(LAB_ID, "magnetic_mixer") + assert not resource_allocation_manager._device_allocation_manager.is_allocated(LAB_ID, "magnetic_mixer_2") + + def test_get_all_active_requests(self, resource_allocation_manager): + requests = [ + ResourceAllocationRequest( + requester=f"test_requester{i}", + reason="Needed for experiment", + experiment_id="water_purification_1", + ) + for i in range(1, 3) + ] + requests[0].add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + requests[1].add_resource("026749f8f40342b38157f9824ae2f512", "", ResourceType.CONTAINER) + + for request in requests: + resource_allocation_manager.request_resources(request, lambda x: None) + + all_active_requests = resource_allocation_manager.get_all_active_requests() + assert len(all_active_requests) == 2 + assert all_active_requests[0].request == requests[0] + assert all_active_requests[1].request == requests[1] + + def test_get_active_request_nonexistent(self, resource_allocation_manager): + nonexistent_id = ObjectId() + assert resource_allocation_manager.get_active_request(nonexistent_id) is None + + def test_clean_requests(self, resource_allocation_manager): + request = ResourceAllocationRequest( + requester="test_requester", + reason="Needed for experiment", + experiment_id="water_purification_1", + ) + request.add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + + active_request = resource_allocation_manager.request_resources(request, lambda x: None) + resource_allocation_manager.process_active_requests() + resource_allocation_manager.release_resources(active_request) + + assert ( + resource_allocation_manager.get_active_request(active_request.id).status + == ResourceRequestAllocationStatus.COMPLETED + ) + + resource_allocation_manager._clean_completed_and_aborted_requests() + + assert len(resource_allocation_manager.get_all_active_requests()) == 0 + + def test_all_or_nothing_allocation(self, resource_allocation_manager): + request = ResourceAllocationRequest( + requester="test_requester", + reason="Needed for experiment", + experiment_id="water_purification_1", + ) + request.add_resource("magnetic_mixer", LAB_ID, ResourceType.DEVICE) + request.add_resource("nonexistent_device", LAB_ID, ResourceType.DEVICE) + + with pytest.raises(EosDeviceNotFoundError): + active_request = resource_allocation_manager.request_resources(request, lambda x: None) + resource_allocation_manager.process_active_requests() + + assert active_request.status == ResourceRequestAllocationStatus.PENDING + + # Verify that neither resource was allocated + assert not resource_allocation_manager._device_allocation_manager.is_allocated(LAB_ID, "magnetic_mixer") + + with pytest.raises(EosDeviceNotFoundError): + assert not resource_allocation_manager._device_allocation_manager.is_allocated(LAB_ID, "nonexistent_device") diff --git a/tests/test_task_executor.py b/tests/test_task_executor.py new file mode 100644 index 0000000..d034fe2 --- /dev/null +++ b/tests/test_task_executor.py @@ -0,0 +1,100 @@ +import asyncio + +from eos.configuration.entities.task import TaskConfig, TaskDeviceConfig +from eos.resource_allocation.entities.resource_request import ( + ResourceAllocationRequest, + ResourceType, +) +from eos.tasks.entities.task_execution_parameters import TaskExecutionParameters +from eos.tasks.exceptions import EosTaskResourceAllocationError +from tests.fixtures import * + + +@pytest.mark.parametrize( + "setup_lab_experiment", + [("small_lab", "water_purification")], + indirect=True, +) +class TestTaskExecutor: + @pytest.mark.asyncio + async def test_request_task_execution( + self, + task_executor, + experiment_manager, + experiment_graph, + ): + experiment_manager.create_experiment("water_purification", "water_purification") + + task_config = experiment_graph.get_task_config("mixing") + task_config.parameters["time"] = 5 + + task_parameters = TaskExecutionParameters( + experiment_id="water_purification", + devices=[TaskDeviceConfig(lab_id="small_lab", id="magnetic_mixer")], + task_config=task_config, + ) + task_output_parameters, _, _ = await task_executor.request_task_execution(task_parameters) + assert task_output_parameters["mixing_time"] == 5 + + task_parameters.task_config.id = "mixing2" + task_output_parameters, _, _ = await task_executor.request_task_execution(task_parameters) + assert task_output_parameters["mixing_time"] == 5 + + task_parameters.task_config.id = "mixing3" + task_output_parameters, _, _ = await task_executor.request_task_execution(task_parameters) + assert task_output_parameters["mixing_time"] == 5 + + @pytest.mark.asyncio + async def test_request_task_execution_resource_request_timeout( + self, + task_executor, + experiment_manager, + experiment_graph, + resource_allocation_manager, + ): + request = ResourceAllocationRequest( + requester="tester", + ) + request.add_resource("magnetic_mixer", "small_lab", ResourceType.DEVICE) + active_request = resource_allocation_manager.request_resources(request, lambda requests: None) + resource_allocation_manager.process_active_requests() + + experiment_manager.create_experiment("water_purification", "water_purification") + + task_config = experiment_graph.get_task_config("mixing") + task_config.parameters["time"] = 5 + task_parameters = TaskExecutionParameters( + experiment_id="water_purification", + devices=[TaskDeviceConfig(lab_id="small_lab", id="magnetic_mixer")], + task_config=task_config, + resource_allocation_timeout=1, + ) + with pytest.raises(EosTaskResourceAllocationError): + await task_executor.request_task_execution(task_parameters) + + resource_allocation_manager.release_resources(active_request) + + @pytest.mark.asyncio + async def test_request_task_cancellation(self, task_executor, experiment_manager): + experiment_manager.create_experiment("water_purification", "water_purification") + + sleep_config = TaskConfig( + id="sleep_task", + type="Sleep", + devices=[TaskDeviceConfig(lab_id="small_lab", id="general_computer")], + parameters={"sleep_time": 2}, + ) + task_parameters = TaskExecutionParameters( + experiment_id="water_purification", + task_config=sleep_config, + ) + + tasks = set() + + task = asyncio.create_task(task_executor.request_task_execution(task_parameters)) + tasks.add(task) + await asyncio.sleep(1) + + await task_executor.request_task_cancellation(task_parameters.experiment_id, task_parameters.task_config.id) + + assert True diff --git a/tests/test_task_input_parameter_validator.py b/tests/test_task_input_parameter_validator.py new file mode 100644 index 0000000..75b1d29 --- /dev/null +++ b/tests/test_task_input_parameter_validator.py @@ -0,0 +1,129 @@ +import pytest +from omegaconf import DictConfig + +from eos.configuration.entities.parameters import ParameterType +from eos.configuration.entities.task import TaskConfig +from eos.configuration.entities.task_specification import TaskSpecification +from eos.tasks.exceptions import EosTaskValidationError +from eos.tasks.task_input_parameter_validator import TaskInputParameterValidator + + +class TestTaskInputParameterValidator: + @pytest.fixture + def task_spec(self): + return TaskSpecification( + type="test_task", + description="A test task", + input_parameters={ + "integer_param": DictConfig( + {"type": "integer", "unit": "n/a", "description": "An integer parameter", "min": 0, "max": 100} + ), + "decimal_param": DictConfig( + {"type": "decimal", "unit": "n/a", "description": "A float parameter", "min": 0.0, "max": 1.0} + ), + "string_param": DictConfig({"type": "string", "description": "A string parameter"}), + "boolean_param": DictConfig({"type": "boolean", "value": False, "description": "A boolean parameter"}), + "list_param": DictConfig( + {"type": "list", "description": "A list parameter", "element_type": "integer", "length": 3} + ), + "dictionary_param": DictConfig({"type": "dictionary", "description": "A dictionary parameter"}), + "choice_param": DictConfig( + {"type": "choice", "value": "A", "description": "A choice parameter", "choices": ["A", "B", "C"]} + ), + }, + ) + + @pytest.fixture + def task_config(self, task_spec): + return TaskConfig( + id="test_task_1", + type="test_task", + parameters={ + "integer_param": 50, + "decimal_param": 0.5, + "string_param": "test", + "boolean_param": True, + "list_param": [1, 2, 3], + "dictionary_param": {"key": "value"}, + "choice_param": "A", + }, + ) + + @pytest.fixture + def validator(self, task_config, task_spec): + return TaskInputParameterValidator(task_config, task_spec) + + def test_valid_input_parameters(self, validator): + validator.validate_input_parameters() # Should not raise any exceptions + + @pytest.mark.parametrize( + ("param_name", "invalid_value"), + [ + ("integer_param", "not_an_int"), + ("decimal_param", "not_a_float"), + ("boolean_param", "not_a_bool"), + ("list_param", "not_a_list"), + ("dictionary_param", "not_a_dict"), + ("choice_param", "D"), + ], + ) + def test_invalid_input_parameters(self, validator, task_config, param_name, invalid_value): + task_config.parameters[param_name] = invalid_value + with pytest.raises(EosTaskValidationError): + validator.validate_input_parameters() + + def test_missing_required_parameter(self, validator, task_config): + del task_config.parameters["integer_param"] + with pytest.raises(EosTaskValidationError): + validator.validate_input_parameters() + + def test_extra_parameter(self, validator, task_config): + task_config.parameters["extra_param"] = "extra" + with pytest.raises(EosTaskValidationError): + validator.validate_input_parameters() + + @pytest.mark.parametrize( + ("param_type", "valid_values", "invalid_values"), + [ + (ParameterType.integer, [0, 50, 100, "50"], [-1, 101, "fifty"]), + (ParameterType.decimal, [0.0, 0.5, 1.0, "0.5"], [-0.1, 1.1, "half"]), + (ParameterType.boolean, [True, False, "true", "false"], ["yes", "no", 2]), + (ParameterType.string, ["test", "123", ""], []), + (ParameterType.list, [[1, 2, 3], [1, 2, 62]], [[1, 2], [1, 2, 3, 4], "not_a_list"]), + (ParameterType.dictionary, [{"key": "value"}, {}], ["not_a_dict", [1, 2, 3]]), + (ParameterType.choice, ["A", "B", "C"], ["D", 1, True]), + ], + ) + def test_parameter_type_conversion( + self, validator, task_config, task_spec, param_type, valid_values, invalid_values + ): + param_name = f"{param_type.value}_param" + task_spec.input_parameters[param_name]["type"] = param_type.value + if param_type == ParameterType.choice: + task_spec.input_parameters[param_name]["choices"] = ["A", "B", "C"] + elif param_type == ParameterType.list: + task_spec.input_parameters[param_name]["element_type"] = "integer" + task_spec.input_parameters[param_name]["length"] = 3 + + for valid_value in valid_values: + task_config.parameters[param_name] = valid_value + validator.validate_input_parameters() # Should not raise any exceptions + + for invalid_value in invalid_values: + task_config.parameters[param_name] = invalid_value + with pytest.raises(EosTaskValidationError): + validator.validate_input_parameters() + + @pytest.mark.parametrize( + ("param_name", "invalid_value", "expected_error"), + [ + ("integer_param", "$.some_reference", EosTaskValidationError), + ("integer_param", "eos_dynamic", EosTaskValidationError), + ("integer_param", 150, EosTaskValidationError), + ("list_param", [1, 2, 3, 4], EosTaskValidationError), + ], + ) + def test_specific_validation_cases(self, validator, task_config, param_name, invalid_value, expected_error): + task_config.parameters[param_name] = invalid_value + with pytest.raises(expected_error): + validator.validate_input_parameters() diff --git a/tests/test_task_manager.py b/tests/test_task_manager.py new file mode 100644 index 0000000..2b74b44 --- /dev/null +++ b/tests/test_task_manager.py @@ -0,0 +1,112 @@ +from eos.tasks.entities.task import TaskStatus, TaskOutput +from eos.tasks.exceptions import EosTaskStateError, EosTaskExistsError +from tests.fixtures import * + +EXPERIMENT_ID = "water_purification" + + +@pytest.fixture +def experiment_manager(configuration_manager, db_manager): + experiment_manager = ExperimentManager(configuration_manager, db_manager) + experiment_manager.create_experiment(EXPERIMENT_ID, "water_purification") + return experiment_manager + + +@pytest.mark.parametrize("setup_lab_experiment", [("small_lab", "water_purification")], indirect=True) +class TestTaskManager: + def test_create_task(self, task_manager, experiment_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + + task = task_manager.get_task(EXPERIMENT_ID, "mixing") + assert task.id == "mixing" + assert task.type == "Magnetic Mixing" + + def test_create_task_nonexistent(self, task_manager, experiment_manager): + with pytest.raises(EosTaskStateError): + task_manager.create_task(EXPERIMENT_ID, "nonexistent", "nonexistent", []) + + def test_create_task_nonexistent_task_type(self, task_manager, experiment_manager): + with pytest.raises(EosTaskStateError): + task_manager.create_task(EXPERIMENT_ID, "nonexistent_task", "Nonexistent", []) + + def test_create_existing_task(self, task_manager, experiment_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + + with pytest.raises(EosTaskExistsError): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + + def test_delete_task(self, task_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + + task_manager.delete_task(EXPERIMENT_ID, "mixing") + + assert task_manager.get_task(EXPERIMENT_ID, "mixing") is None + + def test_delete_nonexistent_task(self, task_manager, experiment_manager): + with pytest.raises(EosTaskStateError): + task_manager.delete_task(EXPERIMENT_ID, "nonexistent_task") + + def test_get_all_tasks_by_status(self, task_manager, experiment_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + task_manager.create_task(EXPERIMENT_ID, "purification", "Purification", []) + + task_manager.start_task(EXPERIMENT_ID, "mixing") + task_manager.complete_task(EXPERIMENT_ID, "purification") + + assert len(task_manager.get_tasks(experiment_id=EXPERIMENT_ID, status=TaskStatus.RUNNING.value)) == 1 + assert len(task_manager.get_tasks(experiment_id=EXPERIMENT_ID, status=TaskStatus.COMPLETED.value)) == 1 + + def test_set_task_status(self, task_manager, experiment_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + + assert task_manager.get_task(EXPERIMENT_ID, "mixing").status == TaskStatus.CREATED + + task_manager.start_task(EXPERIMENT_ID, "mixing") + assert task_manager.get_task(EXPERIMENT_ID, "mixing").status == TaskStatus.RUNNING + + task_manager.complete_task(EXPERIMENT_ID, "mixing") + assert task_manager.get_task(EXPERIMENT_ID, "mixing").status == TaskStatus.COMPLETED + + def test_set_task_status_nonexistent_task(self, task_manager, experiment_manager): + with pytest.raises(EosTaskStateError): + task_manager.start_task(EXPERIMENT_ID, "nonexistent_task") + + def test_start_task(self, task_manager, experiment_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + + task_manager.start_task(EXPERIMENT_ID, "mixing") + assert "mixing" in experiment_manager.get_running_tasks(EXPERIMENT_ID) + + def test_start_task_nonexistent_experiment(self, task_manager, experiment_manager): + with pytest.raises(EosTaskStateError): + task_manager.start_task(EXPERIMENT_ID, "nonexistent_task") + + def test_complete_task(self, task_manager, experiment_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + task_manager.start_task(EXPERIMENT_ID, "mixing") + task_manager.complete_task(EXPERIMENT_ID, "mixing") + assert "mixing" not in experiment_manager.get_running_tasks(EXPERIMENT_ID) + assert "mixing" in experiment_manager.get_completed_tasks(EXPERIMENT_ID) + + def test_complete_task_nonexistent_experiment(self, task_manager, experiment_manager): + with pytest.raises(EosTaskStateError): + task_manager.complete_task(EXPERIMENT_ID, "nonexistent_task") + + def test_add_task_output(self, task_manager): + task_manager.create_task(EXPERIMENT_ID, "mixing", "Magnetic Mixing", []) + + task_output = TaskOutput( + experiment_id=EXPERIMENT_ID, + task_id="mixing", + parameters={"x": 5}, + file_names=["file"], + ) + task_manager.add_task_output(EXPERIMENT_ID, "mixing", task_output) + task_manager.add_task_output_file(EXPERIMENT_ID, "mixing", "file", b"file_data") + + output = task_manager.get_task_output(experiment_id=EXPERIMENT_ID, task_id="mixing") + assert output.parameters == {"x": 5} + assert output.file_names == ["file"] + + output_file = task_manager.get_task_output_file(experiment_id=EXPERIMENT_ID, task_id="mixing", file_name="file") + assert output_file == b"file_data" diff --git a/tests/test_task_specification_validation.py b/tests/test_task_specification_validation.py new file mode 100644 index 0000000..8561c3a --- /dev/null +++ b/tests/test_task_specification_validation.py @@ -0,0 +1,262 @@ +from eos.configuration.entities.parameters import ( + ParameterFactory, + ParameterType, +) +from eos.configuration.entities.task_specification import ( + TaskSpecificationOutputParameter, + TaskSpecification, +) +from eos.configuration.exceptions import EosConfigurationError +from tests.fixtures import * + + +class TestTaskSpecifications: + def test_invalid_parameter_type(self): + with pytest.raises(ValueError): + ParameterFactory.create_parameter( + "invalid_type", + value=120, + description="Duration of evaporation in seconds.", + ) + + def test_numeric_parameter_unit_not_specified(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.integer, + unit="", + value=120, + min=60, + description="Duration of evaporation in seconds.", + ) + + def test_numeric_parameter_value_not_numeric(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.integer, + unit="sec", + value="not_a_number", + min=60, + description="Duration of evaporation in seconds.", + ) + + def test_numeric_parameter_min_greater_than_max(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.integer, + unit="sec", + value=120, + min=300, + max=60, + description="Duration of evaporation in seconds.", + ) + + def test_numeric_parameter_out_of_range_min(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.integer, + unit="sec", + value=5, + min=60, + max=300, + description="Duration of evaporation in seconds.", + ) + + def test_numeric_parameter_out_of_range_max(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.integer, + unit="sec", + value=100, + min=0, + max=80, + description="Duration of evaporation in seconds.", + ) + + def test_boolean_parameter_invalid_value(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.boolean, + value="not_a_boolean", + description="Whether to sparge the evaporation vessel with nitrogen.", + ) + + def test_choice_parameter_choices_not_specified(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.choice, + choices=[], + value="method1", + description="Method to use", + ) + + def test_choice_parameter_no_value(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.choice, + choices=["method1", "method2"], + value=None, + description="Method to use", + ) + + def test_choice_parameter_invalid_value(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.choice, + choices=["method1", "method2"], + value="invalid_method", + description="Method to use", + ) + + def test_list_parameter_invalid_element_type(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="invalid_type", + value=[1, 2, 3], + description="List of elements", + ) + + def test_list_parameter_nested_list(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="list", + value=[[1], [2], [3]], + description="List of elements", + ) + + def test_list_parameter_invalid_value(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=4, + description="List of elements", + ) + + def test_list_parameter_elements_not_same_type(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[1, True, "3"], + description="List of elements", + ) + + def test_list_parameter_invalid_value_element_size(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[1, 2], + description="List of elements", + ) + + def test_list_parameter_invalid_value_element_min(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[1, 2, 3], + min=[2, 2, "INVALID"], + description="List of elements", + ) + + def test_list_parameter_invalid_value_element_max(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[1, 2, 3], + max=[2, 2, "INVALID"], + description="List of elements", + ) + + def test_list_parameter_value_less_than_min(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[2, 2, 2], + min=[2, 2, 3], + description="List of elements", + ) + + def test_list_parameter_value_greater_than_max(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[2, 2, 2], + max=[2, 2, 1], + description="List of elements", + ) + + def test_list_parameter_invalid_min_max_size(self): + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[2, 2, 2], + min=[2, 2], + description="List of elements", + ) + + with pytest.raises(EosConfigurationError): + ParameterFactory.create_parameter( + ParameterType.list, + length=3, + element_type="integer", + value=[2, 2, 2], + max=[2, 2], + description="List of elements", + ) + + def test_parameter_factory_invalid_type(self): + with pytest.raises(ValueError): + ParameterFactory.create_parameter( + "invalid_type", + value=120, + description="Duration of evaporation in seconds.", + ) + + def test_parameter_invalid_name(self, configuration_manager): + task_specs = configuration_manager.task_specs + + task_spec = task_specs.get_spec_by_type("Magnetic Mixing") + + task_spec.input_parameters["invalid_name*"] = { + "type": "integer", + "unit": "sec", + "value": 120, + "description": "Duration of evaporation in seconds.", + } + + with pytest.raises(EosConfigurationError): + TaskSpecification(**task_spec) + + def test_output_numeric_parameter_unit_not_specified(self, configuration_manager): + with pytest.raises(EosConfigurationError): + TaskSpecificationOutputParameter( + type=ParameterType.integer, + unit="", + description="Duration of evaporation in seconds.", + ) + + def test_output_non_numeric_parameter_unit_specified(self, configuration_manager): + with pytest.raises(EosConfigurationError): + TaskSpecificationOutputParameter( + type=ParameterType.boolean, + unit="sec", + description="Whether to sparge the evaporation vessel with nitrogen.", + ) diff --git a/tests/user/testing/common/__init__.py b/tests/user/testing/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/user/testing/devices/abstract_lab/DT1/device.py b/tests/user/testing/devices/abstract_lab/DT1/device.py new file mode 100644 index 0000000..611920d --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT1/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class DT1Device(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/abstract_lab/DT1/device.yml b/tests/user/testing/devices/abstract_lab/DT1/device.yml new file mode 100644 index 0000000..2fd54c5 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT1/device.yml @@ -0,0 +1,2 @@ +type: DT1 +description: An abstract device for testing diff --git a/tests/user/testing/devices/abstract_lab/DT2/device.py b/tests/user/testing/devices/abstract_lab/DT2/device.py new file mode 100644 index 0000000..ba688c5 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT2/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class DT2Device(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/abstract_lab/DT2/device.yml b/tests/user/testing/devices/abstract_lab/DT2/device.yml new file mode 100644 index 0000000..758cf63 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT2/device.yml @@ -0,0 +1,2 @@ +type: DT2 +description: An abstract device for testing diff --git a/tests/user/testing/devices/abstract_lab/DT3/device.py b/tests/user/testing/devices/abstract_lab/DT3/device.py new file mode 100644 index 0000000..04ebc26 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT3/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class DT3Device(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/abstract_lab/DT3/device.yml b/tests/user/testing/devices/abstract_lab/DT3/device.yml new file mode 100644 index 0000000..52e952d --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT3/device.yml @@ -0,0 +1,2 @@ +type: DT3 +description: An abstract device for testing diff --git a/tests/user/testing/devices/abstract_lab/DT4/device.py b/tests/user/testing/devices/abstract_lab/DT4/device.py new file mode 100644 index 0000000..5eaecef --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT4/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class DT4Device(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/abstract_lab/DT4/device.yml b/tests/user/testing/devices/abstract_lab/DT4/device.yml new file mode 100644 index 0000000..2257574 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT4/device.yml @@ -0,0 +1,2 @@ +type: DT4 +description: An abstract device for testing diff --git a/tests/user/testing/devices/abstract_lab/DT5/device.py b/tests/user/testing/devices/abstract_lab/DT5/device.py new file mode 100644 index 0000000..6ab25e4 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT5/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class DT5Device(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/abstract_lab/DT5/device.yml b/tests/user/testing/devices/abstract_lab/DT5/device.yml new file mode 100644 index 0000000..47bc540 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT5/device.yml @@ -0,0 +1,2 @@ +type: DT5 +description: An abstract device for testing diff --git a/tests/user/testing/devices/abstract_lab/DT6/device.py b/tests/user/testing/devices/abstract_lab/DT6/device.py new file mode 100644 index 0000000..ac2ab08 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT6/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class DT6Device(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/abstract_lab/DT6/device.yml b/tests/user/testing/devices/abstract_lab/DT6/device.yml new file mode 100644 index 0000000..32f8009 --- /dev/null +++ b/tests/user/testing/devices/abstract_lab/DT6/device.yml @@ -0,0 +1,2 @@ +type: DT6 +description: An abstract device for testing diff --git a/tests/user/testing/devices/multiplication_lab/analyzer/device.py b/tests/user/testing/devices/multiplication_lab/analyzer/device.py new file mode 100644 index 0000000..0bbbd3f --- /dev/null +++ b/tests/user/testing/devices/multiplication_lab/analyzer/device.py @@ -0,0 +1,17 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class AnalyzerDevice(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass + + def analyze_result(self, number: int, product: int) -> int: + return number + 100 * abs(product - 1024) diff --git a/tests/user/testing/devices/multiplication_lab/analyzer/device.yml b/tests/user/testing/devices/multiplication_lab/analyzer/device.yml new file mode 100644 index 0000000..42333c7 --- /dev/null +++ b/tests/user/testing/devices/multiplication_lab/analyzer/device.yml @@ -0,0 +1,2 @@ +type: analyzer +description: A device for analyzing the result of the multiplication of two numbers diff --git a/tests/user/testing/devices/multiplication_lab/multiplier/device.py b/tests/user/testing/devices/multiplication_lab/multiplier/device.py new file mode 100644 index 0000000..33ed2ba --- /dev/null +++ b/tests/user/testing/devices/multiplication_lab/multiplier/device.py @@ -0,0 +1,17 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class MultiplierDevice(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass + + def multiply(self, a: int, b: int) -> int: + return a * b diff --git a/tests/user/testing/devices/multiplication_lab/multiplier/device.yml b/tests/user/testing/devices/multiplication_lab/multiplier/device.yml new file mode 100644 index 0000000..efe0ee4 --- /dev/null +++ b/tests/user/testing/devices/multiplication_lab/multiplier/device.yml @@ -0,0 +1,2 @@ +type: multiplier +description: A device for multiplying two numbers diff --git a/tests/user/testing/devices/small_lab/computer/device.py b/tests/user/testing/devices/small_lab/computer/device.py new file mode 100644 index 0000000..5fd44c4 --- /dev/null +++ b/tests/user/testing/devices/small_lab/computer/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class ComputerDevice(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/small_lab/computer/device.yml b/tests/user/testing/devices/small_lab/computer/device.yml new file mode 100644 index 0000000..40c6f12 --- /dev/null +++ b/tests/user/testing/devices/small_lab/computer/device.yml @@ -0,0 +1,2 @@ +type: computer +description: General-purpose computer diff --git a/tests/user/testing/devices/small_lab/evaporator/device.py b/tests/user/testing/devices/small_lab/evaporator/device.py new file mode 100644 index 0000000..ceec58c --- /dev/null +++ b/tests/user/testing/devices/small_lab/evaporator/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class EvaporatorDevice(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/small_lab/evaporator/device.yml b/tests/user/testing/devices/small_lab/evaporator/device.yml new file mode 100644 index 0000000..e2dd67b --- /dev/null +++ b/tests/user/testing/devices/small_lab/evaporator/device.yml @@ -0,0 +1,2 @@ +type: evaporator +description: Evaporator for substance purification diff --git a/tests/user/testing/devices/small_lab/fridge/device.py b/tests/user/testing/devices/small_lab/fridge/device.py new file mode 100644 index 0000000..e818aad --- /dev/null +++ b/tests/user/testing/devices/small_lab/fridge/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class FridgeDevice(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/small_lab/fridge/device.yml b/tests/user/testing/devices/small_lab/fridge/device.yml new file mode 100644 index 0000000..8acfc81 --- /dev/null +++ b/tests/user/testing/devices/small_lab/fridge/device.yml @@ -0,0 +1,2 @@ +type: fridge +description: Fridge for storing temperature-sensitive substances diff --git a/tests/user/testing/devices/small_lab/magnetic_mixer/device.py b/tests/user/testing/devices/small_lab/magnetic_mixer/device.py new file mode 100644 index 0000000..68f2cae --- /dev/null +++ b/tests/user/testing/devices/small_lab/magnetic_mixer/device.py @@ -0,0 +1,14 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class MagneticMixerDevice(BaseDevice): + def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + def _cleanup(self) -> None: + pass + + def _report(self) -> dict[str, Any]: + pass diff --git a/tests/user/testing/devices/small_lab/magnetic_mixer/device.yml b/tests/user/testing/devices/small_lab/magnetic_mixer/device.yml new file mode 100644 index 0000000..f3f3128 --- /dev/null +++ b/tests/user/testing/devices/small_lab/magnetic_mixer/device.yml @@ -0,0 +1,2 @@ +type: magnetic_mixer +description: Magnetic mixer for mixing substances diff --git a/tests/user/testing/experiments/abstract_experiment/experiment.yml b/tests/user/testing/experiments/abstract_experiment/experiment.yml new file mode 100644 index 0000000..cac30cc --- /dev/null +++ b/tests/user/testing/experiments/abstract_experiment/experiment.yml @@ -0,0 +1,61 @@ +type: abstract_experiment +description: An abstract experiment for testing + +labs: + - abstract_lab + +tasks: + - id: A + type: Noop + devices: + - lab_id: abstract_lab + id: D2 + + - id: B + type: Noop + dependencies: [ "A" ] + devices: + - lab_id: abstract_lab + id: D1 + + - id: C + type: Noop + dependencies: [ "A" ] + devices: + - lab_id: abstract_lab + id: D3 + + - id: D + type: Noop + dependencies: [ "A" ] + devices: + - lab_id: abstract_lab + id: D1 + + - id: E + type: Noop + dependencies: [ "B" ] + devices: + - lab_id: abstract_lab + id: D3 + + - id: F + type: Noop + dependencies: [ "C" ] + devices: + - lab_id: abstract_lab + id: D2 + + - id: G + type: Noop + dependencies: [ "D", "E", "F" ] + devices: + - lab_id: abstract_lab + id: D5 + + - id: H + type: Noop + dependencies: [ "G" ] + devices: + - lab_id: abstract_lab + id: D6 diff --git a/tests/user/testing/experiments/optimize_multiplication/experiment.yml b/tests/user/testing/experiments/optimize_multiplication/experiment.yml new file mode 100644 index 0000000..8322acb --- /dev/null +++ b/tests/user/testing/experiments/optimize_multiplication/experiment.yml @@ -0,0 +1,33 @@ +type: optimize_multiplication +description: An experiment for finding the smallest number that when multiplied by two factors yields 1024 + +labs: + - multiplication_lab + +tasks: + - id: mult_1 + type: Multiplication + devices: + - lab_id: multiplication_lab + id: multiplier + parameters: + number: eos_dynamic + factor: eos_dynamic + - id: mult_2 + type: Multiplication + devices: + - lab_id: multiplication_lab + id: multiplier + dependencies: [ mult_1 ] + parameters: + number: mult_1.product + factor: eos_dynamic + - id: compute_multiplication_objective + type: Compute Multiplication Objective + devices: + - lab_id: multiplication_lab + id: analyzer + dependencies: [ mult_1, mult_2 ] + parameters: + number: mult_1.number + product: mult_2.product diff --git a/tests/user/testing/experiments/optimize_multiplication/optimizer.py b/tests/user/testing/experiments/optimize_multiplication/optimizer.py new file mode 100644 index 0000000..3db60b7 --- /dev/null +++ b/tests/user/testing/experiments/optimize_multiplication/optimizer.py @@ -0,0 +1,27 @@ +from bofire.data_models.acquisition_functions.acquisition_function import qNEI +from bofire.data_models.enum import SamplingMethodEnum +from bofire.data_models.features.continuous import ContinuousOutput +from bofire.data_models.features.discrete import DiscreteInput +from bofire.data_models.objectives.identity import MinimizeObjective + +from eos.optimization.sequential_bayesian_optimizer import BayesianSequentialOptimizer +from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer + + +def eos_create_campaign_optimizer() -> tuple[dict, type[AbstractSequentialOptimizer]]: + constructor_args = { + "inputs": [ + DiscreteInput(key="mult_1.number", values=list(range(2, 34))), + DiscreteInput(key="mult_1.factor", values=list(range(2, 18))), + DiscreteInput(key="mult_2.factor", values=list(range(2, 18))), + ], + "outputs": [ + ContinuousOutput(key="compute_multiplication_objective.objective", objective=MinimizeObjective(w=1.0)), + ], + "constraints": [], + "acquisition_function": qNEI(), + "num_initial_samples": 5, + "initial_sampling_method": SamplingMethodEnum.SOBOL, + } + + return constructor_args, BayesianSequentialOptimizer diff --git a/tests/user/testing/experiments/water_purification/experiment.yml b/tests/user/testing/experiments/water_purification/experiment.yml new file mode 100644 index 0000000..6f791a1 --- /dev/null +++ b/tests/user/testing/experiments/water_purification/experiment.yml @@ -0,0 +1,42 @@ +type: water_purification +description: Experiment to find best parameters for purifying water using evaporation + +labs: + - small_lab + +containers: + - id: 026749f8f40342b38157f9824ae2f512 + metadata: + substance: salt_water + +tasks: + - id: mixing + type: Magnetic Mixing + devices: + - lab_id: small_lab + id: magnetic_mixer + description: Magnetically mix water and salt + + containers: + beaker: 026749f8f40342b38157f9824ae2f512 + parameters: + speed: 60 + time: eos_dynamic + + - id: evaporation + type: Purification + devices: + - lab_id: small_lab + id: evaporator + description: Purification of water using evaporation + dependencies: [ "mixing" ] + + containers: + beaker: 026749f8f40342b38157f9824ae2f512 + parameters: + method: evaporation + evaporation_time: mixing.mixing_time + evaporation_temperature: eos_dynamic + evaporation_rotation_speed: eos_dynamic + evaporation_sparging: true + evaporation_sparging_flow: eos_dynamic diff --git a/tests/user/testing/labs/abstract_lab/abstract_lab.map b/tests/user/testing/labs/abstract_lab/abstract_lab.map new file mode 100644 index 0000000..e69de29 diff --git a/tests/user/testing/labs/abstract_lab/lab.yml b/tests/user/testing/labs/abstract_lab/lab.yml new file mode 100644 index 0000000..8032105 --- /dev/null +++ b/tests/user/testing/labs/abstract_lab/lab.yml @@ -0,0 +1,22 @@ +type: abstract_lab +description: An abstract laboratory with abstract devices for testing + +devices: + D1: + type: DT1 + computer: eos_computer + D2: + type: DT2 + computer: eos_computer + D3: + type: DT3 + computer: eos_computer + D4: + type: DT4 + computer: eos_computer + D5: + type: DT5 + computer: eos_computer + D6: + type: DT6 + computer: eos_computer diff --git a/tests/user/testing/labs/multiplication_lab/lab.yml b/tests/user/testing/labs/multiplication_lab/lab.yml new file mode 100644 index 0000000..c07c0d6 --- /dev/null +++ b/tests/user/testing/labs/multiplication_lab/lab.yml @@ -0,0 +1,10 @@ +type: multiplication_lab +description: An abstract laboratory for testing multiplication + +devices: + multiplier: + type: multiplier + computer: eos_computer + analyzer: + type: analyzer + computer: eos_computer diff --git a/tests/user/testing/labs/multiplication_lab/multiplication.map b/tests/user/testing/labs/multiplication_lab/multiplication.map new file mode 100644 index 0000000..e69de29 diff --git a/tests/user/testing/labs/small_lab/lab.yml b/tests/user/testing/labs/small_lab/lab.yml new file mode 100644 index 0000000..1b6bbc0 --- /dev/null +++ b/tests/user/testing/labs/small_lab/lab.yml @@ -0,0 +1,131 @@ +type: small_lab +description: A small laboratory for testing + +locations: + gc_1: + description: Gas Chromatography station 1 + metadata: + map_coordinates: { x: 100, y: 32, rotation: 0 } + areas: + injection_port: + description: Injection port for the gas chromatograph + + gc_2: + description: Gas Chromatography station 2 + metadata: + map_coordinates: { x: 110, y: 32, rotation: 0 } + areas: + injection_port: + description: Injection port for the gas chromatograph + + wafer_station: + description: Wafer processing station + metadata: + map_coordinates: { x: 120, y: 32, rotation: 0 } + areas: + wafer_stack: + description: Wafer storage + cartesian_robot_head: + description: Head of the cartesian robot that holds the wafer + + mixing_station: + description: Station equipped with magnetic mixers for substance blending + metadata: + map_coordinates: { x: 140, y: 32, rotation: 0 } + + substance_shelf: + description: Storage shelf for chemical substances + metadata: + map_coordinates: { x: 50, y: 10, rotation: 0 } + + substance_fridge: + description: Refrigerated storage for temperature-sensitive substances + metadata: + map_coordinates: { x: 60, y: 10, rotation: 0 } + + fetch_charging_station: + description: Charging station for the Fetch mobile manipulation robot + metadata: + map_coordinates: { x: 10, y: 10, rotation: 0 } + +devices: + general_computer: + description: General-purpose computer + type: computer + location: gc_1 + computer: eos_computer + + magnetic_mixer: + description: Mixer for substance blending + type: magnetic_mixer + location: mixing_station + computer: eos_computer + + magnetic_mixer_2: + description: Mixer for substance blending + type: magnetic_mixer + location: mixing_station + computer: eos_computer + + evaporator: + description: Evaporator for substance purification + type: evaporator + location: mixing_station + computer: eos_computer + + substance_fridge: + description: Fridge for storing temperature-sensitive substances + type: fridge + location: substance_fridge + computer: eos_computer + +containers: + - type: beaker_250 + location: substance_shelf + metadata: + capacity: 250 + ids: + - ec1ca48cd5d14c0c8cde376476e0d98d + - 4d8488982b8e404c83465308f6211c25 + - 8f55ee53aaf4429392993295476b03bc + - d29185534fee42749a9f13932dfcb7f2 + - type: beaker_350 + metadata: + capacity: 350 + location: substance_shelf + ids: + - 257b4bf4f13d40a49b60cb20db6bdb8d + - 4803e4639b314026a68e7217c5869567 + - ab0b94897b1e439e90446994c88f1208 + - type: beaker_500 + location: substance_shelf + metadata: + capacity: 500 + ids: + - 026749f8f40342b38157f9824ae2f512 + - acf829f859e04fee80d54a1ee918555d + - a3b958aea8bd435386cdcbab20a2d3ec + - 2fe219d41d55449781338ef45f7f49bc + - type: vial_20 + location: substance_shelf + metadata: + capacity: 20 + ids: + - 84eb17d61e884ffd9d1fdebcbad1532b + - daa8748a09ea4e91b32c764fa3e6a3c3 + - d03d93b6ef114ffba7b5b217362458e4 + - 51ba54eab0bd4fa08c7ec8dea2d52fa6 + - e7b25d1ea6844754a55a6c4be2ebbb62 + - 9c94fcdb276e4909aa0408e287e6986c + - b9a14b0e5ee24db0afdc633802698a57 + - cb895e7a7b814bfab294be9f22a8dc2c + - dc8aadece2d64ea59baa1b28d1c62b7b + - b1f6cf664cd542e9857314f1470f9efe + - 3e128a03dfe44709bf6941032fe42038 + - efb5ccbaf9b4465c90b1654fac690821 + - type: flask_250 + location: substance_shelf + metadata: + capacity: 250 + ids: + - dd4703461198463e980de42a6034f8de diff --git a/tests/user/testing/labs/small_lab/small_lab.map b/tests/user/testing/labs/small_lab/small_lab.map new file mode 100644 index 0000000..e69de29 diff --git a/tests/user/testing/tasks/fridge_temperature_control/task.py b/tests/user/testing/tasks/fridge_temperature_control/task.py new file mode 100644 index 0000000..9f03dfe --- /dev/null +++ b/tests/user/testing/tasks/fridge_temperature_control/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class FridgeTemperatureControlTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/fridge_temperature_control/task.yml b/tests/user/testing/tasks/fridge_temperature_control/task.yml new file mode 100644 index 0000000..c74d53e --- /dev/null +++ b/tests/user/testing/tasks/fridge_temperature_control/task.yml @@ -0,0 +1,13 @@ +type: Fridge Temperature Control +description: This task adjusts the temperature of a laboratory refrigerator to a specified target to ensure optimal storage conditions for substances that require precise temperature control. + +device_types: + - fridge + +input_parameters: + target_temperature: + type: integer + unit: celsius + min: -20 + max: 10 + description: The new temperature for the fridge. diff --git a/tests/user/testing/tasks/gc_analysis/task.py b/tests/user/testing/tasks/gc_analysis/task.py new file mode 100644 index 0000000..8758504 --- /dev/null +++ b/tests/user/testing/tasks/gc_analysis/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class GcAnalysisTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/gc_analysis/task.yml b/tests/user/testing/tasks/gc_analysis/task.yml new file mode 100644 index 0000000..601ceac --- /dev/null +++ b/tests/user/testing/tasks/gc_analysis/task.yml @@ -0,0 +1,50 @@ +type: GC Analysis +description: Perform gas chromatography (GC) analysis on a sample. + +device_types: + - gas_chromatograph + +input_parameters: + injection_volume: + type: integer + unit: ul + min: 1 + max: 10 + description: The volume of the sample to be injected into the GC system. + + oven_temperature_initial: + type: integer + unit: C + min: 40 + max: 100 + description: The initial temperature of the GC oven. + + oven_temperature_final: + type: integer + unit: C + min: 150 + max: 300 + description: The final temperature of the GC oven, should be higher than the initial temperature. + + temperature_ramp_rate: + type: integer + unit: C/min + min: 1 + max: 20 + description: The rate at which the oven temperature increases. + + carrier_gas: + type: string + description: The type of carrier gas used in the GC analysis, e.g., Helium. + + flow_rate: + type: integer + unit: ml/min + min: 1 + max: 5 + description: The flow rate of the carrier gas. + +output_parameters: + result_folder_path: + type: string + description: The file path to the folder containing the results of the GC analysis. diff --git a/tests/user/testing/tasks/gc_injection/task.py b/tests/user/testing/tasks/gc_injection/task.py new file mode 100644 index 0000000..0142ce0 --- /dev/null +++ b/tests/user/testing/tasks/gc_injection/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class GcInjectionTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/gc_injection/task.yml b/tests/user/testing/tasks/gc_injection/task.yml new file mode 100644 index 0000000..6953701 --- /dev/null +++ b/tests/user/testing/tasks/gc_injection/task.yml @@ -0,0 +1,10 @@ +type: GC Injection +description: This task involves the use of a mobile robot to perform sample injection in a GC. + +device_types: + - mobile_manipulation_robot + +input_parameters: + gc_target_name: + type: string + description: The name of the GC target as defined in the GC injection task configuration YAML file. diff --git a/tests/user/testing/tasks/hplc_analysis/task.py b/tests/user/testing/tasks/hplc_analysis/task.py new file mode 100644 index 0000000..2394be2 --- /dev/null +++ b/tests/user/testing/tasks/hplc_analysis/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class HplcAnalysisTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/hplc_analysis/task.yml b/tests/user/testing/tasks/hplc_analysis/task.yml new file mode 100644 index 0000000..7ba9c26 --- /dev/null +++ b/tests/user/testing/tasks/hplc_analysis/task.yml @@ -0,0 +1,68 @@ +type: HPLC Analysis + +description: This task performs High-Performance Liquid Chromatography (HPLC) analysis on a sample to separate, identify, and quantify its chemical components. + +device_types: + - high_performance_liquid_chromatograph + +input_containers: + vial: + type: vial + +input_parameters: + column: + type: choice + value: C18 + choices: + - C18 + - C8 + - HILIC + description: The type of HPLC column to be used for separation. + + mobile_phase_a: + type: string + value: water + description: The first mobile phase component (usually an aqueous solvent). + + mobile_phase_b: + type: string + value: acetonitrile + description: The second mobile phase component (usually an organic solvent). + + gradient: + type: string + value: "0 min: 5%B, 10 min: 95%B, 12 min: 95%B, 13 min: 5%B, 15 min: 5%B" + description: The gradient elution profile, specifying the change in mobile phase composition over time. + + flow_rate: + type: decimal + unit: ml/min + value: 1.0 + min: 0.1 + max: 2.0 + description: The flow rate of the mobile phase through the HPLC column. + + injection_volume: + type: integer + unit: uL + value: 10 + min: 1 + max: 100 + description: The volume of sample injected into the HPLC system. + + detection_wavelength: + type: integer + unit: nm + value: 254 + min: 190 + max: 800 + description: The wavelength at which the detector is set to monitor the eluting compounds. + +output_parameters: + peak_table_file_path: + type: string + description: Path to output file summarizing the detected peaks, their retention times, and areas. + + chromatogram_file_path: + type: string + description: Path to output file of chromatogram data representing the detector response over time. \ No newline at end of file diff --git a/tests/user/testing/tasks/magnetic_mixing/task.py b/tests/user/testing/tasks/magnetic_mixing/task.py new file mode 100644 index 0000000..b76877a --- /dev/null +++ b/tests/user/testing/tasks/magnetic_mixing/task.py @@ -0,0 +1,13 @@ +from eos.tasks.base_task import BaseTask + + +class MagneticMixingTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + output_parameters = {"mixing_time": parameters["time"]} + + return output_parameters, None, None diff --git a/tests/user/testing/tasks/magnetic_mixing/task.yml b/tests/user/testing/tasks/magnetic_mixing/task.yml new file mode 100644 index 0000000..1afae50 --- /dev/null +++ b/tests/user/testing/tasks/magnetic_mixing/task.yml @@ -0,0 +1,31 @@ +type: Magnetic Mixing +description: This task involves the use of a magnetic stirrer to blend multiple substances into a homogeneous mixture. Both solid and liquid forms can be mixed to produce a liquid output. + +device_types: + - magnetic_mixer + +input_containers: + beaker: + type: beaker_500 + +input_parameters: + speed: + type: integer + unit: rpm + value: 10 + min: 1 + max: 100 + description: The speed at which the magnetic stirrer operates, measured in revolutions per minute (rpm). + time: + type: integer + unit: sec + value: 360 + min: 3 + max: 720 + description: The total time duration for which the substances will be mixed, measured in seconds. + +output_parameters: + mixing_time: + type: integer + unit: sec + description: The total time duration for which the substances were mixed, measured in seconds. \ No newline at end of file diff --git a/tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.py b/tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.py new file mode 100644 index 0000000..6dc8863 --- /dev/null +++ b/tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.py @@ -0,0 +1,21 @@ +from eos.tasks.base_task import BaseTask + + +class ComputeMultiplicationObjectiveTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + self.cancel_requested = False + analyzer = devices.get_all_by_type("analyzer")[0] + + number = parameters["number"] + product = parameters["product"] + + objective = analyzer.analyze_result(number, product) + + output_parameters = {"objective": objective} + + return output_parameters, None, None diff --git a/tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.yml b/tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.yml new file mode 100644 index 0000000..032e186 --- /dev/null +++ b/tests/user/testing/tasks/multiplication_lab/compute_multiplication_objective/task.yml @@ -0,0 +1,21 @@ +type: Compute Multiplication Objective +description: This task computes the objective for the optimize_multiplication experiment. + +device_types: + - analyzer + +input_parameters: + number: + type: integer + unit: none + description: The number to multiply. + product: + type: integer + unit: none + description: The final product. + +output_parameters: + objective: + type: integer + unit: none + description: The objective for the find_smallest_number experiment. diff --git a/tests/user/testing/tasks/multiplication_lab/multiplication/task.py b/tests/user/testing/tasks/multiplication_lab/multiplication/task.py new file mode 100644 index 0000000..d40b1e3 --- /dev/null +++ b/tests/user/testing/tasks/multiplication_lab/multiplication/task.py @@ -0,0 +1,19 @@ +from eos.tasks.base_task import BaseTask + + +class MultiplicationTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + multiplier = devices.get_all_by_type("multiplier")[0] + number = parameters["number"] + factor = parameters["factor"] + + product = multiplier.multiply(number, factor) + + output_parameters = {"product": product} + + return output_parameters, None, None diff --git a/tests/user/testing/tasks/multiplication_lab/multiplication/task.yml b/tests/user/testing/tasks/multiplication_lab/multiplication/task.yml new file mode 100644 index 0000000..163edd1 --- /dev/null +++ b/tests/user/testing/tasks/multiplication_lab/multiplication/task.yml @@ -0,0 +1,21 @@ +type: Multiplication +description: This task takes a number and a factor and multiplies them together. + +device_types: + - multiplier + +input_parameters: + number: + type: integer + unit: none + description: The number to multiply. + factor: + type: integer + unit: none + description: The factor to multiply the number by. + +output_parameters: + product: + type: integer + unit: none + description: The product of the number and the factor. diff --git a/tests/user/testing/tasks/noop/task.py b/tests/user/testing/tasks/noop/task.py new file mode 100644 index 0000000..205eb0c --- /dev/null +++ b/tests/user/testing/tasks/noop/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class NoopTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/noop/task.yml b/tests/user/testing/tasks/noop/task.yml new file mode 100644 index 0000000..5c5bcc7 --- /dev/null +++ b/tests/user/testing/tasks/noop/task.yml @@ -0,0 +1,2 @@ +type: Noop +description: This task does nothing. diff --git a/tests/user/testing/tasks/purification/task.py b/tests/user/testing/tasks/purification/task.py new file mode 100644 index 0000000..6518b8c --- /dev/null +++ b/tests/user/testing/tasks/purification/task.py @@ -0,0 +1,13 @@ +from eos.tasks.base_task import BaseTask + + +class PurificationTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + output_parameters = {"water_salinity": 0.02} + + return output_parameters, None, None diff --git a/tests/user/testing/tasks/purification/task.yml b/tests/user/testing/tasks/purification/task.yml new file mode 100644 index 0000000..516fe29 --- /dev/null +++ b/tests/user/testing/tasks/purification/task.yml @@ -0,0 +1,72 @@ +type: Purification +description: "This task aims to purify a single substance by separating it from its impurities. The device supports two methods: evaporation and simple mixing." + +device_types: + - evaporator + +input_containers: + beaker: + type: beaker_500 + +input_parameters: + method: + type: choice + value: evaporation + choices: + - evaporation + - simple_mixing + description: The purification method to be used. Choose between evaporation and simple mixing. + + # Evaporation parameters + evaporation_time: + type: integer + unit: sec + value: 120 + min: 60 + description: Duration of evaporation in seconds. + evaporation_temperature: + type: integer + unit: celsius + value: 90 + min: 30 + max: 150 + description: Evaporation temperature in degrees Celsius. + evaporation_rotation_speed: + type: integer + unit: rpm + value: 120 + min: 10 + max: 300 + description: Speed of rotation in rpm. + evaporation_sparging: + type: boolean + value: true + description: Whether to use sparging gas during evaporation. + evaporation_sparging_flow: + type: integer + unit: ml/min + value: 5 + min: 1 + max: 10 + description: Flow rate of sparging gas in ml/min. + + # Simple mixing parameters + simple_mixing_time: + type: integer + unit: sec + value: 120 + min: 60 + description: Duration of simple mixing in seconds. + simple_mixing_rotation_speed: + type: integer + unit: rpm + value: 120 + min: 10 + max: 300 + description: Speed of rotation in rpm. + +output_parameters: + water_salinity: + type: integer + unit: ppm + description: The salinity of the purified water in parts per million. diff --git a/tests/user/testing/tasks/robot_arm_container_transfer/task.py b/tests/user/testing/tasks/robot_arm_container_transfer/task.py new file mode 100644 index 0000000..d1f04c3 --- /dev/null +++ b/tests/user/testing/tasks/robot_arm_container_transfer/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class RobotArmContainerTransferTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/robot_arm_container_transfer/task.yml b/tests/user/testing/tasks/robot_arm_container_transfer/task.yml new file mode 100644 index 0000000..b28a49b --- /dev/null +++ b/tests/user/testing/tasks/robot_arm_container_transfer/task.yml @@ -0,0 +1,22 @@ +type: Container Transfer +description: Transfer a container from one location area to another using a robot arm. + +device_types: + - fixed_arm_robot + +input_parameters: + source_location: + type: string + description: The name of the source location area. + + source_location_area: + type: string + description: The name of the source location area. + + target_location: + type: string + description: The name of the target location area. + + target_location_area: + type: string + description: The name of the target location area. diff --git a/tests/user/testing/tasks/sleep/task.py b/tests/user/testing/tasks/sleep/task.py new file mode 100644 index 0000000..da50d47 --- /dev/null +++ b/tests/user/testing/tasks/sleep/task.py @@ -0,0 +1,26 @@ +import time + +from eos.tasks.base_task import BaseTask + + +class SleepTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + self.cancel_requested = False + + sleep_time = parameters["sleep_time"] + start_time = time.time() + elapsed = 0 + + while elapsed < sleep_time: + if self.cancel_requested: + self.cancel_requested = False + return None + time.sleep(1) + elapsed = time.time() - start_time + + return None diff --git a/tests/user/testing/tasks/sleep/task.yml b/tests/user/testing/tasks/sleep/task.yml new file mode 100644 index 0000000..3a1d992 --- /dev/null +++ b/tests/user/testing/tasks/sleep/task.yml @@ -0,0 +1,10 @@ +type: Sleep +description: This task sleeps for the specified amount of time. + +input_parameters: + time: + type: integer + unit: sec + value: 0 + min: 0 + description: The total time duration for which to sleep for. diff --git a/tests/user/testing/tasks/wafer_sampling/task.py b/tests/user/testing/tasks/wafer_sampling/task.py new file mode 100644 index 0000000..6c3b588 --- /dev/null +++ b/tests/user/testing/tasks/wafer_sampling/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class WaferSamplingTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/wafer_sampling/task.yml b/tests/user/testing/tasks/wafer_sampling/task.yml new file mode 100644 index 0000000..119f365 --- /dev/null +++ b/tests/user/testing/tasks/wafer_sampling/task.yml @@ -0,0 +1,15 @@ +type: Wafer Sampling +description: Perform wafer sampling with a cartesian robot and pump/valve system. + +device_types: + - cartesian_robot + +input_parameters: + wafer_spot: + type: list + element_type: integer + length: 2 + min: [ -10, -10 ] + max: [ 10, 10 ] + value: [ 0, 0 ] + description: The coordinates of the wafer spot in the wafer station. diff --git a/tests/user/testing/tasks/weigh_container/task.py b/tests/user/testing/tasks/weigh_container/task.py new file mode 100644 index 0000000..eb4c93b --- /dev/null +++ b/tests/user/testing/tasks/weigh_container/task.py @@ -0,0 +1,11 @@ +from eos.tasks.base_task import BaseTask + + +class WeighContainerTask(BaseTask): + def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + pass diff --git a/tests/user/testing/tasks/weigh_container/task.yml b/tests/user/testing/tasks/weigh_container/task.yml new file mode 100644 index 0000000..a65ffec --- /dev/null +++ b/tests/user/testing/tasks/weigh_container/task.yml @@ -0,0 +1,19 @@ +type: Weigh Container +description: This task involves using an analytical balance to accurately measure the mass of a container. + +device_types: + - balance + +input_parameters: + minimum_weight: + type: decimal + unit: g + value: 0.1 + min: 0.0001 + description: The minimum weight required for the measurement to be considered valid. + +output_parameters: + weight: + type: decimal + unit: g + description: The measured weight of the container. \ No newline at end of file diff --git a/user/.gitkeep b/user/.gitkeep new file mode 100644 index 0000000..e69de29