UncountablePythonSDK 0.0.175__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.
Files changed (37) hide show
  1. pkgs/type_spec/actions_registry/__main__.py +1 -0
  2. pkgs/type_spec/actions_registry/emit_python.py +4 -0
  3. pkgs/type_spec/actions_registry/emit_typescript.py +4 -0
  4. pkgs/type_spec/value_spec/__main__.py +10 -0
  5. pkgs/type_spec/value_spec/emit_python.py +9 -0
  6. uncountable/integration/cli.py +29 -0
  7. uncountable/integration/construct_client.py +6 -6
  8. uncountable/integration/job.py +27 -6
  9. uncountable/integration/queue_runner/command_server/command_client.py +20 -0
  10. uncountable/integration/queue_runner/command_server/command_server.py +19 -0
  11. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +9 -0
  12. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +9 -5
  13. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +12 -0
  14. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +47 -0
  15. uncountable/integration/queue_runner/command_server/provision_webhook.py +28 -0
  16. uncountable/integration/scan_profiles.py +2 -0
  17. uncountable/integration/secret_retrieval/__init__.py +5 -1
  18. uncountable/integration/secret_retrieval/basic_secret.py +101 -0
  19. uncountable/integration/secret_retrieval/retrieve_secret.py +85 -70
  20. uncountable/integration/webhook_signature_key.py +110 -0
  21. uncountable/types/__init__.py +2 -0
  22. uncountable/types/api/notebooks/add_notebook_content.py +3 -2
  23. uncountable/types/api/recipes/get_recipes_data.py +1 -1
  24. uncountable/types/auth_retrieval_t.py +3 -3
  25. uncountable/types/client_base.py +2 -2
  26. uncountable/types/entity_t.py +4 -0
  27. uncountable/types/integration_server_t.py +1 -0
  28. uncountable/types/job_definition.py +1 -0
  29. uncountable/types/job_definition_t.py +18 -1
  30. uncountable/types/secret_retrieval.py +2 -0
  31. uncountable/types/secret_retrieval_t.py +33 -1
  32. uncountable/types/webhook.py +7 -0
  33. uncountable/types/webhook_t.py +22 -0
  34. {uncountablepythonsdk-0.0.175.dist-info → uncountablepythonsdk-0.0.176.dist-info}/METADATA +2 -2
  35. {uncountablepythonsdk-0.0.175.dist-info → uncountablepythonsdk-0.0.176.dist-info}/RECORD +37 -32
  36. {uncountablepythonsdk-0.0.175.dist-info → uncountablepythonsdk-0.0.176.dist-info}/WHEEL +0 -0
  37. {uncountablepythonsdk-0.0.175.dist-info → uncountablepythonsdk-0.0.176.dist-info}/top_level.txt +0 -0
@@ -92,6 +92,7 @@ def main() -> None:
92
92
  ]
93
93
  if definition.visibility_scope is not None
94
94
  else None,
95
+ kind=definition.kind,
95
96
  arguments=definition.arguments,
96
97
  )
97
98
  action_definitions_with_arguments_list.append(
@@ -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
 
@@ -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
 
@@ -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.retrieve_secret import retrieve_secret
5
- from uncountable.integration.telemetry import JobLogger
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 = retrieve_secret(
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 = retrieve_secret(
22
+ api_id = retrieve_basic_secret(
23
23
  profile_meta.auth_retrieval.api_id_secret, profile_metadata=profile_meta
24
24
  )
25
- api_key = retrieve_secret(
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: JobLogger,
35
+ logger: Logger,
36
36
  *,
37
37
  request_context: RequestContext | None = None,
38
38
  headers: dict[str, str] | None = None,
@@ -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.retrieve_secret import retrieve_secret
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
- assert isinstance(job_definition, job_definition_t.WebhookJobDefinition)
186
- signature_key = retrieve_secret(
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 != passed_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\xf4\x02\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\x62\x06proto3'
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 = 1018
45
- _globals["_CANCELJOBSTATUS"]._serialized_end = 1108
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["_COMMANDSERVER"]._serialized_start = 1111
73
- _globals["_COMMANDSERVER"]._serialized_end = 1483
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,
@@ -1,3 +1,7 @@
1
+ from .basic_secret import retrieve_basic_secret
1
2
  from .retrieve_secret import retrieve_secret
2
3
 
3
- __all__: list[str] = ["retrieve_secret"]
4
+ __all__: list[str] = [
5
+ "retrieve_basic_secret",
6
+ "retrieve_secret",
7
+ ]
@@ -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)