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,336 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from datetime import UTC
|
|
6
|
+
|
|
7
|
+
import paramiko
|
|
8
|
+
|
|
9
|
+
from pkgs.filesystem_utils import (
|
|
10
|
+
FileObjectData,
|
|
11
|
+
FileSystemFileReference,
|
|
12
|
+
FileSystemObject,
|
|
13
|
+
FileSystemS3Config,
|
|
14
|
+
FileSystemSession,
|
|
15
|
+
FileSystemSFTPConfig,
|
|
16
|
+
FileTransfer,
|
|
17
|
+
S3Session,
|
|
18
|
+
SFTPSession,
|
|
19
|
+
)
|
|
20
|
+
from uncountable.core.file_upload import DataFileUpload, FileUpload
|
|
21
|
+
from uncountable.integration.job import Job, JobArguments
|
|
22
|
+
from uncountable.integration.secret_retrieval import retrieve_secret
|
|
23
|
+
from uncountable.integration.telemetry import JobLogger
|
|
24
|
+
from uncountable.types.generic_upload_t import (
|
|
25
|
+
GenericRemoteDirectoryScope,
|
|
26
|
+
GenericUploadStrategy,
|
|
27
|
+
)
|
|
28
|
+
from uncountable.types.job_definition_t import (
|
|
29
|
+
GenericUploadDataSource,
|
|
30
|
+
GenericUploadDataSourceS3,
|
|
31
|
+
GenericUploadDataSourceSFTP,
|
|
32
|
+
JobResult,
|
|
33
|
+
S3CloudProvider,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_extension(filename: str) -> str | None:
|
|
38
|
+
_, ext = os.path.splitext(filename)
|
|
39
|
+
return ext.strip().lower()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_keyword_detection(data: io.BytesIO, keyword: str) -> bool:
|
|
43
|
+
try:
|
|
44
|
+
text = io.TextIOWrapper(data, encoding="utf-8")
|
|
45
|
+
for line in text:
|
|
46
|
+
if (
|
|
47
|
+
keyword in line
|
|
48
|
+
or re.search(keyword, line, flags=re.IGNORECASE) is not None
|
|
49
|
+
):
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
except re.error:
|
|
53
|
+
return False
|
|
54
|
+
except UnicodeError:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _filter_files_by_keyword(
|
|
59
|
+
remote_directory: GenericRemoteDirectoryScope,
|
|
60
|
+
files: list[FileObjectData],
|
|
61
|
+
logger: JobLogger,
|
|
62
|
+
) -> list[FileObjectData]:
|
|
63
|
+
if remote_directory.detection_keyword is None:
|
|
64
|
+
return files
|
|
65
|
+
|
|
66
|
+
filtered_files = []
|
|
67
|
+
|
|
68
|
+
for file in files:
|
|
69
|
+
extension = _get_extension(file.filename)
|
|
70
|
+
|
|
71
|
+
if extension not in (".txt", ".csv"):
|
|
72
|
+
raise NotImplementedError(
|
|
73
|
+
"keyword detection is only supported for csv, txt files"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if _run_keyword_detection(file.file_IO, remote_directory.detection_keyword):
|
|
77
|
+
filtered_files.append(file)
|
|
78
|
+
|
|
79
|
+
return filtered_files
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _filter_by_filename(
|
|
83
|
+
remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
|
|
84
|
+
) -> list[FileSystemObject]:
|
|
85
|
+
if remote_directory.filename_regex is None:
|
|
86
|
+
return files
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
file
|
|
90
|
+
for file in files
|
|
91
|
+
if file.filename is not None
|
|
92
|
+
and re.search(remote_directory.filename_regex, file.filename)
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _filter_by_file_extension(
|
|
97
|
+
remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
|
|
98
|
+
) -> list[FileSystemObject]:
|
|
99
|
+
if remote_directory.valid_file_extensions is None:
|
|
100
|
+
return files
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
file
|
|
104
|
+
for file in files
|
|
105
|
+
if file.filename is not None
|
|
106
|
+
and os.path.splitext(file.filename)[-1]
|
|
107
|
+
in remote_directory.valid_file_extensions
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _filter_by_max_files(
|
|
112
|
+
remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
|
|
113
|
+
) -> list[FileSystemObject]:
|
|
114
|
+
if remote_directory.max_files is None:
|
|
115
|
+
return files
|
|
116
|
+
|
|
117
|
+
return files[: remote_directory.max_files]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _pull_remote_directory_data(
|
|
121
|
+
*,
|
|
122
|
+
filesystem_session: FileSystemSession,
|
|
123
|
+
remote_directory: GenericRemoteDirectoryScope,
|
|
124
|
+
logger: JobLogger,
|
|
125
|
+
) -> list[FileObjectData]:
|
|
126
|
+
files_to_pull = filesystem_session.list_files(
|
|
127
|
+
dir_path=FileSystemFileReference(
|
|
128
|
+
filepath=remote_directory.src_path,
|
|
129
|
+
),
|
|
130
|
+
recursive=remote_directory.recursive,
|
|
131
|
+
)
|
|
132
|
+
logger.log_info(
|
|
133
|
+
f"Pulled the following files {files_to_pull} from the remote directory {remote_directory}.",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
files_to_pull = _filter_by_file_extension(remote_directory, files_to_pull)
|
|
137
|
+
files_to_pull = _filter_by_filename(remote_directory, files_to_pull)
|
|
138
|
+
files_to_pull = _filter_by_max_files(remote_directory, files_to_pull)
|
|
139
|
+
|
|
140
|
+
logger.log_info(
|
|
141
|
+
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])}",
|
|
142
|
+
)
|
|
143
|
+
return filesystem_session.download_files(files_to_pull)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _filter_downloaded_file_data(
|
|
147
|
+
remote_directory: GenericRemoteDirectoryScope,
|
|
148
|
+
pulled_file_data: list[FileObjectData],
|
|
149
|
+
logger: JobLogger,
|
|
150
|
+
) -> list[FileObjectData]:
|
|
151
|
+
filtered_file_data = _filter_files_by_keyword(
|
|
152
|
+
remote_directory=remote_directory, files=pulled_file_data, logger=logger
|
|
153
|
+
)
|
|
154
|
+
return filtered_file_data
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _move_files_post_upload(
|
|
158
|
+
*,
|
|
159
|
+
filesystem_session: FileSystemSession,
|
|
160
|
+
remote_directory_scope: GenericRemoteDirectoryScope,
|
|
161
|
+
success_file_paths: list[str],
|
|
162
|
+
failed_file_paths: list[str],
|
|
163
|
+
) -> None:
|
|
164
|
+
success_file_transfers: list[FileTransfer] = []
|
|
165
|
+
appended_text = ""
|
|
166
|
+
|
|
167
|
+
if remote_directory_scope.prepend_date_on_archive:
|
|
168
|
+
appended_text = f"-{datetime.datetime.now(UTC).timestamp()}"
|
|
169
|
+
|
|
170
|
+
for file_path in success_file_paths:
|
|
171
|
+
filename = os.path.split(file_path)[-1]
|
|
172
|
+
root, extension = os.path.splitext(filename)
|
|
173
|
+
new_filename = f"{root}{appended_text}{extension}"
|
|
174
|
+
# format is source, dest in the tuple
|
|
175
|
+
success_file_transfers.append((
|
|
176
|
+
FileSystemFileReference(file_path),
|
|
177
|
+
FileSystemFileReference(
|
|
178
|
+
os.path.join(
|
|
179
|
+
remote_directory_scope.success_archive_path,
|
|
180
|
+
new_filename,
|
|
181
|
+
)
|
|
182
|
+
),
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
failed_file_transfers: list[FileTransfer] = []
|
|
186
|
+
for file_path in failed_file_paths:
|
|
187
|
+
filename = os.path.split(file_path)[-1]
|
|
188
|
+
root, extension = os.path.splitext(filename)
|
|
189
|
+
new_filename = f"{root}{appended_text}{extension}"
|
|
190
|
+
failed_file_transfers.append((
|
|
191
|
+
FileSystemFileReference(file_path),
|
|
192
|
+
FileSystemFileReference(
|
|
193
|
+
os.path.join(
|
|
194
|
+
remote_directory_scope.failure_archive_path,
|
|
195
|
+
new_filename,
|
|
196
|
+
)
|
|
197
|
+
),
|
|
198
|
+
))
|
|
199
|
+
|
|
200
|
+
filesystem_session.move_files([*success_file_transfers, *failed_file_transfers])
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class GenericUploadJob(Job[None]):
|
|
204
|
+
def __init__(
|
|
205
|
+
self,
|
|
206
|
+
data_source: GenericUploadDataSource,
|
|
207
|
+
remote_directories: list[GenericRemoteDirectoryScope],
|
|
208
|
+
upload_strategy: GenericUploadStrategy,
|
|
209
|
+
) -> None:
|
|
210
|
+
super().__init__()
|
|
211
|
+
self.remote_directories = remote_directories
|
|
212
|
+
self.upload_strategy = upload_strategy
|
|
213
|
+
self.data_source = data_source
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def payload_type(self) -> type[None]:
|
|
217
|
+
return type(None)
|
|
218
|
+
|
|
219
|
+
def _construct_filesystem_session(self, args: JobArguments) -> FileSystemSession:
|
|
220
|
+
match self.data_source:
|
|
221
|
+
case GenericUploadDataSourceSFTP():
|
|
222
|
+
if self.data_source.pem_secret is not None:
|
|
223
|
+
pem_secret = retrieve_secret(
|
|
224
|
+
self.data_source.pem_secret,
|
|
225
|
+
profile_metadata=args.profile_metadata,
|
|
226
|
+
)
|
|
227
|
+
pem_key = paramiko.RSAKey.from_private_key(io.StringIO(pem_secret))
|
|
228
|
+
sftp_config = FileSystemSFTPConfig(
|
|
229
|
+
ip=self.data_source.host,
|
|
230
|
+
username=self.data_source.username,
|
|
231
|
+
pem_path=None,
|
|
232
|
+
pem_key=pem_key,
|
|
233
|
+
)
|
|
234
|
+
elif self.data_source.password_secret is not None:
|
|
235
|
+
password_secret = retrieve_secret(
|
|
236
|
+
self.data_source.password_secret,
|
|
237
|
+
profile_metadata=args.profile_metadata,
|
|
238
|
+
)
|
|
239
|
+
sftp_config = FileSystemSFTPConfig(
|
|
240
|
+
ip=self.data_source.host,
|
|
241
|
+
username=self.data_source.username,
|
|
242
|
+
pem_path=None,
|
|
243
|
+
password=password_secret,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"Either pem_secret or password_secret must be specified for sftp data source"
|
|
248
|
+
)
|
|
249
|
+
return SFTPSession(sftp_config=sftp_config)
|
|
250
|
+
case GenericUploadDataSourceS3():
|
|
251
|
+
if self.data_source.access_key_secret is not None:
|
|
252
|
+
secret_access_key = retrieve_secret(
|
|
253
|
+
self.data_source.access_key_secret,
|
|
254
|
+
profile_metadata=args.profile_metadata,
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
secret_access_key = None
|
|
258
|
+
|
|
259
|
+
if self.data_source.endpoint_url is None:
|
|
260
|
+
assert self.data_source.cloud_provider is not None, (
|
|
261
|
+
"either cloud_provider or endpoint_url must be specified"
|
|
262
|
+
)
|
|
263
|
+
match self.data_source.cloud_provider:
|
|
264
|
+
case S3CloudProvider.AWS:
|
|
265
|
+
endpoint_url = "https://s3.amazonaws.com"
|
|
266
|
+
case S3CloudProvider.OVH:
|
|
267
|
+
assert self.data_source.region_name is not None, (
|
|
268
|
+
"region_name must be specified for cloud_provider OVH"
|
|
269
|
+
)
|
|
270
|
+
endpoint_url = f"https://s3.{self.data_source.region_name}.cloud.ovh.net"
|
|
271
|
+
else:
|
|
272
|
+
endpoint_url = self.data_source.endpoint_url
|
|
273
|
+
|
|
274
|
+
s3_config = FileSystemS3Config(
|
|
275
|
+
endpoint_url=endpoint_url,
|
|
276
|
+
bucket_name=self.data_source.bucket_name,
|
|
277
|
+
region_name=self.data_source.region_name,
|
|
278
|
+
access_key_id=self.data_source.access_key_id,
|
|
279
|
+
secret_access_key=secret_access_key,
|
|
280
|
+
session_token=None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return S3Session(s3_config=s3_config)
|
|
284
|
+
|
|
285
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
286
|
+
client = args.client
|
|
287
|
+
batch_processor = args.batch_processor
|
|
288
|
+
logger = args.logger
|
|
289
|
+
|
|
290
|
+
with self._construct_filesystem_session(args) as filesystem_session:
|
|
291
|
+
files_to_upload: list[FileUpload] = []
|
|
292
|
+
for remote_directory in self.remote_directories:
|
|
293
|
+
pulled_file_data = _pull_remote_directory_data(
|
|
294
|
+
filesystem_session=filesystem_session,
|
|
295
|
+
remote_directory=remote_directory,
|
|
296
|
+
logger=logger,
|
|
297
|
+
)
|
|
298
|
+
filtered_file_data = _filter_downloaded_file_data(
|
|
299
|
+
remote_directory=remote_directory,
|
|
300
|
+
pulled_file_data=pulled_file_data,
|
|
301
|
+
logger=args.logger,
|
|
302
|
+
)
|
|
303
|
+
for file_data in filtered_file_data:
|
|
304
|
+
files_to_upload.append(
|
|
305
|
+
DataFileUpload(
|
|
306
|
+
data=io.BytesIO(file_data.file_data),
|
|
307
|
+
name=file_data.filename,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
if not self.upload_strategy.skip_moving_files:
|
|
311
|
+
_move_files_post_upload(
|
|
312
|
+
filesystem_session=filesystem_session,
|
|
313
|
+
remote_directory_scope=remote_directory,
|
|
314
|
+
success_file_paths=[
|
|
315
|
+
file.filepath
|
|
316
|
+
if file.filepath is not None
|
|
317
|
+
else file.filename
|
|
318
|
+
for file in filtered_file_data
|
|
319
|
+
],
|
|
320
|
+
# IMPROVE: use triggers/webhooks to mark failed files as failed
|
|
321
|
+
failed_file_paths=[],
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
uploaded_files = client.upload_files(file_uploads=files_to_upload)
|
|
325
|
+
|
|
326
|
+
file_ids = [file.file_id for file in uploaded_files]
|
|
327
|
+
|
|
328
|
+
for destination in self.upload_strategy.destinations:
|
|
329
|
+
for file_id in file_ids:
|
|
330
|
+
batch_processor.invoke_uploader(
|
|
331
|
+
file_id=file_id,
|
|
332
|
+
uploader_key=self.upload_strategy.uploader_key,
|
|
333
|
+
destination=destination,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return JobResult(success=True)
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
import os
|
|
3
1
|
import importlib
|
|
4
2
|
import inspect
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
5
|
from uncountable.integration.job import Job
|
|
6
|
-
from uncountable.
|
|
6
|
+
from uncountable.types.job_definition_t import JobExecutorScript, ProfileMetadata
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def resolve_script_executor(
|
|
10
|
-
|
|
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
|
+
])
|
|
11
17
|
job_module = importlib.import_module(job_module_path)
|
|
12
|
-
found_jobs: list[
|
|
18
|
+
found_jobs: list[Job] = []
|
|
13
19
|
for _, job_class in inspect.getmembers(job_module, inspect.isclass):
|
|
14
20
|
if getattr(job_class, "_unc_job_registered", False):
|
|
15
21
|
found_jobs.append(job_class())
|
|
16
|
-
assert (
|
|
17
|
-
len(found_jobs)
|
|
18
|
-
)
|
|
22
|
+
assert len(found_jobs) == 1, (
|
|
23
|
+
f"expected exactly one job class in {executor.import_path}, found {len(found_jobs)}"
|
|
24
|
+
)
|
|
19
25
|
return found_jobs[0]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import functools
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from flask.wrappers import Response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HttpException(Exception):
|
|
10
|
+
error_code: int
|
|
11
|
+
message: str
|
|
12
|
+
|
|
13
|
+
def __init__(self, *, error_code: int, message: str) -> None:
|
|
14
|
+
self.error_code = error_code
|
|
15
|
+
self.message = message
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def payload_failed_signature() -> "HttpException":
|
|
19
|
+
return HttpException(
|
|
20
|
+
error_code=401, message="webhook payload did not match signature"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def no_signature_passed() -> "HttpException":
|
|
25
|
+
return HttpException(error_code=400, message="missing signature")
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def body_parse_error() -> "HttpException":
|
|
29
|
+
return HttpException(error_code=400, message="body parse error")
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def unknown_error() -> "HttpException":
|
|
33
|
+
return HttpException(error_code=500, message="internal server error")
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def configuration_error(
|
|
37
|
+
message: str = "internal configuration error",
|
|
38
|
+
) -> "HttpException":
|
|
39
|
+
return HttpException(error_code=500, message=message)
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return f"[{self.error_code}]: {self.message}"
|
|
43
|
+
|
|
44
|
+
def make_error_response(self) -> Response:
|
|
45
|
+
return Response(
|
|
46
|
+
status=self.error_code,
|
|
47
|
+
response=json.dumps({"error": {"message": str(self)}}),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(kw_only=True, frozen=True)
|
|
52
|
+
class GenericHttpRequest:
|
|
53
|
+
body_base64: str
|
|
54
|
+
headers: dict[str, str]
|
|
55
|
+
|
|
56
|
+
@functools.cached_property
|
|
57
|
+
def body_bytes(self) -> bytes:
|
|
58
|
+
return base64.b64decode(self.body_base64)
|
|
59
|
+
|
|
60
|
+
@functools.cached_property
|
|
61
|
+
def body_text(self) -> str:
|
|
62
|
+
return self.body_bytes.decode()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(kw_only=True)
|
|
66
|
+
class GenericHttpResponse:
|
|
67
|
+
response: str
|
|
68
|
+
status_code: int
|
|
69
|
+
headers: dict[str, str] | None = None
|