UncountablePythonSDK 0.0.174__py3-none-any.whl → 0.0.176__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.
- pkgs/type_spec/actions_registry/__main__.py +1 -0
- pkgs/type_spec/actions_registry/emit_python.py +4 -0
- pkgs/type_spec/actions_registry/emit_typescript.py +4 -0
- pkgs/type_spec/non_discriminated_union_exceptions.py +1 -1
- pkgs/type_spec/value_spec/__main__.py +10 -0
- pkgs/type_spec/value_spec/emit_python.py +9 -0
- uncountable/integration/cli.py +29 -0
- uncountable/integration/construct_client.py +6 -6
- uncountable/integration/job.py +27 -6
- uncountable/integration/queue_runner/command_server/command_client.py +20 -0
- uncountable/integration/queue_runner/command_server/command_server.py +19 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +9 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +9 -5
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +12 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +47 -0
- uncountable/integration/queue_runner/command_server/provision_webhook.py +28 -0
- uncountable/integration/scan_profiles.py +2 -0
- uncountable/integration/secret_retrieval/__init__.py +5 -1
- uncountable/integration/secret_retrieval/basic_secret.py +101 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +85 -70
- uncountable/integration/telemetry.py +20 -31
- uncountable/integration/webhook_signature_key.py +110 -0
- uncountable/types/__init__.py +2 -0
- uncountable/types/api/notebooks/add_notebook_content.py +3 -2
- uncountable/types/api/recipes/get_recipes_data.py +1 -1
- uncountable/types/auth_retrieval_t.py +3 -3
- uncountable/types/client_base.py +2 -2
- uncountable/types/entity_t.py +4 -0
- uncountable/types/integration_server_t.py +1 -0
- uncountable/types/job_definition.py +1 -0
- uncountable/types/job_definition_t.py +18 -1
- uncountable/types/secret_retrieval.py +2 -0
- uncountable/types/secret_retrieval_t.py +33 -1
- uncountable/types/webhook.py +7 -0
- uncountable/types/webhook_t.py +22 -0
- {uncountablepythonsdk-0.0.174.dist-info → uncountablepythonsdk-0.0.176.dist-info}/METADATA +2 -2
- {uncountablepythonsdk-0.0.174.dist-info → uncountablepythonsdk-0.0.176.dist-info}/RECORD +39 -34
- {uncountablepythonsdk-0.0.174.dist-info → uncountablepythonsdk-0.0.176.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.174.dist-info → uncountablepythonsdk-0.0.176.dist-info}/top_level.txt +0 -0
|
@@ -54,6 +54,10 @@ def _emit_action_definition(
|
|
|
54
54
|
out.write(
|
|
55
55
|
f"{sub_indent}visibility_scope={_emit_visibility_scope(action.visibility_scope)},\n"
|
|
56
56
|
)
|
|
57
|
+
if action.kind is not None:
|
|
58
|
+
out.write(
|
|
59
|
+
f"{sub_indent}kind=actions_registry_t.ActionDefinitionKind.{action.kind.name},\n"
|
|
60
|
+
)
|
|
57
61
|
if action.arguments is not None:
|
|
58
62
|
out.write(f"{sub_indent}arguments={_py_str(action.arguments)},\n")
|
|
59
63
|
out.write(f"{indent}),\n")
|
|
@@ -147,6 +147,10 @@ def _emit_action_definition(
|
|
|
147
147
|
out.write(
|
|
148
148
|
f"{sub_indent}visibilityScope: {_emit_visibility_scope(action_definition.visibility_scope)},\n"
|
|
149
149
|
)
|
|
150
|
+
if action_definition.kind is not None:
|
|
151
|
+
out.write(
|
|
152
|
+
f"{sub_indent}kind: ActionsRegistryT.ActionDefinitionKind.{ts_name(action_definition.kind, name_case=builder.NameCase.convert)},\n"
|
|
153
|
+
)
|
|
150
154
|
out.write(f"{indent}}} as ActionsRegistryT.ActionDefinition{argument_type},\n")
|
|
151
155
|
return out.getvalue()
|
|
152
156
|
|
|
@@ -6,7 +6,7 @@ NON_DISCRIMINATED_UNION_EXCEPTIONS = [
|
|
|
6
6
|
"output_parameters.AnalyticalMethodLinkedOptionValue",
|
|
7
7
|
"recipes_redirect.RecipesRedirectResult",
|
|
8
8
|
"value_spec.ResolvedPathAll",
|
|
9
|
-
"
|
|
9
|
+
"deprecated_calculation_types.DeprecatedWeightedSumEntitiesV0",
|
|
10
10
|
"workflows.WorkflowTotalDisplay",
|
|
11
11
|
"type_info.TypeFormActionConstraint",
|
|
12
12
|
"structured_loading.CompatibleFilterNode",
|
|
@@ -173,6 +173,7 @@ key_description = "description"
|
|
|
173
173
|
key_brief = "brief"
|
|
174
174
|
key_name = "name"
|
|
175
175
|
key_draft = "draft"
|
|
176
|
+
key_deferred_value_spec = "deferred_value_spec"
|
|
176
177
|
|
|
177
178
|
|
|
178
179
|
TypeT = TypeVar("TypeT")
|
|
@@ -236,6 +237,14 @@ def main() -> None:
|
|
|
236
237
|
arg_meta = get(args_meta, in_argument.ref_name)
|
|
237
238
|
arg_description = get_as(arg_meta, key_description, str)
|
|
238
239
|
arg_name = get_as(arg_meta, key_name, str)
|
|
240
|
+
deferred_value_spec = None
|
|
241
|
+
if (
|
|
242
|
+
isinstance(arg_meta, dict)
|
|
243
|
+
and arg_meta.get(key_deferred_value_spec) is not None
|
|
244
|
+
):
|
|
245
|
+
deferred_value_spec = get_as(
|
|
246
|
+
arg_meta, key_deferred_value_spec, bool
|
|
247
|
+
)
|
|
239
248
|
|
|
240
249
|
arguments.append(
|
|
241
250
|
value_spec_t.FunctionArgument(
|
|
@@ -245,6 +254,7 @@ def main() -> None:
|
|
|
245
254
|
type=convert_to_value_spec_type(in_argument.type_path),
|
|
246
255
|
on_null=in_argument.on_null,
|
|
247
256
|
extant=in_argument.extant,
|
|
257
|
+
deferred_value_spec=deferred_value_spec,
|
|
248
258
|
)
|
|
249
259
|
)
|
|
250
260
|
|
|
@@ -29,6 +29,7 @@ from decimal import Decimal
|
|
|
29
29
|
|
|
30
30
|
from main.base.types import value_spec_t, base_t
|
|
31
31
|
|
|
32
|
+
from . import expression
|
|
32
33
|
from .function_wrapper import ExtractorArgs, WrapArgs
|
|
33
34
|
"""
|
|
34
35
|
)
|
|
@@ -70,6 +71,12 @@ def _emit_function_wrapper(function: value_spec_t.Function) -> str:
|
|
|
70
71
|
|
|
71
72
|
{INDENT}def get_{argument.ref_name}_at(self, at: int) -> base_t.ExtJsonValue:
|
|
72
73
|
{INDENT}{INDENT}return self._extract_at({index}, at)
|
|
74
|
+
"""
|
|
75
|
+
)
|
|
76
|
+
elif argument.deferred_value_spec:
|
|
77
|
+
out.write(
|
|
78
|
+
f"""{INDENT}def get_{argument.ref_name}(self) -> expression.ParseNode | None:
|
|
79
|
+
{INDENT}{INDENT}return self.extract_node({index})
|
|
73
80
|
"""
|
|
74
81
|
)
|
|
75
82
|
else:
|
|
@@ -213,6 +220,8 @@ def _emit_argument(argument: value_spec_t.FunctionArgument, indent: str) -> str:
|
|
|
213
220
|
out.write(
|
|
214
221
|
f"{sub_indent}extant=value_spec_t.ArgumentExtant.{argument.extant.name},\n"
|
|
215
222
|
)
|
|
223
|
+
if argument.deferred_value_spec:
|
|
224
|
+
out.write(f"{sub_indent}deferred_value_spec={argument.deferred_value_spec},\n")
|
|
216
225
|
out.write(f"{sub_indent}type={_emit_type(argument.type, sub_indent)},\n")
|
|
217
226
|
out.write(f"{indent})")
|
|
218
227
|
|
uncountable/integration/cli.py
CHANGED
|
@@ -12,6 +12,7 @@ from uncountable.integration.queue_runner.command_server.command_client import (
|
|
|
12
12
|
send_job_cancellation_message,
|
|
13
13
|
send_job_queue_message,
|
|
14
14
|
send_list_queued_jobs_message,
|
|
15
|
+
send_provision_webhook_message,
|
|
15
16
|
send_retry_job_message,
|
|
16
17
|
)
|
|
17
18
|
from uncountable.integration.queue_runner.command_server.types import (
|
|
@@ -220,6 +221,33 @@ def register_retry_job_parser(
|
|
|
220
221
|
retry_failed_jobs_parser.set_defaults(func=_handle_retry_job)
|
|
221
222
|
|
|
222
223
|
|
|
224
|
+
def register_provision_webhook_parser(
|
|
225
|
+
sub_parser_manager: argparse._SubParsersAction,
|
|
226
|
+
parents: list[argparse.ArgumentParser],
|
|
227
|
+
) -> None:
|
|
228
|
+
provision_parser = sub_parser_manager.add_parser(
|
|
229
|
+
"provision-webhook",
|
|
230
|
+
parents=parents,
|
|
231
|
+
help="Provision an Uncountable webhook job by creating its webhook entity and signature key",
|
|
232
|
+
description="Provision an Uncountable webhook job by creating its webhook entity and signature key",
|
|
233
|
+
)
|
|
234
|
+
provision_parser.add_argument(
|
|
235
|
+
"job_id",
|
|
236
|
+
type=str,
|
|
237
|
+
help="The id of the Uncountable webhook job to provision",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _handle_provision_webhook(args: argparse.Namespace) -> None:
|
|
241
|
+
profile_name = send_provision_webhook_message(
|
|
242
|
+
job_id=args.job_id,
|
|
243
|
+
host=args.host,
|
|
244
|
+
port=get_local_admin_server_port(),
|
|
245
|
+
)
|
|
246
|
+
print(f"provisioned webhook {profile_name}/{args.job_id}")
|
|
247
|
+
|
|
248
|
+
provision_parser.set_defaults(func=_handle_provision_webhook)
|
|
249
|
+
|
|
250
|
+
|
|
223
251
|
def main() -> None:
|
|
224
252
|
logger = Logger(get_current_span())
|
|
225
253
|
|
|
@@ -242,6 +270,7 @@ def main() -> None:
|
|
|
242
270
|
register_retry_job_parser(subparser_action, parents=[base_parser])
|
|
243
271
|
register_list_queued_jobs(subparser_action, parents=[base_parser])
|
|
244
272
|
register_cancel_queued_job_parser(subparser_action, parents=[base_parser])
|
|
273
|
+
register_provision_webhook_parser(subparser_action, parents=[base_parser])
|
|
245
274
|
|
|
246
275
|
args = main_parser.parse_args()
|
|
247
276
|
with logger.push_scope(args.command):
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from uncountable.core import AuthDetailsApiKey, Client
|
|
2
2
|
from uncountable.core.client import ClientConfig
|
|
3
3
|
from uncountable.core.types import AuthDetailsAll, AuthDetailsOAuth
|
|
4
|
-
from uncountable.integration.secret_retrieval.
|
|
5
|
-
from uncountable.integration.telemetry import
|
|
4
|
+
from uncountable.integration.secret_retrieval.basic_secret import retrieve_basic_secret
|
|
5
|
+
from uncountable.integration.telemetry import Logger
|
|
6
6
|
from uncountable.types import auth_retrieval_t
|
|
7
7
|
from uncountable.types.client_config_t import RequestContext
|
|
8
8
|
from uncountable.types.job_definition_t import (
|
|
@@ -13,16 +13,16 @@ from uncountable.types.job_definition_t import (
|
|
|
13
13
|
def _construct_auth_details(profile_meta: ProfileMetadata) -> AuthDetailsAll:
|
|
14
14
|
match profile_meta.auth_retrieval:
|
|
15
15
|
case auth_retrieval_t.AuthRetrievalOAuth():
|
|
16
|
-
refresh_token =
|
|
16
|
+
refresh_token = retrieve_basic_secret(
|
|
17
17
|
profile_meta.auth_retrieval.refresh_token_secret,
|
|
18
18
|
profile_metadata=profile_meta,
|
|
19
19
|
)
|
|
20
20
|
return AuthDetailsOAuth(refresh_token=refresh_token)
|
|
21
21
|
case auth_retrieval_t.AuthRetrievalBasic():
|
|
22
|
-
api_id =
|
|
22
|
+
api_id = retrieve_basic_secret(
|
|
23
23
|
profile_meta.auth_retrieval.api_id_secret, profile_metadata=profile_meta
|
|
24
24
|
)
|
|
25
|
-
api_key =
|
|
25
|
+
api_key = retrieve_basic_secret(
|
|
26
26
|
profile_meta.auth_retrieval.api_key_secret,
|
|
27
27
|
profile_metadata=profile_meta,
|
|
28
28
|
)
|
|
@@ -32,7 +32,7 @@ def _construct_auth_details(profile_meta: ProfileMetadata) -> AuthDetailsAll:
|
|
|
32
32
|
|
|
33
33
|
def construct_uncountable_client(
|
|
34
34
|
profile_meta: ProfileMetadata,
|
|
35
|
-
logger:
|
|
35
|
+
logger: Logger,
|
|
36
36
|
*,
|
|
37
37
|
request_context: RequestContext | None = None,
|
|
38
38
|
headers: dict[str, str] | None = None,
|
uncountable/integration/job.py
CHANGED
|
@@ -25,8 +25,11 @@ from uncountable.integration.queue_runner.command_server.types import (
|
|
|
25
25
|
CommandServerException,
|
|
26
26
|
)
|
|
27
27
|
from uncountable.integration.request_context import extract_request_context
|
|
28
|
-
from uncountable.integration.secret_retrieval
|
|
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 (
|
|
31
|
+
webhook_signature_key_retrieval,
|
|
32
|
+
)
|
|
30
33
|
from uncountable.types import (
|
|
31
34
|
base_t,
|
|
32
35
|
job_definition_t,
|
|
@@ -166,6 +169,26 @@ class CustomHttpJob(Job[GenericHttpRequest]):
|
|
|
166
169
|
return JobResult(success=False)
|
|
167
170
|
|
|
168
171
|
|
|
172
|
+
def _resolve_webhook_signature_key(
|
|
173
|
+
*,
|
|
174
|
+
job_definition: job_definition_t.HttpJobDefinitionBase,
|
|
175
|
+
profile_meta: ProfileMetadata,
|
|
176
|
+
) -> str:
|
|
177
|
+
if isinstance(job_definition, job_definition_t.WebhookJobDefinition):
|
|
178
|
+
return retrieve_secret(job_definition.signature_key_secret, profile_meta)
|
|
179
|
+
if isinstance(job_definition, job_definition_t.UncountableWebhookJobDefinition):
|
|
180
|
+
return retrieve_secret(
|
|
181
|
+
webhook_signature_key_retrieval(job_id=job_definition.id),
|
|
182
|
+
profile_meta,
|
|
183
|
+
)
|
|
184
|
+
raise HttpException.configuration_error(
|
|
185
|
+
message=(
|
|
186
|
+
"[internal] non-webhook job definition passed to webhook validation: "
|
|
187
|
+
f"{profile_meta.name}/{job_definition.id}"
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
169
192
|
class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
170
193
|
@property
|
|
171
194
|
def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
|
|
@@ -182,10 +205,8 @@ class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
|
182
205
|
job_definition: job_definition_t.HttpJobDefinitionBase,
|
|
183
206
|
profile_meta: ProfileMetadata,
|
|
184
207
|
) -> None:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
profile_metadata=profile_meta,
|
|
188
|
-
secret_retrieval=job_definition.signature_key_secret,
|
|
208
|
+
signature_key = _resolve_webhook_signature_key(
|
|
209
|
+
job_definition=job_definition, profile_meta=profile_meta
|
|
189
210
|
)
|
|
190
211
|
passed_signature = request.headers.get("Uncountable-Webhook-Signature")
|
|
191
212
|
if passed_signature is None:
|
|
@@ -195,7 +216,7 @@ class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
|
195
216
|
signature_key.encode("utf-8"), msg=request.body_bytes, digestmod="sha256"
|
|
196
217
|
).hexdigest()
|
|
197
218
|
|
|
198
|
-
if request_body_signature
|
|
219
|
+
if not hmac.compare_digest(request_body_signature, passed_signature):
|
|
199
220
|
raise HttpException.payload_failed_signature()
|
|
200
221
|
|
|
201
222
|
@staticmethod
|
|
@@ -17,6 +17,8 @@ from uncountable.integration.queue_runner.command_server.protocol.command_server
|
|
|
17
17
|
EnqueueJobResult,
|
|
18
18
|
ListQueuedJobsRequest,
|
|
19
19
|
ListQueuedJobsResult,
|
|
20
|
+
ProvisionWebhookRequest,
|
|
21
|
+
ProvisionWebhookResult,
|
|
20
22
|
RetryJobRequest,
|
|
21
23
|
RetryJobResult,
|
|
22
24
|
VaccuumQueuedJobsRequest,
|
|
@@ -33,6 +35,7 @@ from .protocol.command_server_pb2_grpc import CommandServerStub
|
|
|
33
35
|
|
|
34
36
|
_LOCAL_RPC_HOST = "localhost"
|
|
35
37
|
_DEFAULT_MESSAGE_TIMEOUT_SECS = 2
|
|
38
|
+
_PROVISION_WEBHOOK_TIMEOUT_SECS = 30
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
@contextmanager
|
|
@@ -171,3 +174,20 @@ def send_vaccuum_queued_jobs_message(*, host: str = "localhost", port: int) -> N
|
|
|
171
174
|
|
|
172
175
|
assert isinstance(response, VaccuumQueuedJobsResult)
|
|
173
176
|
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def send_provision_webhook_message(
|
|
180
|
+
*, job_id: str, host: str = "localhost", port: int
|
|
181
|
+
) -> str:
|
|
182
|
+
with command_server_connection(host=host, port=port) as stub:
|
|
183
|
+
request = ProvisionWebhookRequest(job_id=job_id)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
response = stub.ProvisionWebhook(
|
|
187
|
+
request, timeout=_PROVISION_WEBHOOK_TIMEOUT_SECS
|
|
188
|
+
)
|
|
189
|
+
except grpc.RpcError as e:
|
|
190
|
+
raise ValueError(e.details())
|
|
191
|
+
|
|
192
|
+
assert isinstance(response, ProvisionWebhookResult)
|
|
193
|
+
return response.profile_name
|
|
@@ -19,11 +19,16 @@ from uncountable.integration.queue_runner.command_server.protocol.command_server
|
|
|
19
19
|
EnqueueJobResult,
|
|
20
20
|
ListQueuedJobsRequest,
|
|
21
21
|
ListQueuedJobsResult,
|
|
22
|
+
ProvisionWebhookRequest,
|
|
23
|
+
ProvisionWebhookResult,
|
|
22
24
|
RetryJobRequest,
|
|
23
25
|
RetryJobResult,
|
|
24
26
|
VaccuumQueuedJobsRequest,
|
|
25
27
|
VaccuumQueuedJobsResult,
|
|
26
28
|
)
|
|
29
|
+
from uncountable.integration.queue_runner.command_server.provision_webhook import (
|
|
30
|
+
provision_webhook,
|
|
31
|
+
)
|
|
27
32
|
from uncountable.integration.queue_runner.command_server.types import (
|
|
28
33
|
CommandCancelJob,
|
|
29
34
|
CommandCancelJobResponse,
|
|
@@ -199,6 +204,20 @@ async def serve(command_queue: CommandQueue, datastore: DatastoreSqlite) -> None
|
|
|
199
204
|
)
|
|
200
205
|
return VaccuumQueuedJobsResult()
|
|
201
206
|
|
|
207
|
+
async def ProvisionWebhook(
|
|
208
|
+
self, request: ProvisionWebhookRequest, context: grpc_aio.ServicerContext
|
|
209
|
+
) -> ProvisionWebhookResult:
|
|
210
|
+
loop = asyncio.get_running_loop()
|
|
211
|
+
profile_name = await loop.run_in_executor(
|
|
212
|
+
None, lambda: provision_webhook(job_id=request.job_id)
|
|
213
|
+
)
|
|
214
|
+
if profile_name is None:
|
|
215
|
+
await context.abort(
|
|
216
|
+
StatusCode.INVALID_ARGUMENT,
|
|
217
|
+
f"no Uncountable webhook job found with id {request.job_id}",
|
|
218
|
+
)
|
|
219
|
+
return ProvisionWebhookResult(profile_name=profile_name)
|
|
220
|
+
|
|
202
221
|
add_CommandServerServicer_to_server(CommandServerHandler(), server)
|
|
203
222
|
|
|
204
223
|
listen_addr = f"[::]:{get_local_admin_server_port()}"
|
|
@@ -8,6 +8,7 @@ service CommandServer {
|
|
|
8
8
|
rpc ListQueuedJobs(ListQueuedJobsRequest) returns (ListQueuedJobsResult) {}
|
|
9
9
|
rpc VaccuumQueuedJobs(VaccuumQueuedJobsRequest) returns (VaccuumQueuedJobsResult) {}
|
|
10
10
|
rpc CancelJob(CancelJobRequest) returns (CancelJobResult) {}
|
|
11
|
+
rpc ProvisionWebhook(ProvisionWebhookRequest) returns (ProvisionWebhookResult) {}
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
message EnqueueJobRequest {
|
|
@@ -75,3 +76,11 @@ enum CancelJobStatus {
|
|
|
75
76
|
message CancelJobResult {
|
|
76
77
|
CancelJobStatus status = 1;
|
|
77
78
|
}
|
|
79
|
+
|
|
80
|
+
message ProvisionWebhookRequest {
|
|
81
|
+
string job_id = 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
message ProvisionWebhookResult {
|
|
85
|
+
string profile_name = 1;
|
|
86
|
+
}
|
|
@@ -29,7 +29,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
|
32
|
-
b'\nQuncountable/integration/queue_runner/command_server/protocol/command_server.proto\x1a\x1fgoogle/protobuf/timestamp.proto"E\n\x11\x45nqueueJobRequest\x12\x14\n\x0cjob_ref_name\x18\x01 \x01(\t\x12\x1a\n\x12serialized_payload\x18\x02 \x01(\t"H\n\x10\x45nqueueJobResult\x12\x1b\n\x13successfully_queued\x18\x01 \x01(\x08\x12\x17\n\x0fqueued_job_uuid\x18\x02 \x01(\t"\x1f\n\x0fRetryJobRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\t"F\n\x0eRetryJobResult\x12\x1b\n\x13successfully_queued\x18\x01 \x01(\x08\x12\x17\n\x0fqueued_job_uuid\x18\x02 \x01(\t"\x1a\n\x18VaccuumQueuedJobsRequest"\x19\n\x17VaccuumQueuedJobsResult"\x14\n\x12\x43heckHealthRequest"$\n\x11\x43heckHealthResult\x12\x0f\n\x07success\x18\x01 \x01(\x08"\xc2\x01\n\x15ListQueuedJobsRequest\x12\x0e\n\x06offset\x18\x01 \x01(\r\x12\r\n\x05limit\x18\x02 \x01(\r\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x04 \x01(\t\x12\x32\n\x0esubmitted_from\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0csubmitted_to\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xf4\x01\n\x14ListQueuedJobsResult\x12\x43\n\x0bqueued_jobs\x18\x01 \x03(\x0b\x32..ListQueuedJobsResult.ListQueuedJobsResultItem\x1a\x96\x01\n\x18ListQueuedJobsResultItem\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x02 \x01(\t\x12\x14\n\x0cnum_attempts\x18\x03 \x01(\x03\x12\x30\n\x0csubmitted_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06status\x18\x05 \x01(\t"$\n\x10\x43\x61ncelJobRequest\x12\x10\n\x08job_uuid\x18\x01 \x01(\t"3\n\x0f\x43\x61ncelJobResult\x12 \n\x06status\x18\x01 \x01(\x0e\x32\x10.CancelJobStatus*Z\n\x0f\x43\x61ncelJobStatus\x12\x1a\n\x16\x43\x41NCELLED_WITH_RESTART\x10\x00\x12\x10\n\x0cNO_JOB_FOUND\x10\x01\x12\x19\n\x15JOB_ALREADY_COMPLETED\x10\x02\x32\
|
|
32
|
+
b'\nQuncountable/integration/queue_runner/command_server/protocol/command_server.proto\x1a\x1fgoogle/protobuf/timestamp.proto"E\n\x11\x45nqueueJobRequest\x12\x14\n\x0cjob_ref_name\x18\x01 \x01(\t\x12\x1a\n\x12serialized_payload\x18\x02 \x01(\t"H\n\x10\x45nqueueJobResult\x12\x1b\n\x13successfully_queued\x18\x01 \x01(\x08\x12\x17\n\x0fqueued_job_uuid\x18\x02 \x01(\t"\x1f\n\x0fRetryJobRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\t"F\n\x0eRetryJobResult\x12\x1b\n\x13successfully_queued\x18\x01 \x01(\x08\x12\x17\n\x0fqueued_job_uuid\x18\x02 \x01(\t"\x1a\n\x18VaccuumQueuedJobsRequest"\x19\n\x17VaccuumQueuedJobsResult"\x14\n\x12\x43heckHealthRequest"$\n\x11\x43heckHealthResult\x12\x0f\n\x07success\x18\x01 \x01(\x08"\xc2\x01\n\x15ListQueuedJobsRequest\x12\x0e\n\x06offset\x18\x01 \x01(\r\x12\r\n\x05limit\x18\x02 \x01(\r\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x04 \x01(\t\x12\x32\n\x0esubmitted_from\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0csubmitted_to\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xf4\x01\n\x14ListQueuedJobsResult\x12\x43\n\x0bqueued_jobs\x18\x01 \x03(\x0b\x32..ListQueuedJobsResult.ListQueuedJobsResultItem\x1a\x96\x01\n\x18ListQueuedJobsResultItem\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x02 \x01(\t\x12\x14\n\x0cnum_attempts\x18\x03 \x01(\x03\x12\x30\n\x0csubmitted_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06status\x18\x05 \x01(\t"$\n\x10\x43\x61ncelJobRequest\x12\x10\n\x08job_uuid\x18\x01 \x01(\t"3\n\x0f\x43\x61ncelJobResult\x12 \n\x06status\x18\x01 \x01(\x0e\x32\x10.CancelJobStatus")\n\x17ProvisionWebhookRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t".\n\x16ProvisionWebhookResult\x12\x14\n\x0cprofile_name\x18\x01 \x01(\t*Z\n\x0f\x43\x61ncelJobStatus\x12\x1a\n\x16\x43\x41NCELLED_WITH_RESTART\x10\x00\x12\x10\n\x0cNO_JOB_FOUND\x10\x01\x12\x19\n\x15JOB_ALREADY_COMPLETED\x10\x02\x32\xbd\x03\n\rCommandServer\x12\x35\n\nEnqueueJob\x12\x12.EnqueueJobRequest\x1a\x11.EnqueueJobResult"\x00\x12/\n\x08RetryJob\x12\x10.RetryJobRequest\x1a\x0f.RetryJobResult"\x00\x12\x38\n\x0b\x43heckHealth\x12\x13.CheckHealthRequest\x1a\x12.CheckHealthResult"\x00\x12\x41\n\x0eListQueuedJobs\x12\x16.ListQueuedJobsRequest\x1a\x15.ListQueuedJobsResult"\x00\x12J\n\x11VaccuumQueuedJobs\x12\x19.VaccuumQueuedJobsRequest\x1a\x18.VaccuumQueuedJobsResult"\x00\x12\x32\n\tCancelJob\x12\x11.CancelJobRequest\x1a\x10.CancelJobResult"\x00\x12G\n\x10ProvisionWebhook\x12\x18.ProvisionWebhookRequest\x1a\x17.ProvisionWebhookResult"\x00\x62\x06proto3'
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
_globals = globals()
|
|
@@ -41,8 +41,8 @@ _builder.BuildTopDescriptorsAndMessages(
|
|
|
41
41
|
)
|
|
42
42
|
if not _descriptor._USE_C_DESCRIPTORS:
|
|
43
43
|
DESCRIPTOR._loaded_options = None
|
|
44
|
-
_globals["_CANCELJOBSTATUS"]._serialized_start =
|
|
45
|
-
_globals["_CANCELJOBSTATUS"]._serialized_end =
|
|
44
|
+
_globals["_CANCELJOBSTATUS"]._serialized_start = 1109
|
|
45
|
+
_globals["_CANCELJOBSTATUS"]._serialized_end = 1199
|
|
46
46
|
_globals["_ENQUEUEJOBREQUEST"]._serialized_start = 118
|
|
47
47
|
_globals["_ENQUEUEJOBREQUEST"]._serialized_end = 187
|
|
48
48
|
_globals["_ENQUEUEJOBRESULT"]._serialized_start = 189
|
|
@@ -69,6 +69,10 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
|
|
69
69
|
_globals["_CANCELJOBREQUEST"]._serialized_end = 963
|
|
70
70
|
_globals["_CANCELJOBRESULT"]._serialized_start = 965
|
|
71
71
|
_globals["_CANCELJOBRESULT"]._serialized_end = 1016
|
|
72
|
-
_globals["
|
|
73
|
-
_globals["
|
|
72
|
+
_globals["_PROVISIONWEBHOOKREQUEST"]._serialized_start = 1018
|
|
73
|
+
_globals["_PROVISIONWEBHOOKREQUEST"]._serialized_end = 1059
|
|
74
|
+
_globals["_PROVISIONWEBHOOKRESULT"]._serialized_start = 1061
|
|
75
|
+
_globals["_PROVISIONWEBHOOKRESULT"]._serialized_end = 1107
|
|
76
|
+
_globals["_COMMANDSERVER"]._serialized_start = 1202
|
|
77
|
+
_globals["_COMMANDSERVER"]._serialized_end = 1647
|
|
74
78
|
# @@protoc_insertion_point(module_scope)
|
|
@@ -161,3 +161,15 @@ class CancelJobResult(_message.Message):
|
|
|
161
161
|
def __init__(
|
|
162
162
|
self, status: _Optional[_Union[CancelJobStatus, str]] = ...
|
|
163
163
|
) -> None: ...
|
|
164
|
+
|
|
165
|
+
class ProvisionWebhookRequest(_message.Message):
|
|
166
|
+
__slots__ = ("job_id",)
|
|
167
|
+
JOB_ID_FIELD_NUMBER: _ClassVar[int]
|
|
168
|
+
job_id: str
|
|
169
|
+
def __init__(self, job_id: _Optional[str] = ...) -> None: ...
|
|
170
|
+
|
|
171
|
+
class ProvisionWebhookResult(_message.Message):
|
|
172
|
+
__slots__ = ("profile_name",)
|
|
173
|
+
PROFILE_NAME_FIELD_NUMBER: _ClassVar[int]
|
|
174
|
+
profile_name: str
|
|
175
|
+
def __init__(self, profile_name: _Optional[str] = ...) -> None: ...
|
|
@@ -78,6 +78,12 @@ class CommandServerStub(object):
|
|
|
78
78
|
response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobResult.FromString,
|
|
79
79
|
_registered_method=True,
|
|
80
80
|
)
|
|
81
|
+
self.ProvisionWebhook = channel.unary_unary(
|
|
82
|
+
"/CommandServer/ProvisionWebhook",
|
|
83
|
+
request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ProvisionWebhookRequest.SerializeToString,
|
|
84
|
+
response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ProvisionWebhookResult.FromString,
|
|
85
|
+
_registered_method=True,
|
|
86
|
+
)
|
|
81
87
|
|
|
82
88
|
|
|
83
89
|
class CommandServerServicer(object):
|
|
@@ -119,6 +125,12 @@ class CommandServerServicer(object):
|
|
|
119
125
|
context.set_details("Method not implemented!")
|
|
120
126
|
raise NotImplementedError("Method not implemented!")
|
|
121
127
|
|
|
128
|
+
def ProvisionWebhook(self, request, context):
|
|
129
|
+
"""Missing associated documentation comment in .proto file."""
|
|
130
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
131
|
+
context.set_details("Method not implemented!")
|
|
132
|
+
raise NotImplementedError("Method not implemented!")
|
|
133
|
+
|
|
122
134
|
|
|
123
135
|
def add_CommandServerServicer_to_server(servicer, server):
|
|
124
136
|
rpc_method_handlers = {
|
|
@@ -152,6 +164,11 @@ def add_CommandServerServicer_to_server(servicer, server):
|
|
|
152
164
|
request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobRequest.FromString,
|
|
153
165
|
response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobResult.SerializeToString,
|
|
154
166
|
),
|
|
167
|
+
"ProvisionWebhook": grpc.unary_unary_rpc_method_handler(
|
|
168
|
+
servicer.ProvisionWebhook,
|
|
169
|
+
request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ProvisionWebhookRequest.FromString,
|
|
170
|
+
response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ProvisionWebhookResult.SerializeToString,
|
|
171
|
+
),
|
|
155
172
|
}
|
|
156
173
|
generic_handler = grpc.method_handlers_generic_handler(
|
|
157
174
|
"CommandServer", rpc_method_handlers
|
|
@@ -343,3 +360,33 @@ class CommandServer(object):
|
|
|
343
360
|
metadata,
|
|
344
361
|
_registered_method=True,
|
|
345
362
|
)
|
|
363
|
+
|
|
364
|
+
@staticmethod
|
|
365
|
+
def ProvisionWebhook(
|
|
366
|
+
request,
|
|
367
|
+
target,
|
|
368
|
+
options=(),
|
|
369
|
+
channel_credentials=None,
|
|
370
|
+
call_credentials=None,
|
|
371
|
+
insecure=False,
|
|
372
|
+
compression=None,
|
|
373
|
+
wait_for_ready=None,
|
|
374
|
+
timeout=None,
|
|
375
|
+
metadata=None,
|
|
376
|
+
):
|
|
377
|
+
return grpc.experimental.unary_unary(
|
|
378
|
+
request,
|
|
379
|
+
target,
|
|
380
|
+
"/CommandServer/ProvisionWebhook",
|
|
381
|
+
uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ProvisionWebhookRequest.SerializeToString,
|
|
382
|
+
uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ProvisionWebhookResult.FromString,
|
|
383
|
+
options,
|
|
384
|
+
channel_credentials,
|
|
385
|
+
insecure,
|
|
386
|
+
call_credentials,
|
|
387
|
+
compression,
|
|
388
|
+
wait_for_ready,
|
|
389
|
+
timeout,
|
|
390
|
+
metadata,
|
|
391
|
+
_registered_method=True,
|
|
392
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from opentelemetry.trace import get_current_span
|
|
2
|
+
|
|
3
|
+
from uncountable.integration.construct_client import construct_uncountable_client
|
|
4
|
+
from uncountable.integration.scan_profiles import load_profiles
|
|
5
|
+
from uncountable.integration.telemetry import Logger
|
|
6
|
+
from uncountable.integration.webhook_signature_key import (
|
|
7
|
+
bootstrap_webhook_signature_key,
|
|
8
|
+
)
|
|
9
|
+
from uncountable.types import job_definition_t
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def provision_webhook(*, job_id: str) -> str | None:
|
|
13
|
+
logger = Logger(get_current_span())
|
|
14
|
+
for profile_metadata in load_profiles():
|
|
15
|
+
for job in profile_metadata.jobs:
|
|
16
|
+
if (
|
|
17
|
+
isinstance(job, job_definition_t.UncountableWebhookJobDefinition)
|
|
18
|
+
and job.id == job_id
|
|
19
|
+
):
|
|
20
|
+
client = construct_uncountable_client(profile_metadata, logger)
|
|
21
|
+
bootstrap_webhook_signature_key(
|
|
22
|
+
client=client,
|
|
23
|
+
profile_metadata=profile_metadata,
|
|
24
|
+
job_id=job.id,
|
|
25
|
+
purpose=job.purpose,
|
|
26
|
+
)
|
|
27
|
+
return profile_metadata.name
|
|
28
|
+
return None
|
|
@@ -43,6 +43,7 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
|
43
43
|
name=profile_name,
|
|
44
44
|
jobs=definition.jobs,
|
|
45
45
|
base_url=environment_config.base_url,
|
|
46
|
+
integration_server_base_url=environment_config.integration_server_base_url,
|
|
46
47
|
app_base_url=environment_config.app_base_url,
|
|
47
48
|
auth_retrieval=environment_config.auth_retrieval,
|
|
48
49
|
client_options=environment_config.client_options,
|
|
@@ -58,6 +59,7 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
|
58
59
|
name=profile_name,
|
|
59
60
|
jobs=definition.jobs,
|
|
60
61
|
base_url=definition.base_url,
|
|
62
|
+
integration_server_base_url=definition.integration_server_base_url,
|
|
61
63
|
app_base_url=definition.app_base_url,
|
|
62
64
|
auth_retrieval=definition.auth_retrieval,
|
|
63
65
|
client_options=definition.client_options,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import functools
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import assert_never
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
|
|
9
|
+
from pkgs.argument_parser import CachedParser
|
|
10
|
+
from uncountable.types import overrides_t
|
|
11
|
+
from uncountable.types.job_definition_t import ProfileMetadata
|
|
12
|
+
from uncountable.types.secret_retrieval_t import (
|
|
13
|
+
ClientSecretRetrieval,
|
|
14
|
+
SecretRetrieval,
|
|
15
|
+
SecretRetrievalAWS,
|
|
16
|
+
SecretRetrievalEnv,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SecretRetrievalError(Exception):
|
|
21
|
+
def __init__(
|
|
22
|
+
self, secret_retrieval: SecretRetrieval, message: str | None = None
|
|
23
|
+
) -> None:
|
|
24
|
+
self.secret_retrieval = secret_retrieval
|
|
25
|
+
self.message = message
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
append_message = ""
|
|
29
|
+
if self.message is not None:
|
|
30
|
+
append_message = f": {self.message}"
|
|
31
|
+
return f"{self.secret_retrieval.type} secret retrieval failed{append_message}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@functools.cache
|
|
35
|
+
def _get_aws_secret(*, secret_name: str, region_name: str, sub_key: str | None) -> str:
|
|
36
|
+
client = boto3.client("secretsmanager", region_name=region_name)
|
|
37
|
+
response = client.get_secret_value(SecretId=secret_name)
|
|
38
|
+
|
|
39
|
+
if "SecretString" in response:
|
|
40
|
+
secret = response["SecretString"]
|
|
41
|
+
else:
|
|
42
|
+
secret = base64.b64decode(response["SecretBinary"])
|
|
43
|
+
|
|
44
|
+
value = json.loads(secret)
|
|
45
|
+
|
|
46
|
+
if sub_key is not None:
|
|
47
|
+
assert isinstance(value, dict)
|
|
48
|
+
return str(value[sub_key])
|
|
49
|
+
else:
|
|
50
|
+
return str(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@functools.cache
|
|
54
|
+
def _load_secret_overrides(profile_name: str) -> dict[SecretRetrieval, str]:
|
|
55
|
+
overrides_parser = CachedParser(overrides_t.Overrides)
|
|
56
|
+
profiles_module = os.environ["UNC_PROFILES_MODULE"]
|
|
57
|
+
try:
|
|
58
|
+
overrides = overrides_parser.parse_yaml_resource(
|
|
59
|
+
package=f"{profiles_module}.{profile_name}",
|
|
60
|
+
resource="local_overrides.yaml",
|
|
61
|
+
)
|
|
62
|
+
return {
|
|
63
|
+
override.secret_retrieval: override.value for override in overrides.secrets
|
|
64
|
+
}
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def secret_override(
|
|
70
|
+
secret_retrieval: SecretRetrieval, profile_metadata: ProfileMetadata
|
|
71
|
+
) -> str | None:
|
|
72
|
+
return _load_secret_overrides(profile_metadata.name).get(secret_retrieval)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def retrieve_basic_secret(
|
|
76
|
+
secret_retrieval: ClientSecretRetrieval, profile_metadata: ProfileMetadata
|
|
77
|
+
) -> str:
|
|
78
|
+
value_from_override = secret_override(secret_retrieval, profile_metadata)
|
|
79
|
+
if value_from_override is not None:
|
|
80
|
+
return value_from_override
|
|
81
|
+
|
|
82
|
+
match secret_retrieval:
|
|
83
|
+
case SecretRetrievalEnv():
|
|
84
|
+
env_name = f"UNC_{profile_metadata.name.upper()}_{secret_retrieval.env_key.upper()}"
|
|
85
|
+
secret = os.environ.get(env_name)
|
|
86
|
+
if secret is None:
|
|
87
|
+
raise SecretRetrievalError(
|
|
88
|
+
secret_retrieval, f"environment variable {env_name} missing"
|
|
89
|
+
)
|
|
90
|
+
return secret
|
|
91
|
+
case SecretRetrievalAWS():
|
|
92
|
+
try:
|
|
93
|
+
return _get_aws_secret(
|
|
94
|
+
secret_name=secret_retrieval.secret_name,
|
|
95
|
+
region_name=secret_retrieval.region,
|
|
96
|
+
sub_key=secret_retrieval.sub_key,
|
|
97
|
+
)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise SecretRetrievalError(secret_retrieval) from e
|
|
100
|
+
case _:
|
|
101
|
+
assert_never(secret_retrieval)
|