UncountablePythonSDK 0.0.82__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 +22 -17
- 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.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
- UncountablePythonSDK-0.0.82.dist-info/METADATA +0 -60
- UncountablePythonSDK-0.0.82.dist-info/RECORD +0 -292
- docs/quickstart.md +0 -19
- {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import datetime
|
|
1
2
|
import multiprocessing
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import dataclass
|
|
6
|
-
from datetime import
|
|
7
|
+
from datetime import UTC
|
|
8
|
+
from enum import StrEnum
|
|
7
9
|
|
|
8
10
|
from opentelemetry.trace import get_current_span
|
|
9
11
|
|
|
@@ -18,11 +20,19 @@ from uncountable.integration.telemetry import Logger
|
|
|
18
20
|
|
|
19
21
|
SHUTDOWN_TIMEOUT_SECS = 30
|
|
20
22
|
|
|
23
|
+
AnyProcess = multiprocessing.Process | subprocess.Popen[bytes]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProcessName(StrEnum):
|
|
27
|
+
QUEUE_RUNNER = "queue_runner"
|
|
28
|
+
CRON_SERVER = "cron_server"
|
|
29
|
+
UWSGI = "uwsgi"
|
|
30
|
+
|
|
21
31
|
|
|
22
32
|
@dataclass(kw_only=True)
|
|
23
33
|
class ProcessInfo:
|
|
24
|
-
name:
|
|
25
|
-
process:
|
|
34
|
+
name: ProcessName
|
|
35
|
+
process: AnyProcess
|
|
26
36
|
|
|
27
37
|
@property
|
|
28
38
|
def is_alive(self) -> bool:
|
|
@@ -45,14 +55,14 @@ class ProcessInfo:
|
|
|
45
55
|
return self.process.poll()
|
|
46
56
|
|
|
47
57
|
|
|
48
|
-
def handle_shutdown(logger: Logger, processes:
|
|
58
|
+
def handle_shutdown(logger: Logger, processes: dict[ProcessName, ProcessInfo]) -> None:
|
|
49
59
|
logger.log_info("received shutdown command, shutting down sub-processes")
|
|
50
|
-
for proc_info in processes:
|
|
60
|
+
for proc_info in processes.values():
|
|
51
61
|
if proc_info.is_alive:
|
|
52
62
|
proc_info.process.terminate()
|
|
53
63
|
|
|
54
64
|
shutdown_start = time.time()
|
|
55
|
-
still_living_processes = processes
|
|
65
|
+
still_living_processes = list(processes.values())
|
|
56
66
|
while (
|
|
57
67
|
time.time() - shutdown_start < SHUTDOWN_TIMEOUT_SECS
|
|
58
68
|
and len(still_living_processes) > 0
|
|
@@ -81,46 +91,84 @@ def handle_shutdown(logger: Logger, processes: list[ProcessInfo]) -> None:
|
|
|
81
91
|
proc_info.process.kill()
|
|
82
92
|
|
|
83
93
|
|
|
84
|
-
def
|
|
85
|
-
|
|
94
|
+
def restart_process(
|
|
95
|
+
logger: Logger, proc_info: ProcessInfo, processes: dict[ProcessName, ProcessInfo]
|
|
96
|
+
) -> None:
|
|
97
|
+
logger.log_error(
|
|
98
|
+
f"process {proc_info.name} shut down unexpectedly - exit code {proc_info.exitcode}. Restarting..."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
match proc_info.name:
|
|
102
|
+
case ProcessName.QUEUE_RUNNER:
|
|
103
|
+
queue_proc = multiprocessing.Process(target=start_queue_runner)
|
|
104
|
+
queue_proc.start()
|
|
105
|
+
new_info = ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=queue_proc)
|
|
106
|
+
processes[ProcessName.QUEUE_RUNNER] = new_info
|
|
107
|
+
try:
|
|
108
|
+
_wait_queue_runner_online()
|
|
109
|
+
logger.log_info("queue runner restarted successfully")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.log_exception(e)
|
|
112
|
+
logger.log_error(
|
|
113
|
+
"queue runner failed to restart, shutting down scheduler"
|
|
114
|
+
)
|
|
115
|
+
handle_shutdown(logger, processes)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
case ProcessName.CRON_SERVER:
|
|
119
|
+
cron_proc = multiprocessing.Process(target=cron_target)
|
|
120
|
+
cron_proc.start()
|
|
121
|
+
new_info = ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_proc)
|
|
122
|
+
processes[ProcessName.CRON_SERVER] = new_info
|
|
123
|
+
logger.log_info("cron server restarted successfully")
|
|
124
|
+
|
|
125
|
+
case ProcessName.UWSGI:
|
|
126
|
+
uwsgi_proc: AnyProcess = subprocess.Popen(["uwsgi", "--die-on-term"])
|
|
127
|
+
new_info = ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_proc)
|
|
128
|
+
processes[ProcessName.UWSGI] = new_info
|
|
129
|
+
logger.log_info("uwsgi restarted successfully")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_process_alive(
|
|
133
|
+
logger: Logger, processes: dict[ProcessName, ProcessInfo]
|
|
134
|
+
) -> None:
|
|
135
|
+
for proc_info in processes.values():
|
|
86
136
|
if not proc_info.is_alive:
|
|
87
|
-
logger
|
|
88
|
-
f"process {proc_info.name} shut down unexpectedly! shutting down scheduler; exit code is {proc_info.exitcode}"
|
|
89
|
-
)
|
|
90
|
-
handle_shutdown(logger, processes)
|
|
91
|
-
sys.exit(1)
|
|
137
|
+
restart_process(logger, proc_info, processes)
|
|
92
138
|
|
|
93
139
|
|
|
94
140
|
def _wait_queue_runner_online() -> None:
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
MAX_QUEUE_RUNNER_HEALTH_CHECKS = 10
|
|
142
|
+
QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS = 1
|
|
97
143
|
|
|
98
144
|
num_attempts = 0
|
|
99
|
-
before = datetime.now(
|
|
100
|
-
while num_attempts <
|
|
145
|
+
before = datetime.datetime.now(UTC)
|
|
146
|
+
while num_attempts < MAX_QUEUE_RUNNER_HEALTH_CHECKS:
|
|
101
147
|
try:
|
|
102
148
|
if check_health(port=get_local_admin_server_port()):
|
|
103
149
|
return
|
|
104
150
|
except CommandServerTimeout:
|
|
105
151
|
pass
|
|
106
152
|
num_attempts += 1
|
|
107
|
-
time.sleep(
|
|
108
|
-
after = datetime.now(
|
|
153
|
+
time.sleep(QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS)
|
|
154
|
+
after = datetime.datetime.now(UTC)
|
|
109
155
|
duration_secs = (after - before).seconds
|
|
110
156
|
raise Exception(f"queue runner failed to come online after {duration_secs} seconds")
|
|
111
157
|
|
|
112
158
|
|
|
113
159
|
def main() -> None:
|
|
114
160
|
logger = Logger(get_current_span())
|
|
115
|
-
processes:
|
|
161
|
+
processes: dict[ProcessName, ProcessInfo] = {}
|
|
162
|
+
|
|
163
|
+
multiprocessing.set_start_method("forkserver")
|
|
116
164
|
|
|
117
165
|
def add_process(process: ProcessInfo) -> None:
|
|
118
|
-
processes.
|
|
166
|
+
processes[process.name] = process
|
|
119
167
|
logger.log_info(f"started process {process.name}")
|
|
120
168
|
|
|
121
169
|
runner_process = multiprocessing.Process(target=start_queue_runner)
|
|
122
170
|
runner_process.start()
|
|
123
|
-
add_process(ProcessInfo(name=
|
|
171
|
+
add_process(ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=runner_process))
|
|
124
172
|
|
|
125
173
|
try:
|
|
126
174
|
_wait_queue_runner_online()
|
|
@@ -131,13 +179,13 @@ def main() -> None:
|
|
|
131
179
|
|
|
132
180
|
cron_process = multiprocessing.Process(target=cron_target)
|
|
133
181
|
cron_process.start()
|
|
134
|
-
add_process(ProcessInfo(name=
|
|
182
|
+
add_process(ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_process))
|
|
135
183
|
|
|
136
184
|
uwsgi_process = subprocess.Popen([
|
|
137
185
|
"uwsgi",
|
|
138
186
|
"--die-on-term",
|
|
139
187
|
])
|
|
140
|
-
add_process(ProcessInfo(name=
|
|
188
|
+
add_process(ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_process))
|
|
141
189
|
|
|
142
190
|
try:
|
|
143
191
|
while True:
|
|
@@ -147,4 +195,5 @@ def main() -> None:
|
|
|
147
195
|
handle_shutdown(logger, processes=processes)
|
|
148
196
|
|
|
149
197
|
|
|
150
|
-
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|
|
@@ -54,7 +54,7 @@ def _load_secret_overrides(profile_name: str) -> dict[SecretRetrieval, str]:
|
|
|
54
54
|
profiles_module = os.environ["UNC_PROFILES_MODULE"]
|
|
55
55
|
try:
|
|
56
56
|
overrides = overrides_parser.parse_yaml_resource(
|
|
57
|
-
package="."
|
|
57
|
+
package=f"{profiles_module}.{profile_name}",
|
|
58
58
|
resource="local_overrides.yaml",
|
|
59
59
|
)
|
|
60
60
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import signal
|
|
2
2
|
from dataclasses import asdict
|
|
3
3
|
from types import TracebackType
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import assert_never
|
|
5
5
|
|
|
6
6
|
from apscheduler.executors.pool import ThreadPoolExecutor
|
|
7
7
|
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
@@ -11,16 +11,28 @@ from apscheduler.triggers.cron import CronTrigger
|
|
|
11
11
|
from opentelemetry.trace import get_current_span
|
|
12
12
|
from sqlalchemy.engine.base import Engine
|
|
13
13
|
|
|
14
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
14
15
|
from uncountable.integration.cron import CronJobArgs, cron_job_executor
|
|
16
|
+
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
17
|
+
send_vaccuum_queued_jobs_message,
|
|
18
|
+
)
|
|
15
19
|
from uncountable.integration.telemetry import Logger
|
|
16
20
|
from uncountable.types import base_t, job_definition_t
|
|
17
21
|
from uncountable.types.job_definition_t import (
|
|
18
22
|
CronJobDefinition,
|
|
19
|
-
|
|
23
|
+
HttpJobDefinitionBase,
|
|
20
24
|
)
|
|
21
25
|
|
|
22
26
|
_MAX_APSCHEDULER_CONCURRENT_JOBS = 1
|
|
23
27
|
|
|
28
|
+
VACCUUM_QUEUED_JOBS_JOB_ID = "vacuum_queued_jobs"
|
|
29
|
+
|
|
30
|
+
STATIC_JOB_IDS = {VACCUUM_QUEUED_JOBS_JOB_ID}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def vaccuum_queued_jobs() -> None:
|
|
34
|
+
send_vaccuum_queued_jobs_message(port=get_local_admin_server_port())
|
|
35
|
+
|
|
24
36
|
|
|
25
37
|
class IntegrationServer:
|
|
26
38
|
_scheduler: BaseScheduler
|
|
@@ -36,11 +48,27 @@ class IntegrationServer:
|
|
|
36
48
|
)
|
|
37
49
|
self._server_logger = Logger(get_current_span())
|
|
38
50
|
|
|
51
|
+
def _register_static_jobs(self) -> None:
|
|
52
|
+
all_job_ids = {job.id for job in self._scheduler.get_jobs()}
|
|
53
|
+
if VACCUUM_QUEUED_JOBS_JOB_ID in all_job_ids:
|
|
54
|
+
self._scheduler.remove_job(VACCUUM_QUEUED_JOBS_JOB_ID)
|
|
55
|
+
|
|
56
|
+
self._scheduler.add_job(
|
|
57
|
+
vaccuum_queued_jobs,
|
|
58
|
+
max_instances=1,
|
|
59
|
+
coalesce=True,
|
|
60
|
+
trigger=CronTrigger.from_crontab("5 4 * * 4"),
|
|
61
|
+
name="Vaccuum queued jobs",
|
|
62
|
+
id=VACCUUM_QUEUED_JOBS_JOB_ID,
|
|
63
|
+
kwargs={},
|
|
64
|
+
misfire_grace_time=None,
|
|
65
|
+
)
|
|
66
|
+
|
|
39
67
|
def register_jobs(self, profiles: list[job_definition_t.ProfileMetadata]) -> None:
|
|
40
|
-
valid_job_ids =
|
|
68
|
+
valid_job_ids: set[str] = set()
|
|
41
69
|
for profile_metadata in profiles:
|
|
42
70
|
for job_defn in profile_metadata.jobs:
|
|
43
|
-
valid_job_ids.
|
|
71
|
+
valid_job_ids.add(job_defn.id)
|
|
44
72
|
match job_defn:
|
|
45
73
|
case CronJobDefinition():
|
|
46
74
|
# Add to ap scheduler
|
|
@@ -86,14 +114,15 @@ class IntegrationServer:
|
|
|
86
114
|
misfire_grace_time=None,
|
|
87
115
|
**job_opts,
|
|
88
116
|
)
|
|
89
|
-
case
|
|
117
|
+
case HttpJobDefinitionBase():
|
|
90
118
|
pass
|
|
91
119
|
case _:
|
|
92
120
|
assert_never(job_defn)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
all_job_ids = {job.id for job in self._scheduler.get_jobs()}
|
|
122
|
+
invalid_job_ids = all_job_ids.difference(valid_job_ids.union(STATIC_JOB_IDS))
|
|
123
|
+
|
|
124
|
+
for job_id in invalid_job_ids:
|
|
125
|
+
self._scheduler.remove_job(job_id)
|
|
97
126
|
|
|
98
127
|
def serve_forever(self) -> None:
|
|
99
128
|
signal.pause()
|
|
@@ -106,12 +135,13 @@ class IntegrationServer:
|
|
|
106
135
|
|
|
107
136
|
def __enter__(self) -> "IntegrationServer":
|
|
108
137
|
self._start_apscheduler()
|
|
138
|
+
self._register_static_jobs()
|
|
109
139
|
return self
|
|
110
140
|
|
|
111
141
|
def __exit__(
|
|
112
142
|
self,
|
|
113
|
-
exc_type:
|
|
114
|
-
exc_val:
|
|
115
|
-
exc_tb:
|
|
143
|
+
exc_type: type[BaseException] | None,
|
|
144
|
+
exc_val: BaseException | None,
|
|
145
|
+
exc_tb: TracebackType | None,
|
|
116
146
|
) -> None:
|
|
117
147
|
self._stop_apscheduler()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
4
|
import time
|
|
4
5
|
import traceback
|
|
@@ -11,7 +12,10 @@ from opentelemetry import _logs, trace
|
|
|
11
12
|
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
12
13
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
14
|
from opentelemetry.sdk._logs import Logger as OTELLogger
|
|
14
|
-
from opentelemetry.sdk._logs import
|
|
15
|
+
from opentelemetry.sdk._logs import (
|
|
16
|
+
LoggerProvider,
|
|
17
|
+
LogRecord,
|
|
18
|
+
)
|
|
15
19
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
|
|
16
20
|
from opentelemetry.sdk.resources import Attributes, Resource
|
|
17
21
|
from opentelemetry.sdk.trace import TracerProvider
|
|
@@ -32,6 +36,11 @@ def _cast_attributes(attributes: dict[str, base_t.JsonValue]) -> Attributes:
|
|
|
32
36
|
return cast(Attributes, attributes)
|
|
33
37
|
|
|
34
38
|
|
|
39
|
+
def one_line_formatter(record: LogRecord) -> str:
|
|
40
|
+
json_data = record.to_json()
|
|
41
|
+
return json.dumps(json.loads(json_data), separators=(",", ":")) + "\n"
|
|
42
|
+
|
|
43
|
+
|
|
35
44
|
@functools.cache
|
|
36
45
|
def get_otel_resource() -> Resource:
|
|
37
46
|
attributes: dict[str, base_t.JsonValue] = {
|
|
@@ -60,7 +69,9 @@ def get_otel_tracer() -> Tracer:
|
|
|
60
69
|
@functools.cache
|
|
61
70
|
def get_otel_logger() -> OTELLogger:
|
|
62
71
|
provider = LoggerProvider(resource=get_otel_resource())
|
|
63
|
-
provider.add_log_record_processor(
|
|
72
|
+
provider.add_log_record_processor(
|
|
73
|
+
BatchLogRecordProcessor(ConsoleLogExporter(formatter=one_line_formatter))
|
|
74
|
+
)
|
|
64
75
|
if get_otel_enabled():
|
|
65
76
|
provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
|
|
66
77
|
_logs.set_logger_provider(provider)
|
|
@@ -87,8 +98,27 @@ class Logger:
|
|
|
87
98
|
def current_trace_id(self) -> int | None:
|
|
88
99
|
return self.current_span.get_span_context().trace_id
|
|
89
100
|
|
|
90
|
-
def _patch_attributes(
|
|
91
|
-
|
|
101
|
+
def _patch_attributes(
|
|
102
|
+
self,
|
|
103
|
+
attributes: Attributes | None,
|
|
104
|
+
*,
|
|
105
|
+
message: str | None = None,
|
|
106
|
+
severity: LogSeverity | None = None,
|
|
107
|
+
) -> Attributes:
|
|
108
|
+
patched_attributes = {**(attributes if attributes is not None else {})}
|
|
109
|
+
if message is not None:
|
|
110
|
+
patched_attributes["message"] = message
|
|
111
|
+
elif "body" in patched_attributes:
|
|
112
|
+
patched_attributes["message"] = patched_attributes["body"]
|
|
113
|
+
|
|
114
|
+
if severity is not None:
|
|
115
|
+
patched_attributes["status"] = severity.lower()
|
|
116
|
+
elif "severity_text" in patched_attributes and isinstance(
|
|
117
|
+
patched_attributes["severity_text"], str
|
|
118
|
+
):
|
|
119
|
+
patched_attributes["status"] = patched_attributes["severity_text"].lower()
|
|
120
|
+
|
|
121
|
+
return patched_attributes
|
|
92
122
|
|
|
93
123
|
def _emit_log(
|
|
94
124
|
self, message: str, *, severity: LogSeverity, attributes: Attributes | None
|
|
@@ -98,7 +128,9 @@ class Logger:
|
|
|
98
128
|
body=message,
|
|
99
129
|
severity_text=severity,
|
|
100
130
|
timestamp=time.time_ns(),
|
|
101
|
-
attributes=self._patch_attributes(
|
|
131
|
+
attributes=self._patch_attributes(
|
|
132
|
+
message=message, severity=severity, attributes=attributes
|
|
133
|
+
),
|
|
102
134
|
span_id=self.current_span_id,
|
|
103
135
|
trace_id=self.current_trace_id,
|
|
104
136
|
trace_flags=DEFAULT_TRACE_OPTIONS,
|
|
@@ -131,8 +163,10 @@ class Logger:
|
|
|
131
163
|
message: str | None = None,
|
|
132
164
|
attributes: Attributes | None = None,
|
|
133
165
|
) -> None:
|
|
134
|
-
traceback_str = "".join(traceback.
|
|
135
|
-
patched_attributes = self._patch_attributes(
|
|
166
|
+
traceback_str = "".join(traceback.format_exception(exception))
|
|
167
|
+
patched_attributes = self._patch_attributes(
|
|
168
|
+
message=message, severity=LogSeverity.ERROR, attributes=attributes
|
|
169
|
+
)
|
|
136
170
|
self.current_span.record_exception(
|
|
137
171
|
exception=exception, attributes=patched_attributes
|
|
138
172
|
)
|
|
@@ -163,9 +197,17 @@ class JobLogger(Logger):
|
|
|
163
197
|
self.job_definition = job_definition
|
|
164
198
|
super().__init__(base_span)
|
|
165
199
|
|
|
166
|
-
def _patch_attributes(
|
|
200
|
+
def _patch_attributes(
|
|
201
|
+
self,
|
|
202
|
+
attributes: Attributes | None,
|
|
203
|
+
*,
|
|
204
|
+
message: str | None = None,
|
|
205
|
+
severity: LogSeverity | None = None,
|
|
206
|
+
) -> Attributes:
|
|
167
207
|
patched_attributes: dict[str, base_t.JsonValue] = {
|
|
168
|
-
**(
|
|
208
|
+
**super()._patch_attributes(
|
|
209
|
+
attributes=attributes, message=message, severity=severity
|
|
210
|
+
)
|
|
169
211
|
}
|
|
170
212
|
patched_attributes["profile.name"] = self.profile_metadata.name
|
|
171
213
|
patched_attributes["profile.base_url"] = self.profile_metadata.base_url
|
|
@@ -177,7 +219,7 @@ class JobLogger(Logger):
|
|
|
177
219
|
patched_attributes["job.definition.cron_spec"] = (
|
|
178
220
|
self.job_definition.cron_spec
|
|
179
221
|
)
|
|
180
|
-
case job_definition_t.
|
|
222
|
+
case job_definition_t.HttpJobDefinitionBase():
|
|
181
223
|
pass
|
|
182
224
|
case _:
|
|
183
225
|
assert_never(self.job_definition)
|
|
@@ -196,3 +238,14 @@ class JobLogger(Logger):
|
|
|
196
238
|
case _:
|
|
197
239
|
assert_never(self.job_definition.executor)
|
|
198
240
|
return _cast_attributes(patched_attributes)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@contextmanager
|
|
244
|
+
def push_scope_optional(
|
|
245
|
+
logger: Logger | None, scope_name: str, *, attributes: Attributes | None = None
|
|
246
|
+
) -> Generator[None, None, None]:
|
|
247
|
+
if logger is None:
|
|
248
|
+
yield
|
|
249
|
+
else:
|
|
250
|
+
with logger.push_scope(scope_name, attributes=attributes):
|
|
251
|
+
yield
|
|
@@ -1,146 +1,71 @@
|
|
|
1
|
-
import
|
|
2
|
-
import typing
|
|
3
|
-
from dataclasses import dataclass
|
|
1
|
+
import base64
|
|
4
2
|
|
|
5
3
|
import flask
|
|
6
|
-
import simplejson
|
|
7
4
|
from flask.typing import ResponseReturnValue
|
|
8
|
-
from flask.wrappers import Response
|
|
9
5
|
from opentelemetry.trace import get_current_span
|
|
10
6
|
from uncountable.core.environment import (
|
|
11
|
-
|
|
7
|
+
get_http_server_port,
|
|
12
8
|
get_server_env,
|
|
13
|
-
get_webhook_server_port,
|
|
14
|
-
)
|
|
15
|
-
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
16
|
-
send_job_queue_message,
|
|
17
|
-
)
|
|
18
|
-
from uncountable.integration.queue_runner.command_server.types import (
|
|
19
|
-
CommandServerException,
|
|
20
9
|
)
|
|
10
|
+
from uncountable.integration.executors.script_executor import resolve_script_executor
|
|
11
|
+
from uncountable.integration.http_server import GenericHttpRequest, HttpException
|
|
12
|
+
from uncountable.integration.job import CustomHttpJob, WebhookJob
|
|
21
13
|
from uncountable.integration.scan_profiles import load_profiles
|
|
22
|
-
from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
|
|
23
14
|
from uncountable.integration.telemetry import Logger
|
|
24
|
-
from uncountable.types import
|
|
25
|
-
|
|
26
|
-
from pkgs.argument_parser import CachedParser
|
|
15
|
+
from uncountable.types import job_definition_t
|
|
27
16
|
|
|
28
17
|
app = flask.Flask(__name__)
|
|
29
18
|
|
|
30
19
|
|
|
31
|
-
@dataclass(kw_only=True)
|
|
32
|
-
class WebhookResponse:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
webhook_payload_parser = CachedParser(webhook_job_t.WebhookEventBody)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class WebhookException(BaseException):
|
|
40
|
-
error_code: int
|
|
41
|
-
message: str
|
|
42
|
-
|
|
43
|
-
def __init__(self, *, error_code: int, message: str) -> None:
|
|
44
|
-
self.error_code = error_code
|
|
45
|
-
self.message = message
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def payload_failed_signature() -> "WebhookException":
|
|
49
|
-
return WebhookException(
|
|
50
|
-
error_code=401, message="webhook payload did not match signature"
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
@staticmethod
|
|
54
|
-
def no_signature_passed() -> "WebhookException":
|
|
55
|
-
return WebhookException(error_code=400, message="missing signature")
|
|
56
|
-
|
|
57
|
-
@staticmethod
|
|
58
|
-
def body_parse_error() -> "WebhookException":
|
|
59
|
-
return WebhookException(error_code=400, message="body parse error")
|
|
60
|
-
|
|
61
|
-
@staticmethod
|
|
62
|
-
def unknown_error() -> "WebhookException":
|
|
63
|
-
return WebhookException(error_code=500, message="internal server error")
|
|
64
|
-
|
|
65
|
-
def __str__(self) -> str:
|
|
66
|
-
return f"[{self.error_code}]: {self.message}"
|
|
67
|
-
|
|
68
|
-
def make_error_response(self) -> Response:
|
|
69
|
-
return Response(
|
|
70
|
-
status=self.error_code, response={"error": {"message": str(self)}}
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _parse_webhook_payload(
|
|
75
|
-
*, raw_request_body: bytes, signature_key: str, passed_signature: str
|
|
76
|
-
) -> base_t.JsonValue:
|
|
77
|
-
request_body_signature = hmac.new(
|
|
78
|
-
signature_key.encode("utf-8"), msg=raw_request_body, digestmod="sha256"
|
|
79
|
-
).hexdigest()
|
|
80
|
-
|
|
81
|
-
if request_body_signature != passed_signature:
|
|
82
|
-
raise WebhookException.payload_failed_signature()
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
request_body = simplejson.loads(raw_request_body.decode())
|
|
86
|
-
return typing.cast(base_t.JsonValue, request_body)
|
|
87
|
-
except (simplejson.JSONDecodeError, ValueError) as e:
|
|
88
|
-
raise WebhookException.body_parse_error() from e
|
|
89
|
-
|
|
90
|
-
|
|
91
20
|
def register_route(
|
|
92
21
|
*,
|
|
93
22
|
server_logger: Logger,
|
|
94
23
|
profile_meta: job_definition_t.ProfileMetadata,
|
|
95
|
-
job: job_definition_t.
|
|
24
|
+
job: job_definition_t.HttpJobDefinitionBase,
|
|
96
25
|
) -> None:
|
|
97
26
|
route = f"/{profile_meta.name}/{job.id}"
|
|
98
27
|
|
|
99
|
-
def
|
|
28
|
+
def handle_request() -> ResponseReturnValue:
|
|
100
29
|
with server_logger.push_scope(route):
|
|
101
30
|
try:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
31
|
+
if not isinstance(job.executor, job_definition_t.JobExecutorScript):
|
|
32
|
+
raise HttpException.configuration_error(
|
|
33
|
+
message="[internal] http job must use a script executor"
|
|
34
|
+
)
|
|
35
|
+
job_instance = resolve_script_executor(
|
|
36
|
+
executor=job.executor, profile_metadata=profile_meta
|
|
105
37
|
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
38
|
+
if not isinstance(job_instance, (CustomHttpJob, WebhookJob)):
|
|
39
|
+
raise HttpException.configuration_error(
|
|
40
|
+
message="[internal] http job must descend from CustomHttpJob"
|
|
41
|
+
)
|
|
42
|
+
http_request = GenericHttpRequest(
|
|
43
|
+
body_base64=base64.b64encode(flask.request.get_data()).decode(),
|
|
44
|
+
headers=dict(flask.request.headers),
|
|
109
45
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
signature_key=signature_key,
|
|
116
|
-
passed_signature=passed_signature,
|
|
46
|
+
job_instance.validate_request(
|
|
47
|
+
request=http_request, job_definition=job, profile_meta=profile_meta
|
|
48
|
+
)
|
|
49
|
+
http_response = job_instance.handle_request(
|
|
50
|
+
request=http_request, job_definition=job, profile_meta=profile_meta
|
|
117
51
|
)
|
|
118
52
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
),
|
|
127
|
-
port=get_local_admin_server_port(),
|
|
128
|
-
)
|
|
129
|
-
except CommandServerException as e:
|
|
130
|
-
raise WebhookException.unknown_error() from e
|
|
131
|
-
|
|
132
|
-
return flask.jsonify(WebhookResponse())
|
|
133
|
-
except WebhookException as e:
|
|
53
|
+
return flask.make_response(
|
|
54
|
+
http_response.response,
|
|
55
|
+
http_response.status_code,
|
|
56
|
+
http_response.headers,
|
|
57
|
+
)
|
|
58
|
+
except HttpException as e:
|
|
134
59
|
server_logger.log_exception(e)
|
|
135
60
|
return e.make_error_response()
|
|
136
61
|
except Exception as e:
|
|
137
62
|
server_logger.log_exception(e)
|
|
138
|
-
return
|
|
63
|
+
return HttpException.unknown_error().make_error_response()
|
|
139
64
|
|
|
140
65
|
app.add_url_rule(
|
|
141
66
|
route,
|
|
142
|
-
endpoint=f"
|
|
143
|
-
view_func=
|
|
67
|
+
endpoint=f"handle_request_{job.id}",
|
|
68
|
+
view_func=handle_request,
|
|
144
69
|
methods=["POST"],
|
|
145
70
|
)
|
|
146
71
|
|
|
@@ -148,11 +73,13 @@ def register_route(
|
|
|
148
73
|
|
|
149
74
|
|
|
150
75
|
def main() -> None:
|
|
76
|
+
app.add_url_rule("/health", "health", lambda: ("OK", 200))
|
|
77
|
+
|
|
151
78
|
profiles = load_profiles()
|
|
152
79
|
for profile_metadata in profiles:
|
|
153
80
|
server_logger = Logger(get_current_span())
|
|
154
81
|
for job in profile_metadata.jobs:
|
|
155
|
-
if isinstance(job, job_definition_t.
|
|
82
|
+
if isinstance(job, job_definition_t.HttpJobDefinitionBase):
|
|
156
83
|
register_route(
|
|
157
84
|
server_logger=server_logger, profile_meta=profile_metadata, job=job
|
|
158
85
|
)
|
|
@@ -164,7 +91,7 @@ main()
|
|
|
164
91
|
if __name__ == "__main__":
|
|
165
92
|
app.run(
|
|
166
93
|
host="0.0.0.0",
|
|
167
|
-
port=
|
|
94
|
+
port=get_http_server_port(),
|
|
168
95
|
debug=get_server_env() == "playground",
|
|
169
96
|
exclude_patterns=[],
|
|
170
97
|
)
|