UncountablePythonSDK 0.0.24__py3-none-any.whl → 0.0.131__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of UncountablePythonSDK might be problematic. Click here for more details.
- docs/conf.py +60 -8
- docs/index.md +107 -4
- docs/integration_examples/create_ingredient.md +43 -0
- docs/integration_examples/create_output.md +56 -0
- docs/integration_examples/index.md +6 -0
- docs/justfile +2 -2
- docs/requirements.txt +7 -5
- examples/async_batch.py +5 -6
- examples/basic_auth.py +7 -0
- examples/create_entity.py +4 -6
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +50 -0
- examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +21 -0
- examples/integration-server/jobs/materials_auto/example_http.py +47 -0
- examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
- examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
- examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
- examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
- examples/integration-server/jobs/materials_auto/example_wh.py +23 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +104 -0
- examples/integration-server/pyproject.toml +224 -0
- examples/invoke_uploader.py +26 -0
- examples/oauth.py +7 -0
- examples/set_recipe_metadata_file.py +40 -0
- examples/set_recipe_output_file_sdk.py +26 -0
- examples/upload_files.py +2 -3
- pkgs/argument_parser/__init__.py +9 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +295 -74
- pkgs/argument_parser/case_convert.py +4 -3
- pkgs/filesystem_utils/__init__.py +20 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +309 -0
- pkgs/filesystem_utils/_local_session.py +69 -0
- pkgs/filesystem_utils/_s3_session.py +118 -0
- pkgs/filesystem_utils/_sftp_session.py +151 -0
- pkgs/filesystem_utils/file_type_utils.py +91 -0
- pkgs/filesystem_utils/filesystem_session.py +39 -0
- pkgs/py.typed +0 -0
- pkgs/serialization/__init__.py +8 -1
- pkgs/serialization/annotation.py +64 -0
- pkgs/serialization/missing_sentry.py +1 -1
- pkgs/serialization/opaque_key.py +1 -1
- pkgs/serialization/serial_alias.py +47 -0
- pkgs/serialization/serial_class.py +69 -54
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +84 -0
- pkgs/serialization/yaml.py +57 -0
- pkgs/serialization_util/__init__.py +7 -7
- pkgs/serialization_util/convert_to_snakecase.py +27 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +117 -71
- pkgs/type_spec/actions_registry/__main__.py +0 -4
- pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
- pkgs/type_spec/builder.py +438 -109
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +52 -24
- pkgs/type_spec/cross_output_links.py +99 -0
- pkgs/type_spec/emit_io_ts.py +1 -1
- pkgs/type_spec/emit_open_api.py +160 -41
- pkgs/type_spec/emit_open_api_util.py +13 -7
- pkgs/type_spec/emit_python.py +450 -136
- pkgs/type_spec/emit_typescript.py +117 -250
- pkgs/type_spec/emit_typescript_util.py +293 -4
- pkgs/type_spec/load_types.py +20 -5
- pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
- pkgs/type_spec/open_api_util.py +29 -4
- pkgs/type_spec/parts/base.py.prepart +13 -10
- pkgs/type_spec/parts/base.ts.prepart +4 -0
- pkgs/type_spec/type_info/__main__.py +3 -1
- pkgs/type_spec/type_info/emit_type_info.py +161 -32
- pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
- pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
- pkgs/type_spec/util.py +4 -4
- pkgs/type_spec/value_spec/__main__.py +27 -10
- pkgs/type_spec/value_spec/convert_type.py +21 -1
- pkgs/type_spec/value_spec/emit_python.py +25 -7
- pkgs/type_spec/value_spec/types.py +1 -1
- uncountable/__init__.py +1 -2
- uncountable/core/__init__.py +11 -3
- uncountable/core/async_batch.py +16 -1
- uncountable/core/client.py +247 -52
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +67 -22
- uncountable/core/types.py +8 -13
- uncountable/integration/cli.py +142 -0
- uncountable/integration/construct_client.py +43 -27
- uncountable/integration/cron.py +12 -11
- uncountable/integration/db/connect.py +12 -2
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +4 -34
- uncountable/integration/executors/executors.py +147 -0
- uncountable/integration/executors/generic_upload_executor.py +336 -0
- uncountable/integration/executors/script_executor.py +15 -9
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +246 -19
- uncountable/integration/queue_runner/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/__init__.py +28 -0
- uncountable/integration/queue_runner/command_server/command_client.py +133 -0
- uncountable/integration/queue_runner/command_server/command_server.py +142 -0
- uncountable/integration/queue_runner/command_server/constants.py +4 -0
- uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
- uncountable/integration/queue_runner/command_server/types.py +75 -0
- uncountable/integration/queue_runner/datastore/__init__.py +3 -0
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
- uncountable/integration/queue_runner/datastore/interface.py +29 -0
- uncountable/integration/queue_runner/datastore/model.py +24 -0
- uncountable/integration/queue_runner/job_scheduler.py +200 -0
- uncountable/integration/queue_runner/queue_runner.py +34 -0
- uncountable/integration/queue_runner/types.py +7 -0
- uncountable/integration/queue_runner/worker.py +116 -0
- uncountable/integration/scan_profiles.py +67 -0
- uncountable/integration/scheduler.py +199 -0
- uncountable/integration/secret_retrieval/__init__.py +3 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
- uncountable/integration/server.py +103 -54
- uncountable/integration/telemetry.py +251 -0
- uncountable/integration/webhook_server/entrypoint.py +97 -0
- uncountable/types/__init__.py +149 -30
- uncountable/types/api/batch/execute_batch.py +16 -9
- uncountable/types/api/batch/execute_batch_load_async.py +13 -7
- uncountable/types/api/chemical/convert_chemical_formats.py +20 -8
- uncountable/types/api/condition_parameters/__init__.py +1 -0
- uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
- uncountable/types/api/entity/create_entities.py +24 -12
- uncountable/types/api/entity/create_entity.py +22 -13
- uncountable/types/api/entity/create_or_update_entity.py +48 -0
- uncountable/types/api/entity/export_entities.py +59 -0
- uncountable/types/api/entity/get_entities_data.py +18 -9
- uncountable/types/api/entity/grant_entity_permissions.py +48 -0
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +53 -14
- uncountable/types/api/entity/lock_entity.py +45 -0
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +19 -10
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +15 -8
- uncountable/types/api/entity/transition_entity_phase.py +27 -12
- uncountable/types/api/entity/unlock_entity.py +44 -0
- uncountable/types/api/equipment/__init__.py +1 -0
- uncountable/types/api/equipment/associate_equipment_input.py +43 -0
- uncountable/types/api/field_options/__init__.py +1 -0
- uncountable/types/api/field_options/upsert_field_options.py +55 -0
- uncountable/types/api/files/__init__.py +1 -0
- uncountable/types/api/files/download_file.py +77 -0
- uncountable/types/api/id_source/list_id_source.py +20 -11
- uncountable/types/api/id_source/match_id_source.py +15 -10
- uncountable/types/api/input_groups/get_input_group_names.py +16 -7
- uncountable/types/api/inputs/create_inputs.py +28 -14
- uncountable/types/api/inputs/get_input_data.py +34 -16
- uncountable/types/api/inputs/get_input_names.py +19 -10
- uncountable/types/api/inputs/get_inputs_data.py +29 -11
- uncountable/types/api/inputs/set_input_attribute_values.py +16 -10
- uncountable/types/api/inputs/set_input_category.py +44 -0
- uncountable/types/api/inputs/set_input_subcategories.py +45 -0
- uncountable/types/api/inputs/set_intermediate_type.py +50 -0
- uncountable/types/api/integrations/__init__.py +1 -0
- uncountable/types/api/integrations/publish_realtime_data.py +41 -0
- uncountable/types/api/integrations/push_notification.py +49 -0
- uncountable/types/api/integrations/register_sockets_token.py +41 -0
- uncountable/types/api/listing/__init__.py +1 -0
- uncountable/types/api/listing/fetch_listing.py +58 -0
- uncountable/types/api/material_families/__init__.py +1 -0
- uncountable/types/api/material_families/update_entity_material_families.py +47 -0
- uncountable/types/api/notebooks/__init__.py +1 -0
- uncountable/types/api/notebooks/add_notebook_content.py +119 -0
- uncountable/types/api/outputs/get_output_data.py +32 -17
- uncountable/types/api/outputs/get_output_names.py +18 -9
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +23 -11
- uncountable/types/api/permissions/set_core_permissions.py +31 -15
- uncountable/types/api/project/get_projects.py +20 -11
- uncountable/types/api/project/get_projects_data.py +23 -14
- uncountable/types/api/recipe_links/create_recipe_link.py +17 -10
- uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +19 -10
- uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +14 -7
- uncountable/types/api/recipes/associate_recipe_as_input.py +16 -8
- uncountable/types/api/recipes/associate_recipe_as_lot.py +14 -7
- uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
- uncountable/types/api/recipes/create_mix_order.py +44 -0
- uncountable/types/api/recipes/create_recipe.py +21 -14
- uncountable/types/api/recipes/create_recipes.py +25 -13
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +14 -7
- uncountable/types/api/recipes/edit_recipe_inputs.py +208 -19
- uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
- uncountable/types/api/recipes/get_curve.py +15 -9
- uncountable/types/api/recipes/get_recipe_calculations.py +17 -11
- uncountable/types/api/recipes/get_recipe_links.py +14 -8
- uncountable/types/api/recipes/get_recipe_names.py +16 -7
- uncountable/types/api/recipes/get_recipe_output_metadata.py +16 -10
- uncountable/types/api/recipes/get_recipes_data.py +96 -45
- uncountable/types/api/recipes/lock_recipes.py +64 -0
- uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
- uncountable/types/api/recipes/set_recipe_inputs.py +19 -13
- uncountable/types/api/recipes/set_recipe_metadata.py +14 -7
- uncountable/types/api/recipes/set_recipe_output_annotations.py +114 -0
- uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +40 -15
- uncountable/types/api/recipes/set_recipe_tags.py +30 -13
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +41 -0
- uncountable/types/api/recipes/unlock_recipes.py +51 -0
- uncountable/types/api/runsheet/__init__.py +1 -0
- uncountable/types/api/runsheet/complete_async_upload.py +41 -0
- uncountable/types/api/triggers/run_trigger.py +15 -8
- uncountable/types/api/uploader/__init__.py +1 -0
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +46 -0
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +8 -52
- uncountable/types/async_batch_processor.py +694 -18
- uncountable/types/async_batch_t.py +108 -0
- uncountable/types/async_jobs.py +8 -0
- uncountable/types/async_jobs_t.py +52 -0
- uncountable/types/auth_retrieval.py +11 -0
- uncountable/types/auth_retrieval_t.py +75 -0
- uncountable/types/base.py +5 -80
- uncountable/types/base_t.py +87 -0
- uncountable/types/calculations.py +3 -19
- uncountable/types/calculations_t.py +26 -0
- uncountable/types/chemical_structure.py +3 -23
- uncountable/types/chemical_structure_t.py +28 -0
- uncountable/types/client_base.py +1170 -88
- uncountable/types/client_config.py +8 -0
- uncountable/types/client_config_t.py +36 -0
- uncountable/types/curves.py +5 -43
- uncountable/types/curves_t.py +50 -0
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +8 -270
- uncountable/types/entity_t.py +446 -0
- uncountable/types/experiment_groups.py +3 -19
- uncountable/types/experiment_groups_t.py +26 -0
- uncountable/types/exports.py +8 -0
- uncountable/types/exports_t.py +34 -0
- uncountable/types/field_values.py +25 -61
- uncountable/types/field_values_t.py +302 -0
- uncountable/types/fields.py +3 -20
- uncountable/types/fields_t.py +27 -0
- uncountable/types/generic_upload.py +14 -0
- uncountable/types/generic_upload_t.py +119 -0
- uncountable/types/id_source.py +7 -45
- uncountable/types/id_source_t.py +68 -0
- uncountable/types/identifier.py +6 -50
- uncountable/types/identifier_t.py +62 -0
- uncountable/types/input_attributes.py +3 -25
- uncountable/types/input_attributes_t.py +29 -0
- uncountable/types/inputs.py +6 -57
- uncountable/types/inputs_t.py +82 -0
- uncountable/types/integration_server.py +8 -0
- uncountable/types/integration_server_t.py +46 -0
- uncountable/types/integration_session.py +10 -0
- uncountable/types/integration_session_t.py +60 -0
- uncountable/types/integrations.py +10 -0
- uncountable/types/integrations_t.py +62 -0
- uncountable/types/job_definition.py +28 -0
- uncountable/types/job_definition_t.py +285 -0
- uncountable/types/listing.py +9 -0
- uncountable/types/listing_t.py +51 -0
- uncountable/types/notices.py +8 -0
- uncountable/types/notices_t.py +37 -0
- uncountable/types/notifications.py +11 -0
- uncountable/types/notifications_t.py +74 -0
- uncountable/types/outputs.py +3 -22
- uncountable/types/outputs_t.py +29 -0
- uncountable/types/overrides.py +9 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +3 -42
- uncountable/types/permissions_t.py +45 -0
- uncountable/types/phases.py +3 -19
- uncountable/types/phases_t.py +26 -0
- uncountable/types/post_base.py +3 -26
- uncountable/types/post_base_t.py +29 -0
- uncountable/types/queued_job.py +17 -0
- uncountable/types/queued_job_t.py +140 -0
- uncountable/types/recipe_identifiers.py +7 -58
- uncountable/types/recipe_identifiers_t.py +75 -0
- uncountable/types/recipe_inputs.py +4 -26
- uncountable/types/recipe_inputs_t.py +29 -0
- uncountable/types/recipe_links.py +4 -46
- uncountable/types/recipe_links_t.py +53 -0
- uncountable/types/recipe_metadata.py +5 -48
- uncountable/types/recipe_metadata_t.py +57 -0
- uncountable/types/recipe_output_metadata.py +3 -20
- uncountable/types/recipe_output_metadata_t.py +27 -0
- uncountable/types/recipe_tags.py +3 -19
- uncountable/types/recipe_tags_t.py +26 -0
- uncountable/types/recipe_workflow_steps.py +9 -73
- uncountable/types/recipe_workflow_steps_t.py +95 -0
- uncountable/types/recipes.py +7 -0
- uncountable/types/recipes_t.py +25 -0
- uncountable/types/response.py +3 -21
- uncountable/types/response_t.py +26 -0
- uncountable/types/secret_retrieval.py +11 -0
- uncountable/types/secret_retrieval_t.py +75 -0
- uncountable/types/sockets.py +20 -0
- uncountable/types/sockets_t.py +169 -0
- uncountable/types/structured_filters.py +25 -0
- uncountable/types/structured_filters_t.py +248 -0
- uncountable/types/units.py +3 -19
- uncountable/types/units_t.py +26 -0
- uncountable/types/uploader.py +24 -0
- uncountable/types/uploader_t.py +222 -0
- uncountable/types/users.py +3 -20
- uncountable/types/users_t.py +27 -0
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +48 -0
- uncountable/types/workflows.py +4 -28
- uncountable/types/workflows_t.py +38 -0
- uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
- {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -1
- UncountablePythonSDK-0.0.24.dist-info/METADATA +0 -47
- UncountablePythonSDK-0.0.24.dist-info/RECORD +0 -216
- docs/quickstart.md +0 -19
- examples/recipe-import/importer.py +0 -39
- type_spec/external/api/batch/execute_batch.yaml +0 -56
- type_spec/external/api/batch/execute_batch_load_async.yaml +0 -18
- type_spec/external/api/chemical/convert_chemical_formats.yaml +0 -33
- type_spec/external/api/entity/create_entities.yaml +0 -45
- type_spec/external/api/entity/create_entity.yaml +0 -51
- type_spec/external/api/entity/get_entities_data.yaml +0 -29
- type_spec/external/api/entity/list_entities.yaml +0 -52
- type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
- type_spec/external/api/entity/set_values.yaml +0 -18
- type_spec/external/api/entity/transition_entity_phase.yaml +0 -44
- type_spec/external/api/id_source/list_id_source.yaml +0 -35
- type_spec/external/api/id_source/match_id_source.yaml +0 -32
- type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
- type_spec/external/api/inputs/create_inputs.yaml +0 -48
- type_spec/external/api/inputs/get_input_data.yaml +0 -95
- type_spec/external/api/inputs/get_input_names.yaml +0 -38
- type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
- type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
- type_spec/external/api/outputs/get_output_data.yaml +0 -92
- type_spec/external/api/outputs/get_output_names.yaml +0 -35
- type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
- type_spec/external/api/permissions/set_core_permissions.yaml +0 -69
- type_spec/external/api/project/get_projects.yaml +0 -42
- type_spec/external/api/project/get_projects_data.yaml +0 -50
- type_spec/external/api/recipe_links/create_recipe_link.yaml +0 -25
- type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
- type_spec/external/api/recipes/archive_recipes.yaml +0 -20
- type_spec/external/api/recipes/associate_recipe_as_input.yaml +0 -19
- type_spec/external/api/recipes/associate_recipe_as_lot.yaml +0 -19
- type_spec/external/api/recipes/create_recipe.yaml +0 -39
- type_spec/external/api/recipes/create_recipes.yaml +0 -47
- type_spec/external/api/recipes/disassociate_recipe_as_input.yaml +0 -16
- type_spec/external/api/recipes/edit_recipe_inputs.yaml +0 -85
- type_spec/external/api/recipes/get_curve.yaml +0 -21
- type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
- type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
- type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
- type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
- type_spec/external/api/recipes/get_recipes_data.yaml +0 -244
- type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -42
- type_spec/external/api/recipes/set_recipe_metadata.yaml +0 -20
- type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
- type_spec/external/api/recipes/set_recipe_tags.yaml +0 -62
- type_spec/external/api/triggers/run_trigger.yaml +0 -18
- uncountable/integration/types.py +0 -89
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from importlib import resources
|
|
3
|
+
|
|
4
|
+
from pkgs.argument_parser import CachedParser
|
|
5
|
+
from uncountable.core import environment
|
|
6
|
+
from uncountable.types import integration_server_t, job_definition_t
|
|
7
|
+
|
|
8
|
+
profile_parser = CachedParser(job_definition_t.ProfileDefinition)
|
|
9
|
+
|
|
10
|
+
_DEFAULT_PROFILE_ENV = integration_server_t.IntegrationEnvironment.PROD
|
|
11
|
+
_IGNORED_PROFILE_FOLDERS = ["__pycache__"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@functools.cache
|
|
15
|
+
def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
16
|
+
profiles_module = environment.get_profiles_module()
|
|
17
|
+
integration_envs = environment.get_integration_envs()
|
|
18
|
+
profiles = [
|
|
19
|
+
entry
|
|
20
|
+
for entry in resources.files(profiles_module).iterdir()
|
|
21
|
+
if entry.is_dir() and entry.name not in _IGNORED_PROFILE_FOLDERS
|
|
22
|
+
]
|
|
23
|
+
profile_details: list[job_definition_t.ProfileMetadata] = []
|
|
24
|
+
seen_job_ids: set[str] = set()
|
|
25
|
+
for profile_file in profiles:
|
|
26
|
+
profile_name = profile_file.name
|
|
27
|
+
try:
|
|
28
|
+
definition = profile_parser.parse_yaml_resource(
|
|
29
|
+
package=f"{profiles_module}.{profile_name}",
|
|
30
|
+
resource="profile.yaml",
|
|
31
|
+
)
|
|
32
|
+
for job in definition.jobs:
|
|
33
|
+
if job.id in seen_job_ids:
|
|
34
|
+
raise Exception(f"multiple jobs with id {job.id}")
|
|
35
|
+
seen_job_ids.add(job.id)
|
|
36
|
+
|
|
37
|
+
if definition.environments is not None:
|
|
38
|
+
for integration_env in integration_envs:
|
|
39
|
+
environment_config = definition.environments.get(integration_env)
|
|
40
|
+
if environment_config is not None:
|
|
41
|
+
profile_details.append(
|
|
42
|
+
job_definition_t.ProfileMetadata(
|
|
43
|
+
name=profile_name,
|
|
44
|
+
jobs=definition.jobs,
|
|
45
|
+
base_url=environment_config.base_url,
|
|
46
|
+
auth_retrieval=environment_config.auth_retrieval,
|
|
47
|
+
client_options=environment_config.client_options,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
elif _DEFAULT_PROFILE_ENV in integration_envs:
|
|
51
|
+
assert (
|
|
52
|
+
definition.base_url is not None
|
|
53
|
+
and definition.auth_retrieval is not None
|
|
54
|
+
), f"define environments in profile.yaml for {profile_name}"
|
|
55
|
+
profile_details.append(
|
|
56
|
+
job_definition_t.ProfileMetadata(
|
|
57
|
+
name=profile_name,
|
|
58
|
+
jobs=definition.jobs,
|
|
59
|
+
base_url=definition.base_url,
|
|
60
|
+
auth_retrieval=definition.auth_retrieval,
|
|
61
|
+
client_options=definition.client_options,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
except FileNotFoundError as e:
|
|
65
|
+
print(f"WARN: profile.yaml not found for {profile_name}", e)
|
|
66
|
+
continue
|
|
67
|
+
return profile_details
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import multiprocessing
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
|
|
10
|
+
from opentelemetry.trace import get_current_span
|
|
11
|
+
|
|
12
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
13
|
+
from uncountable.integration.entrypoint import main as cron_target
|
|
14
|
+
from uncountable.integration.queue_runner.command_server import (
|
|
15
|
+
CommandServerTimeout,
|
|
16
|
+
check_health,
|
|
17
|
+
)
|
|
18
|
+
from uncountable.integration.queue_runner.queue_runner import start_queue_runner
|
|
19
|
+
from uncountable.integration.telemetry import Logger
|
|
20
|
+
|
|
21
|
+
SHUTDOWN_TIMEOUT_SECS = 30
|
|
22
|
+
|
|
23
|
+
AnyProcess = multiprocessing.Process | subprocess.Popen[bytes]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProcessName(StrEnum):
|
|
27
|
+
QUEUE_RUNNER = "queue_runner"
|
|
28
|
+
CRON_SERVER = "cron_server"
|
|
29
|
+
UWSGI = "uwsgi"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(kw_only=True)
|
|
33
|
+
class ProcessInfo:
|
|
34
|
+
name: ProcessName
|
|
35
|
+
process: AnyProcess
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_alive(self) -> bool:
|
|
39
|
+
match self.process:
|
|
40
|
+
case multiprocessing.Process():
|
|
41
|
+
return self.process.is_alive()
|
|
42
|
+
case subprocess.Popen():
|
|
43
|
+
return self.process.poll() is None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def pid(self) -> int | None:
|
|
47
|
+
return self.process.pid
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def exitcode(self) -> int | None:
|
|
51
|
+
match self.process:
|
|
52
|
+
case multiprocessing.Process():
|
|
53
|
+
return self.process.exitcode
|
|
54
|
+
case subprocess.Popen():
|
|
55
|
+
return self.process.poll()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def handle_shutdown(logger: Logger, processes: dict[ProcessName, ProcessInfo]) -> None:
|
|
59
|
+
logger.log_info("received shutdown command, shutting down sub-processes")
|
|
60
|
+
for proc_info in processes.values():
|
|
61
|
+
if proc_info.is_alive:
|
|
62
|
+
proc_info.process.terminate()
|
|
63
|
+
|
|
64
|
+
shutdown_start = time.time()
|
|
65
|
+
still_living_processes = list(processes.values())
|
|
66
|
+
while (
|
|
67
|
+
time.time() - shutdown_start < SHUTDOWN_TIMEOUT_SECS
|
|
68
|
+
and len(still_living_processes) > 0
|
|
69
|
+
):
|
|
70
|
+
current_loop_processes = [*still_living_processes]
|
|
71
|
+
logger.log_info(
|
|
72
|
+
"waiting for sub-processes to shut down",
|
|
73
|
+
attributes={
|
|
74
|
+
"still_living_processes": [
|
|
75
|
+
proc_info.name for proc_info in still_living_processes
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
still_living_processes = []
|
|
80
|
+
for proc_info in current_loop_processes:
|
|
81
|
+
if not proc_info.is_alive:
|
|
82
|
+
logger.log_info(f"{proc_info.name} shut down successfully")
|
|
83
|
+
else:
|
|
84
|
+
still_living_processes.append(proc_info)
|
|
85
|
+
time.sleep(1)
|
|
86
|
+
|
|
87
|
+
for proc_info in still_living_processes:
|
|
88
|
+
logger.log_warning(
|
|
89
|
+
f"{proc_info.name} failed to shut down after {SHUTDOWN_TIMEOUT_SECS} seconds, forcefully terminating"
|
|
90
|
+
)
|
|
91
|
+
proc_info.process.kill()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def restart_process(
|
|
95
|
+
logger: Logger, proc_info: ProcessInfo, processes: dict[ProcessName, ProcessInfo]
|
|
96
|
+
) -> None:
|
|
97
|
+
logger.log_error(
|
|
98
|
+
f"process {proc_info.name} shut down unexpectedly - exit code {proc_info.exitcode}. Restarting..."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
match proc_info.name:
|
|
102
|
+
case ProcessName.QUEUE_RUNNER:
|
|
103
|
+
queue_proc = multiprocessing.Process(target=start_queue_runner)
|
|
104
|
+
queue_proc.start()
|
|
105
|
+
new_info = ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=queue_proc)
|
|
106
|
+
processes[ProcessName.QUEUE_RUNNER] = new_info
|
|
107
|
+
try:
|
|
108
|
+
_wait_queue_runner_online()
|
|
109
|
+
logger.log_info("queue runner restarted successfully")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.log_exception(e)
|
|
112
|
+
logger.log_error(
|
|
113
|
+
"queue runner failed to restart, shutting down scheduler"
|
|
114
|
+
)
|
|
115
|
+
handle_shutdown(logger, processes)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
case ProcessName.CRON_SERVER:
|
|
119
|
+
cron_proc = multiprocessing.Process(target=cron_target)
|
|
120
|
+
cron_proc.start()
|
|
121
|
+
new_info = ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_proc)
|
|
122
|
+
processes[ProcessName.CRON_SERVER] = new_info
|
|
123
|
+
logger.log_info("cron server restarted successfully")
|
|
124
|
+
|
|
125
|
+
case ProcessName.UWSGI:
|
|
126
|
+
uwsgi_proc: AnyProcess = subprocess.Popen(["uwsgi", "--die-on-term"])
|
|
127
|
+
new_info = ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_proc)
|
|
128
|
+
processes[ProcessName.UWSGI] = new_info
|
|
129
|
+
logger.log_info("uwsgi restarted successfully")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_process_alive(
|
|
133
|
+
logger: Logger, processes: dict[ProcessName, ProcessInfo]
|
|
134
|
+
) -> None:
|
|
135
|
+
for proc_info in processes.values():
|
|
136
|
+
if not proc_info.is_alive:
|
|
137
|
+
restart_process(logger, proc_info, processes)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _wait_queue_runner_online() -> None:
|
|
141
|
+
MAX_QUEUE_RUNNER_HEALTH_CHECKS = 10
|
|
142
|
+
QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS = 1
|
|
143
|
+
|
|
144
|
+
num_attempts = 0
|
|
145
|
+
before = datetime.datetime.now(UTC)
|
|
146
|
+
while num_attempts < MAX_QUEUE_RUNNER_HEALTH_CHECKS:
|
|
147
|
+
try:
|
|
148
|
+
if check_health(port=get_local_admin_server_port()):
|
|
149
|
+
return
|
|
150
|
+
except CommandServerTimeout:
|
|
151
|
+
pass
|
|
152
|
+
num_attempts += 1
|
|
153
|
+
time.sleep(QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS)
|
|
154
|
+
after = datetime.datetime.now(UTC)
|
|
155
|
+
duration_secs = (after - before).seconds
|
|
156
|
+
raise Exception(f"queue runner failed to come online after {duration_secs} seconds")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main() -> None:
|
|
160
|
+
logger = Logger(get_current_span())
|
|
161
|
+
processes: dict[ProcessName, ProcessInfo] = {}
|
|
162
|
+
|
|
163
|
+
multiprocessing.set_start_method("forkserver")
|
|
164
|
+
|
|
165
|
+
def add_process(process: ProcessInfo) -> None:
|
|
166
|
+
processes[process.name] = process
|
|
167
|
+
logger.log_info(f"started process {process.name}")
|
|
168
|
+
|
|
169
|
+
runner_process = multiprocessing.Process(target=start_queue_runner)
|
|
170
|
+
runner_process.start()
|
|
171
|
+
add_process(ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=runner_process))
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
_wait_queue_runner_online()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.log_exception(e)
|
|
177
|
+
handle_shutdown(logger, processes=processes)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
cron_process = multiprocessing.Process(target=cron_target)
|
|
181
|
+
cron_process.start()
|
|
182
|
+
add_process(ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_process))
|
|
183
|
+
|
|
184
|
+
uwsgi_process = subprocess.Popen([
|
|
185
|
+
"uwsgi",
|
|
186
|
+
"--die-on-term",
|
|
187
|
+
])
|
|
188
|
+
add_process(ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_process))
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
while True:
|
|
192
|
+
check_process_alive(logger, processes=processes)
|
|
193
|
+
time.sleep(1)
|
|
194
|
+
except KeyboardInterrupt:
|
|
195
|
+
handle_shutdown(logger, processes=processes)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import functools
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
|
|
8
|
+
from pkgs.argument_parser import CachedParser
|
|
9
|
+
from uncountable.types import overrides_t
|
|
10
|
+
from uncountable.types.job_definition_t import ProfileMetadata
|
|
11
|
+
from uncountable.types.secret_retrieval_t import (
|
|
12
|
+
SecretRetrieval,
|
|
13
|
+
SecretRetrievalAWS,
|
|
14
|
+
SecretRetrievalEnv,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SecretRetrievalError(Exception):
|
|
19
|
+
def __init__(
|
|
20
|
+
self, secret_retrieval: SecretRetrieval, message: str | None = None
|
|
21
|
+
) -> None:
|
|
22
|
+
self.secret_retrieval = secret_retrieval
|
|
23
|
+
self.message = message
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
append_message = ""
|
|
27
|
+
if self.message is not None:
|
|
28
|
+
append_message = f": {self.message}"
|
|
29
|
+
return f"{self.secret_retrieval.type} secret retrieval failed{append_message}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@functools.cache
|
|
33
|
+
def _get_aws_secret(*, secret_name: str, region_name: str, sub_key: str | None) -> str:
|
|
34
|
+
client = boto3.client("secretsmanager", region_name=region_name)
|
|
35
|
+
response = client.get_secret_value(SecretId=secret_name)
|
|
36
|
+
|
|
37
|
+
if "SecretString" in response:
|
|
38
|
+
secret = response["SecretString"]
|
|
39
|
+
else:
|
|
40
|
+
secret = base64.b64decode(response["SecretBinary"])
|
|
41
|
+
|
|
42
|
+
value = json.loads(secret)
|
|
43
|
+
|
|
44
|
+
if sub_key is not None:
|
|
45
|
+
assert isinstance(value, dict)
|
|
46
|
+
return str(value[sub_key])
|
|
47
|
+
else:
|
|
48
|
+
return str(value)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@functools.cache
|
|
52
|
+
def _load_secret_overrides(profile_name: str) -> dict[SecretRetrieval, str]:
|
|
53
|
+
overrides_parser = CachedParser(overrides_t.Overrides)
|
|
54
|
+
profiles_module = os.environ["UNC_PROFILES_MODULE"]
|
|
55
|
+
try:
|
|
56
|
+
overrides = overrides_parser.parse_yaml_resource(
|
|
57
|
+
package=f"{profiles_module}.{profile_name}",
|
|
58
|
+
resource="local_overrides.yaml",
|
|
59
|
+
)
|
|
60
|
+
return {
|
|
61
|
+
override.secret_retrieval: override.value for override in overrides.secrets
|
|
62
|
+
}
|
|
63
|
+
except FileNotFoundError:
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def retrieve_secret(
|
|
68
|
+
secret_retrieval: SecretRetrieval, profile_metadata: ProfileMetadata
|
|
69
|
+
) -> str:
|
|
70
|
+
value_from_override = _load_secret_overrides(profile_metadata.name).get(
|
|
71
|
+
secret_retrieval
|
|
72
|
+
)
|
|
73
|
+
if value_from_override is not None:
|
|
74
|
+
return value_from_override
|
|
75
|
+
|
|
76
|
+
match secret_retrieval:
|
|
77
|
+
case SecretRetrievalEnv():
|
|
78
|
+
env_name = f"UNC_{profile_metadata.name.upper()}_{secret_retrieval.env_key.upper()}"
|
|
79
|
+
secret = os.environ.get(env_name)
|
|
80
|
+
if secret is None:
|
|
81
|
+
raise SecretRetrievalError(
|
|
82
|
+
secret_retrieval, f"environment variable {env_name} missing"
|
|
83
|
+
)
|
|
84
|
+
return secret
|
|
85
|
+
case SecretRetrievalAWS():
|
|
86
|
+
try:
|
|
87
|
+
return _get_aws_secret(
|
|
88
|
+
secret_name=secret_retrieval.secret_name,
|
|
89
|
+
region_name=secret_retrieval.region,
|
|
90
|
+
sub_key=secret_retrieval.sub_key,
|
|
91
|
+
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise SecretRetrievalError(secret_retrieval) from e
|
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
import signal
|
|
2
|
-
from types import TracebackType
|
|
3
2
|
from dataclasses import asdict
|
|
4
|
-
from
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import assert_never
|
|
5
|
+
|
|
6
|
+
from apscheduler.executors.pool import ThreadPoolExecutor
|
|
7
|
+
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
5
8
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
6
9
|
from apscheduler.schedulers.base import BaseScheduler
|
|
7
|
-
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
8
|
-
from apscheduler.executors.pool import ThreadPoolExecutor
|
|
9
|
-
from uncountable.integration.cron import CronJobArgs, cron_job_executor
|
|
10
10
|
from apscheduler.triggers.cron import CronTrigger
|
|
11
|
+
from opentelemetry.trace import get_current_span
|
|
11
12
|
from sqlalchemy.engine.base import Engine
|
|
12
13
|
|
|
13
|
-
from uncountable.
|
|
14
|
-
|
|
14
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
15
|
+
from uncountable.integration.cron import CronJobArgs, cron_job_executor
|
|
16
|
+
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
17
|
+
send_vaccuum_queued_jobs_message,
|
|
18
|
+
)
|
|
19
|
+
from uncountable.integration.telemetry import Logger
|
|
20
|
+
from uncountable.types import base_t, job_definition_t
|
|
21
|
+
from uncountable.types.job_definition_t import (
|
|
15
22
|
CronJobDefinition,
|
|
16
|
-
|
|
17
|
-
ProfileMetadata,
|
|
23
|
+
HttpJobDefinitionBase,
|
|
18
24
|
)
|
|
19
25
|
|
|
20
|
-
|
|
21
26
|
_MAX_APSCHEDULER_CONCURRENT_JOBS = 1
|
|
22
27
|
|
|
28
|
+
VACCUUM_QUEUED_JOBS_JOB_ID = "vacuum_queued_jobs"
|
|
29
|
+
|
|
30
|
+
STATIC_JOB_IDS = {VACCUUM_QUEUED_JOBS_JOB_ID}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def vaccuum_queued_jobs() -> None:
|
|
34
|
+
send_vaccuum_queued_jobs_message(port=get_local_admin_server_port())
|
|
35
|
+
|
|
23
36
|
|
|
24
37
|
class IntegrationServer:
|
|
25
38
|
_scheduler: BaseScheduler
|
|
26
39
|
_engine: Engine
|
|
40
|
+
_server_logger: Logger
|
|
27
41
|
|
|
28
42
|
def __init__(self, engine: Engine) -> None:
|
|
29
43
|
self._engine = engine
|
|
@@ -32,49 +46,83 @@ class IntegrationServer:
|
|
|
32
46
|
jobstores={"default": SQLAlchemyJobStore(engine=engine)},
|
|
33
47
|
executors={"default": ThreadPoolExecutor(_MAX_APSCHEDULER_CONCURRENT_JOBS)},
|
|
34
48
|
)
|
|
49
|
+
self._server_logger = Logger(get_current_span())
|
|
35
50
|
|
|
36
|
-
def
|
|
37
|
-
self
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
else:
|
|
66
|
-
self._scheduler.add_job(
|
|
67
|
-
cron_job_executor,
|
|
68
|
-
# IMPROVE: reconsider these defaults
|
|
69
|
-
max_instances=1,
|
|
70
|
-
coalesce=True,
|
|
71
|
-
trigger=CronTrigger.from_crontab(job_defn.cron_spec),
|
|
72
|
-
name=job_defn.name,
|
|
73
|
-
id=job_defn.id,
|
|
74
|
-
kwargs=job_kwargs,
|
|
51
|
+
def _register_static_jobs(self) -> None:
|
|
52
|
+
all_job_ids = {job.id for job in self._scheduler.get_jobs()}
|
|
53
|
+
if VACCUUM_QUEUED_JOBS_JOB_ID in all_job_ids:
|
|
54
|
+
self._scheduler.remove_job(VACCUUM_QUEUED_JOBS_JOB_ID)
|
|
55
|
+
|
|
56
|
+
self._scheduler.add_job(
|
|
57
|
+
vaccuum_queued_jobs,
|
|
58
|
+
max_instances=1,
|
|
59
|
+
coalesce=True,
|
|
60
|
+
trigger=CronTrigger.from_crontab("5 4 * * 4"),
|
|
61
|
+
name="Vaccuum queued jobs",
|
|
62
|
+
id=VACCUUM_QUEUED_JOBS_JOB_ID,
|
|
63
|
+
kwargs={},
|
|
64
|
+
misfire_grace_time=None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def register_jobs(self, profiles: list[job_definition_t.ProfileMetadata]) -> None:
|
|
68
|
+
valid_job_ids: set[str] = set()
|
|
69
|
+
for profile_metadata in profiles:
|
|
70
|
+
for job_defn in profile_metadata.jobs:
|
|
71
|
+
valid_job_ids.add(job_defn.id)
|
|
72
|
+
match job_defn:
|
|
73
|
+
case CronJobDefinition():
|
|
74
|
+
# Add to ap scheduler
|
|
75
|
+
job_kwargs = asdict(
|
|
76
|
+
CronJobArgs(
|
|
77
|
+
definition=job_defn, profile_metadata=profile_metadata
|
|
78
|
+
)
|
|
75
79
|
)
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
try:
|
|
81
|
+
existing_job = self._scheduler.get_job(job_defn.id)
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
self._server_logger.log_warning(
|
|
84
|
+
f"could not reconstitute job {job_defn.id}: {e}"
|
|
85
|
+
)
|
|
86
|
+
self._scheduler.remove_job(job_defn.id)
|
|
87
|
+
existing_job = None
|
|
88
|
+
if existing_job is not None:
|
|
89
|
+
existing_job.modify(
|
|
90
|
+
name=job_defn.name,
|
|
91
|
+
kwargs=job_kwargs,
|
|
92
|
+
misfire_grace_time=None,
|
|
93
|
+
)
|
|
94
|
+
existing_job.reschedule(
|
|
95
|
+
CronTrigger.from_crontab(job_defn.cron_spec)
|
|
96
|
+
)
|
|
97
|
+
if not job_defn.enabled:
|
|
98
|
+
existing_job.pause()
|
|
99
|
+
else:
|
|
100
|
+
existing_job.resume()
|
|
101
|
+
else:
|
|
102
|
+
job_opts: dict[str, base_t.JsonValue] = {}
|
|
103
|
+
if not job_defn.enabled:
|
|
104
|
+
job_opts["next_run_time"] = None
|
|
105
|
+
self._scheduler.add_job(
|
|
106
|
+
cron_job_executor,
|
|
107
|
+
# IMPROVE: reconsider these defaults
|
|
108
|
+
max_instances=1,
|
|
109
|
+
coalesce=True,
|
|
110
|
+
trigger=CronTrigger.from_crontab(job_defn.cron_spec),
|
|
111
|
+
name=job_defn.name,
|
|
112
|
+
id=job_defn.id,
|
|
113
|
+
kwargs=job_kwargs,
|
|
114
|
+
misfire_grace_time=None,
|
|
115
|
+
**job_opts,
|
|
116
|
+
)
|
|
117
|
+
case HttpJobDefinitionBase():
|
|
118
|
+
pass
|
|
119
|
+
case _:
|
|
120
|
+
assert_never(job_defn)
|
|
121
|
+
all_job_ids = {job.id for job in self._scheduler.get_jobs()}
|
|
122
|
+
invalid_job_ids = all_job_ids.difference(valid_job_ids.union(STATIC_JOB_IDS))
|
|
123
|
+
|
|
124
|
+
for job_id in invalid_job_ids:
|
|
125
|
+
self._scheduler.remove_job(job_id)
|
|
78
126
|
|
|
79
127
|
def serve_forever(self) -> None:
|
|
80
128
|
signal.pause()
|
|
@@ -87,12 +135,13 @@ class IntegrationServer:
|
|
|
87
135
|
|
|
88
136
|
def __enter__(self) -> "IntegrationServer":
|
|
89
137
|
self._start_apscheduler()
|
|
138
|
+
self._register_static_jobs()
|
|
90
139
|
return self
|
|
91
140
|
|
|
92
141
|
def __exit__(
|
|
93
142
|
self,
|
|
94
|
-
exc_type:
|
|
95
|
-
exc_val:
|
|
96
|
-
exc_tb:
|
|
143
|
+
exc_type: type[BaseException] | None,
|
|
144
|
+
exc_val: BaseException | None,
|
|
145
|
+
exc_tb: TracebackType | None,
|
|
97
146
|
) -> None:
|
|
98
147
|
self._stop_apscheduler()
|