UncountablePythonSDK 0.0.7__py3-none-any.whl → 0.0.92__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.
- UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
- UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
- {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
- {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
- docs/.gitignore +1 -0
- docs/conf.py +57 -0
- docs/index.md +13 -0
- docs/justfile +12 -0
- docs/quickstart.md +19 -0
- docs/requirements.txt +7 -0
- docs/static/favicons/android-chrome-192x192.png +0 -0
- docs/static/favicons/android-chrome-512x512.png +0 -0
- docs/static/favicons/apple-touch-icon.png +0 -0
- docs/static/favicons/browserconfig.xml +9 -0
- docs/static/favicons/favicon-16x16.png +0 -0
- docs/static/favicons/favicon-32x32.png +0 -0
- docs/static/favicons/manifest.json +18 -0
- docs/static/favicons/mstile-150x150.png +0 -0
- docs/static/favicons/safari-pinned-tab.svg +32 -0
- docs/static/logo_blue.png +0 -0
- examples/async_batch.py +35 -0
- examples/create_entity.py +22 -17
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +50 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
- examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
- examples/integration-server/pyproject.toml +224 -0
- examples/invoke_uploader.py +26 -0
- examples/set_recipe_metadata_file.py +40 -0
- examples/set_recipe_output_file_sdk.py +26 -0
- examples/upload_files.py +18 -0
- pkgs/argument_parser/__init__.py +5 -0
- pkgs/argument_parser/_is_enum.py +1 -6
- pkgs/argument_parser/argument_parser.py +232 -76
- pkgs/argument_parser/case_convert.py +4 -3
- pkgs/filesystem_utils/__init__.py +20 -0
- pkgs/filesystem_utils/_blob_session.py +137 -0
- pkgs/filesystem_utils/_gdrive_session.py +309 -0
- pkgs/filesystem_utils/_local_session.py +69 -0
- pkgs/filesystem_utils/_s3_session.py +117 -0
- pkgs/filesystem_utils/_sftp_session.py +147 -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/opaque_key.py +1 -1
- pkgs/serialization/serial_alias.py +47 -0
- pkgs/serialization/serial_class.py +65 -50
- 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/_get_type_for_serialization.py +1 -3
- pkgs/serialization_util/convert_to_snakecase.py +27 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +118 -73
- pkgs/strenum_compat/strenum_compat.py +1 -9
- pkgs/type_spec/actions_registry/__init__.py +0 -0
- pkgs/type_spec/actions_registry/__main__.py +126 -0
- pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
- pkgs/type_spec/builder.py +475 -89
- pkgs/type_spec/config.py +24 -19
- pkgs/type_spec/emit_io_ts.py +5 -2
- pkgs/type_spec/emit_open_api.py +266 -32
- pkgs/type_spec/emit_open_api_util.py +32 -13
- pkgs/type_spec/emit_python.py +601 -150
- pkgs/type_spec/emit_typescript.py +74 -273
- pkgs/type_spec/emit_typescript_util.py +239 -5
- pkgs/type_spec/load_types.py +55 -10
- pkgs/type_spec/open_api_util.py +30 -41
- pkgs/type_spec/parts/base.py.prepart +4 -3
- pkgs/type_spec/type_info/emit_type_info.py +178 -16
- pkgs/type_spec/util.py +11 -11
- pkgs/type_spec/value_spec/__main__.py +3 -3
- pkgs/type_spec/value_spec/convert_type.py +8 -1
- pkgs/type_spec/value_spec/emit_python.py +13 -4
- uncountable/__init__.py +1 -2
- uncountable/core/__init__.py +12 -2
- uncountable/core/async_batch.py +37 -0
- uncountable/core/client.py +293 -43
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +135 -0
- uncountable/core/types.py +17 -0
- uncountable/integration/__init__.py +0 -0
- uncountable/integration/cli.py +49 -0
- uncountable/integration/construct_client.py +51 -0
- uncountable/integration/cron.py +29 -0
- uncountable/integration/db/__init__.py +0 -0
- uncountable/integration/db/connect.py +18 -0
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +13 -0
- uncountable/integration/executors/__init__.py +0 -0
- uncountable/integration/executors/executors.py +148 -0
- uncountable/integration/executors/generic_upload_executor.py +284 -0
- uncountable/integration/executors/script_executor.py +25 -0
- uncountable/integration/job.py +87 -0
- uncountable/integration/queue_runner/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/__init__.py +24 -0
- uncountable/integration/queue_runner/command_server/command_client.py +68 -0
- uncountable/integration/queue_runner/command_server/command_server.py +64 -0
- uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
- uncountable/integration/queue_runner/command_server/types.py +52 -0
- uncountable/integration/queue_runner/datastore/__init__.py +3 -0
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
- uncountable/integration/queue_runner/datastore/interface.py +19 -0
- uncountable/integration/queue_runner/datastore/model.py +17 -0
- uncountable/integration/queue_runner/job_scheduler.py +163 -0
- uncountable/integration/queue_runner/queue_runner.py +26 -0
- uncountable/integration/queue_runner/types.py +7 -0
- uncountable/integration/queue_runner/worker.py +119 -0
- uncountable/integration/scan_profiles.py +67 -0
- uncountable/integration/scheduler.py +150 -0
- uncountable/integration/secret_retrieval/__init__.py +3 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
- uncountable/integration/server.py +117 -0
- uncountable/integration/telemetry.py +209 -0
- uncountable/integration/webhook_server/entrypoint.py +170 -0
- uncountable/types/__init__.py +151 -5
- uncountable/types/api/batch/execute_batch.py +15 -7
- uncountable/types/api/batch/execute_batch_load_async.py +42 -0
- uncountable/types/api/chemical/__init__.py +1 -0
- uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
- uncountable/types/api/entity/create_entities.py +23 -10
- uncountable/types/api/entity/create_entity.py +21 -12
- uncountable/types/api/entity/get_entities_data.py +19 -29
- uncountable/types/api/entity/grant_entity_permissions.py +48 -0
- uncountable/types/api/entity/list_entities.py +28 -20
- uncountable/types/api/entity/lock_entity.py +45 -0
- uncountable/types/api/entity/resolve_entity_ids.py +19 -7
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +13 -28
- uncountable/types/api/entity/transition_entity_phase.py +80 -0
- 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 +44 -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/__init__.py +1 -0
- uncountable/types/api/id_source/list_id_source.py +56 -0
- uncountable/types/api/id_source/match_id_source.py +54 -0
- uncountable/types/api/input_groups/get_input_group_names.py +18 -7
- uncountable/types/api/inputs/create_inputs.py +25 -24
- uncountable/types/api/inputs/get_input_data.py +37 -31
- uncountable/types/api/inputs/get_input_names.py +20 -9
- uncountable/types/api/inputs/get_inputs_data.py +33 -27
- uncountable/types/api/inputs/set_input_attribute_values.py +18 -13
- 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/material_families/__init__.py +1 -0
- uncountable/types/api/material_families/update_entity_material_families.py +48 -0
- uncountable/types/api/outputs/get_output_data.py +38 -29
- uncountable/types/api/outputs/get_output_names.py +20 -9
- uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
- uncountable/types/api/permissions/__init__.py +1 -0
- uncountable/types/api/permissions/set_core_permissions.py +105 -0
- uncountable/types/api/project/get_projects.py +23 -19
- uncountable/types/api/project/get_projects_data.py +26 -43
- uncountable/types/api/recipe_links/__init__.py +1 -0
- uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
- uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +21 -10
- uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
- uncountable/types/api/recipes/archive_recipes.py +42 -0
- uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
- uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
- uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
- uncountable/types/api/recipes/create_recipe.py +51 -0
- uncountable/types/api/recipes/create_recipes.py +25 -24
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
- uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
- uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
- uncountable/types/api/recipes/get_curve.py +13 -27
- uncountable/types/api/recipes/get_recipe_calculations.py +21 -21
- uncountable/types/api/recipes/get_recipe_links.py +14 -6
- uncountable/types/api/recipes/get_recipe_names.py +18 -7
- uncountable/types/api/recipes/get_recipe_output_metadata.py +18 -19
- uncountable/types/api/recipes/get_recipes_data.py +83 -144
- uncountable/types/api/recipes/lock_recipes.py +63 -0
- uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
- uncountable/types/api/recipes/set_recipe_inputs.py +21 -11
- uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
- uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
- uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +28 -15
- uncountable/types/api/recipes/set_recipe_tags.py +109 -0
- uncountable/types/api/recipes/unarchive_recipes.py +41 -0
- uncountable/types/api/recipes/unlock_recipes.py +50 -0
- uncountable/types/api/triggers/__init__.py +1 -0
- uncountable/types/api/triggers/run_trigger.py +43 -0
- uncountable/types/api/uploader/__init__.py +1 -0
- uncountable/types/api/uploader/invoke_uploader.py +47 -0
- uncountable/types/async_batch.py +13 -0
- uncountable/types/async_batch_processor.py +384 -0
- uncountable/types/async_batch_t.py +97 -0
- uncountable/types/async_jobs.py +9 -0
- uncountable/types/async_jobs_t.py +53 -0
- uncountable/types/auth_retrieval.py +12 -0
- uncountable/types/auth_retrieval_t.py +75 -0
- uncountable/types/base.py +5 -78
- uncountable/types/base_t.py +85 -0
- uncountable/types/calculations.py +8 -0
- uncountable/types/calculations_t.py +27 -0
- uncountable/types/chemical_structure.py +8 -0
- uncountable/types/chemical_structure_t.py +28 -0
- uncountable/types/client_base.py +1115 -76
- uncountable/types/client_config.py +8 -0
- uncountable/types/client_config_t.py +26 -0
- uncountable/types/curves.py +10 -0
- uncountable/types/curves_t.py +51 -0
- uncountable/types/entity.py +8 -266
- uncountable/types/entity_t.py +393 -0
- uncountable/types/experiment_groups.py +8 -0
- uncountable/types/experiment_groups_t.py +27 -0
- uncountable/types/field_values.py +17 -23
- uncountable/types/field_values_t.py +204 -0
- uncountable/types/fields.py +8 -0
- uncountable/types/fields_t.py +28 -0
- uncountable/types/generic_upload.py +15 -0
- uncountable/types/generic_upload_t.py +119 -0
- uncountable/types/id_source.py +12 -0
- uncountable/types/id_source_t.py +68 -0
- uncountable/types/identifier.py +11 -0
- uncountable/types/identifier_t.py +63 -0
- uncountable/types/input_attributes.py +8 -0
- uncountable/types/input_attributes_t.py +30 -0
- uncountable/types/inputs.py +11 -0
- uncountable/types/inputs_t.py +83 -0
- uncountable/types/integration_server.py +9 -0
- uncountable/types/integration_server_t.py +42 -0
- uncountable/types/job_definition.py +27 -0
- uncountable/types/job_definition_t.py +260 -0
- uncountable/types/outputs.py +8 -0
- uncountable/types/outputs_t.py +30 -0
- uncountable/types/overrides.py +10 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +8 -0
- uncountable/types/permissions_t.py +46 -0
- uncountable/types/phases.py +8 -0
- uncountable/types/phases_t.py +27 -0
- uncountable/types/post_base.py +8 -0
- uncountable/types/post_base_t.py +30 -0
- uncountable/types/queued_job.py +16 -0
- uncountable/types/queued_job_t.py +123 -0
- uncountable/types/recipe_identifiers.py +12 -0
- uncountable/types/recipe_identifiers_t.py +76 -0
- uncountable/types/recipe_inputs.py +9 -0
- uncountable/types/recipe_inputs_t.py +30 -0
- uncountable/types/recipe_links.py +4 -44
- uncountable/types/recipe_links_t.py +54 -0
- uncountable/types/recipe_metadata.py +10 -0
- uncountable/types/recipe_metadata_t.py +58 -0
- uncountable/types/recipe_output_metadata.py +8 -0
- uncountable/types/recipe_output_metadata_t.py +28 -0
- uncountable/types/recipe_tags.py +8 -0
- uncountable/types/recipe_tags_t.py +27 -0
- uncountable/types/recipe_workflow_steps.py +14 -0
- uncountable/types/recipe_workflow_steps_t.py +95 -0
- uncountable/types/recipes.py +8 -0
- uncountable/types/recipes_t.py +25 -0
- uncountable/types/response.py +8 -0
- uncountable/types/response_t.py +26 -0
- uncountable/types/secret_retrieval.py +12 -0
- uncountable/types/secret_retrieval_t.py +75 -0
- uncountable/types/units.py +8 -0
- uncountable/types/units_t.py +27 -0
- uncountable/types/users.py +8 -0
- uncountable/types/users_t.py +28 -0
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +37 -0
- uncountable/types/workflows.py +9 -0
- uncountable/types/workflows_t.py +39 -0
- UncountablePythonSDK-0.0.7.dist-info/METADATA +0 -27
- UncountablePythonSDK-0.0.7.dist-info/RECORD +0 -119
- examples/recipe-import/importer.py +0 -39
- type_spec/external/api/batch/execute_batch.yaml +0 -56
- type_spec/external/api/entity/create_entities.yaml +0 -33
- type_spec/external/api/entity/create_entity.yaml +0 -39
- type_spec/external/api/entity/get_entities_data.yaml +0 -55
- type_spec/external/api/entity/list_entities.yaml +0 -62
- type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
- type_spec/external/api/entity/set_values.yaml +0 -45
- type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
- type_spec/external/api/inputs/create_inputs.yaml +0 -61
- type_spec/external/api/inputs/get_input_data.yaml +0 -108
- type_spec/external/api/inputs/get_input_names.yaml +0 -38
- type_spec/external/api/inputs/get_inputs_data.yaml +0 -95
- type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -37
- type_spec/external/api/outputs/get_output_data.yaml +0 -103
- 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/project/get_projects.yaml +0 -52
- type_spec/external/api/project/get_projects_data.yaml +0 -86
- type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
- type_spec/external/api/recipes/create_recipes.yaml +0 -60
- type_spec/external/api/recipes/get_curve.yaml +0 -50
- type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -49
- 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 -49
- type_spec/external/api/recipes/get_recipes_data.yaml +0 -372
- type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
- type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -56
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from uncountable.core import AuthDetailsApiKey, Client
|
|
2
|
+
from uncountable.core.client import ClientConfig
|
|
3
|
+
from uncountable.core.types import AuthDetailsAll, AuthDetailsOAuth
|
|
4
|
+
from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
|
|
5
|
+
from uncountable.integration.telemetry import JobLogger
|
|
6
|
+
from uncountable.types import auth_retrieval_t
|
|
7
|
+
from uncountable.types.job_definition_t import (
|
|
8
|
+
ProfileMetadata,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _construct_auth_details(profile_meta: ProfileMetadata) -> AuthDetailsAll:
|
|
13
|
+
match profile_meta.auth_retrieval:
|
|
14
|
+
case auth_retrieval_t.AuthRetrievalOAuth():
|
|
15
|
+
refresh_token = retrieve_secret(
|
|
16
|
+
profile_meta.auth_retrieval.refresh_token_secret,
|
|
17
|
+
profile_metadata=profile_meta,
|
|
18
|
+
)
|
|
19
|
+
return AuthDetailsOAuth(refresh_token=refresh_token)
|
|
20
|
+
case auth_retrieval_t.AuthRetrievalBasic():
|
|
21
|
+
api_id = retrieve_secret(
|
|
22
|
+
profile_meta.auth_retrieval.api_id_secret, profile_metadata=profile_meta
|
|
23
|
+
)
|
|
24
|
+
api_key = retrieve_secret(
|
|
25
|
+
profile_meta.auth_retrieval.api_key_secret,
|
|
26
|
+
profile_metadata=profile_meta,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return AuthDetailsApiKey(api_id=api_id, api_secret_key=api_key)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _construct_client_config(
|
|
33
|
+
profile_meta: ProfileMetadata, job_logger: JobLogger
|
|
34
|
+
) -> ClientConfig | None:
|
|
35
|
+
if profile_meta.client_options is None:
|
|
36
|
+
return None
|
|
37
|
+
return ClientConfig(
|
|
38
|
+
allow_insecure_tls=profile_meta.client_options.allow_insecure_tls,
|
|
39
|
+
extra_headers=profile_meta.client_options.extra_headers,
|
|
40
|
+
logger=job_logger,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def construct_uncountable_client(
|
|
45
|
+
profile_meta: ProfileMetadata, logger: JobLogger
|
|
46
|
+
) -> Client:
|
|
47
|
+
return Client(
|
|
48
|
+
base_url=profile_meta.base_url,
|
|
49
|
+
auth_details=_construct_auth_details(profile_meta),
|
|
50
|
+
config=_construct_client_config(profile_meta, logger),
|
|
51
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from pkgs.argument_parser import CachedParser
|
|
4
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
5
|
+
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
6
|
+
send_job_queue_message,
|
|
7
|
+
)
|
|
8
|
+
from uncountable.types import queued_job_t
|
|
9
|
+
from uncountable.types.job_definition_t import JobDefinition, ProfileMetadata
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CronJobArgs:
|
|
14
|
+
definition: JobDefinition
|
|
15
|
+
profile_metadata: ProfileMetadata
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
cron_args_parser = CachedParser(CronJobArgs)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cron_job_executor(**kwargs: dict) -> None:
|
|
22
|
+
args_passed = cron_args_parser.parse_storage(kwargs)
|
|
23
|
+
send_job_queue_message(
|
|
24
|
+
job_ref_name=args_passed.definition.id,
|
|
25
|
+
payload=queued_job_t.QueuedJobPayload(
|
|
26
|
+
invocation_context=queued_job_t.InvocationContextCron()
|
|
27
|
+
),
|
|
28
|
+
port=get_local_admin_server_port(),
|
|
29
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
from sqlalchemy.engine.base import Engine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IntegrationDBService(StrEnum):
|
|
9
|
+
CRON = "cron"
|
|
10
|
+
RUNNER = "runner"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_db_engine(service: IntegrationDBService) -> Engine:
|
|
14
|
+
match service:
|
|
15
|
+
case IntegrationDBService.CRON:
|
|
16
|
+
return create_engine(os.environ["UNC_CRON_SQLITE_URI"])
|
|
17
|
+
case IntegrationDBService.RUNNER:
|
|
18
|
+
return create_engine(os.environ["UNC_RUNNER_SQLITE_URI"])
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from contextlib import _GeneratorContextManager, contextmanager
|
|
2
|
+
from typing import Callable, Generator
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.engine import Engine
|
|
5
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
6
|
+
|
|
7
|
+
DBSessionMaker = Callable[[], _GeneratorContextManager[Session]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_session_maker(engine: Engine) -> DBSessionMaker:
|
|
11
|
+
session_maker = sessionmaker(bind=engine)
|
|
12
|
+
|
|
13
|
+
@contextmanager
|
|
14
|
+
def session_manager() -> Generator[Session, None, None]:
|
|
15
|
+
session = session_maker()
|
|
16
|
+
try:
|
|
17
|
+
yield session
|
|
18
|
+
session.commit()
|
|
19
|
+
except Exception:
|
|
20
|
+
session.rollback()
|
|
21
|
+
raise
|
|
22
|
+
finally:
|
|
23
|
+
session.close()
|
|
24
|
+
|
|
25
|
+
return session_manager
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
|
|
2
|
+
from uncountable.integration.scan_profiles import load_profiles
|
|
3
|
+
from uncountable.integration.server import IntegrationServer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
with IntegrationServer(create_db_engine(IntegrationDBService.CRON)) as server:
|
|
8
|
+
server.register_jobs(load_profiles())
|
|
9
|
+
server.serve_forever()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from typing import assert_never
|
|
2
|
+
|
|
3
|
+
from uncountable.core.client import Client
|
|
4
|
+
from uncountable.integration.executors.generic_upload_executor import GenericUploadJob
|
|
5
|
+
from uncountable.integration.executors.script_executor import resolve_script_executor
|
|
6
|
+
from uncountable.integration.job import Job, JobArguments
|
|
7
|
+
from uncountable.types import (
|
|
8
|
+
async_jobs_t,
|
|
9
|
+
entity_t,
|
|
10
|
+
field_values_t,
|
|
11
|
+
identifier_t,
|
|
12
|
+
integration_server_t,
|
|
13
|
+
job_definition_t,
|
|
14
|
+
transition_entity_phase_t,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_executor(
|
|
19
|
+
job_executor: job_definition_t.JobExecutor,
|
|
20
|
+
profile_metadata: job_definition_t.ProfileMetadata,
|
|
21
|
+
) -> Job:
|
|
22
|
+
match job_executor:
|
|
23
|
+
case job_definition_t.JobExecutorScript():
|
|
24
|
+
return resolve_script_executor(
|
|
25
|
+
job_executor, profile_metadata=profile_metadata
|
|
26
|
+
)
|
|
27
|
+
case job_definition_t.JobExecutorGenericUpload():
|
|
28
|
+
return GenericUploadJob(
|
|
29
|
+
remote_directories=job_executor.remote_directories,
|
|
30
|
+
upload_strategy=job_executor.upload_strategy,
|
|
31
|
+
data_source=job_executor.data_source,
|
|
32
|
+
)
|
|
33
|
+
assert_never(job_executor)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _create_run_entity(
|
|
37
|
+
*,
|
|
38
|
+
client: Client,
|
|
39
|
+
logging_settings: job_definition_t.JobLoggingSettings,
|
|
40
|
+
job_uuid: str,
|
|
41
|
+
) -> entity_t.Entity:
|
|
42
|
+
run_entity = client.create_entity(
|
|
43
|
+
entity_type=entity_t.EntityType.ASYNC_JOB,
|
|
44
|
+
definition_key=identifier_t.IdentifierKeyRefName(
|
|
45
|
+
ref_name="unc_integration_server_run_definition"
|
|
46
|
+
),
|
|
47
|
+
field_values=[
|
|
48
|
+
field_values_t.FieldRefNameValue(
|
|
49
|
+
field_ref_name=async_jobs_t.ASYNC_JOB_TYPE_FIELD_REF_NAME,
|
|
50
|
+
value=async_jobs_t.AsyncJobType.INTEGRATION_SERVER_RUN,
|
|
51
|
+
),
|
|
52
|
+
field_values_t.FieldRefNameValue(
|
|
53
|
+
field_ref_name=async_jobs_t.ASYNC_JOB_STATUS_FIELD_REF_NAME,
|
|
54
|
+
value=async_jobs_t.AsyncJobStatus.IN_PROGRESS,
|
|
55
|
+
),
|
|
56
|
+
field_values_t.FieldRefNameValue(
|
|
57
|
+
field_ref_name=integration_server_t.INTEGRATION_SERVER_RUN_UUID_FIELD_REF_NAME,
|
|
58
|
+
value=job_uuid,
|
|
59
|
+
),
|
|
60
|
+
],
|
|
61
|
+
).entity
|
|
62
|
+
client.transition_entity_phase(
|
|
63
|
+
entity=run_entity,
|
|
64
|
+
transition=transition_entity_phase_t.TransitionIdentifierPhases(
|
|
65
|
+
phase_from_key=identifier_t.IdentifierKeyRefName(
|
|
66
|
+
ref_name="unc_integration_server_run__queued"
|
|
67
|
+
),
|
|
68
|
+
phase_to_key=identifier_t.IdentifierKeyRefName(
|
|
69
|
+
ref_name="unc_integration_server_run__started"
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
if logging_settings.share_with_user_groups is not None:
|
|
74
|
+
client.grant_entity_permissions(
|
|
75
|
+
entity_type=entity_t.EntityType.ASYNC_JOB,
|
|
76
|
+
entity_key=identifier_t.IdentifierKeyId(id=run_entity.id),
|
|
77
|
+
permission_types=[
|
|
78
|
+
entity_t.EntityPermissionType.READ,
|
|
79
|
+
entity_t.EntityPermissionType.WRITE,
|
|
80
|
+
],
|
|
81
|
+
user_group_keys=logging_settings.share_with_user_groups,
|
|
82
|
+
)
|
|
83
|
+
return run_entity
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def execute_job(
|
|
87
|
+
*,
|
|
88
|
+
job_definition: job_definition_t.JobDefinition,
|
|
89
|
+
profile_metadata: job_definition_t.ProfileMetadata,
|
|
90
|
+
args: JobArguments,
|
|
91
|
+
job_uuid: str,
|
|
92
|
+
) -> job_definition_t.JobResult:
|
|
93
|
+
with args.logger.push_scope(job_definition.name) as job_logger:
|
|
94
|
+
job = resolve_executor(job_definition.executor, profile_metadata)
|
|
95
|
+
|
|
96
|
+
job_logger.log_info("running job")
|
|
97
|
+
|
|
98
|
+
run_entity: entity_t.Entity | None = None
|
|
99
|
+
try:
|
|
100
|
+
if (
|
|
101
|
+
job_definition.logging_settings is not None
|
|
102
|
+
and job_definition.logging_settings.enabled
|
|
103
|
+
):
|
|
104
|
+
run_entity = _create_run_entity(
|
|
105
|
+
client=args.client,
|
|
106
|
+
logging_settings=job_definition.logging_settings,
|
|
107
|
+
job_uuid=job_uuid,
|
|
108
|
+
)
|
|
109
|
+
result = job.run_outer(args=args)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
job_logger.log_exception(e)
|
|
112
|
+
if run_entity is not None:
|
|
113
|
+
args.client.set_values(
|
|
114
|
+
entity=run_entity,
|
|
115
|
+
values=[
|
|
116
|
+
field_values_t.ArgumentValueRefName(
|
|
117
|
+
field_ref_name=async_jobs_t.ASYNC_JOB_STATUS_FIELD_REF_NAME,
|
|
118
|
+
value=async_jobs_t.AsyncJobStatus.ERROR,
|
|
119
|
+
),
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
return job_definition_t.JobResult(success=False)
|
|
123
|
+
|
|
124
|
+
if args.batch_processor.current_queue_size() != 0:
|
|
125
|
+
args.batch_processor.send()
|
|
126
|
+
|
|
127
|
+
submitted_batch_job_ids = args.batch_processor.get_submitted_job_ids()
|
|
128
|
+
job_logger.log_info(
|
|
129
|
+
"completed job",
|
|
130
|
+
attributes={
|
|
131
|
+
"submitted_batch_job_ids": submitted_batch_job_ids,
|
|
132
|
+
"success": result.success,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
if run_entity is not None:
|
|
136
|
+
args.client.set_values(
|
|
137
|
+
entity=run_entity,
|
|
138
|
+
values=[
|
|
139
|
+
field_values_t.ArgumentValueRefName(
|
|
140
|
+
field_ref_name=async_jobs_t.ASYNC_JOB_STATUS_FIELD_REF_NAME,
|
|
141
|
+
value=async_jobs_t.AsyncJobStatus.COMPLETED
|
|
142
|
+
if result.success
|
|
143
|
+
else async_jobs_t.AsyncJobStatus.ERROR,
|
|
144
|
+
),
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return result
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
import paramiko
|
|
7
|
+
|
|
8
|
+
from pkgs.filesystem_utils import (
|
|
9
|
+
FileObjectData,
|
|
10
|
+
FileSystemFileReference,
|
|
11
|
+
FileSystemObject,
|
|
12
|
+
FileSystemS3Config,
|
|
13
|
+
FileSystemSession,
|
|
14
|
+
FileSystemSFTPConfig,
|
|
15
|
+
FileTransfer,
|
|
16
|
+
S3Session,
|
|
17
|
+
SFTPSession,
|
|
18
|
+
)
|
|
19
|
+
from uncountable.core.file_upload import DataFileUpload, FileUpload
|
|
20
|
+
from uncountable.integration.job import Job, JobArguments
|
|
21
|
+
from uncountable.integration.secret_retrieval import retrieve_secret
|
|
22
|
+
from uncountable.integration.telemetry import JobLogger
|
|
23
|
+
from uncountable.types.generic_upload_t import (
|
|
24
|
+
GenericRemoteDirectoryScope,
|
|
25
|
+
GenericUploadStrategy,
|
|
26
|
+
)
|
|
27
|
+
from uncountable.types.job_definition_t import (
|
|
28
|
+
GenericUploadDataSource,
|
|
29
|
+
GenericUploadDataSourceS3,
|
|
30
|
+
GenericUploadDataSourceSFTP,
|
|
31
|
+
JobResult,
|
|
32
|
+
S3CloudProvider,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _filter_files_by_keyword(
|
|
37
|
+
remote_directory: GenericRemoteDirectoryScope,
|
|
38
|
+
files: list[FileObjectData],
|
|
39
|
+
logger: JobLogger,
|
|
40
|
+
) -> list[FileObjectData]:
|
|
41
|
+
if remote_directory.detection_keyword is None:
|
|
42
|
+
return files
|
|
43
|
+
|
|
44
|
+
raise NotImplementedError("keyword detection not implemented yet")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _filter_by_filename(
|
|
48
|
+
remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
|
|
49
|
+
) -> list[FileSystemObject]:
|
|
50
|
+
if remote_directory.filename_regex is None:
|
|
51
|
+
return files
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
file
|
|
55
|
+
for file in files
|
|
56
|
+
if file.filename is not None
|
|
57
|
+
and re.search(remote_directory.filename_regex, file.filename)
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _filter_by_file_extension(
|
|
62
|
+
remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
|
|
63
|
+
) -> list[FileSystemObject]:
|
|
64
|
+
if remote_directory.valid_file_extensions is None:
|
|
65
|
+
return files
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
file
|
|
69
|
+
for file in files
|
|
70
|
+
if file.filename is not None
|
|
71
|
+
and os.path.splitext(file.filename)[-1]
|
|
72
|
+
in remote_directory.valid_file_extensions
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _filter_by_max_files(
|
|
77
|
+
remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
|
|
78
|
+
) -> list[FileSystemObject]:
|
|
79
|
+
if remote_directory.max_files is None:
|
|
80
|
+
return files
|
|
81
|
+
|
|
82
|
+
return files[: remote_directory.max_files]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _pull_remote_directory_data(
|
|
86
|
+
*,
|
|
87
|
+
filesystem_session: FileSystemSession,
|
|
88
|
+
remote_directory: GenericRemoteDirectoryScope,
|
|
89
|
+
logger: JobLogger,
|
|
90
|
+
) -> list[FileObjectData]:
|
|
91
|
+
files_to_pull = filesystem_session.list_files(
|
|
92
|
+
dir_path=FileSystemFileReference(
|
|
93
|
+
filepath=remote_directory.src_path,
|
|
94
|
+
),
|
|
95
|
+
recursive=remote_directory.recursive,
|
|
96
|
+
)
|
|
97
|
+
logger.log_info(
|
|
98
|
+
f"Pulled the following files {files_to_pull} from the remote directory {remote_directory}.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
files_to_pull = _filter_by_file_extension(remote_directory, files_to_pull)
|
|
102
|
+
files_to_pull = _filter_by_filename(remote_directory, files_to_pull)
|
|
103
|
+
files_to_pull = _filter_by_max_files(remote_directory, files_to_pull)
|
|
104
|
+
|
|
105
|
+
logger.log_info(
|
|
106
|
+
f"Accessing SFTP directory: {remote_directory.src_path} and pulling files: {', '.join([f.filename for f in files_to_pull if f.filename is not None])}",
|
|
107
|
+
)
|
|
108
|
+
return filesystem_session.download_files(files_to_pull)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _filter_downloaded_file_data(
|
|
112
|
+
remote_directory: GenericRemoteDirectoryScope,
|
|
113
|
+
pulled_file_data: list[FileObjectData],
|
|
114
|
+
logger: JobLogger,
|
|
115
|
+
) -> list[FileObjectData]:
|
|
116
|
+
filtered_file_data = _filter_files_by_keyword(
|
|
117
|
+
remote_directory=remote_directory, files=pulled_file_data, logger=logger
|
|
118
|
+
)
|
|
119
|
+
return filtered_file_data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _move_files_post_upload(
|
|
123
|
+
*,
|
|
124
|
+
filesystem_session: FileSystemSession,
|
|
125
|
+
remote_directory_scope: GenericRemoteDirectoryScope,
|
|
126
|
+
success_file_paths: list[str],
|
|
127
|
+
failed_file_paths: list[str],
|
|
128
|
+
) -> None:
|
|
129
|
+
success_file_transfers: list[FileTransfer] = []
|
|
130
|
+
appended_text = ""
|
|
131
|
+
|
|
132
|
+
if remote_directory_scope.prepend_date_on_archive:
|
|
133
|
+
appended_text = f"-{datetime.now(timezone.utc).timestamp()}"
|
|
134
|
+
|
|
135
|
+
for file_path in success_file_paths:
|
|
136
|
+
filename = os.path.split(file_path)[-1]
|
|
137
|
+
root, extension = os.path.splitext(filename)
|
|
138
|
+
new_filename = f"{root}{appended_text}{extension}"
|
|
139
|
+
# format is source, dest in the tuple
|
|
140
|
+
success_file_transfers.append((
|
|
141
|
+
FileSystemFileReference(file_path),
|
|
142
|
+
FileSystemFileReference(
|
|
143
|
+
os.path.join(
|
|
144
|
+
remote_directory_scope.success_archive_path,
|
|
145
|
+
new_filename,
|
|
146
|
+
)
|
|
147
|
+
),
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
failed_file_transfers: list[FileTransfer] = []
|
|
151
|
+
for file_path in failed_file_paths:
|
|
152
|
+
filename = os.path.split(file_path)[-1]
|
|
153
|
+
root, extension = os.path.splitext(filename)
|
|
154
|
+
new_filename = f"{root}{appended_text}{extension}"
|
|
155
|
+
failed_file_transfers.append((
|
|
156
|
+
FileSystemFileReference(file_path),
|
|
157
|
+
FileSystemFileReference(
|
|
158
|
+
os.path.join(
|
|
159
|
+
remote_directory_scope.failure_archive_path,
|
|
160
|
+
new_filename,
|
|
161
|
+
)
|
|
162
|
+
),
|
|
163
|
+
))
|
|
164
|
+
|
|
165
|
+
filesystem_session.move_files([*success_file_transfers, *failed_file_transfers])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class GenericUploadJob(Job[None]):
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
data_source: GenericUploadDataSource,
|
|
172
|
+
remote_directories: list[GenericRemoteDirectoryScope],
|
|
173
|
+
upload_strategy: GenericUploadStrategy,
|
|
174
|
+
) -> None:
|
|
175
|
+
super().__init__()
|
|
176
|
+
self.remote_directories = remote_directories
|
|
177
|
+
self.upload_strategy = upload_strategy
|
|
178
|
+
self.data_source = data_source
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def payload_type(self) -> type[None]:
|
|
182
|
+
return type(None)
|
|
183
|
+
|
|
184
|
+
def _construct_filesystem_session(self, args: JobArguments) -> FileSystemSession:
|
|
185
|
+
match self.data_source:
|
|
186
|
+
case GenericUploadDataSourceSFTP():
|
|
187
|
+
pem_secret = retrieve_secret(
|
|
188
|
+
self.data_source.pem_secret, profile_metadata=args.profile_metadata
|
|
189
|
+
)
|
|
190
|
+
pem_key = paramiko.RSAKey.from_private_key(io.StringIO(pem_secret))
|
|
191
|
+
sftp_config = FileSystemSFTPConfig(
|
|
192
|
+
ip=self.data_source.host,
|
|
193
|
+
username=self.data_source.username,
|
|
194
|
+
pem_path=None,
|
|
195
|
+
pem_key=pem_key,
|
|
196
|
+
)
|
|
197
|
+
return SFTPSession(sftp_config=sftp_config)
|
|
198
|
+
case GenericUploadDataSourceS3():
|
|
199
|
+
if self.data_source.access_key_secret is not None:
|
|
200
|
+
secret_access_key = retrieve_secret(
|
|
201
|
+
self.data_source.access_key_secret,
|
|
202
|
+
profile_metadata=args.profile_metadata,
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
secret_access_key = None
|
|
206
|
+
|
|
207
|
+
if self.data_source.endpoint_url is None:
|
|
208
|
+
assert self.data_source.cloud_provider is not None, (
|
|
209
|
+
"either cloud_provider or endpoint_url must be specified"
|
|
210
|
+
)
|
|
211
|
+
match self.data_source.cloud_provider:
|
|
212
|
+
case S3CloudProvider.AWS:
|
|
213
|
+
endpoint_url = "https://s3.amazonaws.com"
|
|
214
|
+
case S3CloudProvider.OVH:
|
|
215
|
+
assert self.data_source.region_name is not None, (
|
|
216
|
+
"region_name must be specified for cloud_provider OVH"
|
|
217
|
+
)
|
|
218
|
+
endpoint_url = f"https://s3.{self.data_source.region_name}.cloud.ovh.net"
|
|
219
|
+
else:
|
|
220
|
+
endpoint_url = self.data_source.endpoint_url
|
|
221
|
+
|
|
222
|
+
s3_config = FileSystemS3Config(
|
|
223
|
+
endpoint_url=endpoint_url,
|
|
224
|
+
bucket_name=self.data_source.bucket_name,
|
|
225
|
+
region_name=self.data_source.region_name,
|
|
226
|
+
access_key_id=self.data_source.access_key_id,
|
|
227
|
+
secret_access_key=secret_access_key,
|
|
228
|
+
session_token=None,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return S3Session(s3_config=s3_config)
|
|
232
|
+
|
|
233
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
234
|
+
client = args.client
|
|
235
|
+
batch_processor = args.batch_processor
|
|
236
|
+
logger = args.logger
|
|
237
|
+
|
|
238
|
+
with self._construct_filesystem_session(args) as filesystem_session:
|
|
239
|
+
files_to_upload: list[FileUpload] = []
|
|
240
|
+
for remote_directory in self.remote_directories:
|
|
241
|
+
pulled_file_data = _pull_remote_directory_data(
|
|
242
|
+
filesystem_session=filesystem_session,
|
|
243
|
+
remote_directory=remote_directory,
|
|
244
|
+
logger=logger,
|
|
245
|
+
)
|
|
246
|
+
filtered_file_data = _filter_downloaded_file_data(
|
|
247
|
+
remote_directory=remote_directory,
|
|
248
|
+
pulled_file_data=pulled_file_data,
|
|
249
|
+
logger=args.logger,
|
|
250
|
+
)
|
|
251
|
+
for file_data in filtered_file_data:
|
|
252
|
+
files_to_upload.append(
|
|
253
|
+
DataFileUpload(
|
|
254
|
+
data=io.BytesIO(file_data.file_data),
|
|
255
|
+
name=file_data.filename,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
if not self.upload_strategy.skip_moving_files:
|
|
259
|
+
_move_files_post_upload(
|
|
260
|
+
filesystem_session=filesystem_session,
|
|
261
|
+
remote_directory_scope=remote_directory,
|
|
262
|
+
success_file_paths=[
|
|
263
|
+
file.filepath
|
|
264
|
+
if file.filepath is not None
|
|
265
|
+
else file.filename
|
|
266
|
+
for file in filtered_file_data
|
|
267
|
+
],
|
|
268
|
+
# IMPROVE: use triggers/webhooks to mark failed files as failed
|
|
269
|
+
failed_file_paths=[],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
uploaded_files = client.upload_files(file_uploads=files_to_upload)
|
|
273
|
+
|
|
274
|
+
file_ids = [file.file_id for file in uploaded_files]
|
|
275
|
+
|
|
276
|
+
for destination in self.upload_strategy.destinations:
|
|
277
|
+
for file_id in file_ids:
|
|
278
|
+
batch_processor.invoke_uploader(
|
|
279
|
+
file_id=file_id,
|
|
280
|
+
uploader_key=self.upload_strategy.uploader_key,
|
|
281
|
+
destination=destination,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return JobResult(success=True)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from uncountable.integration.job import Job
|
|
6
|
+
from uncountable.types.job_definition_t import JobExecutorScript, ProfileMetadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_script_executor(
|
|
10
|
+
executor: JobExecutorScript, profile_metadata: ProfileMetadata
|
|
11
|
+
) -> Job:
|
|
12
|
+
job_module_path = ".".join([
|
|
13
|
+
os.environ["UNC_PROFILES_MODULE"],
|
|
14
|
+
profile_metadata.name,
|
|
15
|
+
executor.import_path,
|
|
16
|
+
])
|
|
17
|
+
job_module = importlib.import_module(job_module_path)
|
|
18
|
+
found_jobs: list[Job] = []
|
|
19
|
+
for _, job_class in inspect.getmembers(job_module, inspect.isclass):
|
|
20
|
+
if getattr(job_class, "_unc_job_registered", False):
|
|
21
|
+
found_jobs.append(job_class())
|
|
22
|
+
assert len(found_jobs) == 1, (
|
|
23
|
+
f"expected exactly one job class in {executor.import_path}, found {len(found_jobs)}"
|
|
24
|
+
)
|
|
25
|
+
return found_jobs[0]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import typing
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from pkgs.argument_parser import CachedParser
|
|
7
|
+
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
8
|
+
from uncountable.core.client import Client
|
|
9
|
+
from uncountable.integration.telemetry import JobLogger
|
|
10
|
+
from uncountable.types import base_t, webhook_job_t
|
|
11
|
+
from uncountable.types.job_definition_t import JobDefinition, JobResult, ProfileMetadata
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(kw_only=True)
|
|
15
|
+
class JobArguments:
|
|
16
|
+
job_definition: JobDefinition
|
|
17
|
+
profile_metadata: ProfileMetadata
|
|
18
|
+
client: Client
|
|
19
|
+
batch_processor: AsyncBatchProcessor
|
|
20
|
+
logger: JobLogger
|
|
21
|
+
payload: base_t.JsonValue
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# only for compatibility:
|
|
25
|
+
CronJobArguments = JobArguments
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
PT = typing.TypeVar("PT")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Job(ABC, typing.Generic[PT]):
|
|
32
|
+
_unc_job_registered: bool = False
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def payload_type(self) -> type[PT]: ...
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def run_outer(self, args: JobArguments) -> JobResult: ...
|
|
40
|
+
|
|
41
|
+
@functools.cached_property
|
|
42
|
+
def _cached_payload_parser(self) -> CachedParser[PT]:
|
|
43
|
+
return CachedParser(self.payload_type)
|
|
44
|
+
|
|
45
|
+
def get_payload(self, payload: base_t.JsonValue) -> PT:
|
|
46
|
+
return self._cached_payload_parser.parse_storage(payload)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CronJob(Job):
|
|
50
|
+
@property
|
|
51
|
+
def payload_type(self) -> type[None]:
|
|
52
|
+
return type(None)
|
|
53
|
+
|
|
54
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
55
|
+
assert isinstance(args, CronJobArguments)
|
|
56
|
+
return self.run(args)
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def run(self, args: JobArguments) -> JobResult: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
WPT = typing.TypeVar("WPT")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WebhookJob(Job[webhook_job_t.WebhookEventPayload], typing.Generic[WPT]):
|
|
66
|
+
@property
|
|
67
|
+
def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
|
|
68
|
+
return webhook_job_t.WebhookEventPayload
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def webhook_payload_type(self) -> type[WPT]: ...
|
|
73
|
+
|
|
74
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
75
|
+
webhook_body = self.get_payload(args.payload)
|
|
76
|
+
inner_payload = CachedParser(self.webhook_payload_type).parse_api(
|
|
77
|
+
webhook_body.data
|
|
78
|
+
)
|
|
79
|
+
return self.run(args, inner_payload)
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def run(self, args: JobArguments, payload: WPT) -> JobResult: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def register_job(cls: type[Job]) -> type[Job]:
|
|
86
|
+
cls._unc_job_registered = True
|
|
87
|
+
return cls
|
|
File without changes
|