UncountablePythonSDK 0.0.114__py3-none-any.whl → 0.0.116__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.

Potentially problematic release.


This version of UncountablePythonSDK might be problematic. Click here for more details.

Files changed (34) hide show
  1. examples/integration-server/jobs/materials_auto/example_http.py +35 -0
  2. examples/integration-server/jobs/materials_auto/example_instrument.py +38 -0
  3. examples/integration-server/jobs/materials_auto/profile.yaml +15 -0
  4. pkgs/type_spec/builder.py +18 -5
  5. pkgs/type_spec/config.py +26 -5
  6. pkgs/type_spec/cross_output_links.py +9 -7
  7. pkgs/type_spec/emit_open_api.py +9 -2
  8. pkgs/type_spec/emit_open_api_util.py +1 -0
  9. pkgs/type_spec/emit_python.py +4 -1
  10. pkgs/type_spec/emit_typescript.py +46 -30
  11. pkgs/type_spec/emit_typescript_util.py +16 -0
  12. pkgs/type_spec/load_types.py +1 -1
  13. pkgs/type_spec/open_api_util.py +9 -1
  14. pkgs/type_spec/parts/base.ts.prepart +1 -0
  15. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +19 -5
  16. uncountable/core/environment.py +1 -1
  17. uncountable/integration/http_server/__init__.py +5 -0
  18. uncountable/integration/http_server/types.py +67 -0
  19. uncountable/integration/job.py +129 -5
  20. uncountable/integration/server.py +2 -2
  21. uncountable/integration/telemetry.py +1 -1
  22. uncountable/integration/webhook_server/entrypoint.py +37 -112
  23. uncountable/types/api/entity/create_or_update_entity.py +1 -0
  24. uncountable/types/api/entity/lookup_entity.py +15 -1
  25. uncountable/types/async_batch_processor.py +3 -0
  26. uncountable/types/client_base.py +52 -0
  27. uncountable/types/entity_t.py +8 -0
  28. uncountable/types/integration_server_t.py +2 -0
  29. uncountable/types/job_definition.py +2 -0
  30. uncountable/types/job_definition_t.py +25 -2
  31. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/METADATA +1 -1
  32. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/RECORD +34 -30
  33. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/WHEEL +0 -0
  34. {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,14 @@
1
1
  import json
2
+ import re
2
3
  from dataclasses import dataclass
3
4
  from io import StringIO
4
5
  from pathlib import Path
5
6
  from typing import assert_never
6
7
 
7
8
  from main.base.types import (
9
+ base_t,
8
10
  ui_entry_actions_t,
9
11
  )
10
- from pkgs.argument_parser import snake_to_camel_case
11
12
  from pkgs.serialization_util import serialize_for_api
12
13
  from pkgs.type_spec import emit_typescript_util
13
14
  from pkgs.type_spec.builder import (
@@ -45,8 +46,8 @@ def ui_entry_variable_to_type_spec_type(
45
46
  match variable:
46
47
  case ui_entry_actions_t.UiEntryActionVariableString():
47
48
  return BaseTypeName.s_string
48
- case ui_entry_actions_t.UiEntryActionVariableEntityFields():
49
- return "entity.Entity"
49
+ case ui_entry_actions_t.UiEntryActionVariableSingleEntity():
50
+ return "ObjectId"
50
51
  case _:
51
52
  assert_never(variable)
52
53
 
@@ -157,6 +158,15 @@ def emit_entry_action_definition(
157
158
  )
158
159
 
159
160
 
161
+ def _validate_input(input: ui_entry_actions_t.UiEntryActionVariable) -> None:
162
+ if "_" in input.vs_var_name:
163
+ raise ValueError(f"Expected camelCase for variable {input.vs_var_name}")
164
+ if not re.fullmatch(base_t.REF_NAME_STRICT_REGEX, input.vs_var_name):
165
+ raise ValueError(
166
+ f"Variable {input.vs_var_name} has invalid syntax. See REF_NAME_STRICT_REGEX"
167
+ )
168
+
169
+
160
170
  def emit_query_index(
161
171
  ctx: emit_typescript_util.EmitTypescriptContext,
162
172
  defn_infos: list[EntryActionTypeInfo],
@@ -182,6 +192,7 @@ def emit_query_index(
182
192
  "type": BaseTypeName.s_object,
183
193
  "properties": {
184
194
  "value_spec_var": {"type": "String"},
195
+ "type": {"type": "ui_entry_actions.UiEntryActionDataType"},
185
196
  "variable": {"type": "ui_entry_actions.UiEntryActionVariable"},
186
197
  },
187
198
  },
@@ -215,10 +226,12 @@ def emit_query_index(
215
226
  for scope, defn in definitions.items():
216
227
  inputs = []
217
228
  outputs = []
218
- for name, input in defn.inputs.items():
229
+ for input in defn.inputs.values():
230
+ _validate_input(input)
219
231
  inputs.append(
220
232
  serialize_for_api({
221
- "value_spec_var": snake_to_camel_case(name),
233
+ "value_spec_var": input.vs_var_name,
234
+ "type": input.type,
222
235
  "variable": input,
223
236
  })
224
237
  )
@@ -269,6 +282,7 @@ def generate_entry_actions_typescript(
269
282
  ctx = emit_typescript_util.EmitTypescriptContext(
270
283
  out=definition_buffer,
271
284
  namespace=index_namespace,
285
+ api_endpoints={},
272
286
  )
273
287
  builder.namespaces[index_namespace.name] = index_namespace
274
288
 
@@ -18,7 +18,7 @@ def get_server_env() -> str | None:
18
18
  return os.environ.get("UNC_SERVER_ENV")
19
19
 
20
20
 
21
- def get_webhook_server_port() -> int:
21
+ def get_http_server_port() -> int:
22
22
  return int(os.environ.get("UNC_WEBHOOK_SERVER_PORT", "5001"))
23
23
 
24
24
 
@@ -0,0 +1,5 @@
1
+ # CLOSED MODULE
2
+
3
+ from .types import GenericHttpRequest as GenericHttpRequest
4
+ from .types import GenericHttpResponse as GenericHttpResponse
5
+ from .types import HttpException as HttpException
@@ -0,0 +1,67 @@
1
+ import base64
2
+ import functools
3
+ from dataclasses import dataclass
4
+
5
+ from flask.wrappers import Response
6
+
7
+
8
+ class HttpException(Exception):
9
+ error_code: int
10
+ message: str
11
+
12
+ def __init__(self, *, error_code: int, message: str) -> None:
13
+ self.error_code = error_code
14
+ self.message = message
15
+
16
+ @staticmethod
17
+ def payload_failed_signature() -> "HttpException":
18
+ return HttpException(
19
+ error_code=401, message="webhook payload did not match signature"
20
+ )
21
+
22
+ @staticmethod
23
+ def no_signature_passed() -> "HttpException":
24
+ return HttpException(error_code=400, message="missing signature")
25
+
26
+ @staticmethod
27
+ def body_parse_error() -> "HttpException":
28
+ return HttpException(error_code=400, message="body parse error")
29
+
30
+ @staticmethod
31
+ def unknown_error() -> "HttpException":
32
+ return HttpException(error_code=500, message="internal server error")
33
+
34
+ @staticmethod
35
+ def configuration_error(
36
+ message: str = "internal configuration error",
37
+ ) -> "HttpException":
38
+ return HttpException(error_code=500, message=message)
39
+
40
+ def __str__(self) -> str:
41
+ return f"[{self.error_code}]: {self.message}"
42
+
43
+ def make_error_response(self) -> Response:
44
+ return Response(
45
+ status=self.error_code, response={"error": {"message": str(self)}}
46
+ )
47
+
48
+
49
+ @dataclass(kw_only=True, frozen=True)
50
+ class GenericHttpRequest:
51
+ body_base64: str
52
+ headers: dict[str, str]
53
+
54
+ @functools.cached_property
55
+ def body_bytes(self) -> bytes:
56
+ return base64.b64decode(self.body_base64)
57
+
58
+ @functools.cached_property
59
+ def body_text(self) -> str:
60
+ return self.body_bytes.decode()
61
+
62
+
63
+ @dataclass(kw_only=True)
64
+ class GenericHttpResponse:
65
+ response: str
66
+ status_code: int
67
+ headers: dict[str, str] | None = None
@@ -1,15 +1,43 @@
1
1
  import functools
2
+ import hmac
2
3
  import typing
3
4
  from abc import ABC, abstractmethod
4
5
  from dataclasses import dataclass
5
6
 
7
+ import simplejson
8
+
6
9
  from pkgs.argument_parser import CachedParser
10
+ from pkgs.serialization_util import serialize_for_api
7
11
  from uncountable.core.async_batch import AsyncBatchProcessor
8
12
  from uncountable.core.client import Client
13
+ from uncountable.core.environment import get_local_admin_server_port
9
14
  from uncountable.core.file_upload import FileUpload
15
+ from uncountable.integration.http_server import (
16
+ GenericHttpRequest,
17
+ GenericHttpResponse,
18
+ HttpException,
19
+ )
20
+ from uncountable.integration.queue_runner.command_server.command_client import (
21
+ send_job_queue_message,
22
+ )
23
+ from uncountable.integration.queue_runner.command_server.types import (
24
+ CommandServerException,
25
+ )
26
+ from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
10
27
  from uncountable.integration.telemetry import JobLogger
11
- from uncountable.types import base_t, entity_t, webhook_job_t
12
- from uncountable.types.job_definition_t import JobDefinition, JobResult, ProfileMetadata
28
+ from uncountable.types import (
29
+ base_t,
30
+ entity_t,
31
+ job_definition_t,
32
+ queued_job_t,
33
+ webhook_job_t,
34
+ )
35
+ from uncountable.types.job_definition_t import (
36
+ HttpJobDefinitionBase,
37
+ JobDefinition,
38
+ JobResult,
39
+ ProfileMetadata,
40
+ )
13
41
 
14
42
 
15
43
  @dataclass(kw_only=True)
@@ -26,9 +54,6 @@ class JobArguments:
26
54
  CronJobArguments = JobArguments
27
55
 
28
56
 
29
- PT = typing.TypeVar("PT")
30
-
31
-
32
57
  class Job[PT](ABC):
33
58
  _unc_job_registered: bool = False
34
59
 
@@ -63,6 +88,51 @@ class CronJob(Job):
63
88
  WPT = typing.TypeVar("WPT")
64
89
 
65
90
 
91
+ @dataclass(kw_only=True)
92
+ class WebhookResponse:
93
+ pass
94
+
95
+
96
+ class CustomHttpJob(Job[GenericHttpRequest]):
97
+ @property
98
+ def payload_type(self) -> type[GenericHttpRequest]:
99
+ return GenericHttpRequest
100
+
101
+ @staticmethod
102
+ @abstractmethod
103
+ def validate_request(
104
+ *,
105
+ request: GenericHttpRequest,
106
+ job_definition: HttpJobDefinitionBase,
107
+ profile_meta: ProfileMetadata,
108
+ ) -> None:
109
+ """
110
+ Validate that the request is valid. If the request is invalid, raise an
111
+ exception.
112
+ """
113
+ ...
114
+
115
+ @staticmethod
116
+ @abstractmethod
117
+ def handle_request(
118
+ *,
119
+ request: GenericHttpRequest,
120
+ job_definition: HttpJobDefinitionBase,
121
+ profile_meta: ProfileMetadata,
122
+ ) -> GenericHttpResponse:
123
+ """
124
+ Handle the request synchronously. Normally this should just enqueue a job
125
+ and return immediately (see WebhookJob as an example).
126
+ """
127
+ ...
128
+
129
+ def run_outer(self, args: JobArguments) -> JobResult:
130
+ args.logger.log_warning(
131
+ message=f"Unexpected call to run_outer for CustomHttpJob: {args.job_definition.id}"
132
+ )
133
+ return JobResult(success=False)
134
+
135
+
66
136
  class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
67
137
  @property
68
138
  def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
@@ -72,6 +142,60 @@ class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
72
142
  @abstractmethod
73
143
  def webhook_payload_type(self) -> type[WPT]: ...
74
144
 
145
+ @staticmethod
146
+ def validate_request(
147
+ *,
148
+ request: GenericHttpRequest,
149
+ job_definition: job_definition_t.HttpJobDefinitionBase,
150
+ profile_meta: ProfileMetadata,
151
+ ) -> None:
152
+ assert isinstance(job_definition, job_definition_t.WebhookJobDefinition)
153
+ signature_key = retrieve_secret(
154
+ profile_metadata=profile_meta,
155
+ secret_retrieval=job_definition.signature_key_secret,
156
+ )
157
+ passed_signature = request.headers.get("Uncountable-Webhook-Signature")
158
+ if passed_signature is None:
159
+ raise HttpException.no_signature_passed()
160
+
161
+ request_body_signature = hmac.new(
162
+ signature_key.encode("utf-8"), msg=request.body_bytes, digestmod="sha256"
163
+ ).hexdigest()
164
+
165
+ if request_body_signature != passed_signature:
166
+ raise HttpException.payload_failed_signature()
167
+
168
+ @staticmethod
169
+ def handle_request(
170
+ *,
171
+ request: GenericHttpRequest,
172
+ job_definition: job_definition_t.HttpJobDefinitionBase,
173
+ profile_meta: ProfileMetadata, # noqa: ARG004
174
+ ) -> GenericHttpResponse:
175
+ try:
176
+ request_body = simplejson.loads(request.body_text)
177
+ webhook_payload = typing.cast(base_t.JsonValue, request_body)
178
+ except (simplejson.JSONDecodeError, ValueError) as e:
179
+ raise HttpException.body_parse_error() from e
180
+
181
+ try:
182
+ send_job_queue_message(
183
+ job_ref_name=job_definition.id,
184
+ payload=queued_job_t.QueuedJobPayload(
185
+ invocation_context=queued_job_t.InvocationContextWebhook(
186
+ webhook_payload=webhook_payload
187
+ )
188
+ ),
189
+ port=get_local_admin_server_port(),
190
+ )
191
+ except CommandServerException as e:
192
+ raise HttpException.unknown_error() from e
193
+
194
+ return GenericHttpResponse(
195
+ response=simplejson.dumps(serialize_for_api(WebhookResponse())),
196
+ status_code=200,
197
+ )
198
+
75
199
  def run_outer(self, args: JobArguments) -> JobResult:
76
200
  webhook_body = self.get_payload(args.payload)
77
201
  inner_payload = CachedParser(self.webhook_payload_type).parse_api(
@@ -16,7 +16,7 @@ from uncountable.integration.telemetry import Logger
16
16
  from uncountable.types import base_t, job_definition_t
17
17
  from uncountable.types.job_definition_t import (
18
18
  CronJobDefinition,
19
- WebhookJobDefinition,
19
+ HttpJobDefinitionBase,
20
20
  )
21
21
 
22
22
  _MAX_APSCHEDULER_CONCURRENT_JOBS = 1
@@ -86,7 +86,7 @@ class IntegrationServer:
86
86
  misfire_grace_time=None,
87
87
  **job_opts,
88
88
  )
89
- case WebhookJobDefinition():
89
+ case HttpJobDefinitionBase():
90
90
  pass
91
91
  case _:
92
92
  assert_never(job_defn)
@@ -177,7 +177,7 @@ class JobLogger(Logger):
177
177
  patched_attributes["job.definition.cron_spec"] = (
178
178
  self.job_definition.cron_spec
179
179
  )
180
- case job_definition_t.WebhookJobDefinition():
180
+ case job_definition_t.HttpJobDefinitionBase():
181
181
  pass
182
182
  case _:
183
183
  assert_never(self.job_definition)
@@ -1,146 +1,71 @@
1
- import hmac
2
- import typing
3
- from dataclasses import dataclass
1
+ import base64
4
2
 
5
3
  import flask
6
- import simplejson
7
4
  from flask.typing import ResponseReturnValue
8
- from flask.wrappers import Response
9
5
  from opentelemetry.trace import get_current_span
10
6
  from uncountable.core.environment import (
11
- get_local_admin_server_port,
7
+ get_http_server_port,
12
8
  get_server_env,
13
- get_webhook_server_port,
14
- )
15
- from uncountable.integration.queue_runner.command_server.command_client import (
16
- send_job_queue_message,
17
- )
18
- from uncountable.integration.queue_runner.command_server.types import (
19
- CommandServerException,
20
9
  )
10
+ from uncountable.integration.executors.script_executor import resolve_script_executor
11
+ from uncountable.integration.http_server import GenericHttpRequest, HttpException
12
+ from uncountable.integration.job import CustomHttpJob, WebhookJob
21
13
  from uncountable.integration.scan_profiles import load_profiles
22
- from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
23
14
  from uncountable.integration.telemetry import Logger
24
- from uncountable.types import base_t, job_definition_t, queued_job_t, webhook_job_t
25
-
26
- from pkgs.argument_parser import CachedParser
15
+ from uncountable.types import job_definition_t
27
16
 
28
17
  app = flask.Flask(__name__)
29
18
 
30
19
 
31
- @dataclass(kw_only=True)
32
- class WebhookResponse:
33
- pass
34
-
35
-
36
- webhook_payload_parser = CachedParser(webhook_job_t.WebhookEventBody)
37
-
38
-
39
- class WebhookException(BaseException):
40
- error_code: int
41
- message: str
42
-
43
- def __init__(self, *, error_code: int, message: str) -> None:
44
- self.error_code = error_code
45
- self.message = message
46
-
47
- @staticmethod
48
- def payload_failed_signature() -> "WebhookException":
49
- return WebhookException(
50
- error_code=401, message="webhook payload did not match signature"
51
- )
52
-
53
- @staticmethod
54
- def no_signature_passed() -> "WebhookException":
55
- return WebhookException(error_code=400, message="missing signature")
56
-
57
- @staticmethod
58
- def body_parse_error() -> "WebhookException":
59
- return WebhookException(error_code=400, message="body parse error")
60
-
61
- @staticmethod
62
- def unknown_error() -> "WebhookException":
63
- return WebhookException(error_code=500, message="internal server error")
64
-
65
- def __str__(self) -> str:
66
- return f"[{self.error_code}]: {self.message}"
67
-
68
- def make_error_response(self) -> Response:
69
- return Response(
70
- status=self.error_code, response={"error": {"message": str(self)}}
71
- )
72
-
73
-
74
- def _parse_webhook_payload(
75
- *, raw_request_body: bytes, signature_key: str, passed_signature: str
76
- ) -> base_t.JsonValue:
77
- request_body_signature = hmac.new(
78
- signature_key.encode("utf-8"), msg=raw_request_body, digestmod="sha256"
79
- ).hexdigest()
80
-
81
- if request_body_signature != passed_signature:
82
- raise WebhookException.payload_failed_signature()
83
-
84
- try:
85
- request_body = simplejson.loads(raw_request_body.decode())
86
- return typing.cast(base_t.JsonValue, request_body)
87
- except (simplejson.JSONDecodeError, ValueError) as e:
88
- raise WebhookException.body_parse_error() from e
89
-
90
-
91
20
  def register_route(
92
21
  *,
93
22
  server_logger: Logger,
94
23
  profile_meta: job_definition_t.ProfileMetadata,
95
- job: job_definition_t.WebhookJobDefinition,
24
+ job: job_definition_t.HttpJobDefinitionBase,
96
25
  ) -> None:
97
26
  route = f"/{profile_meta.name}/{job.id}"
98
27
 
99
- def handle_webhook() -> ResponseReturnValue:
28
+ def handle_request() -> ResponseReturnValue:
100
29
  with server_logger.push_scope(route):
101
30
  try:
102
- signature_key = retrieve_secret(
103
- profile_metadata=profile_meta,
104
- secret_retrieval=job.signature_key_secret,
31
+ if not isinstance(job.executor, job_definition_t.JobExecutorScript):
32
+ raise HttpException.configuration_error(
33
+ message="[internal] http job must use a script executor"
34
+ )
35
+ job_instance = resolve_script_executor(
36
+ executor=job.executor, profile_metadata=profile_meta
105
37
  )
106
-
107
- passed_signature = flask.request.headers.get(
108
- "Uncountable-Webhook-Signature"
38
+ if not isinstance(job_instance, (CustomHttpJob, WebhookJob)):
39
+ raise HttpException.configuration_error(
40
+ message="[internal] http job must descend from CustomHttpJob"
41
+ )
42
+ http_request = GenericHttpRequest(
43
+ body_base64=base64.b64encode(flask.request.get_data()).decode(),
44
+ headers=dict(flask.request.headers),
109
45
  )
110
- if passed_signature is None:
111
- raise WebhookException.no_signature_passed()
112
-
113
- webhook_payload = _parse_webhook_payload(
114
- raw_request_body=flask.request.data,
115
- signature_key=signature_key,
116
- passed_signature=passed_signature,
46
+ job_instance.validate_request(
47
+ request=http_request, job_definition=job, profile_meta=profile_meta
48
+ )
49
+ http_response = job_instance.handle_request(
50
+ request=http_request, job_definition=job, profile_meta=profile_meta
117
51
  )
118
52
 
119
- try:
120
- send_job_queue_message(
121
- job_ref_name=job.id,
122
- payload=queued_job_t.QueuedJobPayload(
123
- invocation_context=queued_job_t.InvocationContextWebhook(
124
- webhook_payload=webhook_payload
125
- )
126
- ),
127
- port=get_local_admin_server_port(),
128
- )
129
- except CommandServerException as e:
130
- raise WebhookException.unknown_error() from e
131
-
132
- return flask.jsonify(WebhookResponse())
133
- except WebhookException as e:
53
+ return flask.make_response(
54
+ http_response.response,
55
+ http_response.status_code,
56
+ http_response.headers,
57
+ )
58
+ except HttpException as e:
134
59
  server_logger.log_exception(e)
135
60
  return e.make_error_response()
136
61
  except Exception as e:
137
62
  server_logger.log_exception(e)
138
- return WebhookException.unknown_error().make_error_response()
63
+ return HttpException.unknown_error().make_error_response()
139
64
 
140
65
  app.add_url_rule(
141
66
  route,
142
- endpoint=f"handle_webhook_{job.id}",
143
- view_func=handle_webhook,
67
+ endpoint=f"handle_request_{job.id}",
68
+ view_func=handle_request,
144
69
  methods=["POST"],
145
70
  )
146
71
 
@@ -152,7 +77,7 @@ def main() -> None:
152
77
  for profile_metadata in profiles:
153
78
  server_logger = Logger(get_current_span())
154
79
  for job in profile_metadata.jobs:
155
- if isinstance(job, job_definition_t.WebhookJobDefinition):
80
+ if isinstance(job, job_definition_t.HttpJobDefinitionBase):
156
81
  register_route(
157
82
  server_logger=server_logger, profile_meta=profile_metadata, job=job
158
83
  )
@@ -164,7 +89,7 @@ main()
164
89
  if __name__ == "__main__":
165
90
  app.run(
166
91
  host="0.0.0.0",
167
- port=get_webhook_server_port(),
92
+ port=get_http_server_port(),
168
93
  debug=get_server_env() == "playground",
169
94
  exclude_patterns=[],
170
95
  )
@@ -35,6 +35,7 @@ class Arguments:
35
35
  definition_key: identifier_t.IdentifierKey
36
36
  field_values: list[field_values_t.FieldArgumentValue]
37
37
  entity_key: identifier_t.IdentifierKey | None = None
38
+ on_create_init_field_values: list[field_values_t.FieldArgumentValue] | None = None
38
39
 
39
40
 
40
41
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -21,6 +21,7 @@ __all__: list[str] = [
21
21
  "Data",
22
22
  "ENDPOINT_METHOD",
23
23
  "ENDPOINT_PATH",
24
+ "LookupEntityCompositeFieldValues",
24
25
  "LookupEntityFieldValue",
25
26
  "LookupEntityQuery",
26
27
  "LookupEntityQueryBase",
@@ -35,6 +36,7 @@ ENDPOINT_PATH = "api/external/entity/lookup_entity"
35
36
  # DO NOT MODIFY -- This file is generated by type_spec
36
37
  class LookupEntityQueryType(StrEnum):
37
38
  FIELD_VALUE = "field_value"
39
+ COMPOSITE_FIELD_VALUES = "composite_field_values"
38
40
 
39
41
 
40
42
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -69,14 +71,26 @@ class LookupEntityFieldValue:
69
71
  value: LookupFieldArgumentValue
70
72
 
71
73
 
74
+ # DO NOT MODIFY -- This file is generated by type_spec
75
+ @serial_class(
76
+ named_type_path="sdk.api.entity.lookup_entity.LookupEntityCompositeFieldValues",
77
+ parse_require={"type"},
78
+ )
79
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
80
+ class LookupEntityCompositeFieldValues:
81
+ type: typing.Literal[LookupEntityQueryType.COMPOSITE_FIELD_VALUES] = LookupEntityQueryType.COMPOSITE_FIELD_VALUES
82
+ values: list[LookupFieldArgumentValue]
83
+
84
+
72
85
  # DO NOT MODIFY -- This file is generated by type_spec
73
86
  LookupEntityQuery = typing.Annotated[
74
- typing.Union[LookupEntityFieldValue],
87
+ LookupEntityFieldValue | LookupEntityCompositeFieldValues,
75
88
  serial_union_annotation(
76
89
  named_type_path="sdk.api.entity.lookup_entity.LookupEntityQuery",
77
90
  discriminator="type",
78
91
  discriminator_map={
79
92
  "field_value": LookupEntityFieldValue,
93
+ "composite_field_values": LookupEntityCompositeFieldValues,
80
94
  },
81
95
  ),
82
96
  ]
@@ -258,10 +258,12 @@ class AsyncBatchProcessorBase(ABC):
258
258
  definition_key: identifier_t.IdentifierKey,
259
259
  field_values: list[field_values_t.FieldArgumentValue],
260
260
  entity_key: identifier_t.IdentifierKey | None = None,
261
+ on_create_init_field_values: list[field_values_t.FieldArgumentValue] | None = None,
261
262
  depends_on: list[str] | None = None,
262
263
  ) -> async_batch_t.QueuedAsyncBatchRequest:
263
264
  """Creates or updates field values for an entity
264
265
 
266
+ :param on_create_init_field_values: Field values set only when the entity is created (will be ignored if entity already exists)
265
267
  :param depends_on: A list of batch reference keys to process before processing this request
266
268
  """
267
269
  args = create_or_update_entity_t.Arguments(
@@ -269,6 +271,7 @@ class AsyncBatchProcessorBase(ABC):
269
271
  entity_type=entity_type,
270
272
  definition_key=definition_key,
271
273
  field_values=field_values,
274
+ on_create_init_field_values=on_create_init_field_values,
272
275
  )
273
276
  json_data = serialize_for_api(args)
274
277