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

@@ -6,9 +6,11 @@ from enum import StrEnum
6
6
  from urllib.parse import urljoin
7
7
 
8
8
  import requests
9
+ from requests.exceptions import JSONDecodeError
9
10
 
10
11
  from pkgs.argument_parser import CachedParser
11
12
  from pkgs.serialization_util import serialize_for_api
13
+ from pkgs.serialization_util.serialization_helpers import JsonValue
12
14
  from uncountable.types.client_base import APIRequest, ClientMethods
13
15
 
14
16
  from .file_upload import FileUpload, FileUploader, UploadedFile
@@ -46,16 +48,77 @@ class HTTPPostRequest(HTTPRequestBase):
46
48
  HTTPRequest = HTTPPostRequest | HTTPGetRequest
47
49
 
48
50
 
51
+
52
+ @dataclass(kw_only=True)
53
+ class ClientConfig():
54
+ allow_insecure_tls: bool = False
55
+
56
+
57
+ class APIResponseError(BaseException):
58
+ status_code: int
59
+ message: str
60
+ extra_details: dict[str, JsonValue] | None
61
+
62
+ def __init__(
63
+ self, status_code: int, message: str, extra_details: dict[str, JsonValue] | None
64
+ ) -> None:
65
+ super().__init__(status_code, message, extra_details)
66
+ self.status_code = status_code
67
+ self.message = message
68
+ self.extra_details = extra_details
69
+
70
+ @classmethod
71
+ def construct_error(
72
+ cls, status_code: int, extra_details: dict[str, JsonValue] | None
73
+ ) -> "APIResponseError":
74
+ message: str
75
+ match status_code:
76
+ case 403:
77
+ message = "unexpected: unauthorized"
78
+ case 410:
79
+ message = "unexpected: not found"
80
+ case 400:
81
+ message = "unexpected: bad arguments"
82
+ case 501:
83
+ message = "unexpected: unimplemented"
84
+ case 504:
85
+ message = "unexpected: timeout"
86
+ case 404:
87
+ message = "not found"
88
+ case 409:
89
+ message = "bad arguments"
90
+ case 422:
91
+ message = "unprocessable"
92
+ case _:
93
+ message = "unknown error"
94
+ return APIResponseError(
95
+ status_code=status_code, message=message, extra_details=extra_details
96
+ )
97
+
98
+
99
+ class SDKError(BaseException):
100
+ message: str
101
+
102
+ def __init__(self, message: str) -> None:
103
+ super().__init__(message)
104
+ self.message = message
105
+
106
+ def __str__(self) -> str:
107
+ return f"internal SDK error, please contact Uncountable support: {self.message}"
108
+
109
+
49
110
  class Client(ClientMethods):
50
111
  _parser_map: dict[type, CachedParser] = {}
51
112
  _auth_details: AuthDetails
52
113
  _base_url: str
53
114
  _file_uploader: FileUploader
115
+ _cfg: ClientConfig
54
116
 
55
- def __init__(self, *, base_url: str, auth_details: AuthDetails):
117
+ def __init__(self, *, base_url: str, auth_details: AuthDetails, config: ClientConfig | None = None):
56
118
  self._auth_details = auth_details
57
119
  self._base_url = base_url
58
120
  self._file_uploader = FileUploader(self._base_url, self._auth_details)
121
+ self._cfg = config or ClientConfig()
59
122
 
60
123
  def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
61
124
  http_request = self._build_http_request(api_request=api_request)
@@ -65,6 +128,7 @@ class Client(ClientMethods):
65
128
  http_request.url,
66
129
  headers=http_request.headers,
67
130
  params=http_request.query_params,
131
+ verify=not self._cfg.allow_insecure_tls
68
132
  )
69
133
  case HTTPPostRequest():
70
134
  response = requests.post(
@@ -72,19 +136,27 @@ class Client(ClientMethods):
72
136
  headers=http_request.headers,
73
137
  data=http_request.body,
74
138
  params=http_request.query_params,
139
+ verify=not self._cfg.allow_insecure_tls
75
140
  )
76
141
  case _:
77
142
  typing.assert_never(http_request)
78
143
  if response.status_code < 200 or response.status_code > 299:
79
- # TODO: handle_error
80
- pass
144
+ extra_details: dict[str, JsonValue] | None = None
145
+ try:
146
+ data = response.json()
147
+ if "error" in data:
148
+ extra_details = data["error"]
149
+ except JSONDecodeError:
150
+ pass
151
+ raise APIResponseError.construct_error(
152
+ status_code=response.status_code, extra_details=extra_details
153
+ )
81
154
  cached_parser = self._get_cached_parser(return_type)
82
155
  try:
83
156
  data = response.json()["data"]
84
157
  return cached_parser.parse_api(data)
85
- except ValueError as err:
86
- # TODO: handle parse error
87
- raise err
158
+ except ValueError | JSONDecodeError:
159
+ raise SDKError("unable to process response")
88
160
 
89
161
  def _get_cached_parser(self, data_type: type[DT]) -> CachedParser[DT]:
90
162
  if data_type not in self._parser_map:
@@ -34,7 +34,7 @@ class EntityToCreate:
34
34
  @dataclass(kw_only=True)
35
35
  class Arguments:
36
36
  definition_id: base_t.ObjectId
37
- entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
37
+ entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
38
38
  entities_to_create: list[EntityToCreate]
39
39
 
40
40
 
@@ -40,7 +40,7 @@ class EntityFieldInitialValue:
40
40
  @dataclass(kw_only=True)
41
41
  class Arguments:
42
42
  definition_id: base_t.ObjectId
43
- entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
43
+ entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
44
44
  field_values: typing.Optional[typing.Optional[list[field_values_t.FieldRefNameValue]]] = None
45
45
 
46
46
 
@@ -10,7 +10,6 @@ from decimal import Decimal # noqa: F401
10
10
  from pkgs.strenum_compat import StrEnum
11
11
  from dataclasses import dataclass
12
12
  from pkgs.serialization import serial_class
13
- from ... import base as base_t
14
13
  from ... import identifier as identifier_t
15
14
  from ... import recipe_inputs as recipe_inputs_t
16
15
  from ... import recipe_workflow_steps as recipe_workflow_steps_t
@@ -96,12 +95,12 @@ RecipeInputEdit = typing.Union[RecipeInputEditClearInputs, RecipeInputEditUpsert
96
95
  @dataclass(kw_only=True)
97
96
  class Arguments:
98
97
  recipe_key: identifier_t.IdentifierKey
99
- recipe_workflow_step_identifier: recipe_workflow_steps_t.RecipeWorkflowStepIdentifierType
98
+ recipe_workflow_step_identifier: recipe_workflow_steps_t.RecipeWorkflowStepIdentifier
100
99
  edits: list[RecipeInputEdit]
101
100
 
102
101
 
103
102
  # DO NOT MODIFY -- This file is generated by type_spec
104
103
  @dataclass(kw_only=True)
105
104
  class Data:
106
- result_id: base_t.ObjectId
105
+ pass
107
106
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -25,6 +25,7 @@ class AsyncBatchRequestPath(StrEnum):
25
25
  CREATE_RECIPE = "recipes/create_recipe"
26
26
  SET_RECIPE_METADATA = "recipes/set_recipe_metadata"
27
27
  SET_RECIPE_TAGS = "recipes/set_recipe_tags"
28
+ EDIT_RECIPE_INPUTS = "recipes/edit_recipe_inputs"
28
29
 
29
30
 
30
31
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -43,7 +43,7 @@ class AsyncBatchProcessorBase(ABC):
43
43
  identifiers: typing.Optional[recipe_identifiers_t.RecipeIdentifiers] = None,
44
44
  definition_key: typing.Optional[identifier_t.IdentifierKey] = None,
45
45
  depends_on: typing.Optional[list[str]] = None,
46
- ) -> async_batch_t.QueuedBatchRequest:
46
+ ) -> async_batch_t.QueuedAsyncBatchRequest:
47
47
  """Returns the id of the recipe being created.
48
48
 
49
49
  :param name: The name for the recipe
@@ -77,7 +77,7 @@ class AsyncBatchProcessorBase(ABC):
77
77
 
78
78
  self._enqueue(req)
79
79
 
80
- return async_batch_t.QueuedBatchRequest(
80
+ return async_batch_t.QueuedAsyncBatchRequest(
81
81
  path=req.path,
82
82
  batch_reference=req.batch_reference,
83
83
  )
@@ -86,10 +86,10 @@ class AsyncBatchProcessorBase(ABC):
86
86
  self,
87
87
  *,
88
88
  recipe_key: identifier_t.IdentifierKey,
89
- recipe_workflow_step_identifier: recipe_workflow_steps_t.RecipeWorkflowStepIdentifierType,
89
+ recipe_workflow_step_identifier: recipe_workflow_steps_t.RecipeWorkflowStepIdentifier,
90
90
  edits: list[edit_recipe_inputs_t.RecipeInputEdit],
91
91
  depends_on: typing.Optional[list[str]] = None,
92
- ) -> async_batch_t.QueuedBatchRequest:
92
+ ) -> async_batch_t.QueuedAsyncBatchRequest:
93
93
  """Clear, update, or add inputs on a recipe
94
94
 
95
95
  :param recipe_key: Identifier for the recipe
@@ -113,7 +113,7 @@ class AsyncBatchProcessorBase(ABC):
113
113
 
114
114
  self._enqueue(req)
115
115
 
116
- return async_batch_t.QueuedBatchRequest(
116
+ return async_batch_t.QueuedAsyncBatchRequest(
117
117
  path=req.path,
118
118
  batch_reference=req.batch_reference,
119
119
  )
@@ -124,7 +124,7 @@ class AsyncBatchProcessorBase(ABC):
124
124
  recipe_key: identifier_t.IdentifierKey,
125
125
  recipe_metadata: list[recipe_metadata_t.MetadataValue],
126
126
  depends_on: typing.Optional[list[str]] = None,
127
- ) -> async_batch_t.QueuedBatchRequest:
127
+ ) -> async_batch_t.QueuedAsyncBatchRequest:
128
128
  """Set metadata values on a recipe
129
129
 
130
130
  :param recipe_key: Identifier for the recipe
@@ -148,7 +148,7 @@ class AsyncBatchProcessorBase(ABC):
148
148
 
149
149
  self._enqueue(req)
150
150
 
151
- return async_batch_t.QueuedBatchRequest(
151
+ return async_batch_t.QueuedAsyncBatchRequest(
152
152
  path=req.path,
153
153
  batch_reference=req.batch_reference,
154
154
  )
@@ -21,6 +21,7 @@ import uncountable.types.api.recipes.create_recipe as create_recipe_t
21
21
  import uncountable.types.api.recipe_links.create_recipe_link as create_recipe_link_t
22
22
  import uncountable.types.api.recipes.create_recipes as create_recipes_t
23
23
  import uncountable.types.api.recipes.disassociate_recipe_as_input as disassociate_recipe_as_input_t
24
+ import uncountable.types.api.recipes.edit_recipe_inputs as edit_recipe_inputs_t
24
25
  from uncountable.types import entity as entity_t
25
26
  import uncountable.types.api.batch.execute_batch as execute_batch_t
26
27
  import uncountable.types.api.batch.execute_batch_load_async as execute_batch_load_async_t
@@ -51,6 +52,7 @@ from uncountable.types import post_base as post_base_t
51
52
  from uncountable.types import recipe_identifiers as recipe_identifiers_t
52
53
  from uncountable.types import recipe_links as recipe_links_t
53
54
  from uncountable.types import recipe_metadata as recipe_metadata_t
55
+ from uncountable.types import recipe_workflow_steps as recipe_workflow_steps_t
54
56
  import uncountable.types.api.entity.resolve_entity_ids as resolve_entity_ids_t
55
57
  import uncountable.types.api.outputs.resolve_output_conditions as resolve_output_conditions_t
56
58
  import uncountable.types.api.triggers.run_trigger as run_trigger_t
@@ -147,7 +149,7 @@ class ClientMethods(ABC):
147
149
  self,
148
150
  *,
149
151
  definition_id: base_t.ObjectId,
150
- entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
152
+ entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
151
153
  entities_to_create: list[create_entities_t.EntityToCreate],
152
154
  ) -> create_entities_t.Data:
153
155
  """Creates new Uncountable entities
@@ -172,7 +174,7 @@ class ClientMethods(ABC):
172
174
  self,
173
175
  *,
174
176
  definition_id: base_t.ObjectId,
175
- entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
177
+ entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
176
178
  field_values: typing.Optional[typing.Optional[list[field_values_t.FieldRefNameValue]]] = None,
177
179
  ) -> create_entity_t.Data:
178
180
  """Creates a new Uncountable entity
@@ -319,6 +321,29 @@ class ClientMethods(ABC):
319
321
  )
320
322
  return self.do_request(api_request=api_request, return_type=disassociate_recipe_as_input_t.Data)
321
323
 
324
+ def edit_recipe_inputs(
325
+ self,
326
+ *,
327
+ recipe_key: identifier_t.IdentifierKey,
328
+ recipe_workflow_step_identifier: recipe_workflow_steps_t.RecipeWorkflowStepIdentifier,
329
+ edits: list[edit_recipe_inputs_t.RecipeInputEdit],
330
+ ) -> edit_recipe_inputs_t.Data:
331
+ """Clear, update, or add inputs on a recipe
332
+
333
+ :param recipe_key: Identifier for the recipe
334
+ """
335
+ args = edit_recipe_inputs_t.Arguments(
336
+ recipe_key=recipe_key,
337
+ recipe_workflow_step_identifier=recipe_workflow_step_identifier,
338
+ edits=edits,
339
+ )
340
+ api_request = APIRequest(
341
+ method=edit_recipe_inputs_t.ENDPOINT_METHOD,
342
+ endpoint=edit_recipe_inputs_t.ENDPOINT_PATH,
343
+ args=args,
344
+ )
345
+ return self.do_request(api_request=api_request, return_type=edit_recipe_inputs_t.Data)
346
+
322
347
  def execute_batch(
323
348
  self,
324
349
  *,
@@ -23,7 +23,7 @@ __all__: list[str] = [
23
23
  @serial_class(
24
24
  parse_require={"type"},
25
25
  )
26
- @dataclass(kw_only=True)
26
+ @dataclass(kw_only=True, frozen=True, eq=True)
27
27
  class IdentifierKeyId:
28
28
  type: typing.Literal["id"] = "id"
29
29
  id: base_t.ObjectId
@@ -33,7 +33,7 @@ class IdentifierKeyId:
33
33
  @serial_class(
34
34
  parse_require={"type"},
35
35
  )
36
- @dataclass(kw_only=True)
36
+ @dataclass(kw_only=True, frozen=True, eq=True)
37
37
  class IdentifierKeyRefName:
38
38
  type: typing.Literal["ref_name"] = "ref_name"
39
39
  ref_name: str
@@ -43,7 +43,7 @@ class IdentifierKeyRefName:
43
43
  @serial_class(
44
44
  parse_require={"type"},
45
45
  )
46
- @dataclass(kw_only=True)
46
+ @dataclass(kw_only=True, frozen=True, eq=True)
47
47
  class IdentifierKeyBatchReference:
48
48
  type: typing.Literal["batch_reference"] = "batch_reference"
49
49
  reference: str
@@ -59,7 +59,7 @@ class RecipeWorkflowStepPosition(StrEnum):
59
59
  class RecipeWorkflowStepIdentifierWorkflowStep(RecipeWorkflowStepIdentifierBase):
60
60
  type: typing.Literal[RecipeWorkflowStepIdentifierType.WORKFLOW_STEP] = RecipeWorkflowStepIdentifierType.WORKFLOW_STEP
61
61
  workflow_step_key: identifier_t.IdentifierKey
62
- position: typing.Optional[RecipeWorkflowStepPosition] = None
62
+ position: RecipeWorkflowStepPosition = RecipeWorkflowStepPosition.FIRST
63
63
 
64
64
 
65
65
  # DO NOT MODIFY -- This file is generated by type_spec