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

docs/conf.py CHANGED
@@ -70,14 +70,33 @@ def _hook_missing_reference(
70
70
  Manually resolve reference when autoapi reference resolution fails.
71
71
  This is necessary because autoapi does not fully support type aliases.
72
72
  """
73
+ # example reftarget value: uncountable.types.identifier_t.IdentifierKey
73
74
  target = node.get("reftarget", "")
75
+
76
+ # example refdoc value: api/uncountable/types/generic_upload_t/GenericUploadStrategy
77
+ current_doc = node.get("refdoc", "")
78
+
74
79
  if not target.startswith("uncountable"):
75
80
  return None
76
- module, name = target.rsplit(".", 1)
81
+
82
+ target_module, target_name = target.rsplit(".", 1)
83
+
84
+ # construct relative path from current doc page to target page
85
+ relative_segments_to_root = [".." for _ in current_doc.split("/")]
86
+ relative_segments_to_target = target_module.split(".")
87
+
88
+ # example full relative path: ../../../../../api/uncountable/types/identifier_t/#uncountable.types.identifier_t.IdentifierKey
89
+ full_relative_path = "/".join([
90
+ *relative_segments_to_root,
91
+ autoapi_root,
92
+ *relative_segments_to_target,
93
+ f"#{target}",
94
+ ])
95
+
77
96
  return nodes.reference(
78
- text=name if python_use_unqualified_type_names else target,
97
+ text=target_name if python_use_unqualified_type_names else target,
79
98
  children=[contnode],
80
- refuri=f"/{autoapi_root}/{module.replace('.', '/')}/#{target}",
99
+ refuri=full_relative_path,
81
100
  )
82
101
 
83
102
 
docs/index.md CHANGED
@@ -112,5 +112,5 @@ def fetch_all_projects(client: Client) -> list[IdName]:
112
112
  Overview <self>
113
113
  Available SDK Methods <api/uncountable/core/client/Client>
114
114
  integration_examples/index
115
- SDK Reference <autoapi/uncountable/index>
115
+ SDK Reference <api/uncountable/index>
116
116
  ```
docs/requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  furo==2025.7.19
2
- myst-parser==4.0.0
2
+ myst-parser==4.0.1
3
3
  sphinx-autoapi==3.6.0
4
4
  sphinx-copybutton==0.5.2
5
5
  Sphinx==8.2.0
@@ -9,7 +9,7 @@ dependencies = [
9
9
  "ruff == 0.*",
10
10
  "openpyxl == 3.*",
11
11
  "more_itertools == 10.*",
12
- "types-paramiko ==3.5.0.20250708",
12
+ "types-paramiko ==3.5.0.20250801",
13
13
  "types-openpyxl == 3.*",
14
14
  "types-pysftp == 0.*",
15
15
  "types-pytz ==2025.*",
@@ -1,5 +1,6 @@
1
1
  import base64
2
2
  import functools
3
+ import json
3
4
  from dataclasses import dataclass
4
5
 
5
6
  from flask.wrappers import Response
@@ -42,7 +43,8 @@ class HttpException(Exception):
42
43
 
43
44
  def make_error_response(self) -> Response:
44
45
  return Response(
45
- status=self.error_code, response={"error": {"message": str(self)}}
46
+ status=self.error_code,
47
+ response=json.dumps({"error": {"message": str(self)}}),
46
48
  )
47
49
 
48
50
 
@@ -5,6 +5,7 @@ import sys
5
5
  import time
6
6
  from dataclasses import dataclass
7
7
  from datetime import UTC
8
+ from enum import StrEnum
8
9
 
9
10
  from opentelemetry.trace import get_current_span
10
11
 
@@ -19,11 +20,19 @@ from uncountable.integration.telemetry import Logger
19
20
 
20
21
  SHUTDOWN_TIMEOUT_SECS = 30
21
22
 
23
+ AnyProcess = multiprocessing.Process | subprocess.Popen[bytes]
24
+
25
+
26
+ class ProcessName(StrEnum):
27
+ QUEUE_RUNNER = "queue_runner"
28
+ CRON_SERVER = "cron_server"
29
+ UWSGI = "uwsgi"
30
+
22
31
 
23
32
  @dataclass(kw_only=True)
24
33
  class ProcessInfo:
25
- name: str
26
- process: multiprocessing.Process | subprocess.Popen[bytes]
34
+ name: ProcessName
35
+ process: AnyProcess
27
36
 
28
37
  @property
29
38
  def is_alive(self) -> bool:
@@ -46,14 +55,14 @@ class ProcessInfo:
46
55
  return self.process.poll()
47
56
 
48
57
 
49
- def handle_shutdown(logger: Logger, processes: list[ProcessInfo]) -> None:
58
+ def handle_shutdown(logger: Logger, processes: dict[ProcessName, ProcessInfo]) -> None:
50
59
  logger.log_info("received shutdown command, shutting down sub-processes")
51
- for proc_info in processes:
60
+ for proc_info in processes.values():
52
61
  if proc_info.is_alive:
53
62
  proc_info.process.terminate()
54
63
 
55
64
  shutdown_start = time.time()
56
- still_living_processes = processes
65
+ still_living_processes = list(processes.values())
57
66
  while (
58
67
  time.time() - shutdown_start < SHUTDOWN_TIMEOUT_SECS
59
68
  and len(still_living_processes) > 0
@@ -82,14 +91,50 @@ def handle_shutdown(logger: Logger, processes: list[ProcessInfo]) -> None:
82
91
  proc_info.process.kill()
83
92
 
84
93
 
85
- def check_process_alive(logger: Logger, processes: list[ProcessInfo]) -> None:
86
- for proc_info in processes:
94
+ def restart_process(
95
+ logger: Logger, proc_info: ProcessInfo, processes: dict[ProcessName, ProcessInfo]
96
+ ) -> None:
97
+ logger.log_error(
98
+ f"process {proc_info.name} shut down unexpectedly - exit code {proc_info.exitcode}. Restarting..."
99
+ )
100
+
101
+ match proc_info.name:
102
+ case ProcessName.QUEUE_RUNNER:
103
+ queue_proc = multiprocessing.Process(target=start_queue_runner)
104
+ queue_proc.start()
105
+ new_info = ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=queue_proc)
106
+ processes[ProcessName.QUEUE_RUNNER] = new_info
107
+ try:
108
+ _wait_queue_runner_online()
109
+ logger.log_info("queue runner restarted successfully")
110
+ except Exception as e:
111
+ logger.log_exception(e)
112
+ logger.log_error(
113
+ "queue runner failed to restart, shutting down scheduler"
114
+ )
115
+ handle_shutdown(logger, processes)
116
+ sys.exit(1)
117
+
118
+ case ProcessName.CRON_SERVER:
119
+ cron_proc = multiprocessing.Process(target=cron_target)
120
+ cron_proc.start()
121
+ new_info = ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_proc)
122
+ processes[ProcessName.CRON_SERVER] = new_info
123
+ logger.log_info("cron server restarted successfully")
124
+
125
+ case ProcessName.UWSGI:
126
+ uwsgi_proc: AnyProcess = subprocess.Popen(["uwsgi", "--die-on-term"])
127
+ new_info = ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_proc)
128
+ processes[ProcessName.UWSGI] = new_info
129
+ logger.log_info("uwsgi restarted successfully")
130
+
131
+
132
+ def check_process_alive(
133
+ logger: Logger, processes: dict[ProcessName, ProcessInfo]
134
+ ) -> None:
135
+ for proc_info in processes.values():
87
136
  if not proc_info.is_alive:
88
- logger.log_error(
89
- f"process {proc_info.name} shut down unexpectedly! shutting down scheduler; exit code is {proc_info.exitcode}"
90
- )
91
- handle_shutdown(logger, processes)
92
- sys.exit(1)
137
+ restart_process(logger, proc_info, processes)
93
138
 
94
139
 
95
140
  def _wait_queue_runner_online() -> None:
@@ -113,17 +158,17 @@ def _wait_queue_runner_online() -> None:
113
158
 
114
159
  def main() -> None:
115
160
  logger = Logger(get_current_span())
116
- processes: list[ProcessInfo] = []
161
+ processes: dict[ProcessName, ProcessInfo] = {}
117
162
 
118
163
  multiprocessing.set_start_method("forkserver")
119
164
 
120
165
  def add_process(process: ProcessInfo) -> None:
121
- processes.append(process)
166
+ processes[process.name] = process
122
167
  logger.log_info(f"started process {process.name}")
123
168
 
124
169
  runner_process = multiprocessing.Process(target=start_queue_runner)
125
170
  runner_process.start()
126
- add_process(ProcessInfo(name="queue runner", process=runner_process))
171
+ add_process(ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=runner_process))
127
172
 
128
173
  try:
129
174
  _wait_queue_runner_online()
@@ -134,13 +179,13 @@ def main() -> None:
134
179
 
135
180
  cron_process = multiprocessing.Process(target=cron_target)
136
181
  cron_process.start()
137
- add_process(ProcessInfo(name="cron server", process=cron_process))
182
+ add_process(ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_process))
138
183
 
139
184
  uwsgi_process = subprocess.Popen([
140
185
  "uwsgi",
141
186
  "--die-on-term",
142
187
  ])
143
- add_process(ProcessInfo(name="uwsgi", process=uwsgi_process))
188
+ add_process(ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_process))
144
189
 
145
190
  try:
146
191
  while True:
@@ -34,7 +34,8 @@ def _cast_attributes(attributes: dict[str, base_t.JsonValue]) -> Attributes:
34
34
 
35
35
 
36
36
  def one_line_formatter(record: LogRecord) -> str:
37
- return json.dumps(record.to_json(), separators=(",", ":"))
37
+ json_data = record.to_json()
38
+ return json.dumps(json.loads(json_data), separators=(",", ":")) + "\n"
38
39
 
39
40
 
40
41
  @functools.cache
@@ -50,6 +50,7 @@ from .api.inputs import get_input_names as get_input_names_t
50
50
  from .api.inputs import get_inputs_data as get_inputs_data_t
51
51
  from .api.outputs import get_output_data as get_output_data_t
52
52
  from .api.outputs import get_output_names as get_output_names_t
53
+ from .api.outputs import get_output_organization as get_output_organization_t
53
54
  from .api.project import get_projects as get_projects_t
54
55
  from .api.project import get_projects_data as get_projects_data_t
55
56
  from .api.recipes import get_recipe_calculations as get_recipe_calculations_t
@@ -74,12 +75,14 @@ from .api.entity import lock_entity as lock_entity_t
74
75
  from .api.recipes import lock_recipes as lock_recipes_t
75
76
  from .api.entity import lookup_entity as lookup_entity_t
76
77
  from .api.id_source import match_id_source as match_id_source_t
78
+ from . import notifications_t as notifications_t
77
79
  from . import outputs_t as outputs_t
78
80
  from . import overrides_t as overrides_t
79
81
  from . import permissions_t as permissions_t
80
82
  from . import phases_t as phases_t
81
83
  from . import post_base_t as post_base_t
82
84
  from .api.integrations import publish_realtime_data as publish_realtime_data_t
85
+ from .api.integrations import push_notification as push_notification_t
83
86
  from . import queued_job_t as queued_job_t
84
87
  from . import recipe_identifiers_t as recipe_identifiers_t
85
88
  from . import recipe_inputs_t as recipe_inputs_t
@@ -172,6 +175,7 @@ __all__: list[str] = [
172
175
  "get_inputs_data_t",
173
176
  "get_output_data_t",
174
177
  "get_output_names_t",
178
+ "get_output_organization_t",
175
179
  "get_projects_t",
176
180
  "get_projects_data_t",
177
181
  "get_recipe_calculations_t",
@@ -196,12 +200,14 @@ __all__: list[str] = [
196
200
  "lock_recipes_t",
197
201
  "lookup_entity_t",
198
202
  "match_id_source_t",
203
+ "notifications_t",
199
204
  "outputs_t",
200
205
  "overrides_t",
201
206
  "permissions_t",
202
207
  "phases_t",
203
208
  "post_base_t",
204
209
  "publish_realtime_data_t",
210
+ "push_notification_t",
205
211
  "queued_job_t",
206
212
  "recipe_identifiers_t",
207
213
  "recipe_inputs_t",
@@ -0,0 +1,47 @@
1
+ # DO NOT MODIFY -- This file is generated by type_spec
2
+ # ruff: noqa: E402 Q003
3
+ # fmt: off
4
+ # isort: skip_file
5
+ from __future__ import annotations
6
+ import typing # noqa: F401
7
+ import datetime # noqa: F401
8
+ from decimal import Decimal # noqa: F401
9
+ import dataclasses
10
+ from pkgs.serialization import serial_class
11
+ from ... import async_batch_t
12
+ from ... import base_t
13
+ from ... import entity_t
14
+ from ... import notifications_t
15
+
16
+ __all__: list[str] = [
17
+ "Arguments",
18
+ "Data",
19
+ "ENDPOINT_METHOD",
20
+ "ENDPOINT_PATH",
21
+ ]
22
+
23
+ ENDPOINT_METHOD = "POST"
24
+ ENDPOINT_PATH = "api/external/integrations/push_notification"
25
+
26
+
27
+ # DO NOT MODIFY -- This file is generated by type_spec
28
+ @serial_class(
29
+ named_type_path="sdk.api.integrations.push_notification.Arguments",
30
+ )
31
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
32
+ class Arguments:
33
+ notification_targets: list[notifications_t.NotificationTarget]
34
+ subject: str
35
+ message: str
36
+ display_notice: bool = False
37
+ entity: entity_t.EntityIdentifier | None = None
38
+
39
+
40
+ # DO NOT MODIFY -- This file is generated by type_spec
41
+ @serial_class(
42
+ named_type_path="sdk.api.integrations.push_notification.Data",
43
+ )
44
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
45
+ class Data(async_batch_t.AsyncBatchActionReturn):
46
+ pass
47
+ # DO NOT MODIFY -- This file is generated by type_spec
@@ -0,0 +1,173 @@
1
+ # DO NOT MODIFY -- This file is generated by type_spec
2
+ # ruff: noqa: E402 Q003
3
+ # fmt: off
4
+ # isort: skip_file
5
+ from __future__ import annotations
6
+ import typing # noqa: F401
7
+ import datetime # noqa: F401
8
+ from decimal import Decimal # noqa: F401
9
+ from enum import StrEnum
10
+ import dataclasses
11
+ from pkgs.serialization import serial_class
12
+ from pkgs.serialization import serial_union_annotation
13
+ from pkgs.serialization import serial_alias_annotation
14
+ from ... import base_t
15
+
16
+ __all__: list[str] = [
17
+ "Arguments",
18
+ "Data",
19
+ "ENDPOINT_METHOD",
20
+ "ENDPOINT_PATH",
21
+ "OrganizationParameter",
22
+ "OrganizationParameterBase",
23
+ "OrganizationParameterCategory",
24
+ "OrganizationParameterConditionParameter",
25
+ "OrganizationParameterRecipeInput",
26
+ "OrganizationParameterType",
27
+ "OutputOrganizationRequest",
28
+ "OutputOrganizationRequestMaterialFamily",
29
+ "OutputOrganizationRequestProject",
30
+ "OutputOrganizationRequestScope",
31
+ "OutputOrganizationRequestUser",
32
+ ]
33
+
34
+ ENDPOINT_METHOD = "GET"
35
+ ENDPOINT_PATH = "api/external/outputs/get_output_organization"
36
+
37
+
38
+ # DO NOT MODIFY -- This file is generated by type_spec
39
+ class OutputOrganizationRequestScope(StrEnum):
40
+ MATERIAL_FAMILY = "material_family"
41
+ PROJECT = "project"
42
+ USER = "user"
43
+
44
+
45
+ # DO NOT MODIFY -- This file is generated by type_spec
46
+ @serial_class(
47
+ named_type_path="sdk.api.outputs.get_output_organization.OutputOrganizationRequestMaterialFamily",
48
+ parse_require={"scope"},
49
+ )
50
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
51
+ class OutputOrganizationRequestMaterialFamily:
52
+ scope: typing.Literal[OutputOrganizationRequestScope.MATERIAL_FAMILY] = OutputOrganizationRequestScope.MATERIAL_FAMILY
53
+ material_family_id: base_t.ObjectId
54
+
55
+
56
+ # DO NOT MODIFY -- This file is generated by type_spec
57
+ @serial_class(
58
+ named_type_path="sdk.api.outputs.get_output_organization.OutputOrganizationRequestProject",
59
+ parse_require={"scope"},
60
+ )
61
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
62
+ class OutputOrganizationRequestProject:
63
+ scope: typing.Literal[OutputOrganizationRequestScope.PROJECT] = OutputOrganizationRequestScope.PROJECT
64
+ project_id: base_t.ObjectId
65
+
66
+
67
+ # DO NOT MODIFY -- This file is generated by type_spec
68
+ @serial_class(
69
+ named_type_path="sdk.api.outputs.get_output_organization.OutputOrganizationRequestUser",
70
+ parse_require={"scope"},
71
+ )
72
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
73
+ class OutputOrganizationRequestUser:
74
+ scope: typing.Literal[OutputOrganizationRequestScope.USER] = OutputOrganizationRequestScope.USER
75
+ material_family_id: base_t.ObjectId
76
+ user_id: base_t.ObjectId
77
+ project_id: base_t.ObjectId | None = None
78
+
79
+
80
+ # DO NOT MODIFY -- This file is generated by type_spec
81
+ OutputOrganizationRequest = typing.Annotated[
82
+ OutputOrganizationRequestMaterialFamily | OutputOrganizationRequestProject | OutputOrganizationRequestUser,
83
+ serial_alias_annotation(
84
+ named_type_path="sdk.api.outputs.get_output_organization.OutputOrganizationRequest",
85
+ ),
86
+ ]
87
+
88
+
89
+ # DO NOT MODIFY -- This file is generated by type_spec
90
+ @serial_class(
91
+ named_type_path="sdk.api.outputs.get_output_organization.Arguments",
92
+ )
93
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
94
+ class Arguments:
95
+ request: OutputOrganizationRequest
96
+
97
+
98
+ # DO NOT MODIFY -- This file is generated by type_spec
99
+ class OrganizationParameterType(StrEnum):
100
+ CATEGORY = "category"
101
+ RECIPE_INPUT = "recipe_input"
102
+ CONDITION_PARAMETER = "condition_parameter"
103
+
104
+
105
+ # DO NOT MODIFY -- This file is generated by type_spec
106
+ @serial_class(
107
+ named_type_path="sdk.api.outputs.get_output_organization.OrganizationParameterBase",
108
+ )
109
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
110
+ class OrganizationParameterBase:
111
+ output_organization_parameter_id: base_t.ObjectId
112
+ null_on_top: bool
113
+ sort_asc: bool
114
+ type: OrganizationParameterType
115
+
116
+
117
+ # DO NOT MODIFY -- This file is generated by type_spec
118
+ @serial_class(
119
+ named_type_path="sdk.api.outputs.get_output_organization.OrganizationParameterCategory",
120
+ parse_require={"type"},
121
+ )
122
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
123
+ class OrganizationParameterCategory(OrganizationParameterBase):
124
+ type: typing.Literal[OrganizationParameterType.CATEGORY] = OrganizationParameterType.CATEGORY
125
+
126
+
127
+ # DO NOT MODIFY -- This file is generated by type_spec
128
+ @serial_class(
129
+ named_type_path="sdk.api.outputs.get_output_organization.OrganizationParameterRecipeInput",
130
+ parse_require={"type"},
131
+ )
132
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
133
+ class OrganizationParameterRecipeInput(OrganizationParameterBase):
134
+ input_id: base_t.ObjectId
135
+ type: typing.Literal[OrganizationParameterType.RECIPE_INPUT] = OrganizationParameterType.RECIPE_INPUT
136
+
137
+
138
+ # DO NOT MODIFY -- This file is generated by type_spec
139
+ @serial_class(
140
+ named_type_path="sdk.api.outputs.get_output_organization.OrganizationParameterConditionParameter",
141
+ parse_require={"type"},
142
+ )
143
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
144
+ class OrganizationParameterConditionParameter(OrganizationParameterBase):
145
+ condition_parameter_id: base_t.ObjectId
146
+ type: typing.Literal[OrganizationParameterType.CONDITION_PARAMETER] = OrganizationParameterType.CONDITION_PARAMETER
147
+
148
+
149
+ # DO NOT MODIFY -- This file is generated by type_spec
150
+ OrganizationParameter = typing.Annotated[
151
+ OrganizationParameterCategory | OrganizationParameterRecipeInput | OrganizationParameterConditionParameter,
152
+ serial_union_annotation(
153
+ named_type_path="sdk.api.outputs.get_output_organization.OrganizationParameter",
154
+ discriminator="type",
155
+ discriminator_map={
156
+ "category": OrganizationParameterCategory,
157
+ "recipe_input": OrganizationParameterRecipeInput,
158
+ "condition_parameter": OrganizationParameterConditionParameter,
159
+ },
160
+ ),
161
+ ]
162
+
163
+
164
+ # DO NOT MODIFY -- This file is generated by type_spec
165
+ @serial_class(
166
+ named_type_path="sdk.api.outputs.get_output_organization.Data",
167
+ )
168
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
169
+ class Data:
170
+ organization_id: base_t.ObjectId
171
+ column_definitions: list[OrganizationParameter]
172
+ table_definitions: list[OrganizationParameter]
173
+ # DO NOT MODIFY -- This file is generated by type_spec
@@ -25,6 +25,8 @@ import uncountable.types.api.entity.grant_entity_permissions as grant_entity_per
25
25
  from uncountable.types import identifier_t
26
26
  import uncountable.types.api.uploader.invoke_uploader as invoke_uploader_t
27
27
  import uncountable.types.api.entity.lookup_entity as lookup_entity_t
28
+ from uncountable.types import notifications_t
29
+ import uncountable.types.api.integrations.push_notification as push_notification_t
28
30
  from uncountable.types import recipe_identifiers_t
29
31
  from uncountable.types import recipe_metadata_t
30
32
  from uncountable.types import recipe_workflow_steps_t
@@ -492,6 +494,45 @@ class AsyncBatchProcessorBase(ABC):
492
494
  batch_reference=req.batch_reference,
493
495
  )
494
496
 
497
+ def push_notification(
498
+ self,
499
+ *,
500
+ notification_targets: list[notifications_t.NotificationTarget],
501
+ subject: str,
502
+ message: str,
503
+ display_notice: bool = False,
504
+ entity: entity_t.EntityIdentifier | None = None,
505
+ depends_on: list[str] | None = None,
506
+ ) -> async_batch_t.QueuedAsyncBatchRequest:
507
+ """Push a notification to a user or user group
508
+
509
+ :param depends_on: A list of batch reference keys to process before processing this request
510
+ """
511
+ args = push_notification_t.Arguments(
512
+ notification_targets=notification_targets,
513
+ subject=subject,
514
+ message=message,
515
+ entity=entity,
516
+ display_notice=display_notice,
517
+ )
518
+ json_data = serialize_for_api(args)
519
+
520
+ batch_reference = str(uuid.uuid4())
521
+
522
+ req = async_batch_t.AsyncBatchRequest(
523
+ path=async_batch_t.AsyncBatchRequestPath.PUSH_NOTIFICATION,
524
+ data=json_data,
525
+ depends_on=depends_on,
526
+ batch_reference=batch_reference,
527
+ )
528
+
529
+ self._enqueue(req)
530
+
531
+ return async_batch_t.QueuedAsyncBatchRequest(
532
+ path=req.path,
533
+ batch_reference=req.batch_reference,
534
+ )
535
+
495
536
  def set_entity_field_values(
496
537
  self,
497
538
  *,
@@ -45,6 +45,7 @@ class AsyncBatchRequestPath(StrEnum):
45
45
  UPSERT_CONDITION_MATCH = "condition_parameters/upsert_condition_match"
46
46
  COMPLETE_ASYNC_UPLOAD = "runsheet/complete_async_upload"
47
47
  CREATE_MIX_ORDER = "recipes/create_mix_order"
48
+ PUSH_NOTIFICATION = "integrations/push_notification"
48
49
 
49
50
 
50
51
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -64,8 +64,10 @@ import uncountable.types.api.entity.lock_entity as lock_entity_t
64
64
  import uncountable.types.api.recipes.lock_recipes as lock_recipes_t
65
65
  import uncountable.types.api.entity.lookup_entity as lookup_entity_t
66
66
  import uncountable.types.api.id_source.match_id_source as match_id_source_t
67
+ from uncountable.types import notifications_t
67
68
  from uncountable.types import permissions_t
68
69
  from uncountable.types import post_base_t
70
+ import uncountable.types.api.integrations.push_notification as push_notification_t
69
71
  from uncountable.types import recipe_identifiers_t
70
72
  from uncountable.types import recipe_links_t
71
73
  from uncountable.types import recipe_metadata_t
@@ -1258,6 +1260,32 @@ class ClientMethods(ABC):
1258
1260
  )
1259
1261
  return self.do_request(api_request=api_request, return_type=match_id_source_t.Data)
1260
1262
 
1263
+ def push_notification(
1264
+ self,
1265
+ *,
1266
+ notification_targets: list[notifications_t.NotificationTarget],
1267
+ subject: str,
1268
+ message: str,
1269
+ display_notice: bool = False,
1270
+ entity: entity_t.EntityIdentifier | None = None,
1271
+ ) -> push_notification_t.Data:
1272
+ """Push a notification to a user or user group
1273
+
1274
+ """
1275
+ args = push_notification_t.Arguments(
1276
+ notification_targets=notification_targets,
1277
+ subject=subject,
1278
+ message=message,
1279
+ entity=entity,
1280
+ display_notice=display_notice,
1281
+ )
1282
+ api_request = APIRequest(
1283
+ method=push_notification_t.ENDPOINT_METHOD,
1284
+ endpoint=push_notification_t.ENDPOINT_PATH,
1285
+ args=args,
1286
+ )
1287
+ return self.do_request(api_request=api_request, return_type=push_notification_t.Data)
1288
+
1261
1289
  def remove_recipe_from_project(
1262
1290
  self,
1263
1291
  *,
@@ -0,0 +1,11 @@
1
+ # ruff: noqa: E402 Q003
2
+ # fmt: off
3
+ # isort: skip_file
4
+ # DO NOT MODIFY -- This file is generated by type_spec
5
+ # Kept only for SDK backwards compatibility
6
+ from .notifications_t import NotificationTargetType as NotificationTargetType
7
+ from .notifications_t import NotificationTargetBase as NotificationTargetBase
8
+ from .notifications_t import NotificationTargetUser as NotificationTargetUser
9
+ from .notifications_t import NotificationTargetUserGroup as NotificationTargetUserGroup
10
+ from .notifications_t import NotificationTarget as NotificationTarget
11
+ # DO NOT MODIFY -- This file is generated by type_spec
@@ -0,0 +1,74 @@
1
+ # DO NOT MODIFY -- This file is generated by type_spec
2
+ # ruff: noqa: E402 Q003
3
+ # fmt: off
4
+ # isort: skip_file
5
+ from __future__ import annotations
6
+ import typing # noqa: F401
7
+ import datetime # noqa: F401
8
+ from decimal import Decimal # noqa: F401
9
+ from enum import StrEnum
10
+ import dataclasses
11
+ from pkgs.serialization import serial_class
12
+ from pkgs.serialization import serial_union_annotation
13
+ from . import base_t
14
+ from . import identifier_t
15
+
16
+ __all__: list[str] = [
17
+ "NotificationTarget",
18
+ "NotificationTargetBase",
19
+ "NotificationTargetType",
20
+ "NotificationTargetUser",
21
+ "NotificationTargetUserGroup",
22
+ ]
23
+
24
+
25
+ # DO NOT MODIFY -- This file is generated by type_spec
26
+ class NotificationTargetType(StrEnum):
27
+ USER = "user"
28
+ USER_GROUP = "user_group"
29
+
30
+
31
+ # DO NOT MODIFY -- This file is generated by type_spec
32
+ @serial_class(
33
+ named_type_path="sdk.notifications.NotificationTargetBase",
34
+ )
35
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
36
+ class NotificationTargetBase:
37
+ type: NotificationTargetType
38
+
39
+
40
+ # DO NOT MODIFY -- This file is generated by type_spec
41
+ @serial_class(
42
+ named_type_path="sdk.notifications.NotificationTargetUser",
43
+ parse_require={"type"},
44
+ )
45
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
46
+ class NotificationTargetUser(NotificationTargetBase):
47
+ type: typing.Literal[NotificationTargetType.USER] = NotificationTargetType.USER
48
+ user_key: identifier_t.IdentifierKey
49
+
50
+
51
+ # DO NOT MODIFY -- This file is generated by type_spec
52
+ @serial_class(
53
+ named_type_path="sdk.notifications.NotificationTargetUserGroup",
54
+ parse_require={"type"},
55
+ )
56
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
57
+ class NotificationTargetUserGroup(NotificationTargetBase):
58
+ type: typing.Literal[NotificationTargetType.USER_GROUP] = NotificationTargetType.USER_GROUP
59
+ user_group_key: identifier_t.IdentifierKey
60
+
61
+
62
+ # DO NOT MODIFY -- This file is generated by type_spec
63
+ NotificationTarget = typing.Annotated[
64
+ NotificationTargetUser | NotificationTargetUserGroup,
65
+ serial_union_annotation(
66
+ named_type_path="sdk.notifications.NotificationTarget",
67
+ discriminator="type",
68
+ discriminator_map={
69
+ "user": NotificationTargetUser,
70
+ "user_group": NotificationTargetUserGroup,
71
+ },
72
+ ),
73
+ ]
74
+ # DO NOT MODIFY -- This file is generated by type_spec
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UncountablePythonSDK
3
- Version: 0.0.123
3
+ Version: 0.0.125
4
4
  Summary: Uncountable SDK
5
5
  Project-URL: Homepage, https://github.com/uncountableinc/uncountable-python-sdk
6
6
  Project-URL: Repository, https://github.com/uncountableinc/uncountable-python-sdk.git
@@ -1,8 +1,8 @@
1
1
  docs/.gitignore,sha256=_ebkZUcwfvfnGEJ95rfj1lxoBNd6EE9ZvtOc7FsbfFE,7
2
- docs/conf.py,sha256=bNk-lXzCTltXz_ZyQmBWXjHa_wL_-YuvPb7yHTuCbZ8,2858
3
- docs/index.md,sha256=YHwBQmoVjIfJ5nXi4-JSAGkzy7IHDD_DugV-u9s7IrQ,3197
2
+ docs/conf.py,sha256=Ky-_Y76T7pwN2aBG-dSF79Av70e7ASgcOXEdQ1qyor4,3542
3
+ docs/index.md,sha256=g4Yi5831fEkywYkkcFohYLkKzSI91SOZF7DxKsm9zgI,3193
4
4
  docs/justfile,sha256=WymCEQ6W2A8Ak79iUPmecmuaUNN2htb7STUrz5K7ELE,273
5
- docs/requirements.txt,sha256=Q5qvOf7nQa19R4kCWb_1DBhwW-Vtm3SAtZTPDR_aF9c,171
5
+ docs/requirements.txt,sha256=AAVxGQUFUCjyRe2gQSCD8Nezn42K_TVeEfrpIKJwmD0,171
6
6
  docs/integration_examples/create_ingredient.md,sha256=bzTQ943YhINxa3HQylEA26rbAsjr6HvvN_HkVkrzUeA,1547
7
7
  docs/integration_examples/create_output.md,sha256=aDn2TjzKgY-HnxnvgsZS578cvajmHpF1y2HKkHfdtd4,2104
8
8
  docs/integration_examples/index.md,sha256=lVP6k79rGgdWPfEKM8oJvxeJsBKlpRJaZfrqn9lkiBc,73
@@ -27,7 +27,7 @@ examples/oauth.py,sha256=QUmv4c27UDs3q98yigyA_Sm3hdK5qNfnDvxh7k06ZYg,213
27
27
  examples/set_recipe_metadata_file.py,sha256=cRVXGz4UN4aqnNrNSzyBmikYHpe63lMIuzOpMwD9EDU,1036
28
28
  examples/set_recipe_output_file_sdk.py,sha256=Lz1amqppnWTX83z-C090wCJ4hcKmCD3kb-4v0uBRi0Y,782
29
29
  examples/upload_files.py,sha256=qMaSvMSdTMPOOP55y1AwEurc0SOdZAMvEydlqJPsGpg,432
30
- examples/integration-server/pyproject.toml,sha256=-ZZ1R3B-Pf-F6gQX0-Me6u3G9cVW2B2_eechemCe7_4,9149
30
+ examples/integration-server/pyproject.toml,sha256=ASgrDz9wnJRSI-M_zmfGDBAPXwTgQcCHGjmhDp6CY4E,9149
31
31
  examples/integration-server/jobs/materials_auto/concurrent_cron.py,sha256=xsK3H9ZEaniedC2nJUB0rqOcFI8y-ojfl_nLSJb9AMM,312
32
32
  examples/integration-server/jobs/materials_auto/example_cron.py,sha256=spUMiiTEFaepbVXecjD_4aEEfqEtZGGZuWTKs9J6Xcw,736
33
33
  examples/integration-server/jobs/materials_auto/example_http.py,sha256=eIL46ElWo8SKY7W5JWWkwZk6Qo7KRd9EJBxfy7YQ_sE,1429
@@ -111,9 +111,9 @@ uncountable/integration/cron.py,sha256=6eH-kIs3sdYPCyb62_L2M7U_uQTdMTdwY5hreEJb0
111
111
  uncountable/integration/entrypoint.py,sha256=BHOYPQgKvZE6HG8Rv15MkdYl8lRkvfDgv1OdLo0oQ9Q,433
112
112
  uncountable/integration/job.py,sha256=HFYA3YxqwyCvQLqXpMnKxp2IJUjFgjMsWVz_DTb_5eo,8229
113
113
  uncountable/integration/scan_profiles.py,sha256=RHBmPc5E10YZzf4cmglwrn2yAy0jHBhQ-P_GlAk2TeU,2919
114
- uncountable/integration/scheduler.py,sha256=KK-1XCr8Rxi8puaynb3H0BySvsDBJJaPcGumy49ZMB8,4864
114
+ uncountable/integration/scheduler.py,sha256=vJQGpuMgryFp5aCQEg5BIvywnu4t3SSFcGoLfY10LKg,6610
115
115
  uncountable/integration/server.py,sha256=lL9zmzqkQRf7V1fBT20SvIy-7ryz5hFf7DF4QX4pj1E,4699
116
- uncountable/integration/telemetry.py,sha256=usB82d1hVCoGaBUv2xM4grQN5WqHwVCASdgNJ1RoaPc,7588
116
+ uncountable/integration/telemetry.py,sha256=5nA_DwqLOBxDdV4cl8Ybo6I6GiJDTdqETlP5Y81SCWg,7633
117
117
  uncountable/integration/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
118
118
  uncountable/integration/db/connect.py,sha256=mE3bdV0huclH2iT_dXCQdRL4LkjIuf_myAR64RTWXEs,498
119
119
  uncountable/integration/db/session.py,sha256=96cGQXpe6IugBTdSsjdP0S5yhJ6toSmbVB6qhc3FJzE,693
@@ -122,7 +122,7 @@ uncountable/integration/executors/executors.py,sha256=Kzisp1eKufGCWrHIw4mmAj-l1U
122
122
  uncountable/integration/executors/generic_upload_executor.py,sha256=z0HfvuBR1wUbRpMVxJQ5Jlzbdk8G7YmAGENmze85Tr8,12076
123
123
  uncountable/integration/executors/script_executor.py,sha256=BBQ9f0l7uH2hgKf60jtm-pONzwk-EeOhM2qBAbv_URo,846
124
124
  uncountable/integration/http_server/__init__.py,sha256=WY2HMcL0UCAGYv8y6Pz-j0azbDGXwubFF21EH_zNPkc,189
125
- uncountable/integration/http_server/types.py,sha256=zVXXN8FPstrF9qFduwQBtxPG8I4AOK41nXAnxrtSgxw,1832
125
+ uncountable/integration/http_server/types.py,sha256=3JJSulRfv784SbXnXo1Pywto7RwGxgS-iJ2-a6TOnDI,1869
126
126
  uncountable/integration/queue_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
127
  uncountable/integration/queue_runner/job_scheduler.py,sha256=Roh7-mTj6rwMzFhBXv7hASNS2dMeTcAZEynJGVjkhEs,6080
128
128
  uncountable/integration/queue_runner/queue_runner.py,sha256=N4sUXmlGzVquybiJ7NQZavCJOBGrxBj6k7mb-TITaN0,1139
@@ -145,10 +145,10 @@ uncountable/integration/queue_runner/datastore/model.py,sha256=8-RI5A2yPZVGBLWIN
145
145
  uncountable/integration/secret_retrieval/__init__.py,sha256=3QXVj35w8rRMxVvmmsViFYDi3lcb3g70incfalOEm6o,87
146
146
  uncountable/integration/secret_retrieval/retrieve_secret.py,sha256=LBEf18KHtXZxg-ZZ80stJ1vW39AWf0CQllP6pNu3Eq8,2994
147
147
  uncountable/integration/webhook_server/entrypoint.py,sha256=NQawXl_JCRojdVniS5RF7dobQQKW_Wy03bwy-uXknuA,3441
148
- uncountable/types/__init__.py,sha256=bkleojXUnG9CvMaJCGiqsVk3eVTM0clOtCLvjXUx3L4,10415
148
+ uncountable/types/__init__.py,sha256=rTLpqWlx5eqZ_O1hqnBp7rHWyGII3qyscNYortb6EgU,10696
149
149
  uncountable/types/async_batch.py,sha256=yCCWrrLQfxXVqZp-KskxLBNkNmuELdz4PJjx8ULppgs,662
150
- uncountable/types/async_batch_processor.py,sha256=h_8Snzt3lbEFlZAZFByt4Hg4dv2YlxMijHjTHjZ0aXY,22062
151
- uncountable/types/async_batch_t.py,sha256=JuswurXlYW38MfAXJ0UWb7hE2rmzFaHBAsNhRYAyMD4,3779
150
+ uncountable/types/async_batch_processor.py,sha256=hvTGI4NNBlHF1q6ec_DuZhLSvX8F32L9lFztNLNjWHw,23451
151
+ uncountable/types/async_batch_t.py,sha256=mAQ2AXao_v76e7hZGzUCoSroq2Kf2FVz6hmmMX0icHo,3836
152
152
  uncountable/types/async_jobs.py,sha256=JI0ScfawaqMRbJ2jbgW3YQLhijPnBeYdMnZJjygSxHg,322
153
153
  uncountable/types/async_jobs_t.py,sha256=u4xd3i512PZ-9592Q2ZgWh_faMiI4UMm0F_gOmZnerI,1389
154
154
  uncountable/types/auth_retrieval.py,sha256=770zjN1K9EF5zs1Xml7x6ke6Hkze7rcMT5FdDVCIl9M,549
@@ -159,7 +159,7 @@ uncountable/types/calculations.py,sha256=fApOFpgBemt_t7IVneVR0VdI3X5EOxiG6Xhzr6R
159
159
  uncountable/types/calculations_t.py,sha256=pl-lhjyDQuj11Sf9g1-0BsSkN7Ez8UxDp8-KMQ_3enM,709
160
160
  uncountable/types/chemical_structure.py,sha256=ujyragaD26-QG5jgKnWhO7TN3N1V9b_04T2WhqNYxxo,281
161
161
  uncountable/types/chemical_structure_t.py,sha256=VFFyits_vx4t5L2euu_qFiSpsGJjURkDPr3ISnr3nPc,855
162
- uncountable/types/client_base.py,sha256=Letfk7hK3OBX7vjqONbPytlQwGLTGsDijrLt0O4OPWk,76997
162
+ uncountable/types/client_base.py,sha256=HK2EHdHiNRbtHj4BqTGo4-hxsFepJ45UuY2RYwuhAJw,78005
163
163
  uncountable/types/client_config.py,sha256=qLpHt4O_B098CyN6qQajoxZ2zjZ1DILXLUEGyyGP0TQ,280
164
164
  uncountable/types/client_config_t.py,sha256=yTFIYAitMrcc4oV9J-HADODS_Hwi45z-piz7rr7QT04,781
165
165
  uncountable/types/curves.py,sha256=QyEyC20jsG-LGKVx6miiF-w70vKMwNkILFBDIJ5Ok9g,345
@@ -194,6 +194,8 @@ uncountable/types/integrations.py,sha256=0fOhtbLIOl9w1GP9J3PTagRU8mjOKV48JNLLH3S
194
194
  uncountable/types/integrations_t.py,sha256=ihyhuMDKtJarQ19OppS0fYpJUYd8o5-w6YCDE440O-w,1871
195
195
  uncountable/types/job_definition.py,sha256=hYp5jPYLLYm3NKEqzQrQfXL0Ms5KgEQGTON13YWSPYk,1804
196
196
  uncountable/types/job_definition_t.py,sha256=E4IQvcYF3VDHbwRlvopy8y-HNAyEMZpwy7jkmp74fgQ,9563
197
+ uncountable/types/notifications.py,sha256=ZGr1ULMG3cPMED83NbMjrjmgVzCeOTS1Tc-pFTNuY4Y,600
198
+ uncountable/types/notifications_t.py,sha256=qS2mhCkYHFPe2XtBespABJ3dNvisxrmIw_r8ZlUCh_g,2444
197
199
  uncountable/types/outputs.py,sha256=I6zP2WHXg_jXgMqmuEJuJOlsjKjQGHjfs1JOwW9YxBM,260
198
200
  uncountable/types/outputs_t.py,sha256=atsOkBBgnMeCgPaKPidk9eNouWVnynSrMI_ZbqxRJeY,795
199
201
  uncountable/types/overrides.py,sha256=fOvj8P9K9ul8fnTwA--l140EWHuc1BFq8tXgtBkYld4,410
@@ -281,11 +283,13 @@ uncountable/types/api/inputs/set_input_subcategories.py,sha256=w5U6eXes5KquPW1Uc
281
283
  uncountable/types/api/inputs/set_intermediate_type.py,sha256=S1RLI2RtrRze0NdMUfK2nwR4Twn_DnLnWNsg0-ivi_A,1431
282
284
  uncountable/types/api/integrations/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
283
285
  uncountable/types/api/integrations/publish_realtime_data.py,sha256=-5r2U78AwKUCpModcUIchVIZ9b7L-Ln6O6T-9d57M2A,1181
286
+ uncountable/types/api/integrations/push_notification.py,sha256=_ycqsGSd7pdt480JWCwMf-SoL_XIwWzEF-lyWoPmqIY,1404
284
287
  uncountable/types/api/material_families/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
285
288
  uncountable/types/api/material_families/update_entity_material_families.py,sha256=qWJgAKH0MayadXvxckePCdo9yd34QXOmGZ7cKz5VLNo,1761
286
289
  uncountable/types/api/outputs/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
287
290
  uncountable/types/api/outputs/get_output_data.py,sha256=luGoQZzbZsGIzo2dXMD5f6rDlXEgBjnnUU9n5T-VL9Q,3069
288
291
  uncountable/types/api/outputs/get_output_names.py,sha256=myxLS1YedzWlKs3st64jmM9XMUphrUltxKISBz4pVSo,1539
292
+ uncountable/types/api/outputs/get_output_organization.py,sha256=uxfpuyu7wCYSSAt4pn98voyBfmP4sjZjvL0I65eZH_s,6602
289
293
  uncountable/types/api/outputs/resolve_output_conditions.py,sha256=X8qHd_xZUxIlmfPyLyaBbVjdH_dIN4tj7xVuFFvaQsw,2578
290
294
  uncountable/types/api/permissions/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
291
295
  uncountable/types/api/permissions/set_core_permissions.py,sha256=RtI5l9iyR80mkh9PzpCvn02xfCKsuvHYYCXDr48FT_Q,3651
@@ -334,7 +338,7 @@ uncountable/types/api/uploader/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr
334
338
  uncountable/types/api/uploader/invoke_uploader.py,sha256=Bj7Dq4A90k00suacwk3bLA_dCb2aovS1kAbVam2AQnM,1395
335
339
  uncountable/types/api/user/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
336
340
  uncountable/types/api/user/get_current_user_info.py,sha256=Avqi_RXtRgbefrT_dwJ9MrO6eDNSSa_Nu650FSuESlg,1109
337
- uncountablepythonsdk-0.0.123.dist-info/METADATA,sha256=c7NEs2mALBXTpQlBv3lF7A8gkSNJ8VlC0EO2rGz-RyI,2174
338
- uncountablepythonsdk-0.0.123.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
339
- uncountablepythonsdk-0.0.123.dist-info/top_level.txt,sha256=1UVGjAU-6hJY9qw2iJ7nCBeEwZ793AEN5ZfKX9A1uj4,31
340
- uncountablepythonsdk-0.0.123.dist-info/RECORD,,
341
+ uncountablepythonsdk-0.0.125.dist-info/METADATA,sha256=5frkDRF0Omv5XGxz5ALGwDKLJut56mm9Ts35JTE4tFA,2174
342
+ uncountablepythonsdk-0.0.125.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
343
+ uncountablepythonsdk-0.0.125.dist-info/top_level.txt,sha256=1UVGjAU-6hJY9qw2iJ7nCBeEwZ793AEN5ZfKX9A1uj4,31
344
+ uncountablepythonsdk-0.0.125.dist-info/RECORD,,