UncountablePythonSDK 0.0.52__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 +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/async_batch.py +3 -3
- examples/basic_auth.py +7 -0
- examples/create_entity.py +3 -1
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +4 -2
- 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 +4 -1
- 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 +1 -2
- pkgs/argument_parser/__init__.py +9 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +217 -70
- pkgs/filesystem_utils/__init__.py +1 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +10 -7
- pkgs/filesystem_utils/_s3_session.py +15 -13
- pkgs/filesystem_utils/_sftp_session.py +11 -7
- pkgs/filesystem_utils/file_type_utils.py +30 -10
- pkgs/py.typed +0 -0
- 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 +47 -26
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +17 -14
- pkgs/serialization/yaml.py +4 -1
- 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 +5 -5
- pkgs/type_spec/builder.py +354 -119
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +51 -11
- 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 +127 -36
- pkgs/type_spec/emit_open_api_util.py +5 -6
- pkgs/type_spec/emit_python.py +329 -121
- pkgs/type_spec/emit_typescript.py +117 -256
- pkgs/type_spec/emit_typescript_util.py +291 -2
- pkgs/type_spec/load_types.py +18 -4
- 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 +124 -29
- 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 +26 -9
- 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/core/async_batch.py +1 -1
- uncountable/core/client.py +142 -39
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +52 -18
- uncountable/integration/cli.py +142 -0
- uncountable/integration/construct_client.py +8 -8
- uncountable/integration/cron.py +11 -37
- uncountable/integration/db/connect.py +12 -2
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +8 -37
- uncountable/integration/executors/executors.py +125 -2
- uncountable/integration/executors/generic_upload_executor.py +87 -29
- uncountable/integration/executors/script_executor.py +3 -3
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +242 -12
- 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/retrieve_secret.py +26 -4
- uncountable/integration/server.py +94 -69
- uncountable/integration/telemetry.py +150 -34
- uncountable/integration/webhook_server/entrypoint.py +97 -0
- uncountable/types/__init__.py +78 -1
- uncountable/types/api/batch/execute_batch.py +13 -6
- uncountable/types/api/batch/execute_batch_load_async.py +9 -3
- uncountable/types/api/chemical/convert_chemical_formats.py +17 -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 +19 -7
- uncountable/types/api/entity/create_entity.py +17 -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 +13 -4
- 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 +42 -10
- uncountable/types/api/entity/lock_entity.py +11 -4
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +15 -6
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +10 -3
- uncountable/types/api/entity/transition_entity_phase.py +22 -7
- uncountable/types/api/entity/unlock_entity.py +10 -3
- uncountable/types/api/equipment/associate_equipment_input.py +9 -3
- uncountable/types/api/field_options/upsert_field_options.py +17 -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 +16 -7
- uncountable/types/api/id_source/match_id_source.py +14 -5
- uncountable/types/api/input_groups/get_input_group_names.py +13 -4
- uncountable/types/api/inputs/create_inputs.py +23 -9
- uncountable/types/api/inputs/get_input_data.py +30 -12
- uncountable/types/api/inputs/get_input_names.py +16 -7
- uncountable/types/api/inputs/get_inputs_data.py +25 -7
- uncountable/types/api/inputs/set_input_attribute_values.py +12 -6
- uncountable/types/api/inputs/set_input_category.py +12 -5
- uncountable/types/api/inputs/set_input_subcategories.py +10 -3
- uncountable/types/api/inputs/set_intermediate_type.py +11 -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 +10 -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 +28 -13
- uncountable/types/api/outputs/get_output_names.py +15 -6
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +20 -8
- uncountable/types/api/permissions/set_core_permissions.py +26 -10
- uncountable/types/api/project/get_projects.py +16 -7
- uncountable/types/api/project/get_projects_data.py +17 -8
- uncountable/types/api/recipe_links/create_recipe_link.py +12 -5
- uncountable/types/api/recipe_links/remove_recipe_link.py +11 -4
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +16 -7
- uncountable/types/api/recipes/add_recipe_to_project.py +10 -3
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +11 -4
- uncountable/types/api/recipes/associate_recipe_as_input.py +12 -5
- uncountable/types/api/recipes/associate_recipe_as_lot.py +10 -3
- 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 +15 -9
- uncountable/types/api/recipes/create_recipes.py +21 -9
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +10 -3
- uncountable/types/api/recipes/edit_recipe_inputs.py +134 -22
- uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
- uncountable/types/api/recipes/get_curve.py +11 -5
- uncountable/types/api/recipes/get_recipe_calculations.py +13 -7
- uncountable/types/api/recipes/get_recipe_links.py +10 -4
- uncountable/types/api/recipes/get_recipe_names.py +13 -4
- uncountable/types/api/recipes/get_recipe_output_metadata.py +12 -6
- uncountable/types/api/recipes/get_recipes_data.py +87 -33
- uncountable/types/api/recipes/lock_recipes.py +19 -8
- uncountable/types/api/recipes/remove_recipe_from_project.py +10 -3
- uncountable/types/api/recipes/set_recipe_inputs.py +16 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +10 -3
- uncountable/types/api/recipes/set_recipe_output_annotations.py +24 -12
- uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +35 -12
- uncountable/types/api/recipes/set_recipe_tags.py +26 -9
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +10 -3
- uncountable/types/api/recipes/unlock_recipes.py +14 -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 +11 -4
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +13 -6
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +2 -1
- uncountable/types/async_batch_processor.py +618 -18
- uncountable/types/async_batch_t.py +54 -7
- 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 +0 -1
- uncountable/types/base_t.py +13 -11
- uncountable/types/calculations.py +0 -1
- uncountable/types/calculations_t.py +5 -2
- uncountable/types/chemical_structure.py +0 -1
- uncountable/types/chemical_structure_t.py +6 -5
- uncountable/types/client_base.py +751 -70
- uncountable/types/client_config.py +1 -1
- uncountable/types/client_config_t.py +17 -3
- uncountable/types/curves.py +0 -1
- uncountable/types/curves_t.py +10 -7
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +4 -1
- uncountable/types/entity_t.py +125 -7
- uncountable/types/experiment_groups.py +0 -1
- uncountable/types/experiment_groups_t.py +5 -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 +246 -9
- uncountable/types/fields.py +0 -1
- uncountable/types/fields_t.py +5 -2
- uncountable/types/generic_upload.py +6 -1
- uncountable/types/generic_upload_t.py +88 -9
- uncountable/types/id_source.py +0 -1
- uncountable/types/id_source_t.py +26 -7
- uncountable/types/identifier.py +0 -1
- uncountable/types/identifier_t.py +13 -5
- uncountable/types/input_attributes.py +0 -1
- uncountable/types/input_attributes_t.py +4 -4
- uncountable/types/inputs.py +1 -1
- uncountable/types/inputs_t.py +24 -4
- 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 +4 -6
- uncountable/types/job_definition_t.py +96 -65
- 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 +6 -3
- uncountable/types/overrides.py +9 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +0 -1
- uncountable/types/permissions_t.py +1 -2
- uncountable/types/phases.py +0 -1
- uncountable/types/phases_t.py +5 -2
- uncountable/types/post_base.py +0 -1
- uncountable/types/post_base_t.py +1 -2
- uncountable/types/queued_job.py +17 -0
- uncountable/types/queued_job_t.py +140 -0
- uncountable/types/recipe_identifiers.py +0 -1
- uncountable/types/recipe_identifiers_t.py +21 -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 +7 -4
- uncountable/types/recipe_metadata.py +0 -1
- uncountable/types/recipe_metadata_t.py +14 -9
- uncountable/types/recipe_output_metadata.py +0 -1
- uncountable/types/recipe_output_metadata_t.py +5 -2
- uncountable/types/recipe_tags.py +0 -1
- uncountable/types/recipe_tags_t.py +5 -2
- uncountable/types/recipe_workflow_steps.py +0 -1
- uncountable/types/recipe_workflow_steps_t.py +14 -7
- uncountable/types/recipes.py +0 -1
- uncountable/types/recipes_t.py +6 -2
- uncountable/types/response.py +0 -1
- uncountable/types/response_t.py +3 -2
- uncountable/types/secret_retrieval.py +0 -1
- uncountable/types/secret_retrieval_t.py +13 -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 +5 -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 +5 -2
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +48 -0
- uncountable/types/workflows.py +0 -1
- uncountable/types/workflows_t.py +10 -4
- uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
- UncountablePythonSDK-0.0.52.dist-info/METADATA +0 -56
- UncountablePythonSDK-0.0.52.dist-info/RECORD +0 -246
- docs/quickstart.md +0 -19
- uncountable/core/version.py +0 -11
- {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -0
|
@@ -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(
|
|
@@ -68,7 +103,8 @@ def _filter_by_file_extension(
|
|
|
68
103
|
file
|
|
69
104
|
for file in files
|
|
70
105
|
if file.filename is not None
|
|
71
|
-
and os.path.splitext(file.filename)[-1]
|
|
106
|
+
and os.path.splitext(file.filename)[-1]
|
|
107
|
+
in remote_directory.valid_file_extensions
|
|
72
108
|
]
|
|
73
109
|
|
|
74
110
|
|
|
@@ -129,7 +165,7 @@ def _move_files_post_upload(
|
|
|
129
165
|
appended_text = ""
|
|
130
166
|
|
|
131
167
|
if remote_directory_scope.prepend_date_on_archive:
|
|
132
|
-
appended_text = f"-{datetime.now(
|
|
168
|
+
appended_text = f"-{datetime.datetime.now(UTC).timestamp()}"
|
|
133
169
|
|
|
134
170
|
for file_path in success_file_paths:
|
|
135
171
|
filename = os.path.split(file_path)[-1]
|
|
@@ -164,7 +200,7 @@ def _move_files_post_upload(
|
|
|
164
200
|
filesystem_session.move_files([*success_file_transfers, *failed_file_transfers])
|
|
165
201
|
|
|
166
202
|
|
|
167
|
-
class GenericUploadJob(Job):
|
|
203
|
+
class GenericUploadJob(Job[None]):
|
|
168
204
|
def __init__(
|
|
169
205
|
self,
|
|
170
206
|
data_source: GenericUploadDataSource,
|
|
@@ -176,19 +212,40 @@ class GenericUploadJob(Job):
|
|
|
176
212
|
self.upload_strategy = upload_strategy
|
|
177
213
|
self.data_source = data_source
|
|
178
214
|
|
|
215
|
+
@property
|
|
216
|
+
def payload_type(self) -> type[None]:
|
|
217
|
+
return type(None)
|
|
218
|
+
|
|
179
219
|
def _construct_filesystem_session(self, args: JobArguments) -> FileSystemSession:
|
|
180
220
|
match self.data_source:
|
|
181
221
|
case GenericUploadDataSourceSFTP():
|
|
182
|
-
pem_secret
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
)
|
|
192
249
|
return SFTPSession(sftp_config=sftp_config)
|
|
193
250
|
case GenericUploadDataSourceS3():
|
|
194
251
|
if self.data_source.access_key_secret is not None:
|
|
@@ -200,19 +257,17 @@ class GenericUploadJob(Job):
|
|
|
200
257
|
secret_access_key = None
|
|
201
258
|
|
|
202
259
|
if self.data_source.endpoint_url is None:
|
|
203
|
-
assert (
|
|
204
|
-
|
|
205
|
-
)
|
|
260
|
+
assert self.data_source.cloud_provider is not None, (
|
|
261
|
+
"either cloud_provider or endpoint_url must be specified"
|
|
262
|
+
)
|
|
206
263
|
match self.data_source.cloud_provider:
|
|
207
264
|
case S3CloudProvider.AWS:
|
|
208
265
|
endpoint_url = "https://s3.amazonaws.com"
|
|
209
266
|
case S3CloudProvider.OVH:
|
|
210
|
-
assert (
|
|
211
|
-
|
|
212
|
-
), "region_name must be specified for cloud_provider OVH"
|
|
213
|
-
endpoint_url = (
|
|
214
|
-
f"https://s3.{self.data_source.region_name}.cloud.ovh.net"
|
|
267
|
+
assert self.data_source.region_name is not None, (
|
|
268
|
+
"region_name must be specified for cloud_provider OVH"
|
|
215
269
|
)
|
|
270
|
+
endpoint_url = f"https://s3.{self.data_source.region_name}.cloud.ovh.net"
|
|
216
271
|
else:
|
|
217
272
|
endpoint_url = self.data_source.endpoint_url
|
|
218
273
|
|
|
@@ -227,7 +282,7 @@ class GenericUploadJob(Job):
|
|
|
227
282
|
|
|
228
283
|
return S3Session(s3_config=s3_config)
|
|
229
284
|
|
|
230
|
-
def
|
|
285
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
231
286
|
client = args.client
|
|
232
287
|
batch_processor = args.batch_processor
|
|
233
288
|
logger = args.logger
|
|
@@ -248,7 +303,8 @@ class GenericUploadJob(Job):
|
|
|
248
303
|
for file_data in filtered_file_data:
|
|
249
304
|
files_to_upload.append(
|
|
250
305
|
DataFileUpload(
|
|
251
|
-
data=io.BytesIO(file_data.file_data),
|
|
306
|
+
data=io.BytesIO(file_data.file_data),
|
|
307
|
+
name=file_data.filename,
|
|
252
308
|
)
|
|
253
309
|
)
|
|
254
310
|
if not self.upload_strategy.skip_moving_files:
|
|
@@ -256,7 +312,9 @@ class GenericUploadJob(Job):
|
|
|
256
312
|
filesystem_session=filesystem_session,
|
|
257
313
|
remote_directory_scope=remote_directory,
|
|
258
314
|
success_file_paths=[
|
|
259
|
-
file.filepath
|
|
315
|
+
file.filepath
|
|
316
|
+
if file.filepath is not None
|
|
317
|
+
else file.filename
|
|
260
318
|
for file in filtered_file_data
|
|
261
319
|
],
|
|
262
320
|
# IMPROVE: use triggers/webhooks to mark failed files as failed
|
|
@@ -267,12 +325,12 @@ class GenericUploadJob(Job):
|
|
|
267
325
|
|
|
268
326
|
file_ids = [file.file_id for file in uploaded_files]
|
|
269
327
|
|
|
270
|
-
for
|
|
328
|
+
for destination in self.upload_strategy.destinations:
|
|
271
329
|
for file_id in file_ids:
|
|
272
330
|
batch_processor.invoke_uploader(
|
|
273
331
|
file_id=file_id,
|
|
274
332
|
uploader_key=self.upload_strategy.uploader_key,
|
|
275
|
-
|
|
333
|
+
destination=destination,
|
|
276
334
|
)
|
|
277
335
|
|
|
278
336
|
return JobResult(success=True)
|
|
@@ -19,7 +19,7 @@ def resolve_script_executor(
|
|
|
19
19
|
for _, job_class in inspect.getmembers(job_module, inspect.isclass):
|
|
20
20
|
if getattr(job_class, "_unc_job_registered", False):
|
|
21
21
|
found_jobs.append(job_class())
|
|
22
|
-
assert (
|
|
23
|
-
len(found_jobs)
|
|
24
|
-
)
|
|
22
|
+
assert len(found_jobs) == 1, (
|
|
23
|
+
f"expected exactly one job class in {executor.import_path}, found {len(found_jobs)}"
|
|
24
|
+
)
|
|
25
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
|
uncountable/integration/job.py
CHANGED
|
@@ -1,42 +1,272 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import hmac
|
|
3
|
+
import typing
|
|
1
4
|
from abc import ABC, abstractmethod
|
|
2
5
|
from dataclasses import dataclass
|
|
3
6
|
|
|
7
|
+
import simplejson
|
|
8
|
+
|
|
9
|
+
from pkgs.argument_parser import CachedParser
|
|
10
|
+
from pkgs.serialization_util import serialize_for_api
|
|
4
11
|
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
5
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
|
|
6
28
|
from uncountable.integration.telemetry import JobLogger
|
|
7
|
-
from uncountable.types
|
|
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
|
+
)
|
|
8
41
|
|
|
9
42
|
|
|
10
|
-
@dataclass
|
|
11
|
-
class
|
|
43
|
+
@dataclass(kw_only=True)
|
|
44
|
+
class JobArguments:
|
|
12
45
|
job_definition: JobDefinition
|
|
13
46
|
profile_metadata: ProfileMetadata
|
|
14
47
|
client: Client
|
|
15
48
|
batch_processor: AsyncBatchProcessor
|
|
16
49
|
logger: JobLogger
|
|
50
|
+
payload: base_t.JsonValue
|
|
51
|
+
job_uuid: str
|
|
17
52
|
|
|
18
53
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# can imagine passing additional data such as in the sftp or webhook cases
|
|
22
|
-
pass
|
|
54
|
+
# only for compatibility:
|
|
55
|
+
CronJobArguments = JobArguments
|
|
23
56
|
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
class Job[PT](ABC):
|
|
59
|
+
_unc_job_registered: bool = False
|
|
26
60
|
|
|
61
|
+
@property
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def payload_type(self) -> type[PT]: ...
|
|
27
64
|
|
|
28
|
-
|
|
29
|
-
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def run_outer(self, args: JobArguments) -> JobResult: ...
|
|
67
|
+
|
|
68
|
+
@functools.cached_property
|
|
69
|
+
def _cached_payload_parser(self) -> CachedParser[PT]:
|
|
70
|
+
return CachedParser(self.payload_type)
|
|
71
|
+
|
|
72
|
+
def get_payload(self, payload: base_t.JsonValue) -> PT:
|
|
73
|
+
return self._cached_payload_parser.parse_storage(payload)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CronJob(Job):
|
|
77
|
+
@property
|
|
78
|
+
def payload_type(self) -> type[None]:
|
|
79
|
+
return type(None)
|
|
80
|
+
|
|
81
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
82
|
+
assert isinstance(args, CronJobArguments)
|
|
83
|
+
return self.run(args)
|
|
30
84
|
|
|
31
85
|
@abstractmethod
|
|
32
86
|
def run(self, args: JobArguments) -> JobResult: ...
|
|
33
87
|
|
|
34
88
|
|
|
35
|
-
|
|
89
|
+
WPT = typing.TypeVar("WPT")
|
|
90
|
+
|
|
91
|
+
|
|
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]):
|
|
169
|
+
@property
|
|
170
|
+
def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
|
|
171
|
+
return webhook_job_t.WebhookEventPayload
|
|
172
|
+
|
|
173
|
+
@property
|
|
36
174
|
@abstractmethod
|
|
37
|
-
def
|
|
175
|
+
def webhook_payload_type(self) -> type[WPT]: ...
|
|
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
|
+
|
|
231
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
232
|
+
webhook_body = self.get_payload(args.payload)
|
|
233
|
+
inner_payload = CachedParser(self.webhook_payload_type).parse_api(
|
|
234
|
+
webhook_body.data
|
|
235
|
+
)
|
|
236
|
+
return self.run(args, inner_payload)
|
|
237
|
+
|
|
238
|
+
@abstractmethod
|
|
239
|
+
def run(self, args: JobArguments, payload: WPT) -> JobResult: ...
|
|
38
240
|
|
|
39
241
|
|
|
40
242
|
def register_job(cls: type[Job]) -> type[Job]:
|
|
41
243
|
cls._unc_job_registered = True
|
|
42
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
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .command_client import check_health, send_job_queue_message
|
|
2
|
+
from .command_server import serve
|
|
3
|
+
from .types import (
|
|
4
|
+
CommandEnqueueJob,
|
|
5
|
+
CommandEnqueueJobResponse,
|
|
6
|
+
CommandQueue,
|
|
7
|
+
CommandRetryJob,
|
|
8
|
+
CommandRetryJobResponse,
|
|
9
|
+
CommandServerBadResponse,
|
|
10
|
+
CommandServerException,
|
|
11
|
+
CommandServerTimeout,
|
|
12
|
+
CommandTask,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = [
|
|
16
|
+
"serve",
|
|
17
|
+
"check_health",
|
|
18
|
+
"send_job_queue_message",
|
|
19
|
+
"CommandEnqueueJob",
|
|
20
|
+
"CommandEnqueueJobResponse",
|
|
21
|
+
"CommandRetryJob",
|
|
22
|
+
"CommandRetryJobResponse",
|
|
23
|
+
"CommandTask",
|
|
24
|
+
"CommandQueue",
|
|
25
|
+
"CommandServerTimeout",
|
|
26
|
+
"CommandServerException",
|
|
27
|
+
"CommandServerBadResponse",
|
|
28
|
+
]
|