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.
- examples/integration-server/jobs/materials_auto/profile.yaml +8 -0
- uncountable/integration/entrypoint.py +4 -1
- uncountable/integration/http_server/types.py +4 -0
- uncountable/integration/job.py +1 -1
- uncountable/integration/queue_runner/command_server/provision_webhook.py +4 -8
- uncountable/integration/webhook_bootstrap.py +67 -0
- uncountable/integration/webhook_entity.py +147 -0
- uncountable/integration/webhook_server/admin_routes/__init__.py +3 -0
- uncountable/integration/webhook_server/admin_routes/_boundary.py +39 -0
- uncountable/integration/webhook_server/admin_routes/health.py +30 -0
- uncountable/integration/webhook_server/admin_routes/jobs.py +147 -0
- uncountable/integration/webhook_server/admin_routes/register.py +13 -0
- uncountable/types/generic_upload.py +1 -0
- uncountable/types/generic_upload_t.py +14 -1
- {uncountablepythonsdk-0.0.181.dist-info → uncountablepythonsdk-0.0.182.dist-info}/METADATA +1 -1
- {uncountablepythonsdk-0.0.181.dist-info → uncountablepythonsdk-0.0.182.dist-info}/RECORD +18 -13
- uncountable/integration/webhook_server/admin_routes.py +0 -49
- uncountable/integration/webhook_signature_key.py +0 -110
- {uncountablepythonsdk-0.0.181.dist-info → uncountablepythonsdk-0.0.182.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.181.dist-info → uncountablepythonsdk-0.0.182.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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")
|
uncountable/integration/job.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
client=client,
|
|
18
|
+
create_missing_webhook_entities(
|
|
19
|
+
client=construct_uncountable_client(profile_metadata, logger),
|
|
23
20
|
profile_metadata=profile_metadata,
|
|
24
|
-
|
|
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,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.
|
|
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=
|
|
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=
|
|
138
|
-
uncountable/integration/job.py,sha256=
|
|
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/
|
|
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=
|
|
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=
|
|
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=
|
|
221
|
-
uncountable/types/generic_upload_t.py,sha256=
|
|
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.
|
|
438
|
-
uncountablepythonsdk-0.0.
|
|
439
|
-
uncountablepythonsdk-0.0.
|
|
440
|
-
uncountablepythonsdk-0.0.
|
|
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
|
|
File without changes
|
{uncountablepythonsdk-0.0.181.dist-info → uncountablepythonsdk-0.0.182.dist-info}/top_level.txt
RENAMED
|
File without changes
|