UncountablePythonSDK 0.0.175__py3-none-any.whl → 0.0.177__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.
- docs/conf.py +20 -0
- 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/value_spec/__main__.py +10 -0
- pkgs/type_spec/value_spec/emit_python.py +9 -0
- uncountable/core/file_upload.py +18 -13
- uncountable/core/query/builder.py +10 -1
- uncountable/core/query/column.py +29 -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/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 +6 -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.175.dist-info → uncountablepythonsdk-0.0.177.dist-info}/METADATA +2 -2
- {uncountablepythonsdk-0.0.175.dist-info → uncountablepythonsdk-0.0.177.dist-info}/RECORD +41 -36
- {uncountablepythonsdk-0.0.175.dist-info → uncountablepythonsdk-0.0.177.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.175.dist-info → uncountablepythonsdk-0.0.177.dist-info}/top_level.txt +0 -0
docs/conf.py
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
10
|
+
import importlib
|
|
11
|
+
import inspect
|
|
10
12
|
|
|
11
13
|
from docutils import nodes # type: ignore[import-untyped]
|
|
12
14
|
from sphinx.addnodes import pending_xref # type: ignore[import-not-found]
|
|
@@ -63,6 +65,20 @@ favicons = [
|
|
|
63
65
|
]
|
|
64
66
|
|
|
65
67
|
|
|
68
|
+
def _resolve_alias_to_real_module(target_module: str) -> str:
|
|
69
|
+
if "." not in target_module:
|
|
70
|
+
return target_module
|
|
71
|
+
parent_name, attr_name = target_module.rsplit(".", 1)
|
|
72
|
+
try:
|
|
73
|
+
parent = importlib.import_module(parent_name)
|
|
74
|
+
except ImportError:
|
|
75
|
+
return target_module
|
|
76
|
+
aliased = getattr(parent, attr_name, None)
|
|
77
|
+
if not inspect.ismodule(aliased) or aliased.__name__ == target_module:
|
|
78
|
+
return target_module
|
|
79
|
+
return aliased.__name__
|
|
80
|
+
|
|
81
|
+
|
|
66
82
|
def _hook_missing_reference(
|
|
67
83
|
_app: Sphinx, _env: BuildEnvironment, node: pending_xref, contnode: nodes.Text
|
|
68
84
|
) -> nodes.reference | None:
|
|
@@ -80,6 +96,10 @@ def _hook_missing_reference(
|
|
|
80
96
|
return None
|
|
81
97
|
|
|
82
98
|
target_module, target_name = target.rsplit(".", 1)
|
|
99
|
+
real_module = _resolve_alias_to_real_module(target_module)
|
|
100
|
+
if real_module != target_module:
|
|
101
|
+
target_module = real_module
|
|
102
|
+
target = f"{target_module}.{target_name}"
|
|
83
103
|
|
|
84
104
|
# construct relative path from current doc page to target page
|
|
85
105
|
relative_segments_to_root = [".." for _ in current_doc.split("/")]
|
|
@@ -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
|
|
uncountable/core/file_upload.py
CHANGED
|
@@ -17,6 +17,7 @@ from uncountable.types import uploader_t
|
|
|
17
17
|
from .types import AuthDetailsAll, AuthDetailsApiKey
|
|
18
18
|
|
|
19
19
|
_CHUNK_SIZE = 5 * 1024 * 1024 # s3 requires 5MiB minimum
|
|
20
|
+
_PARSED_FILE_DATA_FILENAME = "parsed_file_data.json"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class FileUploadType(StrEnum):
|
|
@@ -51,17 +52,27 @@ class UploaderFileUpload:
|
|
|
51
52
|
FileUpload = MediaFileUpload | DataFileUpload | UploaderFileUpload
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
@dataclass(kw_only=True)
|
|
55
|
-
class _ParsedFileDataWrapper:
|
|
56
|
-
parsed_file_data: list[uploader_t.ParsedFileData]
|
|
57
|
-
|
|
58
|
-
|
|
59
55
|
@dataclass(kw_only=True)
|
|
60
56
|
class FileBytes:
|
|
61
57
|
name: str
|
|
62
58
|
bytes_data: BytesIO
|
|
63
59
|
|
|
64
60
|
|
|
61
|
+
def _build_parsed_file_data_bytes(
|
|
62
|
+
*, parsed_file_data: list[uploader_t.ParsedFileData]
|
|
63
|
+
) -> FileBytes:
|
|
64
|
+
buffer = BytesIO()
|
|
65
|
+
buffer.write(b'{"parsed_file_data": [')
|
|
66
|
+
for index, file_data in enumerate(parsed_file_data):
|
|
67
|
+
if index > 0:
|
|
68
|
+
buffer.write(b", ")
|
|
69
|
+
serialized_file_data = serialize_for_storage(file_data)
|
|
70
|
+
buffer.write(json.dumps(serialized_file_data).encode("utf-8"))
|
|
71
|
+
buffer.write(b"]}")
|
|
72
|
+
buffer.seek(0)
|
|
73
|
+
return FileBytes(name=_PARSED_FILE_DATA_FILENAME, bytes_data=buffer)
|
|
74
|
+
|
|
75
|
+
|
|
65
76
|
@contextmanager
|
|
66
77
|
def file_upload_data(file_upload: FileUpload) -> Generator[FileBytes, None, None]:
|
|
67
78
|
match file_upload:
|
|
@@ -73,15 +84,9 @@ def file_upload_data(file_upload: FileUpload) -> Generator[FileBytes, None, None
|
|
|
73
84
|
case DataFileUpload():
|
|
74
85
|
yield FileBytes(name=file_upload.name, bytes_data=file_upload.data)
|
|
75
86
|
case UploaderFileUpload():
|
|
76
|
-
|
|
87
|
+
yield _build_parsed_file_data_bytes(
|
|
77
88
|
parsed_file_data=file_upload.parsed_file_data
|
|
78
89
|
)
|
|
79
|
-
serialized = serialize_for_storage(wrapper)
|
|
80
|
-
json_bytes = json.dumps(serialized).encode("utf-8")
|
|
81
|
-
yield FileBytes(
|
|
82
|
-
name="parsed_file_data.json",
|
|
83
|
-
bytes_data=BytesIO(json_bytes),
|
|
84
|
-
)
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
@dataclass(kw_only=True)
|
|
@@ -135,7 +140,7 @@ class FileUploader:
|
|
|
135
140
|
case DataFileUpload():
|
|
136
141
|
attributes["file_name"] = file_upload.name
|
|
137
142
|
case UploaderFileUpload():
|
|
138
|
-
attributes["file_name"] =
|
|
143
|
+
attributes["file_name"] = _PARSED_FILE_DATA_FILENAME
|
|
139
144
|
case _:
|
|
140
145
|
assert_never(file_upload)
|
|
141
146
|
with push_scope_optional(
|
|
@@ -13,6 +13,7 @@ from uncountable.core.query.column import (
|
|
|
13
13
|
Column,
|
|
14
14
|
ColumnFilter,
|
|
15
15
|
EnumColumn,
|
|
16
|
+
JsonColumn,
|
|
16
17
|
NullableColumn,
|
|
17
18
|
PrimaryKeyColumn,
|
|
18
19
|
get_column_base,
|
|
@@ -72,8 +73,16 @@ class QueryBuilder[QueryRowT: QueryRow]:
|
|
|
72
73
|
column.identifier for column in column_definitions.values()
|
|
73
74
|
)
|
|
74
75
|
self._property_names: tuple[str, ...] = tuple(column_definitions.keys())
|
|
76
|
+
json_property_names = {
|
|
77
|
+
name
|
|
78
|
+
for name, column in column_definitions.items()
|
|
79
|
+
if isinstance(column, JsonColumn)
|
|
80
|
+
}
|
|
75
81
|
self._parser: CachedParser = CachedParser(
|
|
76
|
-
serial_class(
|
|
82
|
+
serial_class(
|
|
83
|
+
unconverted_keys={*self._property_names},
|
|
84
|
+
unconverted_values=json_property_names,
|
|
85
|
+
)(
|
|
77
86
|
make_dataclass(
|
|
78
87
|
cls_name=self._model.__name__,
|
|
79
88
|
fields=[
|
uncountable/core/query/column.py
CHANGED
|
@@ -465,6 +465,27 @@ class NullableTextsColumn[
|
|
|
465
465
|
# IMPROVE: Implement "Not Exists" filter (when supported in SDK)
|
|
466
466
|
|
|
467
467
|
|
|
468
|
+
class JsonColumn(Column[base_t.JsonValue, NullableColumn]):
|
|
469
|
+
__hash__ = Column.__hash__
|
|
470
|
+
|
|
471
|
+
def __init__(self, identifier: listing_t.ColumnIdentifier) -> None:
|
|
472
|
+
super().__init__(
|
|
473
|
+
identifier=identifier,
|
|
474
|
+
nullable=NullableColumn,
|
|
475
|
+
value_type=(
|
|
476
|
+
dict[str, base_t.JsonValue] | list[base_t.JsonValue] | base_t.JsonScalar
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
def __ne__(self, other: None) -> ColumnFilter: # type: ignore[override]
|
|
481
|
+
return ColumnFilter(
|
|
482
|
+
identifier=self.identifier,
|
|
483
|
+
filter=lambda identifier: listing_t.FilterSpecExists(column=identifier),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# IMPROVE: Implement "Not Exists" filter (when supported in SDK)
|
|
487
|
+
|
|
488
|
+
|
|
468
489
|
class PrimaryKeyColumn(
|
|
469
490
|
Column[base_t.ObjectId, NonNullableColumn],
|
|
470
491
|
):
|
|
@@ -684,6 +705,10 @@ class IdColumn[
|
|
|
684
705
|
column: NullableTextsColumn[BaseNullableT],
|
|
685
706
|
) -> NullableTextsColumn[BaseNullableT]: ...
|
|
686
707
|
|
|
708
|
+
# Json
|
|
709
|
+
@overload
|
|
710
|
+
def __rshift__(self, column: JsonColumn) -> JsonColumn: ...
|
|
711
|
+
|
|
687
712
|
# Ids
|
|
688
713
|
@overload
|
|
689
714
|
def __rshift__[
|
|
@@ -734,6 +759,7 @@ class IdColumn[
|
|
|
734
759
|
| NumericColumn[BaseNullableT]
|
|
735
760
|
| TextsColumn[BaseNullableT]
|
|
736
761
|
| NullableTextsColumn[BaseNullableT]
|
|
762
|
+
| JsonColumn
|
|
737
763
|
| IdsColumn[BaseNullableT]
|
|
738
764
|
| IdColumn[BaseNullableT],
|
|
739
765
|
) -> (
|
|
@@ -768,6 +794,7 @@ class IdColumn[
|
|
|
768
794
|
| NullableTextsColumn[BaseNullableT]
|
|
769
795
|
| NullableTextsColumn[NonNullableColumn | NullableColumn]
|
|
770
796
|
| NullableTextsColumn[NullableColumn]
|
|
797
|
+
| JsonColumn
|
|
771
798
|
| IdsColumn[BaseNullableT]
|
|
772
799
|
| IdsColumn[NonNullableColumn | NullableColumn]
|
|
773
800
|
| IdsColumn[NullableColumn]
|
|
@@ -869,6 +896,8 @@ class IdColumn[
|
|
|
869
896
|
if self.nullable is NonNullableColumn
|
|
870
897
|
else NullableColumn,
|
|
871
898
|
)
|
|
899
|
+
case JsonColumn():
|
|
900
|
+
return JsonColumn(identifier=transitive_identifier)
|
|
872
901
|
case IdsColumn():
|
|
873
902
|
return IdsColumn(
|
|
874
903
|
id_type=column.id_type,
|
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
|
+
)
|