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,28 +1,38 @@
|
|
|
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
|
|
8
8
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
9
9
|
from apscheduler.schedulers.base import BaseScheduler
|
|
10
10
|
from apscheduler.triggers.cron import CronTrigger
|
|
11
|
+
from opentelemetry.trace import get_current_span
|
|
11
12
|
from sqlalchemy.engine.base import Engine
|
|
12
13
|
|
|
14
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
13
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
|
+
)
|
|
14
19
|
from uncountable.integration.telemetry import Logger
|
|
15
|
-
from uncountable.types import base_t
|
|
16
|
-
from uncountable.types.client_config_t import ClientConfigOptions
|
|
20
|
+
from uncountable.types import base_t, job_definition_t
|
|
17
21
|
from uncountable.types.job_definition_t import (
|
|
18
|
-
AuthRetrieval,
|
|
19
22
|
CronJobDefinition,
|
|
20
|
-
|
|
21
|
-
ProfileMetadata,
|
|
23
|
+
HttpJobDefinitionBase,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
_MAX_APSCHEDULER_CONCURRENT_JOBS = 1
|
|
25
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
|
+
|
|
26
36
|
|
|
27
37
|
class IntegrationServer:
|
|
28
38
|
_scheduler: BaseScheduler
|
|
@@ -36,69 +46,83 @@ class IntegrationServer:
|
|
|
36
46
|
jobstores={"default": SQLAlchemyJobStore(engine=engine)},
|
|
37
47
|
executors={"default": ThreadPoolExecutor(_MAX_APSCHEDULER_CONCURRENT_JOBS)},
|
|
38
48
|
)
|
|
39
|
-
self._server_logger = Logger()
|
|
49
|
+
self._server_logger = Logger(get_current_span())
|
|
40
50
|
|
|
41
|
-
def
|
|
42
|
-
self
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
f"could not reconstitute job {job_defn.id}: {e}"
|
|
70
|
-
)
|
|
71
|
-
self._scheduler.remove_job(job_defn.id)
|
|
72
|
-
existing_job = None
|
|
73
|
-
if existing_job is not None:
|
|
74
|
-
existing_job.modify(
|
|
75
|
-
name=job_defn.name,
|
|
76
|
-
kwargs=job_kwargs,
|
|
77
|
-
)
|
|
78
|
-
existing_job.reschedule(
|
|
79
|
-
CronTrigger.from_crontab(job_defn.cron_spec)
|
|
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
|
+
|
|
67
|
+
def register_jobs(self, profiles: list[job_definition_t.ProfileMetadata]) -> None:
|
|
68
|
+
valid_job_ids: set[str] = set()
|
|
69
|
+
for profile_metadata in profiles:
|
|
70
|
+
for job_defn in profile_metadata.jobs:
|
|
71
|
+
valid_job_ids.add(job_defn.id)
|
|
72
|
+
match job_defn:
|
|
73
|
+
case CronJobDefinition():
|
|
74
|
+
# Add to ap scheduler
|
|
75
|
+
job_kwargs = asdict(
|
|
76
|
+
CronJobArgs(
|
|
77
|
+
definition=job_defn, profile_metadata=profile_metadata
|
|
78
|
+
)
|
|
80
79
|
)
|
|
81
|
-
|
|
82
|
-
existing_job.
|
|
80
|
+
try:
|
|
81
|
+
existing_job = self._scheduler.get_job(job_defn.id)
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
self._server_logger.log_warning(
|
|
84
|
+
f"could not reconstitute job {job_defn.id}: {e}"
|
|
85
|
+
)
|
|
86
|
+
self._scheduler.remove_job(job_defn.id)
|
|
87
|
+
existing_job = None
|
|
88
|
+
if existing_job is not None:
|
|
89
|
+
existing_job.modify(
|
|
90
|
+
name=job_defn.name,
|
|
91
|
+
kwargs=job_kwargs,
|
|
92
|
+
misfire_grace_time=None,
|
|
93
|
+
)
|
|
94
|
+
existing_job.reschedule(
|
|
95
|
+
CronTrigger.from_crontab(job_defn.cron_spec)
|
|
96
|
+
)
|
|
97
|
+
if not job_defn.enabled:
|
|
98
|
+
existing_job.pause()
|
|
99
|
+
else:
|
|
100
|
+
existing_job.resume()
|
|
83
101
|
else:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
job_opts: dict[str, base_t.JsonValue] = {}
|
|
103
|
+
if not job_defn.enabled:
|
|
104
|
+
job_opts["next_run_time"] = None
|
|
105
|
+
self._scheduler.add_job(
|
|
106
|
+
cron_job_executor,
|
|
107
|
+
# IMPROVE: reconsider these defaults
|
|
108
|
+
max_instances=1,
|
|
109
|
+
coalesce=True,
|
|
110
|
+
trigger=CronTrigger.from_crontab(job_defn.cron_spec),
|
|
111
|
+
name=job_defn.name,
|
|
112
|
+
id=job_defn.id,
|
|
113
|
+
kwargs=job_kwargs,
|
|
114
|
+
misfire_grace_time=None,
|
|
115
|
+
**job_opts,
|
|
116
|
+
)
|
|
117
|
+
case HttpJobDefinitionBase():
|
|
118
|
+
pass
|
|
119
|
+
case _:
|
|
120
|
+
assert_never(job_defn)
|
|
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)
|
|
102
126
|
|
|
103
127
|
def serve_forever(self) -> None:
|
|
104
128
|
signal.pause()
|
|
@@ -111,12 +135,13 @@ class IntegrationServer:
|
|
|
111
135
|
|
|
112
136
|
def __enter__(self) -> "IntegrationServer":
|
|
113
137
|
self._start_apscheduler()
|
|
138
|
+
self._register_static_jobs()
|
|
114
139
|
return self
|
|
115
140
|
|
|
116
141
|
def __exit__(
|
|
117
142
|
self,
|
|
118
|
-
exc_type:
|
|
119
|
-
exc_val:
|
|
120
|
-
exc_tb:
|
|
143
|
+
exc_type: type[BaseException] | None,
|
|
144
|
+
exc_val: BaseException | None,
|
|
145
|
+
exc_tb: TracebackType | None,
|
|
121
146
|
) -> None:
|
|
122
147
|
self._stop_apscheduler()
|
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
|
-
import sys
|
|
4
4
|
import time
|
|
5
|
+
import traceback
|
|
6
|
+
import typing
|
|
5
7
|
from contextlib import contextmanager
|
|
6
8
|
from enum import StrEnum
|
|
7
|
-
from typing import Generator,
|
|
9
|
+
from typing import Generator, assert_never, cast
|
|
8
10
|
|
|
9
|
-
from opentelemetry import trace
|
|
11
|
+
from opentelemetry import _logs, trace
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
10
13
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
11
|
-
from opentelemetry.sdk._logs import
|
|
14
|
+
from opentelemetry.sdk._logs import Logger as OTELLogger
|
|
15
|
+
from opentelemetry.sdk._logs import (
|
|
16
|
+
LoggerProvider,
|
|
17
|
+
LogRecord,
|
|
18
|
+
)
|
|
19
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
|
|
12
20
|
from opentelemetry.sdk.resources import Attributes, Resource
|
|
13
21
|
from opentelemetry.sdk.trace import TracerProvider
|
|
14
22
|
from opentelemetry.sdk.trace.export import (
|
|
15
23
|
SimpleSpanProcessor,
|
|
16
24
|
)
|
|
17
|
-
from opentelemetry.trace import Tracer
|
|
25
|
+
from opentelemetry.trace import DEFAULT_TRACE_OPTIONS, Span, Tracer
|
|
18
26
|
|
|
19
|
-
from uncountable.core.
|
|
27
|
+
from uncountable.core.environment import (
|
|
28
|
+
get_otel_enabled,
|
|
29
|
+
get_server_env,
|
|
30
|
+
get_version,
|
|
31
|
+
)
|
|
20
32
|
from uncountable.types import base_t, job_definition_t
|
|
21
33
|
|
|
22
34
|
|
|
@@ -24,8 +36,13 @@ def _cast_attributes(attributes: dict[str, base_t.JsonValue]) -> Attributes:
|
|
|
24
36
|
return cast(Attributes, attributes)
|
|
25
37
|
|
|
26
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
|
+
|
|
27
44
|
@functools.cache
|
|
28
|
-
def
|
|
45
|
+
def get_otel_resource() -> Resource:
|
|
29
46
|
attributes: dict[str, base_t.JsonValue] = {
|
|
30
47
|
"service.name": "integration-server",
|
|
31
48
|
"sdk.version": get_version(),
|
|
@@ -33,16 +50,34 @@ def get_tracer() -> Tracer:
|
|
|
33
50
|
unc_version = os.environ.get("UNC_VERSION")
|
|
34
51
|
if unc_version is not None:
|
|
35
52
|
attributes["service.version"] = unc_version
|
|
36
|
-
unc_env =
|
|
53
|
+
unc_env = get_server_env()
|
|
37
54
|
if unc_env is not None:
|
|
38
55
|
attributes["deployment.environment"] = unc_env
|
|
39
56
|
resource = Resource.create(attributes=_cast_attributes(attributes))
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
return resource
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@functools.cache
|
|
61
|
+
def get_otel_tracer() -> Tracer:
|
|
62
|
+
provider = TracerProvider(resource=get_otel_resource())
|
|
63
|
+
if get_otel_enabled():
|
|
64
|
+
provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter()))
|
|
42
65
|
trace.set_tracer_provider(provider)
|
|
43
66
|
return provider.get_tracer("integration.telemetry")
|
|
44
67
|
|
|
45
68
|
|
|
69
|
+
@functools.cache
|
|
70
|
+
def get_otel_logger() -> OTELLogger:
|
|
71
|
+
provider = LoggerProvider(resource=get_otel_resource())
|
|
72
|
+
provider.add_log_record_processor(
|
|
73
|
+
BatchLogRecordProcessor(ConsoleLogExporter(formatter=one_line_formatter))
|
|
74
|
+
)
|
|
75
|
+
if get_otel_enabled():
|
|
76
|
+
provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
|
|
77
|
+
_logs.set_logger_provider(provider)
|
|
78
|
+
return provider.get_logger("integration.telemetry")
|
|
79
|
+
|
|
80
|
+
|
|
46
81
|
class LogSeverity(StrEnum):
|
|
47
82
|
INFO = "Info"
|
|
48
83
|
WARN = "Warn"
|
|
@@ -50,50 +85,129 @@ class LogSeverity(StrEnum):
|
|
|
50
85
|
|
|
51
86
|
|
|
52
87
|
class Logger:
|
|
53
|
-
|
|
54
|
-
current_trace_id: int | None = None
|
|
88
|
+
current_span: Span
|
|
55
89
|
|
|
56
|
-
def
|
|
57
|
-
|
|
90
|
+
def __init__(self, base_span: Span) -> None:
|
|
91
|
+
self.current_span = base_span
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def current_span_id(self) -> int:
|
|
95
|
+
return self.current_span.get_span_context().span_id
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def current_trace_id(self) -> int | None:
|
|
99
|
+
return self.current_span.get_span_context().trace_id
|
|
100
|
+
|
|
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
|
|
58
122
|
|
|
59
123
|
def _emit_log(
|
|
60
124
|
self, message: str, *, severity: LogSeverity, attributes: Attributes | None
|
|
61
125
|
) -> None:
|
|
126
|
+
otel_logger = get_otel_logger()
|
|
62
127
|
log_record = LogRecord(
|
|
63
128
|
body=message,
|
|
64
129
|
severity_text=severity,
|
|
65
130
|
timestamp=time.time_ns(),
|
|
66
|
-
attributes=self._patch_attributes(
|
|
131
|
+
attributes=self._patch_attributes(
|
|
132
|
+
message=message, severity=severity, attributes=attributes
|
|
133
|
+
),
|
|
67
134
|
span_id=self.current_span_id,
|
|
68
135
|
trace_id=self.current_trace_id,
|
|
136
|
+
trace_flags=DEFAULT_TRACE_OPTIONS,
|
|
137
|
+
severity_number=_logs.SeverityNumber.UNSPECIFIED,
|
|
138
|
+
resource=get_otel_resource(),
|
|
69
139
|
)
|
|
70
|
-
|
|
71
|
-
log_file.write(log_record.to_json())
|
|
72
|
-
log_file.flush()
|
|
140
|
+
otel_logger.emit(log_record)
|
|
73
141
|
|
|
74
142
|
def log_info(self, message: str, *, attributes: Attributes | None = None) -> None:
|
|
75
|
-
self._emit_log(
|
|
143
|
+
self._emit_log(
|
|
144
|
+
message=message, severity=LogSeverity.INFO, attributes=attributes
|
|
145
|
+
)
|
|
76
146
|
|
|
77
|
-
def log_warning(
|
|
78
|
-
self
|
|
147
|
+
def log_warning(
|
|
148
|
+
self, message: str, *, attributes: Attributes | None = None
|
|
149
|
+
) -> None:
|
|
150
|
+
self._emit_log(
|
|
151
|
+
message=message, severity=LogSeverity.WARN, attributes=attributes
|
|
152
|
+
)
|
|
79
153
|
|
|
80
154
|
def log_error(self, message: str, *, attributes: Attributes | None = None) -> None:
|
|
81
|
-
self._emit_log(
|
|
155
|
+
self._emit_log(
|
|
156
|
+
message=message, severity=LogSeverity.ERROR, attributes=attributes
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def log_exception(
|
|
160
|
+
self,
|
|
161
|
+
exception: BaseException,
|
|
162
|
+
*,
|
|
163
|
+
message: str | None = None,
|
|
164
|
+
attributes: Attributes | None = None,
|
|
165
|
+
) -> None:
|
|
166
|
+
traceback_str = "".join(traceback.format_exception(exception))
|
|
167
|
+
patched_attributes = self._patch_attributes(
|
|
168
|
+
message=message, severity=LogSeverity.ERROR, attributes=attributes
|
|
169
|
+
)
|
|
170
|
+
self.current_span.record_exception(
|
|
171
|
+
exception=exception, attributes=patched_attributes
|
|
172
|
+
)
|
|
173
|
+
self.log_error(
|
|
174
|
+
message=f"error: {message}\nexception: {exception}{traceback_str}",
|
|
175
|
+
attributes=patched_attributes,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@contextmanager
|
|
179
|
+
def push_scope(
|
|
180
|
+
self, scope_name: str, *, attributes: Attributes | None = None
|
|
181
|
+
) -> Generator[typing.Self, None, None]:
|
|
182
|
+
with get_otel_tracer().start_as_current_span(
|
|
183
|
+
scope_name, attributes=self._patch_attributes(attributes)
|
|
184
|
+
):
|
|
185
|
+
yield self
|
|
82
186
|
|
|
83
187
|
|
|
84
188
|
class JobLogger(Logger):
|
|
85
189
|
def __init__(
|
|
86
190
|
self,
|
|
87
191
|
*,
|
|
192
|
+
base_span: Span,
|
|
88
193
|
profile_metadata: job_definition_t.ProfileMetadata,
|
|
89
194
|
job_definition: job_definition_t.JobDefinition,
|
|
90
195
|
) -> None:
|
|
91
196
|
self.profile_metadata = profile_metadata
|
|
92
197
|
self.job_definition = job_definition
|
|
198
|
+
super().__init__(base_span)
|
|
93
199
|
|
|
94
|
-
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:
|
|
95
207
|
patched_attributes: dict[str, base_t.JsonValue] = {
|
|
96
|
-
**(
|
|
208
|
+
**super()._patch_attributes(
|
|
209
|
+
attributes=attributes, message=message, severity=severity
|
|
210
|
+
)
|
|
97
211
|
}
|
|
98
212
|
patched_attributes["profile.name"] = self.profile_metadata.name
|
|
99
213
|
patched_attributes["profile.base_url"] = self.profile_metadata.base_url
|
|
@@ -105,6 +219,8 @@ class JobLogger(Logger):
|
|
|
105
219
|
patched_attributes["job.definition.cron_spec"] = (
|
|
106
220
|
self.job_definition.cron_spec
|
|
107
221
|
)
|
|
222
|
+
case job_definition_t.HttpJobDefinitionBase():
|
|
223
|
+
pass
|
|
108
224
|
case _:
|
|
109
225
|
assert_never(self.job_definition)
|
|
110
226
|
patched_attributes["job.definition.executor.type"] = (
|
|
@@ -123,13 +239,13 @@ class JobLogger(Logger):
|
|
|
123
239
|
assert_never(self.job_definition.executor)
|
|
124
240
|
return _cast_attributes(patched_attributes)
|
|
125
241
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
yield
|
|
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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
import flask
|
|
4
|
+
from flask.typing import ResponseReturnValue
|
|
5
|
+
from opentelemetry.trace import get_current_span
|
|
6
|
+
from uncountable.core.environment import (
|
|
7
|
+
get_http_server_port,
|
|
8
|
+
get_server_env,
|
|
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
|
|
13
|
+
from uncountable.integration.scan_profiles import load_profiles
|
|
14
|
+
from uncountable.integration.telemetry import Logger
|
|
15
|
+
from uncountable.types import job_definition_t
|
|
16
|
+
|
|
17
|
+
app = flask.Flask(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register_route(
|
|
21
|
+
*,
|
|
22
|
+
server_logger: Logger,
|
|
23
|
+
profile_meta: job_definition_t.ProfileMetadata,
|
|
24
|
+
job: job_definition_t.HttpJobDefinitionBase,
|
|
25
|
+
) -> None:
|
|
26
|
+
route = f"/{profile_meta.name}/{job.id}"
|
|
27
|
+
|
|
28
|
+
def handle_request() -> ResponseReturnValue:
|
|
29
|
+
with server_logger.push_scope(route):
|
|
30
|
+
try:
|
|
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
|
|
37
|
+
)
|
|
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),
|
|
45
|
+
)
|
|
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
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return flask.make_response(
|
|
54
|
+
http_response.response,
|
|
55
|
+
http_response.status_code,
|
|
56
|
+
http_response.headers,
|
|
57
|
+
)
|
|
58
|
+
except HttpException as e:
|
|
59
|
+
server_logger.log_exception(e)
|
|
60
|
+
return e.make_error_response()
|
|
61
|
+
except Exception as e:
|
|
62
|
+
server_logger.log_exception(e)
|
|
63
|
+
return HttpException.unknown_error().make_error_response()
|
|
64
|
+
|
|
65
|
+
app.add_url_rule(
|
|
66
|
+
route,
|
|
67
|
+
endpoint=f"handle_request_{job.id}",
|
|
68
|
+
view_func=handle_request,
|
|
69
|
+
methods=["POST"],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
server_logger.log_info(f"job {job.id} webhook registered at: {route}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main() -> None:
|
|
76
|
+
app.add_url_rule("/health", "health", lambda: ("OK", 200))
|
|
77
|
+
|
|
78
|
+
profiles = load_profiles()
|
|
79
|
+
for profile_metadata in profiles:
|
|
80
|
+
server_logger = Logger(get_current_span())
|
|
81
|
+
for job in profile_metadata.jobs:
|
|
82
|
+
if isinstance(job, job_definition_t.HttpJobDefinitionBase):
|
|
83
|
+
register_route(
|
|
84
|
+
server_logger=server_logger, profile_meta=profile_metadata, job=job
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
main()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
app.run(
|
|
93
|
+
host="0.0.0.0",
|
|
94
|
+
port=get_http_server_port(),
|
|
95
|
+
debug=get_server_env() == "playground",
|
|
96
|
+
exclude_patterns=[],
|
|
97
|
+
)
|