UncountablePythonSDK 0.0.83__py3-none-any.whl → 0.0.132__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 +54 -7
- 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 +6 -4
- examples/basic_auth.py +7 -0
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +3 -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 +17 -9
- examples/integration-server/jobs/materials_auto/profile.yaml +61 -0
- examples/integration-server/pyproject.toml +10 -10
- examples/oauth.py +7 -0
- examples/set_recipe_metadata_file.py +1 -1
- examples/upload_files.py +1 -2
- pkgs/argument_parser/__init__.py +8 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +196 -63
- pkgs/filesystem_utils/__init__.py +1 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +5 -5
- pkgs/filesystem_utils/_s3_session.py +2 -1
- pkgs/filesystem_utils/_sftp_session.py +6 -3
- pkgs/filesystem_utils/file_type_utils.py +30 -10
- pkgs/serialization/__init__.py +7 -2
- 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 +40 -48
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +16 -16
- pkgs/serialization_util/__init__.py +6 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +15 -5
- pkgs/type_spec/actions_registry/__main__.py +0 -4
- pkgs/type_spec/actions_registry/emit_typescript.py +2 -4
- pkgs/type_spec/builder.py +248 -70
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +40 -7
- pkgs/type_spec/cross_output_links.py +99 -0
- pkgs/type_spec/emit_open_api.py +121 -34
- pkgs/type_spec/emit_open_api_util.py +5 -5
- pkgs/type_spec/emit_python.py +277 -86
- pkgs/type_spec/emit_typescript.py +102 -29
- pkgs/type_spec/emit_typescript_util.py +66 -10
- pkgs/type_spec/load_types.py +16 -3
- 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 +11 -8
- 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 +115 -22
- 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 +3 -3
- pkgs/type_spec/value_spec/__main__.py +26 -9
- pkgs/type_spec/value_spec/convert_type.py +18 -0
- pkgs/type_spec/value_spec/emit_python.py +13 -3
- pkgs/type_spec/value_spec/types.py +1 -1
- uncountable/core/async_batch.py +1 -1
- uncountable/core/client.py +133 -34
- uncountable/core/environment.py +3 -3
- uncountable/core/file_upload.py +39 -15
- uncountable/integration/cli.py +116 -23
- uncountable/integration/construct_client.py +3 -3
- uncountable/integration/executors/executors.py +12 -2
- uncountable/integration/executors/generic_upload_executor.py +66 -14
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +192 -7
- uncountable/integration/queue_runner/command_server/__init__.py +4 -0
- uncountable/integration/queue_runner/command_server/command_client.py +65 -0
- uncountable/integration/queue_runner/command_server/command_server.py +83 -5
- uncountable/integration/queue_runner/command_server/constants.py +4 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +36 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +28 -11
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +77 -1
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
- uncountable/integration/queue_runner/command_server/types.py +25 -2
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +168 -11
- uncountable/integration/queue_runner/datastore/interface.py +10 -0
- uncountable/integration/queue_runner/datastore/model.py +8 -1
- uncountable/integration/queue_runner/job_scheduler.py +63 -23
- uncountable/integration/queue_runner/queue_runner.py +10 -2
- uncountable/integration/queue_runner/worker.py +3 -5
- uncountable/integration/scan_profiles.py +1 -1
- uncountable/integration/scheduler.py +74 -25
- uncountable/integration/secret_retrieval/retrieve_secret.py +1 -1
- uncountable/integration/server.py +42 -12
- uncountable/integration/telemetry.py +63 -10
- uncountable/integration/webhook_server/entrypoint.py +39 -112
- uncountable/types/__init__.py +58 -1
- uncountable/types/api/batch/execute_batch.py +5 -6
- uncountable/types/api/batch/execute_batch_load_async.py +2 -3
- uncountable/types/api/chemical/convert_chemical_formats.py +10 -5
- 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 +7 -7
- uncountable/types/api/entity/create_entity.py +8 -8
- 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 +3 -4
- uncountable/types/api/entity/grant_entity_permissions.py +6 -6
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +34 -10
- uncountable/types/api/entity/lock_entity.py +4 -4
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +5 -6
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +3 -3
- uncountable/types/api/entity/transition_entity_phase.py +14 -7
- uncountable/types/api/entity/unlock_entity.py +3 -3
- uncountable/types/api/equipment/associate_equipment_input.py +2 -3
- uncountable/types/api/field_options/upsert_field_options.py +7 -7
- 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 +6 -7
- uncountable/types/api/id_source/match_id_source.py +4 -5
- uncountable/types/api/input_groups/get_input_group_names.py +3 -4
- uncountable/types/api/inputs/create_inputs.py +10 -9
- uncountable/types/api/inputs/get_input_data.py +11 -12
- uncountable/types/api/inputs/get_input_names.py +6 -7
- uncountable/types/api/inputs/get_inputs_data.py +6 -7
- uncountable/types/api/inputs/set_input_attribute_values.py +5 -6
- uncountable/types/api/inputs/set_input_category.py +5 -5
- uncountable/types/api/inputs/set_input_subcategories.py +3 -3
- uncountable/types/api/inputs/set_intermediate_type.py +4 -4
- 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/update_entity_material_families.py +3 -4
- 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 +12 -13
- uncountable/types/api/outputs/get_output_names.py +5 -6
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +7 -8
- uncountable/types/api/permissions/set_core_permissions.py +16 -10
- uncountable/types/api/project/get_projects.py +6 -7
- uncountable/types/api/project/get_projects_data.py +7 -8
- uncountable/types/api/recipe_links/create_recipe_link.py +5 -5
- uncountable/types/api/recipe_links/remove_recipe_link.py +4 -4
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +6 -7
- uncountable/types/api/recipes/add_recipe_to_project.py +3 -3
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +4 -4
- uncountable/types/api/recipes/associate_recipe_as_input.py +5 -5
- uncountable/types/api/recipes/associate_recipe_as_lot.py +3 -3
- uncountable/types/api/recipes/clear_recipe_outputs.py +3 -3
- uncountable/types/api/recipes/create_mix_order.py +44 -0
- uncountable/types/api/recipes/create_recipe.py +8 -9
- uncountable/types/api/recipes/create_recipes.py +8 -9
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +3 -3
- uncountable/types/api/recipes/edit_recipe_inputs.py +101 -24
- uncountable/types/api/recipes/get_column_calculation_values.py +4 -5
- uncountable/types/api/recipes/get_curve.py +4 -5
- uncountable/types/api/recipes/get_recipe_calculations.py +6 -7
- uncountable/types/api/recipes/get_recipe_links.py +3 -4
- uncountable/types/api/recipes/get_recipe_names.py +3 -4
- uncountable/types/api/recipes/get_recipe_output_metadata.py +5 -6
- uncountable/types/api/recipes/get_recipes_data.py +62 -34
- uncountable/types/api/recipes/lock_recipes.py +9 -8
- uncountable/types/api/recipes/remove_recipe_from_project.py +3 -3
- uncountable/types/api/recipes/set_recipe_inputs.py +9 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +3 -3
- uncountable/types/api/recipes/set_recipe_output_annotations.py +11 -12
- uncountable/types/api/recipes/set_recipe_output_file.py +5 -6
- uncountable/types/api/recipes/set_recipe_outputs.py +24 -13
- uncountable/types/api/recipes/set_recipe_tags.py +14 -9
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +3 -3
- uncountable/types/api/recipes/unlock_recipes.py +7 -6
- 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 +4 -4
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +4 -5
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +1 -1
- uncountable/types/async_batch_processor.py +506 -23
- uncountable/types/async_batch_t.py +35 -8
- uncountable/types/async_jobs.py +0 -1
- uncountable/types/async_jobs_t.py +1 -2
- uncountable/types/auth_retrieval.py +0 -1
- uncountable/types/auth_retrieval_t.py +6 -6
- uncountable/types/base.py +0 -1
- uncountable/types/base_t.py +11 -9
- uncountable/types/calculations.py +0 -1
- uncountable/types/calculations_t.py +1 -2
- uncountable/types/chemical_structure.py +0 -1
- uncountable/types/chemical_structure_t.py +5 -5
- uncountable/types/client_base.py +614 -69
- uncountable/types/client_config.py +1 -1
- uncountable/types/client_config_t.py +13 -3
- uncountable/types/curves.py +0 -1
- uncountable/types/curves_t.py +6 -7
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +1 -1
- uncountable/types/entity_t.py +90 -10
- uncountable/types/experiment_groups.py +0 -1
- uncountable/types/experiment_groups_t.py +1 -2
- uncountable/types/exports.py +8 -0
- uncountable/types/exports_t.py +34 -0
- uncountable/types/field_values.py +19 -1
- uncountable/types/field_values_t.py +242 -9
- uncountable/types/fields.py +0 -1
- uncountable/types/fields_t.py +1 -2
- uncountable/types/generic_upload.py +0 -1
- uncountable/types/generic_upload_t.py +14 -14
- uncountable/types/id_source.py +0 -1
- uncountable/types/id_source_t.py +13 -7
- uncountable/types/identifier.py +0 -1
- uncountable/types/identifier_t.py +10 -5
- uncountable/types/input_attributes.py +0 -1
- uncountable/types/input_attributes_t.py +3 -4
- uncountable/types/inputs.py +0 -1
- uncountable/types/inputs_t.py +3 -4
- uncountable/types/integration_server.py +0 -1
- uncountable/types/integration_server_t.py +13 -4
- 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 +2 -1
- uncountable/types/job_definition_t.py +57 -32
- 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 +0 -1
- uncountable/types/outputs_t.py +2 -3
- uncountable/types/overrides.py +0 -1
- uncountable/types/overrides_t.py +10 -4
- uncountable/types/permissions.py +0 -1
- uncountable/types/permissions_t.py +1 -2
- uncountable/types/phases.py +0 -1
- uncountable/types/phases_t.py +1 -2
- uncountable/types/post_base.py +0 -1
- uncountable/types/post_base_t.py +1 -2
- uncountable/types/queued_job.py +2 -1
- uncountable/types/queued_job_t.py +29 -12
- uncountable/types/recipe_identifiers.py +0 -1
- uncountable/types/recipe_identifiers_t.py +18 -8
- uncountable/types/recipe_inputs.py +0 -1
- uncountable/types/recipe_inputs_t.py +1 -2
- uncountable/types/recipe_links.py +0 -1
- uncountable/types/recipe_links_t.py +3 -4
- uncountable/types/recipe_metadata.py +0 -1
- uncountable/types/recipe_metadata_t.py +9 -10
- uncountable/types/recipe_output_metadata.py +0 -1
- uncountable/types/recipe_output_metadata_t.py +1 -2
- uncountable/types/recipe_tags.py +0 -1
- uncountable/types/recipe_tags_t.py +1 -2
- uncountable/types/recipe_workflow_steps.py +0 -1
- uncountable/types/recipe_workflow_steps_t.py +7 -7
- uncountable/types/recipes.py +0 -1
- uncountable/types/recipes_t.py +2 -2
- uncountable/types/response.py +0 -1
- uncountable/types/response_t.py +2 -2
- uncountable/types/secret_retrieval.py +0 -1
- uncountable/types/secret_retrieval_t.py +7 -7
- 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 +0 -1
- uncountable/types/units_t.py +1 -2
- uncountable/types/uploader.py +24 -0
- uncountable/types/uploader_t.py +222 -0
- uncountable/types/users.py +0 -1
- uncountable/types/users_t.py +1 -2
- uncountable/types/webhook_job.py +1 -1
- uncountable/types/webhook_job_t.py +14 -3
- uncountable/types/workflows.py +0 -1
- uncountable/types/workflows_t.py +3 -4
- uncountablepythonsdk-0.0.132.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.132.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.83.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
- UncountablePythonSDK-0.0.83.dist-info/METADATA +0 -60
- UncountablePythonSDK-0.0.83.dist-info/RECORD +0 -292
- docs/quickstart.md +0 -19
- {UncountablePythonSDK-0.0.83.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ from uncountable.types import (
|
|
|
9
9
|
entity_t,
|
|
10
10
|
field_values_t,
|
|
11
11
|
identifier_t,
|
|
12
|
+
integration_server_t,
|
|
12
13
|
job_definition_t,
|
|
13
14
|
transition_entity_phase_t,
|
|
14
15
|
)
|
|
@@ -33,7 +34,10 @@ def resolve_executor(
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def _create_run_entity(
|
|
36
|
-
*,
|
|
37
|
+
*,
|
|
38
|
+
client: Client,
|
|
39
|
+
logging_settings: job_definition_t.JobLoggingSettings,
|
|
40
|
+
job_uuid: str,
|
|
37
41
|
) -> entity_t.Entity:
|
|
38
42
|
run_entity = client.create_entity(
|
|
39
43
|
entity_type=entity_t.EntityType.ASYNC_JOB,
|
|
@@ -49,6 +53,10 @@ def _create_run_entity(
|
|
|
49
53
|
field_ref_name=async_jobs_t.ASYNC_JOB_STATUS_FIELD_REF_NAME,
|
|
50
54
|
value=async_jobs_t.AsyncJobStatus.IN_PROGRESS,
|
|
51
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
|
+
),
|
|
52
60
|
],
|
|
53
61
|
).entity
|
|
54
62
|
client.transition_entity_phase(
|
|
@@ -93,7 +101,9 @@ def execute_job(
|
|
|
93
101
|
and job_definition.logging_settings.enabled
|
|
94
102
|
):
|
|
95
103
|
run_entity = _create_run_entity(
|
|
96
|
-
client=args.client,
|
|
104
|
+
client=args.client,
|
|
105
|
+
logging_settings=job_definition.logging_settings,
|
|
106
|
+
job_uuid=args.job_uuid,
|
|
97
107
|
)
|
|
98
108
|
result = job.run_outer(args=args)
|
|
99
109
|
except Exception as e:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import datetime
|
|
1
2
|
import io
|
|
2
3
|
import os
|
|
3
4
|
import re
|
|
4
|
-
from datetime import
|
|
5
|
+
from datetime import UTC
|
|
5
6
|
|
|
6
7
|
import paramiko
|
|
7
8
|
|
|
@@ -10,12 +11,12 @@ from pkgs.filesystem_utils import (
|
|
|
10
11
|
FileSystemFileReference,
|
|
11
12
|
FileSystemObject,
|
|
12
13
|
FileSystemS3Config,
|
|
14
|
+
FileSystemSession,
|
|
13
15
|
FileSystemSFTPConfig,
|
|
14
16
|
FileTransfer,
|
|
15
17
|
S3Session,
|
|
16
18
|
SFTPSession,
|
|
17
19
|
)
|
|
18
|
-
from pkgs.filesystem_utils.filesystem_session import FileSystemSession
|
|
19
20
|
from uncountable.core.file_upload import DataFileUpload, FileUpload
|
|
20
21
|
from uncountable.integration.job import Job, JobArguments
|
|
21
22
|
from uncountable.integration.secret_retrieval import retrieve_secret
|
|
@@ -33,6 +34,27 @@ from uncountable.types.job_definition_t import (
|
|
|
33
34
|
)
|
|
34
35
|
|
|
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
|
+
|
|
36
58
|
def _filter_files_by_keyword(
|
|
37
59
|
remote_directory: GenericRemoteDirectoryScope,
|
|
38
60
|
files: list[FileObjectData],
|
|
@@ -41,7 +63,20 @@ def _filter_files_by_keyword(
|
|
|
41
63
|
if remote_directory.detection_keyword is None:
|
|
42
64
|
return files
|
|
43
65
|
|
|
44
|
-
|
|
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
|
|
45
80
|
|
|
46
81
|
|
|
47
82
|
def _filter_by_filename(
|
|
@@ -130,7 +165,7 @@ def _move_files_post_upload(
|
|
|
130
165
|
appended_text = ""
|
|
131
166
|
|
|
132
167
|
if remote_directory_scope.prepend_date_on_archive:
|
|
133
|
-
appended_text = f"-{datetime.now(
|
|
168
|
+
appended_text = f"-{datetime.datetime.now(UTC).timestamp()}"
|
|
134
169
|
|
|
135
170
|
for file_path in success_file_paths:
|
|
136
171
|
filename = os.path.split(file_path)[-1]
|
|
@@ -184,16 +219,33 @@ class GenericUploadJob(Job[None]):
|
|
|
184
219
|
def _construct_filesystem_session(self, args: JobArguments) -> FileSystemSession:
|
|
185
220
|
match self.data_source:
|
|
186
221
|
case GenericUploadDataSourceSFTP():
|
|
187
|
-
pem_secret
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
)
|
|
197
249
|
return SFTPSession(sftp_config=sftp_config)
|
|
198
250
|
case GenericUploadDataSourceS3():
|
|
199
251
|
if self.data_source.access_key_secret is not None:
|
|
@@ -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
|
uncountable/integration/job.py
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import hmac
|
|
2
3
|
import typing
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
|
|
7
|
+
import simplejson
|
|
8
|
+
|
|
6
9
|
from pkgs.argument_parser import CachedParser
|
|
10
|
+
from pkgs.serialization_util import serialize_for_api
|
|
7
11
|
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
8
12
|
from uncountable.core.client import Client
|
|
13
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
14
|
+
from uncountable.core.file_upload import FileUpload
|
|
15
|
+
from uncountable.core.types import AuthDetailsOAuth
|
|
16
|
+
from uncountable.integration.http_server import (
|
|
17
|
+
GenericHttpRequest,
|
|
18
|
+
GenericHttpResponse,
|
|
19
|
+
HttpException,
|
|
20
|
+
)
|
|
21
|
+
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
22
|
+
send_job_queue_message,
|
|
23
|
+
)
|
|
24
|
+
from uncountable.integration.queue_runner.command_server.types import (
|
|
25
|
+
CommandServerException,
|
|
26
|
+
)
|
|
27
|
+
from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
|
|
9
28
|
from uncountable.integration.telemetry import JobLogger
|
|
10
|
-
from uncountable.types import
|
|
11
|
-
|
|
29
|
+
from uncountable.types import (
|
|
30
|
+
base_t,
|
|
31
|
+
job_definition_t,
|
|
32
|
+
queued_job_t,
|
|
33
|
+
webhook_job_t,
|
|
34
|
+
)
|
|
35
|
+
from uncountable.types.job_definition_t import (
|
|
36
|
+
HttpJobDefinitionBase,
|
|
37
|
+
JobDefinition,
|
|
38
|
+
JobResult,
|
|
39
|
+
ProfileMetadata,
|
|
40
|
+
)
|
|
12
41
|
|
|
13
42
|
|
|
14
43
|
@dataclass(kw_only=True)
|
|
@@ -19,16 +48,14 @@ class JobArguments:
|
|
|
19
48
|
batch_processor: AsyncBatchProcessor
|
|
20
49
|
logger: JobLogger
|
|
21
50
|
payload: base_t.JsonValue
|
|
51
|
+
job_uuid: str
|
|
22
52
|
|
|
23
53
|
|
|
24
54
|
# only for compatibility:
|
|
25
55
|
CronJobArguments = JobArguments
|
|
26
56
|
|
|
27
57
|
|
|
28
|
-
PT
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class Job(ABC, typing.Generic[PT]):
|
|
58
|
+
class Job[PT](ABC):
|
|
32
59
|
_unc_job_registered: bool = False
|
|
33
60
|
|
|
34
61
|
@property
|
|
@@ -62,7 +89,83 @@ class CronJob(Job):
|
|
|
62
89
|
WPT = typing.TypeVar("WPT")
|
|
63
90
|
|
|
64
91
|
|
|
65
|
-
|
|
92
|
+
@dataclass(kw_only=True)
|
|
93
|
+
class WebhookResponse:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _RequestValidatorClient(Client):
|
|
98
|
+
def __init__(self, *, base_url: str, oauth_bearer_token: str):
|
|
99
|
+
super().__init__(
|
|
100
|
+
base_url=base_url,
|
|
101
|
+
auth_details=AuthDetailsOAuth(refresh_token=""),
|
|
102
|
+
config=None,
|
|
103
|
+
)
|
|
104
|
+
self._oauth_bearer_token = oauth_bearer_token
|
|
105
|
+
|
|
106
|
+
def _get_oauth_bearer_token(self, *, oauth_details: AuthDetailsOAuth) -> str:
|
|
107
|
+
return self._oauth_bearer_token
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CustomHttpJob(Job[GenericHttpRequest]):
|
|
111
|
+
@property
|
|
112
|
+
def payload_type(self) -> type[GenericHttpRequest]:
|
|
113
|
+
return GenericHttpRequest
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def validate_request(
|
|
118
|
+
*,
|
|
119
|
+
request: GenericHttpRequest,
|
|
120
|
+
job_definition: HttpJobDefinitionBase,
|
|
121
|
+
profile_meta: ProfileMetadata,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Validate that the request is valid. If the request is invalid, raise an
|
|
125
|
+
exception.
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def get_validated_oauth_request_user_id(
|
|
131
|
+
*, profile_metadata: ProfileMetadata, request: GenericHttpRequest
|
|
132
|
+
) -> base_t.ObjectId:
|
|
133
|
+
token = request.headers.get("Authorization", "").replace("Bearer ", "")
|
|
134
|
+
if token == "":
|
|
135
|
+
raise HttpException(
|
|
136
|
+
message="unauthorized; no bearer token in request", error_code=401
|
|
137
|
+
)
|
|
138
|
+
return (
|
|
139
|
+
_RequestValidatorClient(
|
|
140
|
+
base_url=profile_metadata.base_url,
|
|
141
|
+
oauth_bearer_token=token,
|
|
142
|
+
)
|
|
143
|
+
.get_current_user_info()
|
|
144
|
+
.user_id
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
@abstractmethod
|
|
149
|
+
def handle_request(
|
|
150
|
+
*,
|
|
151
|
+
request: GenericHttpRequest,
|
|
152
|
+
job_definition: HttpJobDefinitionBase,
|
|
153
|
+
profile_meta: ProfileMetadata,
|
|
154
|
+
) -> GenericHttpResponse:
|
|
155
|
+
"""
|
|
156
|
+
Handle the request synchronously. Normally this should just enqueue a job
|
|
157
|
+
and return immediately (see WebhookJob as an example).
|
|
158
|
+
"""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
162
|
+
args.logger.log_warning(
|
|
163
|
+
message=f"Unexpected call to run_outer for CustomHttpJob: {args.job_definition.id}"
|
|
164
|
+
)
|
|
165
|
+
return JobResult(success=False)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
66
169
|
@property
|
|
67
170
|
def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
|
|
68
171
|
return webhook_job_t.WebhookEventPayload
|
|
@@ -71,6 +174,60 @@ class WebhookJob(Job[webhook_job_t.WebhookEventPayload], typing.Generic[WPT]):
|
|
|
71
174
|
@abstractmethod
|
|
72
175
|
def webhook_payload_type(self) -> type[WPT]: ...
|
|
73
176
|
|
|
177
|
+
@staticmethod
|
|
178
|
+
def validate_request(
|
|
179
|
+
*,
|
|
180
|
+
request: GenericHttpRequest,
|
|
181
|
+
job_definition: job_definition_t.HttpJobDefinitionBase,
|
|
182
|
+
profile_meta: ProfileMetadata,
|
|
183
|
+
) -> None:
|
|
184
|
+
assert isinstance(job_definition, job_definition_t.WebhookJobDefinition)
|
|
185
|
+
signature_key = retrieve_secret(
|
|
186
|
+
profile_metadata=profile_meta,
|
|
187
|
+
secret_retrieval=job_definition.signature_key_secret,
|
|
188
|
+
)
|
|
189
|
+
passed_signature = request.headers.get("Uncountable-Webhook-Signature")
|
|
190
|
+
if passed_signature is None:
|
|
191
|
+
raise HttpException.no_signature_passed()
|
|
192
|
+
|
|
193
|
+
request_body_signature = hmac.new(
|
|
194
|
+
signature_key.encode("utf-8"), msg=request.body_bytes, digestmod="sha256"
|
|
195
|
+
).hexdigest()
|
|
196
|
+
|
|
197
|
+
if request_body_signature != passed_signature:
|
|
198
|
+
raise HttpException.payload_failed_signature()
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def handle_request(
|
|
202
|
+
*,
|
|
203
|
+
request: GenericHttpRequest,
|
|
204
|
+
job_definition: job_definition_t.HttpJobDefinitionBase,
|
|
205
|
+
profile_meta: ProfileMetadata, # noqa: ARG004
|
|
206
|
+
) -> GenericHttpResponse:
|
|
207
|
+
try:
|
|
208
|
+
request_body = simplejson.loads(request.body_text)
|
|
209
|
+
webhook_payload = typing.cast(base_t.JsonValue, request_body)
|
|
210
|
+
except (simplejson.JSONDecodeError, ValueError) as e:
|
|
211
|
+
raise HttpException.body_parse_error() from e
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
send_job_queue_message(
|
|
215
|
+
job_ref_name=job_definition.id,
|
|
216
|
+
payload=queued_job_t.QueuedJobPayload(
|
|
217
|
+
invocation_context=queued_job_t.InvocationContextWebhook(
|
|
218
|
+
webhook_payload=webhook_payload
|
|
219
|
+
)
|
|
220
|
+
),
|
|
221
|
+
port=get_local_admin_server_port(),
|
|
222
|
+
)
|
|
223
|
+
except CommandServerException as e:
|
|
224
|
+
raise HttpException.unknown_error() from e
|
|
225
|
+
|
|
226
|
+
return GenericHttpResponse(
|
|
227
|
+
response=simplejson.dumps(serialize_for_api(WebhookResponse())),
|
|
228
|
+
status_code=200,
|
|
229
|
+
)
|
|
230
|
+
|
|
74
231
|
def run_outer(self, args: JobArguments) -> JobResult:
|
|
75
232
|
webhook_body = self.get_payload(args.payload)
|
|
76
233
|
inner_payload = CachedParser(self.webhook_payload_type).parse_api(
|
|
@@ -85,3 +242,31 @@ class WebhookJob(Job[webhook_job_t.WebhookEventPayload], typing.Generic[WPT]):
|
|
|
85
242
|
def register_job(cls: type[Job]) -> type[Job]:
|
|
86
243
|
cls._unc_job_registered = True
|
|
87
244
|
return cls
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class RunsheetWebhookJob(WebhookJob[webhook_job_t.RunsheetWebhookPayload]):
|
|
248
|
+
@property
|
|
249
|
+
def webhook_payload_type(self) -> type:
|
|
250
|
+
return webhook_job_t.RunsheetWebhookPayload
|
|
251
|
+
|
|
252
|
+
@abstractmethod
|
|
253
|
+
def build_runsheet(
|
|
254
|
+
self,
|
|
255
|
+
*,
|
|
256
|
+
args: JobArguments,
|
|
257
|
+
payload: webhook_job_t.RunsheetWebhookPayload,
|
|
258
|
+
) -> FileUpload: ...
|
|
259
|
+
|
|
260
|
+
def run(
|
|
261
|
+
self, args: JobArguments, payload: webhook_job_t.RunsheetWebhookPayload
|
|
262
|
+
) -> JobResult:
|
|
263
|
+
runsheet = self.build_runsheet(args=args, payload=payload)
|
|
264
|
+
|
|
265
|
+
files = args.client.upload_files(file_uploads=[runsheet])
|
|
266
|
+
args.client.complete_async_upload(
|
|
267
|
+
async_job_id=payload.async_job_id, file_id=files[0].file_id
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return JobResult(
|
|
271
|
+
success=True,
|
|
272
|
+
)
|
|
@@ -4,6 +4,8 @@ from .types import (
|
|
|
4
4
|
CommandEnqueueJob,
|
|
5
5
|
CommandEnqueueJobResponse,
|
|
6
6
|
CommandQueue,
|
|
7
|
+
CommandRetryJob,
|
|
8
|
+
CommandRetryJobResponse,
|
|
7
9
|
CommandServerBadResponse,
|
|
8
10
|
CommandServerException,
|
|
9
11
|
CommandServerTimeout,
|
|
@@ -16,6 +18,8 @@ __all__: list[str] = [
|
|
|
16
18
|
"send_job_queue_message",
|
|
17
19
|
"CommandEnqueueJob",
|
|
18
20
|
"CommandEnqueueJobResponse",
|
|
21
|
+
"CommandRetryJob",
|
|
22
|
+
"CommandRetryJobResponse",
|
|
19
23
|
"CommandTask",
|
|
20
24
|
"CommandQueue",
|
|
21
25
|
"CommandServerTimeout",
|
|
@@ -10,6 +10,12 @@ from uncountable.integration.queue_runner.command_server.protocol.command_server
|
|
|
10
10
|
CheckHealthResult,
|
|
11
11
|
EnqueueJobRequest,
|
|
12
12
|
EnqueueJobResult,
|
|
13
|
+
ListQueuedJobsRequest,
|
|
14
|
+
ListQueuedJobsResult,
|
|
15
|
+
RetryJobRequest,
|
|
16
|
+
RetryJobResult,
|
|
17
|
+
VaccuumQueuedJobsRequest,
|
|
18
|
+
VaccuumQueuedJobsResult,
|
|
13
19
|
)
|
|
14
20
|
from uncountable.integration.queue_runner.command_server.types import (
|
|
15
21
|
CommandServerBadResponse,
|
|
@@ -57,6 +63,26 @@ def send_job_queue_message(
|
|
|
57
63
|
return response.queued_job_uuid
|
|
58
64
|
|
|
59
65
|
|
|
66
|
+
def send_retry_job_message(
|
|
67
|
+
*,
|
|
68
|
+
job_uuid: str,
|
|
69
|
+
host: str = "localhost",
|
|
70
|
+
port: int,
|
|
71
|
+
) -> str:
|
|
72
|
+
with command_server_connection(host=host, port=port) as stub:
|
|
73
|
+
request = RetryJobRequest(uuid=job_uuid)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
response = stub.RetryJob(request, timeout=_DEFAULT_MESSAGE_TIMEOUT_SECS)
|
|
77
|
+
assert isinstance(response, RetryJobResult)
|
|
78
|
+
if not response.successfully_queued:
|
|
79
|
+
raise CommandServerBadResponse("queue operation was not successful")
|
|
80
|
+
|
|
81
|
+
return response.queued_job_uuid
|
|
82
|
+
except grpc.RpcError as e:
|
|
83
|
+
raise ValueError(e.details()) # type: ignore
|
|
84
|
+
|
|
85
|
+
|
|
60
86
|
def check_health(*, host: str = _LOCAL_RPC_HOST, port: int) -> bool:
|
|
61
87
|
with command_server_connection(host=host, port=port) as stub:
|
|
62
88
|
request = CheckHealthRequest()
|
|
@@ -66,3 +92,42 @@ def check_health(*, host: str = _LOCAL_RPC_HOST, port: int) -> bool:
|
|
|
66
92
|
assert isinstance(response, CheckHealthResult)
|
|
67
93
|
|
|
68
94
|
return response.success
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def send_list_queued_jobs_message(
|
|
98
|
+
*,
|
|
99
|
+
offset: int,
|
|
100
|
+
limit: int,
|
|
101
|
+
host: str = "localhost",
|
|
102
|
+
port: int,
|
|
103
|
+
) -> list[ListQueuedJobsResult.ListQueuedJobsResultItem]:
|
|
104
|
+
with command_server_connection(host=host, port=port) as stub:
|
|
105
|
+
request = ListQueuedJobsRequest(
|
|
106
|
+
offset=offset,
|
|
107
|
+
limit=limit,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
response = stub.ListQueuedJobs(
|
|
112
|
+
request, timeout=_DEFAULT_MESSAGE_TIMEOUT_SECS
|
|
113
|
+
)
|
|
114
|
+
except grpc.RpcError as e:
|
|
115
|
+
raise ValueError(e.details()) # type: ignore
|
|
116
|
+
|
|
117
|
+
assert isinstance(response, ListQueuedJobsResult)
|
|
118
|
+
return list(response.queued_jobs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def send_vaccuum_queued_jobs_message(*, host: str = "localhost", port: int) -> None:
|
|
122
|
+
with command_server_connection(host=host, port=port) as stub:
|
|
123
|
+
request = VaccuumQueuedJobsRequest()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
response = stub.VaccuumQueuedJobs(
|
|
127
|
+
request, timeout=_DEFAULT_MESSAGE_TIMEOUT_SECS
|
|
128
|
+
)
|
|
129
|
+
except grpc.RpcError as e:
|
|
130
|
+
raise ValueError(e.details()) # type: ignore
|
|
131
|
+
|
|
132
|
+
assert isinstance(response, VaccuumQueuedJobsResult)
|
|
133
|
+
return None
|