UncountablePythonSDK 0.0.8__py3-none-any.whl → 0.0.92__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of UncountablePythonSDK might be problematic. Click here for more details.
- UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
- UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
- {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
- {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
- docs/.gitignore +1 -0
- docs/conf.py +57 -0
- docs/index.md +13 -0
- docs/justfile +12 -0
- docs/quickstart.md +19 -0
- docs/requirements.txt +7 -0
- docs/static/favicons/android-chrome-192x192.png +0 -0
- docs/static/favicons/android-chrome-512x512.png +0 -0
- docs/static/favicons/apple-touch-icon.png +0 -0
- docs/static/favicons/browserconfig.xml +9 -0
- docs/static/favicons/favicon-16x16.png +0 -0
- docs/static/favicons/favicon-32x32.png +0 -0
- docs/static/favicons/manifest.json +18 -0
- docs/static/favicons/mstile-150x150.png +0 -0
- docs/static/favicons/safari-pinned-tab.svg +32 -0
- docs/static/logo_blue.png +0 -0
- examples/async_batch.py +35 -0
- examples/create_entity.py +22 -17
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +50 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
- examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
- examples/integration-server/pyproject.toml +224 -0
- examples/invoke_uploader.py +26 -0
- examples/set_recipe_metadata_file.py +40 -0
- examples/set_recipe_output_file_sdk.py +26 -0
- examples/upload_files.py +18 -0
- pkgs/argument_parser/__init__.py +5 -0
- pkgs/argument_parser/_is_enum.py +1 -6
- pkgs/argument_parser/argument_parser.py +232 -76
- pkgs/argument_parser/case_convert.py +4 -3
- pkgs/filesystem_utils/__init__.py +20 -0
- pkgs/filesystem_utils/_blob_session.py +137 -0
- pkgs/filesystem_utils/_gdrive_session.py +309 -0
- pkgs/filesystem_utils/_local_session.py +69 -0
- pkgs/filesystem_utils/_s3_session.py +117 -0
- pkgs/filesystem_utils/_sftp_session.py +147 -0
- pkgs/filesystem_utils/file_type_utils.py +91 -0
- pkgs/filesystem_utils/filesystem_session.py +39 -0
- pkgs/py.typed +0 -0
- pkgs/serialization/__init__.py +8 -1
- pkgs/serialization/annotation.py +64 -0
- pkgs/serialization/missing_sentry.py +1 -1
- pkgs/serialization/opaque_key.py +1 -1
- pkgs/serialization/serial_alias.py +47 -0
- pkgs/serialization/serial_class.py +65 -50
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +84 -0
- pkgs/serialization/yaml.py +57 -0
- pkgs/serialization_util/__init__.py +7 -7
- pkgs/serialization_util/_get_type_for_serialization.py +1 -3
- pkgs/serialization_util/convert_to_snakecase.py +27 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +116 -74
- pkgs/strenum_compat/strenum_compat.py +1 -9
- pkgs/type_spec/actions_registry/__init__.py +0 -0
- pkgs/type_spec/actions_registry/__main__.py +126 -0
- pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
- pkgs/type_spec/builder.py +475 -89
- pkgs/type_spec/config.py +24 -19
- pkgs/type_spec/emit_io_ts.py +5 -2
- pkgs/type_spec/emit_open_api.py +266 -32
- pkgs/type_spec/emit_open_api_util.py +32 -13
- pkgs/type_spec/emit_python.py +599 -151
- pkgs/type_spec/emit_typescript.py +74 -273
- pkgs/type_spec/emit_typescript_util.py +239 -5
- pkgs/type_spec/load_types.py +55 -10
- pkgs/type_spec/open_api_util.py +30 -41
- pkgs/type_spec/parts/base.py.prepart +4 -3
- pkgs/type_spec/type_info/emit_type_info.py +178 -16
- pkgs/type_spec/util.py +11 -11
- pkgs/type_spec/value_spec/__main__.py +3 -3
- pkgs/type_spec/value_spec/convert_type.py +8 -1
- pkgs/type_spec/value_spec/emit_python.py +13 -4
- uncountable/__init__.py +1 -2
- uncountable/core/__init__.py +12 -2
- uncountable/core/async_batch.py +37 -0
- uncountable/core/client.py +293 -43
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +135 -0
- uncountable/core/types.py +17 -0
- uncountable/integration/__init__.py +0 -0
- uncountable/integration/cli.py +49 -0
- uncountable/integration/construct_client.py +51 -0
- uncountable/integration/cron.py +29 -0
- uncountable/integration/db/__init__.py +0 -0
- uncountable/integration/db/connect.py +18 -0
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +13 -0
- uncountable/integration/executors/__init__.py +0 -0
- uncountable/integration/executors/executors.py +148 -0
- uncountable/integration/executors/generic_upload_executor.py +284 -0
- uncountable/integration/executors/script_executor.py +25 -0
- uncountable/integration/job.py +87 -0
- uncountable/integration/queue_runner/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/__init__.py +24 -0
- uncountable/integration/queue_runner/command_server/command_client.py +68 -0
- uncountable/integration/queue_runner/command_server/command_server.py +64 -0
- uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
- uncountable/integration/queue_runner/command_server/types.py +52 -0
- uncountable/integration/queue_runner/datastore/__init__.py +3 -0
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
- uncountable/integration/queue_runner/datastore/interface.py +19 -0
- uncountable/integration/queue_runner/datastore/model.py +17 -0
- uncountable/integration/queue_runner/job_scheduler.py +163 -0
- uncountable/integration/queue_runner/queue_runner.py +26 -0
- uncountable/integration/queue_runner/types.py +7 -0
- uncountable/integration/queue_runner/worker.py +119 -0
- uncountable/integration/scan_profiles.py +67 -0
- uncountable/integration/scheduler.py +150 -0
- uncountable/integration/secret_retrieval/__init__.py +3 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
- uncountable/integration/server.py +117 -0
- uncountable/integration/telemetry.py +209 -0
- uncountable/integration/webhook_server/entrypoint.py +170 -0
- uncountable/types/__init__.py +136 -20
- uncountable/types/api/batch/execute_batch.py +15 -7
- uncountable/types/api/batch/execute_batch_load_async.py +42 -0
- uncountable/types/api/chemical/__init__.py +1 -0
- uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
- uncountable/types/api/entity/create_entities.py +23 -11
- uncountable/types/api/entity/create_entity.py +21 -12
- uncountable/types/api/entity/get_entities_data.py +18 -8
- uncountable/types/api/entity/grant_entity_permissions.py +48 -0
- uncountable/types/api/entity/list_entities.py +27 -12
- uncountable/types/api/entity/lock_entity.py +45 -0
- uncountable/types/api/entity/resolve_entity_ids.py +17 -7
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +14 -7
- uncountable/types/api/entity/transition_entity_phase.py +80 -0
- uncountable/types/api/entity/unlock_entity.py +44 -0
- uncountable/types/api/equipment/__init__.py +1 -0
- uncountable/types/api/equipment/associate_equipment_input.py +44 -0
- uncountable/types/api/field_options/__init__.py +1 -0
- uncountable/types/api/field_options/upsert_field_options.py +55 -0
- uncountable/types/api/files/__init__.py +1 -0
- uncountable/types/api/files/download_file.py +77 -0
- uncountable/types/api/id_source/__init__.py +1 -0
- uncountable/types/api/id_source/list_id_source.py +56 -0
- uncountable/types/api/id_source/match_id_source.py +54 -0
- uncountable/types/api/input_groups/get_input_group_names.py +16 -6
- uncountable/types/api/inputs/create_inputs.py +24 -11
- uncountable/types/api/inputs/get_input_data.py +32 -13
- uncountable/types/api/inputs/get_input_names.py +18 -8
- uncountable/types/api/inputs/get_inputs_data.py +29 -10
- uncountable/types/api/inputs/set_input_attribute_values.py +16 -9
- uncountable/types/api/inputs/set_input_category.py +44 -0
- uncountable/types/api/inputs/set_input_subcategories.py +45 -0
- uncountable/types/api/inputs/set_intermediate_type.py +50 -0
- uncountable/types/api/material_families/__init__.py +1 -0
- uncountable/types/api/material_families/update_entity_material_families.py +48 -0
- uncountable/types/api/outputs/get_output_data.py +32 -16
- uncountable/types/api/outputs/get_output_names.py +18 -8
- uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
- uncountable/types/api/permissions/__init__.py +1 -0
- uncountable/types/api/permissions/set_core_permissions.py +105 -0
- uncountable/types/api/project/get_projects.py +17 -7
- uncountable/types/api/project/get_projects_data.py +21 -11
- uncountable/types/api/recipe_links/__init__.py +1 -0
- uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
- uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +18 -8
- uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
- uncountable/types/api/recipes/archive_recipes.py +42 -0
- uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
- uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
- uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
- uncountable/types/api/recipes/create_recipe.py +51 -0
- uncountable/types/api/recipes/create_recipes.py +25 -12
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
- uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
- uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
- uncountable/types/api/recipes/get_curve.py +15 -7
- uncountable/types/api/recipes/get_recipe_calculations.py +17 -10
- uncountable/types/api/recipes/get_recipe_links.py +13 -6
- uncountable/types/api/recipes/get_recipe_names.py +16 -6
- uncountable/types/api/recipes/get_recipe_output_metadata.py +14 -7
- uncountable/types/api/recipes/get_recipes_data.py +63 -38
- uncountable/types/api/recipes/lock_recipes.py +63 -0
- uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
- uncountable/types/api/recipes/set_recipe_inputs.py +19 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
- uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
- uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +26 -12
- uncountable/types/api/recipes/set_recipe_tags.py +109 -0
- uncountable/types/api/recipes/unarchive_recipes.py +41 -0
- uncountable/types/api/recipes/unlock_recipes.py +50 -0
- uncountable/types/api/triggers/__init__.py +1 -0
- uncountable/types/api/triggers/run_trigger.py +43 -0
- uncountable/types/api/uploader/__init__.py +1 -0
- uncountable/types/api/uploader/invoke_uploader.py +47 -0
- uncountable/types/async_batch.py +13 -0
- uncountable/types/async_batch_processor.py +384 -0
- uncountable/types/async_batch_t.py +97 -0
- uncountable/types/async_jobs.py +9 -0
- uncountable/types/async_jobs_t.py +53 -0
- uncountable/types/auth_retrieval.py +12 -0
- uncountable/types/auth_retrieval_t.py +75 -0
- uncountable/types/base.py +5 -78
- uncountable/types/base_t.py +85 -0
- uncountable/types/calculations.py +3 -18
- uncountable/types/calculations_t.py +27 -0
- uncountable/types/chemical_structure.py +8 -0
- uncountable/types/chemical_structure_t.py +28 -0
- uncountable/types/client_base.py +1093 -54
- uncountable/types/client_config.py +8 -0
- uncountable/types/client_config_t.py +26 -0
- uncountable/types/curves.py +5 -42
- uncountable/types/curves_t.py +51 -0
- uncountable/types/entity.py +8 -269
- uncountable/types/entity_t.py +393 -0
- uncountable/types/experiment_groups.py +3 -18
- uncountable/types/experiment_groups_t.py +27 -0
- uncountable/types/field_values.py +17 -60
- uncountable/types/field_values_t.py +204 -0
- uncountable/types/fields.py +3 -19
- uncountable/types/fields_t.py +28 -0
- uncountable/types/generic_upload.py +15 -0
- uncountable/types/generic_upload_t.py +119 -0
- uncountable/types/id_source.py +12 -0
- uncountable/types/id_source_t.py +68 -0
- uncountable/types/identifier.py +11 -0
- uncountable/types/identifier_t.py +63 -0
- uncountable/types/input_attributes.py +3 -24
- uncountable/types/input_attributes_t.py +30 -0
- uncountable/types/inputs.py +6 -56
- uncountable/types/inputs_t.py +83 -0
- uncountable/types/integration_server.py +9 -0
- uncountable/types/integration_server_t.py +42 -0
- uncountable/types/job_definition.py +27 -0
- uncountable/types/job_definition_t.py +260 -0
- uncountable/types/outputs.py +3 -21
- uncountable/types/outputs_t.py +30 -0
- uncountable/types/overrides.py +10 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +8 -0
- uncountable/types/permissions_t.py +46 -0
- uncountable/types/phases.py +3 -18
- uncountable/types/phases_t.py +27 -0
- uncountable/types/post_base.py +8 -0
- uncountable/types/post_base_t.py +30 -0
- uncountable/types/queued_job.py +16 -0
- uncountable/types/queued_job_t.py +123 -0
- uncountable/types/recipe_identifiers.py +12 -0
- uncountable/types/recipe_identifiers_t.py +76 -0
- uncountable/types/recipe_inputs.py +9 -0
- uncountable/types/recipe_inputs_t.py +30 -0
- uncountable/types/recipe_links.py +4 -45
- uncountable/types/recipe_links_t.py +54 -0
- uncountable/types/recipe_metadata.py +5 -45
- uncountable/types/recipe_metadata_t.py +58 -0
- uncountable/types/recipe_output_metadata.py +3 -19
- uncountable/types/recipe_output_metadata_t.py +28 -0
- uncountable/types/recipe_tags.py +3 -18
- uncountable/types/recipe_tags_t.py +27 -0
- uncountable/types/recipe_workflow_steps.py +14 -0
- uncountable/types/recipe_workflow_steps_t.py +95 -0
- uncountable/types/recipes.py +8 -0
- uncountable/types/recipes_t.py +25 -0
- uncountable/types/response.py +3 -20
- uncountable/types/response_t.py +26 -0
- uncountable/types/secret_retrieval.py +12 -0
- uncountable/types/secret_retrieval_t.py +75 -0
- uncountable/types/units.py +3 -18
- uncountable/types/units_t.py +27 -0
- uncountable/types/users.py +3 -19
- uncountable/types/users_t.py +28 -0
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +37 -0
- uncountable/types/workflows.py +4 -27
- uncountable/types/workflows_t.py +39 -0
- UncountablePythonSDK-0.0.8.dist-info/METADATA +0 -27
- UncountablePythonSDK-0.0.8.dist-info/RECORD +0 -134
- examples/recipe-import/importer.py +0 -39
- type_spec/external/api/batch/execute_batch.yaml +0 -56
- type_spec/external/api/entity/create_entities.yaml +0 -33
- type_spec/external/api/entity/create_entity.yaml +0 -39
- type_spec/external/api/entity/get_entities_data.yaml +0 -29
- type_spec/external/api/entity/list_entities.yaml +0 -52
- type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
- type_spec/external/api/entity/set_values.yaml +0 -18
- type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
- type_spec/external/api/inputs/create_inputs.yaml +0 -48
- type_spec/external/api/inputs/get_input_data.yaml +0 -95
- type_spec/external/api/inputs/get_input_names.yaml +0 -38
- type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
- type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
- type_spec/external/api/outputs/get_output_data.yaml +0 -92
- type_spec/external/api/outputs/get_output_names.yaml +0 -35
- type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
- type_spec/external/api/project/get_projects.yaml +0 -42
- type_spec/external/api/project/get_projects_data.yaml +0 -50
- type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
- type_spec/external/api/recipes/create_recipes.yaml +0 -47
- type_spec/external/api/recipes/get_curve.yaml +0 -18
- type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
- type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
- type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
- type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
- type_spec/external/api/recipes/get_recipes_data.yaml +0 -238
- type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
- type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Optional, assert_never
|
|
5
|
+
|
|
6
|
+
from apscheduler.executors.pool import ThreadPoolExecutor
|
|
7
|
+
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
8
|
+
from apscheduler.schedulers.background import BackgroundScheduler
|
|
9
|
+
from apscheduler.schedulers.base import BaseScheduler
|
|
10
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
11
|
+
from opentelemetry.trace import get_current_span
|
|
12
|
+
from sqlalchemy.engine.base import Engine
|
|
13
|
+
|
|
14
|
+
from uncountable.integration.cron import CronJobArgs, cron_job_executor
|
|
15
|
+
from uncountable.integration.telemetry import Logger
|
|
16
|
+
from uncountable.types import base_t, job_definition_t
|
|
17
|
+
from uncountable.types.job_definition_t import (
|
|
18
|
+
CronJobDefinition,
|
|
19
|
+
WebhookJobDefinition,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_MAX_APSCHEDULER_CONCURRENT_JOBS = 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IntegrationServer:
|
|
26
|
+
_scheduler: BaseScheduler
|
|
27
|
+
_engine: Engine
|
|
28
|
+
_server_logger: Logger
|
|
29
|
+
|
|
30
|
+
def __init__(self, engine: Engine) -> None:
|
|
31
|
+
self._engine = engine
|
|
32
|
+
self._scheduler = BackgroundScheduler(
|
|
33
|
+
timezone="UTC",
|
|
34
|
+
jobstores={"default": SQLAlchemyJobStore(engine=engine)},
|
|
35
|
+
executors={"default": ThreadPoolExecutor(_MAX_APSCHEDULER_CONCURRENT_JOBS)},
|
|
36
|
+
)
|
|
37
|
+
self._server_logger = Logger(get_current_span())
|
|
38
|
+
|
|
39
|
+
def register_jobs(self, profiles: list[job_definition_t.ProfileMetadata]) -> None:
|
|
40
|
+
valid_job_ids = []
|
|
41
|
+
for profile_metadata in profiles:
|
|
42
|
+
for job_defn in profile_metadata.jobs:
|
|
43
|
+
valid_job_ids.append(job_defn.id)
|
|
44
|
+
match job_defn:
|
|
45
|
+
case CronJobDefinition():
|
|
46
|
+
# Add to ap scheduler
|
|
47
|
+
job_kwargs = asdict(
|
|
48
|
+
CronJobArgs(
|
|
49
|
+
definition=job_defn, profile_metadata=profile_metadata
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
try:
|
|
53
|
+
existing_job = self._scheduler.get_job(job_defn.id)
|
|
54
|
+
except ValueError as e:
|
|
55
|
+
self._server_logger.log_warning(
|
|
56
|
+
f"could not reconstitute job {job_defn.id}: {e}"
|
|
57
|
+
)
|
|
58
|
+
self._scheduler.remove_job(job_defn.id)
|
|
59
|
+
existing_job = None
|
|
60
|
+
if existing_job is not None:
|
|
61
|
+
existing_job.modify(
|
|
62
|
+
name=job_defn.name,
|
|
63
|
+
kwargs=job_kwargs,
|
|
64
|
+
misfire_grace_time=None,
|
|
65
|
+
)
|
|
66
|
+
existing_job.reschedule(
|
|
67
|
+
CronTrigger.from_crontab(job_defn.cron_spec)
|
|
68
|
+
)
|
|
69
|
+
if not job_defn.enabled:
|
|
70
|
+
existing_job.pause()
|
|
71
|
+
else:
|
|
72
|
+
existing_job.resume()
|
|
73
|
+
else:
|
|
74
|
+
job_opts: dict[str, base_t.JsonValue] = {}
|
|
75
|
+
if not job_defn.enabled:
|
|
76
|
+
job_opts["next_run_time"] = None
|
|
77
|
+
self._scheduler.add_job(
|
|
78
|
+
cron_job_executor,
|
|
79
|
+
# IMPROVE: reconsider these defaults
|
|
80
|
+
max_instances=1,
|
|
81
|
+
coalesce=True,
|
|
82
|
+
trigger=CronTrigger.from_crontab(job_defn.cron_spec),
|
|
83
|
+
name=job_defn.name,
|
|
84
|
+
id=job_defn.id,
|
|
85
|
+
kwargs=job_kwargs,
|
|
86
|
+
misfire_grace_time=None,
|
|
87
|
+
**job_opts,
|
|
88
|
+
)
|
|
89
|
+
case WebhookJobDefinition():
|
|
90
|
+
pass
|
|
91
|
+
case _:
|
|
92
|
+
assert_never(job_defn)
|
|
93
|
+
all_jobs = self._scheduler.get_jobs()
|
|
94
|
+
for job in all_jobs:
|
|
95
|
+
if job.id not in valid_job_ids:
|
|
96
|
+
self._scheduler.remove_job(job.id)
|
|
97
|
+
|
|
98
|
+
def serve_forever(self) -> None:
|
|
99
|
+
signal.pause()
|
|
100
|
+
|
|
101
|
+
def _start_apscheduler(self) -> None:
|
|
102
|
+
self._scheduler.start()
|
|
103
|
+
|
|
104
|
+
def _stop_apscheduler(self) -> None:
|
|
105
|
+
self._scheduler.shutdown()
|
|
106
|
+
|
|
107
|
+
def __enter__(self) -> "IntegrationServer":
|
|
108
|
+
self._start_apscheduler()
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def __exit__(
|
|
112
|
+
self,
|
|
113
|
+
exc_type: Optional[type[BaseException]],
|
|
114
|
+
exc_val: Optional[BaseException],
|
|
115
|
+
exc_tb: Optional[TracebackType],
|
|
116
|
+
) -> None:
|
|
117
|
+
self._stop_apscheduler()
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import traceback
|
|
5
|
+
import typing
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Generator, assert_never, cast
|
|
9
|
+
|
|
10
|
+
from opentelemetry import _logs, trace
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
|
+
from opentelemetry.sdk._logs import Logger as OTELLogger
|
|
14
|
+
from opentelemetry.sdk._logs import LoggerProvider, LogRecord
|
|
15
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
|
|
16
|
+
from opentelemetry.sdk.resources import Attributes, Resource
|
|
17
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
18
|
+
from opentelemetry.sdk.trace.export import (
|
|
19
|
+
SimpleSpanProcessor,
|
|
20
|
+
)
|
|
21
|
+
from opentelemetry.trace import DEFAULT_TRACE_OPTIONS, Span, Tracer
|
|
22
|
+
|
|
23
|
+
from uncountable.core.environment import (
|
|
24
|
+
get_otel_enabled,
|
|
25
|
+
get_server_env,
|
|
26
|
+
get_version,
|
|
27
|
+
)
|
|
28
|
+
from uncountable.types import base_t, job_definition_t
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cast_attributes(attributes: dict[str, base_t.JsonValue]) -> Attributes:
|
|
32
|
+
return cast(Attributes, attributes)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@functools.cache
|
|
36
|
+
def get_otel_resource() -> Resource:
|
|
37
|
+
attributes: dict[str, base_t.JsonValue] = {
|
|
38
|
+
"service.name": "integration-server",
|
|
39
|
+
"sdk.version": get_version(),
|
|
40
|
+
}
|
|
41
|
+
unc_version = os.environ.get("UNC_VERSION")
|
|
42
|
+
if unc_version is not None:
|
|
43
|
+
attributes["service.version"] = unc_version
|
|
44
|
+
unc_env = get_server_env()
|
|
45
|
+
if unc_env is not None:
|
|
46
|
+
attributes["deployment.environment"] = unc_env
|
|
47
|
+
resource = Resource.create(attributes=_cast_attributes(attributes))
|
|
48
|
+
return resource
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@functools.cache
|
|
52
|
+
def get_otel_tracer() -> Tracer:
|
|
53
|
+
provider = TracerProvider(resource=get_otel_resource())
|
|
54
|
+
if get_otel_enabled():
|
|
55
|
+
provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter()))
|
|
56
|
+
trace.set_tracer_provider(provider)
|
|
57
|
+
return provider.get_tracer("integration.telemetry")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@functools.cache
|
|
61
|
+
def get_otel_logger() -> OTELLogger:
|
|
62
|
+
provider = LoggerProvider(resource=get_otel_resource())
|
|
63
|
+
provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
|
|
64
|
+
if get_otel_enabled():
|
|
65
|
+
provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
|
|
66
|
+
_logs.set_logger_provider(provider)
|
|
67
|
+
return provider.get_logger("integration.telemetry")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LogSeverity(StrEnum):
|
|
71
|
+
INFO = "Info"
|
|
72
|
+
WARN = "Warn"
|
|
73
|
+
ERROR = "Error"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Logger:
|
|
77
|
+
current_span: Span
|
|
78
|
+
|
|
79
|
+
def __init__(self, base_span: Span) -> None:
|
|
80
|
+
self.current_span = base_span
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def current_span_id(self) -> int:
|
|
84
|
+
return self.current_span.get_span_context().span_id
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def current_trace_id(self) -> int | None:
|
|
88
|
+
return self.current_span.get_span_context().trace_id
|
|
89
|
+
|
|
90
|
+
def _patch_attributes(self, attributes: Attributes | None) -> Attributes:
|
|
91
|
+
return attributes or {}
|
|
92
|
+
|
|
93
|
+
def _emit_log(
|
|
94
|
+
self, message: str, *, severity: LogSeverity, attributes: Attributes | None
|
|
95
|
+
) -> None:
|
|
96
|
+
otel_logger = get_otel_logger()
|
|
97
|
+
log_record = LogRecord(
|
|
98
|
+
body=message,
|
|
99
|
+
severity_text=severity,
|
|
100
|
+
timestamp=time.time_ns(),
|
|
101
|
+
attributes=self._patch_attributes(attributes),
|
|
102
|
+
span_id=self.current_span_id,
|
|
103
|
+
trace_id=self.current_trace_id,
|
|
104
|
+
trace_flags=DEFAULT_TRACE_OPTIONS,
|
|
105
|
+
severity_number=_logs.SeverityNumber.UNSPECIFIED,
|
|
106
|
+
resource=get_otel_resource(),
|
|
107
|
+
)
|
|
108
|
+
otel_logger.emit(log_record)
|
|
109
|
+
|
|
110
|
+
def log_info(self, message: str, *, attributes: Attributes | None = None) -> None:
|
|
111
|
+
self._emit_log(
|
|
112
|
+
message=message, severity=LogSeverity.INFO, attributes=attributes
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def log_warning(
|
|
116
|
+
self, message: str, *, attributes: Attributes | None = None
|
|
117
|
+
) -> None:
|
|
118
|
+
self._emit_log(
|
|
119
|
+
message=message, severity=LogSeverity.WARN, attributes=attributes
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def log_error(self, message: str, *, attributes: Attributes | None = None) -> None:
|
|
123
|
+
self._emit_log(
|
|
124
|
+
message=message, severity=LogSeverity.ERROR, attributes=attributes
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def log_exception(
|
|
128
|
+
self,
|
|
129
|
+
exception: BaseException,
|
|
130
|
+
*,
|
|
131
|
+
message: str | None = None,
|
|
132
|
+
attributes: Attributes | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
traceback_str = "".join(traceback.format_tb(exception.__traceback__))
|
|
135
|
+
patched_attributes = self._patch_attributes(attributes)
|
|
136
|
+
self.current_span.record_exception(
|
|
137
|
+
exception=exception, attributes=patched_attributes
|
|
138
|
+
)
|
|
139
|
+
self.log_error(
|
|
140
|
+
message=f"error: {message}\nexception: {exception}{traceback_str}",
|
|
141
|
+
attributes=patched_attributes,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@contextmanager
|
|
145
|
+
def push_scope(
|
|
146
|
+
self, scope_name: str, *, attributes: Attributes | None = None
|
|
147
|
+
) -> Generator[typing.Self, None, None]:
|
|
148
|
+
with get_otel_tracer().start_as_current_span(
|
|
149
|
+
scope_name, attributes=self._patch_attributes(attributes)
|
|
150
|
+
):
|
|
151
|
+
yield self
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class JobLogger(Logger):
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
base_span: Span,
|
|
159
|
+
profile_metadata: job_definition_t.ProfileMetadata,
|
|
160
|
+
job_definition: job_definition_t.JobDefinition,
|
|
161
|
+
) -> None:
|
|
162
|
+
self.profile_metadata = profile_metadata
|
|
163
|
+
self.job_definition = job_definition
|
|
164
|
+
super().__init__(base_span)
|
|
165
|
+
|
|
166
|
+
def _patch_attributes(self, attributes: Attributes | None) -> Attributes:
|
|
167
|
+
patched_attributes: dict[str, base_t.JsonValue] = {
|
|
168
|
+
**(attributes if attributes is not None else {})
|
|
169
|
+
}
|
|
170
|
+
patched_attributes["profile.name"] = self.profile_metadata.name
|
|
171
|
+
patched_attributes["profile.base_url"] = self.profile_metadata.base_url
|
|
172
|
+
patched_attributes["job.name"] = self.job_definition.name
|
|
173
|
+
patched_attributes["job.id"] = self.job_definition.id
|
|
174
|
+
patched_attributes["job.definition_type"] = self.job_definition.type
|
|
175
|
+
match self.job_definition:
|
|
176
|
+
case job_definition_t.CronJobDefinition():
|
|
177
|
+
patched_attributes["job.definition.cron_spec"] = (
|
|
178
|
+
self.job_definition.cron_spec
|
|
179
|
+
)
|
|
180
|
+
case job_definition_t.WebhookJobDefinition():
|
|
181
|
+
pass
|
|
182
|
+
case _:
|
|
183
|
+
assert_never(self.job_definition)
|
|
184
|
+
patched_attributes["job.definition.executor.type"] = (
|
|
185
|
+
self.job_definition.executor.type
|
|
186
|
+
)
|
|
187
|
+
match self.job_definition.executor:
|
|
188
|
+
case job_definition_t.JobExecutorScript():
|
|
189
|
+
patched_attributes["job.definition.executor.import_path"] = (
|
|
190
|
+
self.job_definition.executor.import_path
|
|
191
|
+
)
|
|
192
|
+
case job_definition_t.JobExecutorGenericUpload():
|
|
193
|
+
patched_attributes["job.definition.executor.data_source.type"] = (
|
|
194
|
+
self.job_definition.executor.data_source.type
|
|
195
|
+
)
|
|
196
|
+
case _:
|
|
197
|
+
assert_never(self.job_definition.executor)
|
|
198
|
+
return _cast_attributes(patched_attributes)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@contextmanager
|
|
202
|
+
def push_scope_optional(
|
|
203
|
+
logger: Logger | None, scope_name: str, *, attributes: Attributes | None = None
|
|
204
|
+
) -> Generator[None, None, None]:
|
|
205
|
+
if logger is None:
|
|
206
|
+
yield
|
|
207
|
+
else:
|
|
208
|
+
with logger.push_scope(scope_name, attributes=attributes):
|
|
209
|
+
yield
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import typing
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import flask
|
|
6
|
+
import simplejson
|
|
7
|
+
from flask.typing import ResponseReturnValue
|
|
8
|
+
from flask.wrappers import Response
|
|
9
|
+
from opentelemetry.trace import get_current_span
|
|
10
|
+
from uncountable.core.environment import (
|
|
11
|
+
get_local_admin_server_port,
|
|
12
|
+
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
|
+
)
|
|
21
|
+
from uncountable.integration.scan_profiles import load_profiles
|
|
22
|
+
from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
|
|
23
|
+
from uncountable.integration.telemetry import Logger
|
|
24
|
+
from uncountable.types import base_t, job_definition_t, queued_job_t, webhook_job_t
|
|
25
|
+
|
|
26
|
+
from pkgs.argument_parser import CachedParser
|
|
27
|
+
|
|
28
|
+
app = flask.Flask(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
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
|
+
def register_route(
|
|
92
|
+
*,
|
|
93
|
+
server_logger: Logger,
|
|
94
|
+
profile_meta: job_definition_t.ProfileMetadata,
|
|
95
|
+
job: job_definition_t.WebhookJobDefinition,
|
|
96
|
+
) -> None:
|
|
97
|
+
route = f"/{profile_meta.name}/{job.id}"
|
|
98
|
+
|
|
99
|
+
def handle_webhook() -> ResponseReturnValue:
|
|
100
|
+
with server_logger.push_scope(route):
|
|
101
|
+
try:
|
|
102
|
+
signature_key = retrieve_secret(
|
|
103
|
+
profile_metadata=profile_meta,
|
|
104
|
+
secret_retrieval=job.signature_key_secret,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
passed_signature = flask.request.headers.get(
|
|
108
|
+
"Uncountable-Webhook-Signature"
|
|
109
|
+
)
|
|
110
|
+
if passed_signature is None:
|
|
111
|
+
raise WebhookException.no_signature_passed()
|
|
112
|
+
|
|
113
|
+
webhook_payload = _parse_webhook_payload(
|
|
114
|
+
raw_request_body=flask.request.data,
|
|
115
|
+
signature_key=signature_key,
|
|
116
|
+
passed_signature=passed_signature,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
send_job_queue_message(
|
|
121
|
+
job_ref_name=job.id,
|
|
122
|
+
payload=queued_job_t.QueuedJobPayload(
|
|
123
|
+
invocation_context=queued_job_t.InvocationContextWebhook(
|
|
124
|
+
webhook_payload=webhook_payload
|
|
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:
|
|
134
|
+
server_logger.log_exception(e)
|
|
135
|
+
return e.make_error_response()
|
|
136
|
+
except Exception as e:
|
|
137
|
+
server_logger.log_exception(e)
|
|
138
|
+
return WebhookException.unknown_error().make_error_response()
|
|
139
|
+
|
|
140
|
+
app.add_url_rule(
|
|
141
|
+
route,
|
|
142
|
+
endpoint=f"handle_webhook_{job.id}",
|
|
143
|
+
view_func=handle_webhook,
|
|
144
|
+
methods=["POST"],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
server_logger.log_info(f"job {job.id} webhook registered at: {route}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main() -> None:
|
|
151
|
+
profiles = load_profiles()
|
|
152
|
+
for profile_metadata in profiles:
|
|
153
|
+
server_logger = Logger(get_current_span())
|
|
154
|
+
for job in profile_metadata.jobs:
|
|
155
|
+
if isinstance(job, job_definition_t.WebhookJobDefinition):
|
|
156
|
+
register_route(
|
|
157
|
+
server_logger=server_logger, profile_meta=profile_metadata, job=job
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
main()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
app.run(
|
|
166
|
+
host="0.0.0.0",
|
|
167
|
+
port=get_webhook_server_port(),
|
|
168
|
+
debug=get_server_env() == "playground",
|
|
169
|
+
exclude_patterns=[],
|
|
170
|
+
)
|