UncountablePythonSDK 0.0.181__py3-none-any.whl → 0.0.182__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.
@@ -1,4 +1,5 @@
1
1
  base_url: http://host.docker.internal:5000
2
+ integration_server_base_url: http://localhost:5001
2
3
  auth_retrieval:
3
4
  type: basic
4
5
  api_id_secret:
@@ -60,6 +61,13 @@ jobs:
60
61
  executor:
61
62
  type: script
62
63
  import_path: example_wh
64
+ - id: example_unc_wh
65
+ type: uncountable_webhook
66
+ name: Uncountable Webhook
67
+ purpose: trigger
68
+ executor:
69
+ type: script
70
+ import_path: example_wh
63
71
  - id: example_http
64
72
  type: custom_http
65
73
  name: Custom HTTP
@@ -1,11 +1,14 @@
1
1
  from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
2
2
  from uncountable.integration.scan_profiles import load_profiles
3
3
  from uncountable.integration.server import IntegrationServer
4
+ from uncountable.integration.webhook_bootstrap import start_webhook_entity_bootstrap
4
5
 
5
6
 
6
7
  def main() -> None:
8
+ profiles = load_profiles()
7
9
  with IntegrationServer(create_db_engine(IntegrationDBService.CRON)) as server:
8
- server.register_jobs(load_profiles())
10
+ start_webhook_entity_bootstrap(profiles)
11
+ server.register_jobs(profiles)
9
12
  server.serve_forever()
10
13
 
11
14
 
@@ -28,6 +28,10 @@ class HttpException(Exception):
28
28
  def body_parse_error() -> "HttpException":
29
29
  return HttpException(error_code=400, message="body parse error")
30
30
 
31
+ @staticmethod
32
+ def bad_request(message: str = "bad request") -> "HttpException":
33
+ return HttpException(error_code=400, message=message)
34
+
31
35
  @staticmethod
32
36
  def unauthorized() -> "HttpException":
33
37
  return HttpException(error_code=401, message="unauthorized")
@@ -27,7 +27,7 @@ from uncountable.integration.queue_runner.command_server.types import (
27
27
  from uncountable.integration.request_context import extract_request_context
28
28
  from uncountable.integration.secret_retrieval import retrieve_secret
29
29
  from uncountable.integration.telemetry import JobLogger
30
- from uncountable.integration.webhook_signature_key import (
30
+ from uncountable.integration.webhook_entity import (
31
31
  webhook_signature_key_retrieval,
32
32
  )
33
33
  from uncountable.types import (
@@ -3,9 +3,7 @@ from opentelemetry.trace import get_current_span
3
3
  from uncountable.integration.construct_client import construct_uncountable_client
4
4
  from uncountable.integration.scan_profiles import load_profiles
5
5
  from uncountable.integration.telemetry import Logger
6
- from uncountable.integration.webhook_signature_key import (
7
- bootstrap_webhook_signature_key,
8
- )
6
+ from uncountable.integration.webhook_entity import create_missing_webhook_entities
9
7
  from uncountable.types import job_definition_t
10
8
 
11
9
 
@@ -17,12 +15,10 @@ def provision_webhook(*, job_id: str) -> str | None:
17
15
  isinstance(job, job_definition_t.UncountableWebhookJobDefinition)
18
16
  and job.id == job_id
19
17
  ):
20
- client = construct_uncountable_client(profile_metadata, logger)
21
- bootstrap_webhook_signature_key(
22
- client=client,
18
+ create_missing_webhook_entities(
19
+ client=construct_uncountable_client(profile_metadata, logger),
23
20
  profile_metadata=profile_metadata,
24
- job_id=job.id,
25
- purpose=job.purpose,
21
+ jobs=[job],
26
22
  )
27
23
  return profile_metadata.name
28
24
  return None
@@ -0,0 +1,67 @@
1
+ import threading
2
+
3
+ from opentelemetry.trace import get_current_span
4
+
5
+ from uncountable.integration.construct_client import construct_uncountable_client
6
+ from uncountable.integration.telemetry import Logger
7
+ from uncountable.integration.webhook_entity import create_missing_webhook_entities
8
+ from uncountable.types import job_definition_t
9
+
10
+ _BOOTSTRAP_SCOPE_NAME = "bootstrap_webhook_entities"
11
+ _BOOTSTRAP_THREAD_NAME = "webhook-entity-bootstrap"
12
+
13
+
14
+ def _bootstrap_profile_webhook_entities(
15
+ *,
16
+ profile_metadata: job_definition_t.ProfileMetadata,
17
+ jobs: list[job_definition_t.UncountableWebhookJobDefinition],
18
+ logger: Logger,
19
+ ) -> None:
20
+ created_job_ids = create_missing_webhook_entities(
21
+ client=construct_uncountable_client(profile_metadata, logger),
22
+ profile_metadata=profile_metadata,
23
+ jobs=jobs,
24
+ )
25
+ logger.log_debug(
26
+ f"bootstrapped {len(created_job_ids)} webhook entities for profile {profile_metadata.name}",
27
+ attributes={
28
+ "profile.name": profile_metadata.name,
29
+ "webhook.created_job_ids": created_job_ids,
30
+ },
31
+ )
32
+
33
+
34
+ def _bootstrap_webhook_entities(
35
+ profiles: list[job_definition_t.ProfileMetadata],
36
+ ) -> None:
37
+ logger = Logger(get_current_span())
38
+ with logger.push_scope(_BOOTSTRAP_SCOPE_NAME):
39
+ for profile_metadata in profiles:
40
+ webhook_jobs = [
41
+ job
42
+ for job in profile_metadata.jobs
43
+ if isinstance(job, job_definition_t.UncountableWebhookJobDefinition)
44
+ ]
45
+ if len(webhook_jobs) == 0:
46
+ continue
47
+ try:
48
+ _bootstrap_profile_webhook_entities(
49
+ profile_metadata=profile_metadata, jobs=webhook_jobs, logger=logger
50
+ )
51
+ except Exception as e:
52
+ logger.log_exception(
53
+ e,
54
+ message=f"failed to bootstrap webhook entities for profile {profile_metadata.name}",
55
+ attributes={"profile.name": profile_metadata.name},
56
+ )
57
+
58
+
59
+ def start_webhook_entity_bootstrap(
60
+ profiles: list[job_definition_t.ProfileMetadata],
61
+ ) -> None:
62
+ threading.Thread(
63
+ target=_bootstrap_webhook_entities,
64
+ args=(profiles,),
65
+ name=_BOOTSTRAP_THREAD_NAME,
66
+ daemon=True,
67
+ ).start()
@@ -0,0 +1,147 @@
1
+ import secrets
2
+ from collections.abc import Sequence
3
+
4
+ from uncountable.core.client import Client
5
+ from uncountable.core.query.builder import QueryBuilder
6
+ from uncountable.core.query.column import (
7
+ NullableColumn,
8
+ TextColumn,
9
+ )
10
+ from uncountable.core.query.row import QueryRow
11
+ from uncountable.types import (
12
+ create_or_update_entities_t,
13
+ entity_t,
14
+ field_values_t,
15
+ identifier_t,
16
+ listing_t,
17
+ )
18
+ from uncountable.types.job_definition_t import (
19
+ ProfileMetadata,
20
+ UncountableWebhookJobDefinition,
21
+ )
22
+ from uncountable.types.secret_retrieval_t import SecretRetrievalEntityColumn
23
+
24
+ _WEBHOOK_DEFINITION_REF_NAME = "unc_webhook_definition"
25
+ _WEBHOOK_REF_NAME_FIELD_REF_NAME = "refName"
26
+ _WEBHOOK_NAME_FIELD_REF_NAME = "name"
27
+ _WEBHOOK_URL_FIELD_REF_NAME = "core_webhook_url"
28
+ _WEBHOOK_SIGNATURE_KEY_FIELD_REF_NAME = "core_webhook_signatureKey"
29
+ _WEBHOOK_PURPOSE_FIELD_REF_NAME = "core_webhook_purpose"
30
+ _SIGNATURE_KEY_NUM_BYTES = 32
31
+
32
+
33
+ class _WebhookEntityQueryRow(QueryRow):
34
+ ref_name = TextColumn(
35
+ identifier=listing_t.ColumnIdentifierEntityRefName(
36
+ entity_type=entity_t.EntityType.WEBHOOK,
37
+ ref_name=_WEBHOOK_REF_NAME_FIELD_REF_NAME,
38
+ ),
39
+ nullable=NullableColumn,
40
+ )
41
+
42
+
43
+ def webhook_signature_key_retrieval(*, job_id: str) -> SecretRetrievalEntityColumn:
44
+ return SecretRetrievalEntityColumn(
45
+ entity_key=identifier_t.IdentifierKeyRefName(ref_name=job_id),
46
+ column_identifier=listing_t.ColumnIdentifierEntityRefName(
47
+ entity_type=entity_t.EntityType.WEBHOOK,
48
+ ref_name=_WEBHOOK_SIGNATURE_KEY_FIELD_REF_NAME,
49
+ ),
50
+ )
51
+
52
+
53
+ def _resolve_webhook_url(*, profile_metadata: ProfileMetadata, job_id: str) -> str:
54
+ integration_server_base_url = profile_metadata.integration_server_base_url
55
+ if integration_server_base_url is None:
56
+ raise ValueError(
57
+ "integration_server_base_url is required to resolve the webhook entity "
58
+ f"for {profile_metadata.name}/{job_id}"
59
+ )
60
+ return f"{integration_server_base_url.rstrip('/')}/{profile_metadata.name}/{job_id}"
61
+
62
+
63
+ def _fetch_existing_webhook_ref_names(
64
+ *, client: Client, job_ids: Sequence[str]
65
+ ) -> set[str]:
66
+ existing_webhooks = (
67
+ QueryBuilder(client=client, model=_WebhookEntityQueryRow)
68
+ .filter(_WebhookEntityQueryRow.ref_name.includes(tuple(job_ids)))
69
+ .all()
70
+ )
71
+ return {
72
+ webhook.ref_name
73
+ for webhook in existing_webhooks
74
+ if webhook.ref_name is not None
75
+ }
76
+
77
+
78
+ def _text_field_value(
79
+ *, field_ref_name: str, value: str
80
+ ) -> field_values_t.FieldArgumentValue:
81
+ return field_values_t.FieldArgumentValue(
82
+ field_key=identifier_t.IdentifierKeyRefName(ref_name=field_ref_name),
83
+ value=field_values_t.FieldValueText(value=value),
84
+ )
85
+
86
+
87
+ def _build_webhook_operation(
88
+ *, profile_metadata: ProfileMetadata, job: UncountableWebhookJobDefinition
89
+ ) -> create_or_update_entities_t.EntityToCreateOrUpdate:
90
+ # entity_key is deliberately omitted so the operation always takes the create
91
+ # path. Supplying a ref_name key that resolves to nothing raises not-found
92
+ # rather than creating the entity, and callers only pass jobs already known
93
+ # to have no webhook entity.
94
+ return create_or_update_entities_t.EntityToCreateOrUpdate(
95
+ field_values=[],
96
+ on_create_init_field_values=[
97
+ _text_field_value(
98
+ field_ref_name=_WEBHOOK_REF_NAME_FIELD_REF_NAME, value=job.id
99
+ ),
100
+ _text_field_value(
101
+ field_ref_name=_WEBHOOK_NAME_FIELD_REF_NAME,
102
+ value=f"{profile_metadata.name}/{job.id}",
103
+ ),
104
+ _text_field_value(
105
+ field_ref_name=_WEBHOOK_URL_FIELD_REF_NAME,
106
+ value=_resolve_webhook_url(
107
+ profile_metadata=profile_metadata, job_id=job.id
108
+ ),
109
+ ),
110
+ _text_field_value(
111
+ field_ref_name=_WEBHOOK_SIGNATURE_KEY_FIELD_REF_NAME,
112
+ value=secrets.token_urlsafe(_SIGNATURE_KEY_NUM_BYTES),
113
+ ),
114
+ field_values_t.FieldArgumentValue(
115
+ field_key=identifier_t.IdentifierKeyRefName(
116
+ ref_name=_WEBHOOK_PURPOSE_FIELD_REF_NAME
117
+ ),
118
+ value=field_values_t.FieldValueFieldOption(value=job.purpose.value),
119
+ ),
120
+ ],
121
+ )
122
+
123
+
124
+ def create_missing_webhook_entities(
125
+ *,
126
+ client: Client,
127
+ profile_metadata: ProfileMetadata,
128
+ jobs: Sequence[UncountableWebhookJobDefinition],
129
+ ) -> list[str]:
130
+ existing_ref_names = _fetch_existing_webhook_ref_names(
131
+ client=client, job_ids=[job.id for job in jobs]
132
+ )
133
+ missing_jobs = [job for job in jobs if job.id not in existing_ref_names]
134
+ if len(missing_jobs) == 0:
135
+ return []
136
+
137
+ client.create_or_update_entities(
138
+ entity_type=entity_t.EntityType.WEBHOOK,
139
+ definition_key=identifier_t.IdentifierKeyRefName(
140
+ ref_name=_WEBHOOK_DEFINITION_REF_NAME
141
+ ),
142
+ operations=[
143
+ _build_webhook_operation(profile_metadata=profile_metadata, job=job)
144
+ for job in missing_jobs
145
+ ],
146
+ )
147
+ return [job.id for job in missing_jobs]
@@ -0,0 +1,3 @@
1
+ from uncountable.integration.webhook_server.admin_routes.register import (
2
+ register_admin_routes as register_admin_routes,
3
+ )
@@ -0,0 +1,39 @@
1
+ import functools
2
+ from collections.abc import Callable
3
+
4
+ import flask
5
+ from flask.typing import ResponseReturnValue
6
+ from uncountable.integration.http_server import HttpException
7
+ from uncountable.integration.queue_runner.command_server.types import (
8
+ CommandServerException,
9
+ )
10
+ from uncountable.integration.telemetry import Logger
11
+
12
+
13
+ def register_admin_route(
14
+ *,
15
+ app: flask.Flask,
16
+ logger: Logger,
17
+ route: str,
18
+ endpoint: str,
19
+ view: Callable[..., ResponseReturnValue],
20
+ methods: list[str],
21
+ ) -> None:
22
+ @functools.wraps(view)
23
+ def wrapped(*args: object, **kwargs: object) -> ResponseReturnValue:
24
+ with logger.push_scope(route):
25
+ try:
26
+ return view(*args, **kwargs)
27
+ except HttpException as e:
28
+ return e.make_error_response()
29
+ except CommandServerException as e:
30
+ logger.log_exception(
31
+ e,
32
+ message=f"admin route {route} could not reach the command server",
33
+ )
34
+ return HttpException.bad_gateway().make_error_response()
35
+ except Exception as e:
36
+ logger.log_exception(e, message=f"unexpected error on {route}")
37
+ return HttpException.unknown_error().make_error_response()
38
+
39
+ app.add_url_rule(route, endpoint=endpoint, view_func=wrapped, methods=methods)
@@ -0,0 +1,30 @@
1
+ import flask
2
+ from flask.typing import ResponseReturnValue
3
+ from uncountable.core.environment import get_local_admin_server_port
4
+ from uncountable.integration.queue_runner.command_server.command_client import (
5
+ check_health,
6
+ )
7
+ from uncountable.integration.telemetry import Logger
8
+ from uncountable.integration.webhook_server.admin_auth import require_admin_auth
9
+ from uncountable.integration.webhook_server.admin_routes._boundary import (
10
+ register_admin_route,
11
+ )
12
+
13
+ _ADMIN_HEALTH_ROUTE = "/admin/health"
14
+
15
+
16
+ @require_admin_auth
17
+ def _handle_admin_health() -> ResponseReturnValue:
18
+ healthy = check_health(port=get_local_admin_server_port())
19
+ return flask.jsonify({"healthy": healthy})
20
+
21
+
22
+ def register_health_routes(*, app: flask.Flask, logger: Logger) -> None:
23
+ register_admin_route(
24
+ app=app,
25
+ logger=logger,
26
+ route=_ADMIN_HEALTH_ROUTE,
27
+ endpoint="admin_health",
28
+ view=_handle_admin_health,
29
+ methods=["GET"],
30
+ )
@@ -0,0 +1,147 @@
1
+ import datetime
2
+
3
+ import flask
4
+ from flask.typing import ResponseReturnValue
5
+ from uncountable.core.environment import get_local_admin_server_port
6
+ from uncountable.integration.http_server import HttpException
7
+ from uncountable.integration.queue_runner.command_server.command_client import (
8
+ send_job_cancellation_message,
9
+ send_job_queue_message,
10
+ send_list_queued_jobs_message,
11
+ send_retry_job_message,
12
+ )
13
+ from uncountable.integration.queue_runner.command_server.protocol.command_server_pb2 import (
14
+ ListQueuedJobsResult,
15
+ )
16
+ from uncountable.integration.telemetry import Logger
17
+ from uncountable.integration.webhook_server.admin_auth import require_admin_auth
18
+ from uncountable.integration.webhook_server.admin_routes._boundary import (
19
+ register_admin_route,
20
+ )
21
+ from uncountable.types import base_t, queued_job_t
22
+
23
+ from pkgs.serialization_util import serialize_for_api
24
+
25
+ _ADMIN_JOBS_ROUTE = "/admin/jobs"
26
+ _ADMIN_ENQUEUE_JOB_ROUTE = "/admin/jobs/enqueue"
27
+ _ADMIN_RETRY_JOB_ROUTE = "/admin/jobs/<job_uuid>/retry"
28
+ _ADMIN_CANCEL_JOB_ROUTE = "/admin/jobs/<job_uuid>/cancel"
29
+ _DEFAULT_JOBS_LIMIT = 100
30
+ _VALID_JOB_STATUSES = {status.value for status in queued_job_t.JobStatus}
31
+
32
+
33
+ def _parse_job_status(raw_status: str | None) -> queued_job_t.JobStatus | None:
34
+ if raw_status is None:
35
+ return None
36
+ if raw_status not in _VALID_JOB_STATUSES:
37
+ raise HttpException.bad_request(f"invalid status: {raw_status}")
38
+ return queued_job_t.JobStatus(raw_status)
39
+
40
+
41
+ def _parse_non_negative_int(raw_value: str | None, *, default: int) -> int:
42
+ if raw_value is None:
43
+ return default
44
+ if not raw_value.isdigit():
45
+ raise HttpException.bad_request(
46
+ f"expected a non-negative integer, got: {raw_value}"
47
+ )
48
+ return int(raw_value)
49
+
50
+
51
+ def _serialize_queued_job(
52
+ item: ListQueuedJobsResult.ListQueuedJobsResultItem,
53
+ ) -> dict[str, base_t.JsonValue]:
54
+ metadata = queued_job_t.QueuedJobMetadata(
55
+ queued_job_uuid=item.uuid,
56
+ job_ref_name=item.job_ref_name,
57
+ num_attempts=item.num_attempts,
58
+ status=queued_job_t.JobStatus(item.status),
59
+ submitted_at=item.submitted_at.ToDatetime(datetime.timezone.utc),
60
+ )
61
+ return serialize_for_api(metadata)
62
+
63
+
64
+ @require_admin_auth
65
+ def _handle_list_jobs() -> ResponseReturnValue:
66
+ status = _parse_job_status(flask.request.args.get("status"))
67
+ offset = _parse_non_negative_int(flask.request.args.get("offset"), default=0)
68
+ limit = _parse_non_negative_int(
69
+ flask.request.args.get("limit"), default=_DEFAULT_JOBS_LIMIT
70
+ )
71
+ jobs = send_list_queued_jobs_message(
72
+ status=status, offset=offset, limit=limit, port=get_local_admin_server_port()
73
+ )
74
+ return flask.jsonify({"jobs": [_serialize_queued_job(job) for job in jobs]})
75
+
76
+
77
+ @require_admin_auth
78
+ def _handle_enqueue_job() -> ResponseReturnValue:
79
+ body = flask.request.get_json(silent=True)
80
+ if not isinstance(body, dict):
81
+ raise HttpException.bad_request("expected a JSON object body")
82
+ job_ref_name = body.get("job_ref_name")
83
+ if not isinstance(job_ref_name, str):
84
+ raise HttpException.bad_request("job_ref_name is required")
85
+ webhook_payload = body.get("webhook_payload")
86
+ invocation_context: queued_job_t.InvocationContext = (
87
+ queued_job_t.InvocationContextManual()
88
+ if webhook_payload is None
89
+ else queued_job_t.InvocationContextWebhook(webhook_payload=webhook_payload)
90
+ )
91
+ queued_job_uuid = send_job_queue_message(
92
+ job_ref_name=job_ref_name,
93
+ payload=queued_job_t.QueuedJobPayload(invocation_context=invocation_context),
94
+ port=get_local_admin_server_port(),
95
+ )
96
+ return flask.jsonify({"queuedJobUuid": queued_job_uuid})
97
+
98
+
99
+ @require_admin_auth
100
+ def _handle_retry_job(job_uuid: str) -> ResponseReturnValue:
101
+ queued_job_uuid = send_retry_job_message(
102
+ job_uuid=job_uuid, port=get_local_admin_server_port()
103
+ )
104
+ return flask.jsonify({"queuedJobUuid": queued_job_uuid})
105
+
106
+
107
+ @require_admin_auth
108
+ def _handle_cancel_job(job_uuid: str) -> ResponseReturnValue:
109
+ status = send_job_cancellation_message(
110
+ queued_job_uuid=job_uuid, port=get_local_admin_server_port()
111
+ )
112
+ return flask.jsonify({"status": status.value})
113
+
114
+
115
+ def register_job_routes(*, app: flask.Flask, logger: Logger) -> None:
116
+ register_admin_route(
117
+ app=app,
118
+ logger=logger,
119
+ route=_ADMIN_JOBS_ROUTE,
120
+ endpoint="admin_list_jobs",
121
+ view=_handle_list_jobs,
122
+ methods=["GET"],
123
+ )
124
+ register_admin_route(
125
+ app=app,
126
+ logger=logger,
127
+ route=_ADMIN_ENQUEUE_JOB_ROUTE,
128
+ endpoint="admin_enqueue_job",
129
+ view=_handle_enqueue_job,
130
+ methods=["POST"],
131
+ )
132
+ register_admin_route(
133
+ app=app,
134
+ logger=logger,
135
+ route=_ADMIN_RETRY_JOB_ROUTE,
136
+ endpoint="admin_retry_job",
137
+ view=_handle_retry_job,
138
+ methods=["POST"],
139
+ )
140
+ register_admin_route(
141
+ app=app,
142
+ logger=logger,
143
+ route=_ADMIN_CANCEL_JOB_ROUTE,
144
+ endpoint="admin_cancel_job",
145
+ view=_handle_cancel_job,
146
+ methods=["POST"],
147
+ )
@@ -0,0 +1,13 @@
1
+ import flask
2
+ from opentelemetry.trace import get_current_span
3
+ from uncountable.integration.telemetry import Logger
4
+ from uncountable.integration.webhook_server.admin_routes.health import (
5
+ register_health_routes,
6
+ )
7
+ from uncountable.integration.webhook_server.admin_routes.jobs import register_job_routes
8
+
9
+
10
+ def register_admin_routes(app: flask.Flask) -> None:
11
+ logger = Logger(get_current_span())
12
+ register_health_routes(app=app, logger=logger)
13
+ register_job_routes(app=app, logger=logger)
@@ -9,6 +9,7 @@ from .generic_upload_t import UploadDestinationBase as UploadDestinationBase
9
9
  from .generic_upload_t import UploadDestinationProject as UploadDestinationProject
10
10
  from .generic_upload_t import UploadDestinationMaterialFamily as UploadDestinationMaterialFamily
11
11
  from .generic_upload_t import UploadDestinationRecipe as UploadDestinationRecipe
12
+ from .generic_upload_t import UploadDestinationAutoMatch as UploadDestinationAutoMatch
12
13
  from .generic_upload_t import UploadDestination as UploadDestination
13
14
  from .generic_upload_t import GenericUploadStrategy as GenericUploadStrategy
14
15
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -16,6 +16,7 @@ __all__: list[str] = [
16
16
  "GenericRemoteDirectoryScope",
17
17
  "GenericUploadStrategy",
18
18
  "UploadDestination",
19
+ "UploadDestinationAutoMatch",
19
20
  "UploadDestinationBase",
20
21
  "UploadDestinationMaterialFamily",
21
22
  "UploadDestinationProject",
@@ -47,6 +48,7 @@ class UploadDestinationType(StrEnum):
47
48
  PROJECT = "project"
48
49
  MATERIAL_FAMILY = "material_family"
49
50
  RECIPE = "recipe"
51
+ AUTO_MATCH = "auto_match"
50
52
 
51
53
 
52
54
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -91,9 +93,19 @@ class UploadDestinationRecipe(UploadDestinationBase):
91
93
  recipe_key: identifier_t.IdentifierKey
92
94
 
93
95
 
96
+ # DO NOT MODIFY -- This file is generated by type_spec
97
+ @serial_class(
98
+ named_type_path="sdk.generic_upload.UploadDestinationAutoMatch",
99
+ parse_require={"type"},
100
+ )
101
+ @dataclasses.dataclass(slots=True, kw_only=True)
102
+ class UploadDestinationAutoMatch(UploadDestinationBase):
103
+ type: typing.Literal[UploadDestinationType.AUTO_MATCH] = UploadDestinationType.AUTO_MATCH
104
+
105
+
94
106
  # DO NOT MODIFY -- This file is generated by type_spec
95
107
  UploadDestination = typing.Annotated[
96
- UploadDestinationProject | UploadDestinationMaterialFamily | UploadDestinationRecipe,
108
+ UploadDestinationProject | UploadDestinationMaterialFamily | UploadDestinationRecipe | UploadDestinationAutoMatch,
97
109
  serial_union_annotation(
98
110
  named_type_path="sdk.generic_upload.UploadDestination",
99
111
  discriminator="type",
@@ -101,6 +113,7 @@ UploadDestination = typing.Annotated[
101
113
  "project": UploadDestinationProject,
102
114
  "material_family": UploadDestinationMaterialFamily,
103
115
  "recipe": UploadDestinationRecipe,
116
+ "auto_match": UploadDestinationAutoMatch,
104
117
  },
105
118
  ),
106
119
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UncountablePythonSDK
3
- Version: 0.0.181
3
+ Version: 0.0.182
4
4
  Summary: Uncountable SDK
5
5
  Project-URL: Homepage, https://github.com/uncountableinc/uncountable-python-sdk
6
6
  Project-URL: Repository, https://github.com/uncountableinc/uncountable-python-sdk.git
@@ -42,7 +42,7 @@ examples/integration-server/jobs/materials_auto/example_parse.py,sha256=Z3iqAUdI
42
42
  examples/integration-server/jobs/materials_auto/example_predictions.py,sha256=5fO4rqRa80_968A1uVZn2TlMOUib54A8rumGW02sIMM,2112
43
43
  examples/integration-server/jobs/materials_auto/example_runsheet_wh.py,sha256=7FWDz3QpueVcf83fPrwGSqxs9pX06iKcQFte_vz3dC8,2944
44
44
  examples/integration-server/jobs/materials_auto/example_wh.py,sha256=PN-skP27yJwDZboWk5g5EZEc3AKfVayQLfnopjsDKJc,659
45
- examples/integration-server/jobs/materials_auto/profile.yaml,sha256=ywDrDRAyqiUdj_HvosNP5bXBL8mCWsvdJ1eYQd-mGYo,2369
45
+ examples/integration-server/jobs/materials_auto/profile.yaml,sha256=N9kX-xczfgBSqnfY5qsxmjxXkBXLhMnZU1TOMWI1hfg,2587
46
46
  pkgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  pkgs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  pkgs/argument_parser/__init__.py,sha256=cEEOAQS8UJ4Kbuo0oCVAHHhkbYCfrH6cUTTt2Mq3PmI,1445
@@ -134,14 +134,15 @@ uncountable/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
134
134
  uncountable/integration/cli.py,sha256=5RzlfScaZHapiXp4XoaYh0QqSenIjkEf-kyjEue1ycg,9301
135
135
  uncountable/integration/construct_client.py,sha256=9c8ZqMjyXebsptiGkJbg53_af4TLOumY1hOQtIbC15w,2229
136
136
  uncountable/integration/cron.py,sha256=6eH-kIs3sdYPCyb62_L2M7U_uQTdMTdwY5hreEJb0hw,887
137
- uncountable/integration/entrypoint.py,sha256=BHOYPQgKvZE6HG8Rv15MkdYl8lRkvfDgv1OdLo0oQ9Q,433
138
- uncountable/integration/job.py,sha256=eKaRiVagBR0qKUp96jR2NZdxOGKWQbJpBrkcRWoJcXw,9194
137
+ uncountable/integration/entrypoint.py,sha256=seXMXxUPjBLzpP6UAB6fRVodkgGea6vnl2oy4FWvCyE,591
138
+ uncountable/integration/job.py,sha256=Jeiiwvk-T3TxNYpesGbyTuQirw8gX1qQlmCIZ7r7Wuc,9187
139
139
  uncountable/integration/request_context.py,sha256=N_FJJxqvfUJ0yV9h3I3vFTGNJiDfyLYObczcYa44pw8,999
140
140
  uncountable/integration/scan_profiles.py,sha256=86CQgrQ-zXl3p41ERazzbzhTIIjIh_BprWVfYTSHHE4,3259
141
141
  uncountable/integration/scheduler.py,sha256=Z_CL2a-kOAJFpMWzFKtuvRz4AlCIRYbxWdN8fpM0RSQ,9150
142
142
  uncountable/integration/server.py,sha256=wnqpowr3xdAXfi4gCTdfFBvmQVyTro439kqrXewBauM,6002
143
143
  uncountable/integration/telemetry.py,sha256=W7z2h47Rrn7F3gF1aazbBdthNbSVi4dkQVbZFIs99-c,16155
144
- uncountable/integration/webhook_signature_key.py,sha256=vXlNtj0qLqZxjKDLmRvlChvLS5xoPAGmYcYcs4WIVaM,3882
144
+ uncountable/integration/webhook_bootstrap.py,sha256=RlMsdfoWhsOX8eQCVz9XhyQLqfr0hPPwW6pdGhhmfeI,2332
145
+ uncountable/integration/webhook_entity.py,sha256=As4x_W_zXuJ4uCAoVZwUKHPJILxP12ow0psPEv5sv9M,5220
145
146
  uncountable/integration/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
147
  uncountable/integration/db/connect.py,sha256=-xYWcJYxwKJMBvdQJ5J0-4vwBU-m24uTBtE-VrjwgKw,1479
147
148
  uncountable/integration/db/session.py,sha256=z9kor6wy8fdHhHSmdBj9HVvDOQWvs1QM_0kisD1A06E,702
@@ -150,7 +151,7 @@ uncountable/integration/executors/executors.py,sha256=E3a0_yrQesfigDzjhUAwD7-l-o
150
151
  uncountable/integration/executors/generic_upload_executor.py,sha256=MuNzrMnAaj6PBhrfTKX6e6vyQhwoDeBrYLz96ai653k,12149
151
152
  uncountable/integration/executors/script_executor.py,sha256=hKq09fOG89OAZ1Iq7wm2OgpUz1bh7DIYdZ2yn6ySr10,881
152
153
  uncountable/integration/http_server/__init__.py,sha256=ld7hD3wOGwAbnVRBrkTUCP5zNG-kptMd4wGJyQvUWVc,170
153
- uncountable/integration/http_server/types.py,sha256=eCdYZdEWjdFj9JDTQvhLF3W83HkdRowgX3X_axbA6-0,2274
154
+ uncountable/integration/http_server/types.py,sha256=9Hnt_rvrhGh0MCj1O2k2KHF81U85jURUnG6vt0boPDk,2425
154
155
  uncountable/integration/queue_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
155
156
  uncountable/integration/queue_runner/job_process.py,sha256=OONJ7aLsE80qIJ2ABLY3HYyKI7VdpBjI_pPQ3GCC13U,2586
156
157
  uncountable/integration/queue_runner/job_runner.py,sha256=Yh9hYfEYOya3BR6ypsal3LKPKrrEb4VQwfGuh4qyU2w,6115
@@ -162,7 +163,7 @@ uncountable/integration/queue_runner/command_server/__init__.py,sha256=hMCDLWct8
162
163
  uncountable/integration/queue_runner/command_server/command_client.py,sha256=hvioJc3XcJ4HsJmEwOFpQ2VZ56IqXHldZdR5yvWo39U,6361
163
164
  uncountable/integration/queue_runner/command_server/command_server.py,sha256=IsM_4YfuRqr8Ev2tbk6TpfdkBWh9mOTu-8dPUijfVQ8,8581
164
165
  uncountable/integration/queue_runner/command_server/constants.py,sha256=7J9mQIAMOfV50wnwpn7HgrPFEi3Ritj6HwrGYwxGLoU,88
165
- uncountable/integration/queue_runner/command_server/provision_webhook.py,sha256=yPYEqCi9lDFYOeGBJgwcLS3IdfYGcaZFEYo6_8sFiS8,1102
166
+ uncountable/integration/queue_runner/command_server/provision_webhook.py,sha256=TqH_6YjRZz8EHMKxOcCxvmK6vweR8nuY8ilWNEQoogk,1010
166
167
  uncountable/integration/queue_runner/command_server/types.py,sha256=ZNqJE6b6rfMZGdcPS-7umB_8x2a7dzTfRBsdGoCzDjY,2077
167
168
  uncountable/integration/queue_runner/command_server/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
168
169
  uncountable/integration/queue_runner/command_server/protocol/command_server.proto,sha256=mvgCHAIQocdtou7e8SZmmJIBK7jIzJ2fT4yDz6Bur3c,1869
@@ -180,8 +181,12 @@ uncountable/integration/secret_retrieval/__init__.py,sha256=sisf8cxCYcJmCXCKjEPo
180
181
  uncountable/integration/secret_retrieval/basic_secret.py,sha256=PfpeaEDwdi8nFlHUJnzCob9GYgGG5O_3mmcaxPPY6zI,3272
181
182
  uncountable/integration/secret_retrieval/retrieve_secret.py,sha256=c0VkCvppWKXwgxdblQuj2WhRaEGBPa7vLDoS6mlNOAQ,3919
182
183
  uncountable/integration/webhook_server/admin_auth.py,sha256=RYyK54wH5SHfzAVS4CeGgaKHeD4qAhqsvNZqkWuN34Q,1234
183
- uncountable/integration/webhook_server/admin_routes.py,sha256=S73fIQu7-0U8RKHqvgKIkRMzkEcaYKwplKSexWA5vIQ,1862
184
184
  uncountable/integration/webhook_server/entrypoint.py,sha256=LT0IDQtCqNoZBtzb-x6Ev96e1Xug3GRv4hn5NUbG_A4,4385
185
+ uncountable/integration/webhook_server/admin_routes/__init__.py,sha256=iAgtrlljGe-yBCFNMwz8ZkH_KCC_DgW_hazAZgHb8I8,129
186
+ uncountable/integration/webhook_server/admin_routes/_boundary.py,sha256=RKM5LfnMKslRvOBFVi_5iIKDB-3t1RpAVDO5sV1wLIo,1371
187
+ uncountable/integration/webhook_server/admin_routes/health.py,sha256=OJ3fwzchLLeS33rIU23cdKnB3rtTxKlHZqqcpUA3Frc,963
188
+ uncountable/integration/webhook_server/admin_routes/jobs.py,sha256=TR9VeHB1nZQ2KHTipSXWTxvELWRj9f1KxIOOtRTO3Q0,5112
189
+ uncountable/integration/webhook_server/admin_routes/register.py,sha256=Jr1qVEhQgzp8T8zLifAQ5dDAlorFad-1su5kdyvtYWg,501
185
190
  uncountable/types/__init__.py,sha256=7fIYhDsz995jFGK7wBXIVMzIxUJbltUmdqm9U6HA_QQ,14367
186
191
  uncountable/types/async_batch.py,sha256=yCCWrrLQfxXVqZp-KskxLBNkNmuELdz4PJjx8ULppgs,662
187
192
  uncountable/types/async_batch_processor.py,sha256=6Z1qx_H9IqLqp0rzpCtrRvw0DqQ2O5ctkEnw0yNVaV8,48727
@@ -217,8 +222,8 @@ uncountable/types/field_values.py,sha256=iG4TvITLnlz023GuhFrlDwXB7oov5DPpAs_FBaM
217
222
  uncountable/types/field_values_t.py,sha256=jfvHmnMLnPz_q6bjdJvWi204uKN2F_KtIzXrfU701Jg,9096
218
223
  uncountable/types/fields.py,sha256=M0_ZZr0QdNLXkdHAGo5mfU90kEtHedCSKrcod-FG30Y,245
219
224
  uncountable/types/fields_t.py,sha256=MbV4S0hX137FZLe-fXEMRuTo53wjdy1TxodtJmjXbiI,656
220
- uncountable/types/generic_upload.py,sha256=bNep2nT0fbKAlJaGvHWPmuvfX5KtS8kgTqTh8FQk1NA,858
221
- uncountable/types/generic_upload_t.py,sha256=prMriVeukeiJa5gWfIOHuFad9rz_18FiOtlNjhb2WZ0,3828
225
+ uncountable/types/generic_upload.py,sha256=XhPnQDxsCqQec3lfq9Afs9t8lzQmVJYmY97Bg5CxvTE,945
226
+ uncountable/types/generic_upload_t.py,sha256=MAsNFqfcsbdCEsFkB4dQx7k7WyW0jX683e18eHVeGUo,4346
222
227
  uncountable/types/id_source.py,sha256=sBlDfUwHQ7bGWMschSD_aPQL7LVnCPiV2RAlPLXrAqk,546
223
228
  uncountable/types/id_source_t.py,sha256=KhOK1wXgVgbRy1vSdEdbwyz9z4prWRcAx8hOJyjCvyQ,1886
224
229
  uncountable/types/identifier.py,sha256=J-ptCFE0_R0bBAvrYp-gvHk8H9Qq9rhbmeyXgwb9nos,482
@@ -434,7 +439,7 @@ uncountable/types/api/uploader/complete_async_parse.py,sha256=Os1HAubxTlbut6Gylj
434
439
  uncountable/types/api/uploader/invoke_uploader.py,sha256=QU1KmAFgGHSWskZEKfaAww8rCj-hVNP0i4kMQE3x3Fc,1822
435
440
  uncountable/types/api/user/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
436
441
  uncountable/types/api/user/get_current_user_info.py,sha256=BT7bGp5SOHz3XdKWhhdZ6acnInafT2PgdJ-H4YH3W7c,1181
437
- uncountablepythonsdk-0.0.181.dist-info/METADATA,sha256=3K5y5CfPXJkAFZQvtT-N4ziTKT3hNqTUovVVjbaucfg,2211
438
- uncountablepythonsdk-0.0.181.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
439
- uncountablepythonsdk-0.0.181.dist-info/top_level.txt,sha256=1UVGjAU-6hJY9qw2iJ7nCBeEwZ793AEN5ZfKX9A1uj4,31
440
- uncountablepythonsdk-0.0.181.dist-info/RECORD,,
442
+ uncountablepythonsdk-0.0.182.dist-info/METADATA,sha256=oGJSxcjq_RKLaOjhKMpBEnj3_JRLoewFIegeuf9_ZWI,2211
443
+ uncountablepythonsdk-0.0.182.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
444
+ uncountablepythonsdk-0.0.182.dist-info/top_level.txt,sha256=1UVGjAU-6hJY9qw2iJ7nCBeEwZ793AEN5ZfKX9A1uj4,31
445
+ uncountablepythonsdk-0.0.182.dist-info/RECORD,,
@@ -1,49 +0,0 @@
1
- import flask
2
- from flask.typing import ResponseReturnValue
3
- from opentelemetry.trace import get_current_span
4
- from uncountable.core.environment import get_local_admin_server_port
5
- from uncountable.integration.http_server import HttpException
6
- from uncountable.integration.queue_runner.command_server.command_client import (
7
- check_health,
8
- )
9
- from uncountable.integration.queue_runner.command_server.types import (
10
- CommandServerException,
11
- )
12
- from uncountable.integration.telemetry import Logger
13
- from uncountable.integration.webhook_server.admin_auth import require_admin_auth
14
-
15
- _ADMIN_HEALTH_ROUTE = "/admin/health"
16
-
17
-
18
- def register_admin_routes(app: flask.Flask) -> None:
19
- server_logger = Logger(get_current_span())
20
-
21
- @require_admin_auth
22
- def handle_admin_health() -> ResponseReturnValue:
23
- healthy = check_health(port=get_local_admin_server_port())
24
- return flask.jsonify({"healthy": healthy})
25
-
26
- def admin_health() -> ResponseReturnValue:
27
- with server_logger.push_scope(_ADMIN_HEALTH_ROUTE):
28
- try:
29
- return handle_admin_health()
30
- except HttpException as e:
31
- return e.make_error_response()
32
- except CommandServerException as e:
33
- server_logger.log_exception(
34
- e,
35
- message="admin health check could not reach the command server",
36
- )
37
- return HttpException.bad_gateway().make_error_response()
38
- except Exception as e:
39
- server_logger.log_exception(
40
- e, message=f"unexpected error on GET {_ADMIN_HEALTH_ROUTE}"
41
- )
42
- return HttpException.unknown_error().make_error_response()
43
-
44
- app.add_url_rule(
45
- _ADMIN_HEALTH_ROUTE,
46
- endpoint="admin_health",
47
- view_func=admin_health,
48
- methods=["GET"],
49
- )
@@ -1,110 +0,0 @@
1
- import secrets
2
-
3
- from uncountable.core.client import Client
4
- from uncountable.core.query.builder import QueryBuilder
5
- from uncountable.core.query.column import (
6
- NonNullableColumn,
7
- TextColumn,
8
- )
9
- from uncountable.core.query.row import QueryRow
10
- from uncountable.types import (
11
- entity_t,
12
- field_values_t,
13
- identifier_t,
14
- listing_t,
15
- webhook_t,
16
- )
17
- from uncountable.types.job_definition_t import ProfileMetadata
18
- from uncountable.types.secret_retrieval_t import SecretRetrievalEntityColumn
19
-
20
- _WEBHOOK_DEFINITION_REF_NAME = "unc_webhook_definition"
21
- _WEBHOOK_REF_NAME_FIELD_REF_NAME = "refName"
22
- _WEBHOOK_NAME_FIELD_REF_NAME = "name"
23
- _WEBHOOK_URL_FIELD_REF_NAME = "core_webhook_url"
24
- _WEBHOOK_SIGNATURE_KEY_FIELD_REF_NAME = "core_webhook_signatureKey"
25
- _WEBHOOK_PURPOSE_FIELD_REF_NAME = "core_webhook_purpose"
26
-
27
-
28
- class _WebhookEntityQueryRow(QueryRow):
29
- ref_name = TextColumn(
30
- identifier=listing_t.ColumnIdentifierEntityRefName(
31
- entity_type=entity_t.EntityType.WEBHOOK,
32
- ref_name=_WEBHOOK_REF_NAME_FIELD_REF_NAME,
33
- ),
34
- nullable=NonNullableColumn,
35
- )
36
- signature_key = TextColumn(
37
- identifier=listing_t.ColumnIdentifierEntityRefName(
38
- entity_type=entity_t.EntityType.WEBHOOK,
39
- ref_name=_WEBHOOK_SIGNATURE_KEY_FIELD_REF_NAME,
40
- ),
41
- nullable=NonNullableColumn,
42
- )
43
-
44
-
45
- def webhook_signature_key_retrieval(*, job_id: str) -> SecretRetrievalEntityColumn:
46
- return SecretRetrievalEntityColumn(
47
- entity_key=identifier_t.IdentifierKeyRefName(ref_name=job_id),
48
- column_identifier=listing_t.ColumnIdentifierEntityRefName(
49
- entity_type=entity_t.EntityType.WEBHOOK,
50
- ref_name=_WEBHOOK_SIGNATURE_KEY_FIELD_REF_NAME,
51
- ),
52
- )
53
-
54
-
55
- def _resolve_webhook_url(*, profile_metadata: ProfileMetadata, job_id: str) -> str:
56
- integration_server_base_url = profile_metadata.integration_server_base_url
57
- if integration_server_base_url is None:
58
- raise ValueError(
59
- "integration_server_base_url is required to resolve the webhook entity "
60
- f"for {profile_metadata.name}/{job_id}"
61
- )
62
- return f"{integration_server_base_url.rstrip('/')}/{profile_metadata.name}/{job_id}"
63
-
64
-
65
- def bootstrap_webhook_signature_key(
66
- *,
67
- client: Client,
68
- profile_metadata: ProfileMetadata,
69
- job_id: str,
70
- purpose: webhook_t.WebhookPurpose,
71
- ) -> str:
72
- existing_webhook = (
73
- QueryBuilder(client=client, model=_WebhookEntityQueryRow)
74
- .filter(_WebhookEntityQueryRow.ref_name == job_id)
75
- .one_or_none()
76
- )
77
- if existing_webhook is not None:
78
- return existing_webhook.signature_key
79
-
80
- webhook_url = _resolve_webhook_url(profile_metadata=profile_metadata, job_id=job_id)
81
- signature_key = secrets.token_urlsafe(32)
82
- client.create_entity(
83
- entity_type=entity_t.EntityType.WEBHOOK,
84
- definition_key=identifier_t.IdentifierKeyRefName(
85
- ref_name=_WEBHOOK_DEFINITION_REF_NAME
86
- ),
87
- field_values=[
88
- field_values_t.FieldRefNameValue(
89
- field_ref_name=_WEBHOOK_REF_NAME_FIELD_REF_NAME,
90
- value=job_id,
91
- ),
92
- field_values_t.FieldRefNameValue(
93
- field_ref_name=_WEBHOOK_NAME_FIELD_REF_NAME,
94
- value=f"{profile_metadata.name}/{job_id}",
95
- ),
96
- field_values_t.FieldRefNameValue(
97
- field_ref_name=_WEBHOOK_URL_FIELD_REF_NAME,
98
- value=webhook_url,
99
- ),
100
- field_values_t.FieldRefNameValue(
101
- field_ref_name=_WEBHOOK_SIGNATURE_KEY_FIELD_REF_NAME,
102
- value=signature_key,
103
- ),
104
- field_values_t.FieldRefNameValue(
105
- field_ref_name=_WEBHOOK_PURPOSE_FIELD_REF_NAME,
106
- value=purpose.value,
107
- ),
108
- ],
109
- )
110
- return signature_key