retab 0.0.42__py3-none-any.whl → 0.0.44__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. retab/__init__.py +2 -1
  2. retab/client.py +26 -51
  3. retab/generate_types.py +180 -0
  4. retab/resources/consensus/client.py +1 -1
  5. retab/resources/consensus/responses.py +1 -1
  6. retab/resources/deployments/__init__.py +3 -0
  7. retab/resources/deployments/automations/__init__.py +9 -0
  8. retab/resources/deployments/automations/client.py +244 -0
  9. retab/resources/deployments/automations/endpoints.py +290 -0
  10. retab/resources/deployments/automations/links.py +303 -0
  11. retab/resources/deployments/automations/logs.py +222 -0
  12. retab/resources/deployments/automations/mailboxes.py +423 -0
  13. retab/resources/deployments/automations/outlook.py +377 -0
  14. retab/resources/deployments/automations/tests.py +161 -0
  15. retab/resources/deployments/client.py +148 -0
  16. retab/resources/documents/client.py +94 -68
  17. retab/resources/documents/extractions.py +55 -46
  18. retab/resources/evaluations/__init__.py +2 -2
  19. retab/resources/evaluations/client.py +61 -77
  20. retab/resources/evaluations/documents.py +48 -37
  21. retab/resources/evaluations/iterations.py +58 -40
  22. retab/resources/jsonlUtils.py +3 -4
  23. retab/resources/processors/automations/endpoints.py +49 -39
  24. retab/resources/processors/automations/links.py +52 -43
  25. retab/resources/processors/automations/mailboxes.py +74 -59
  26. retab/resources/processors/automations/outlook.py +104 -82
  27. retab/resources/processors/client.py +35 -30
  28. retab/resources/projects/__init__.py +3 -0
  29. retab/resources/projects/client.py +285 -0
  30. retab/resources/projects/documents.py +244 -0
  31. retab/resources/projects/iterations.py +470 -0
  32. retab/resources/usage.py +2 -0
  33. retab/types/ai_models.py +2 -1
  34. retab/types/deprecated_evals.py +195 -0
  35. retab/types/evaluations/__init__.py +5 -2
  36. retab/types/evaluations/iterations.py +9 -43
  37. retab/types/evaluations/model.py +19 -24
  38. retab/types/extractions.py +1 -0
  39. retab/types/jobs/base.py +1 -1
  40. retab/types/jobs/evaluation.py +1 -1
  41. retab/types/logs.py +5 -6
  42. retab/types/mime.py +1 -10
  43. retab/types/projects/__init__.py +34 -0
  44. retab/types/projects/documents.py +30 -0
  45. retab/types/projects/iterations.py +78 -0
  46. retab/types/projects/model.py +68 -0
  47. retab/types/schemas/enhance.py +22 -5
  48. retab/types/schemas/evaluate.py +2 -2
  49. retab/types/schemas/object.py +27 -25
  50. retab/types/standards.py +2 -2
  51. retab/utils/__init__.py +3 -0
  52. retab/utils/ai_models.py +127 -12
  53. retab/utils/hashing.py +24 -0
  54. retab/utils/json_schema.py +1 -26
  55. retab/utils/mime.py +0 -17
  56. retab/utils/usage/usage.py +0 -1
  57. {retab-0.0.42.dist-info → retab-0.0.44.dist-info}/METADATA +4 -6
  58. {retab-0.0.42.dist-info → retab-0.0.44.dist-info}/RECORD +60 -55
  59. retab/_utils/__init__.py +0 -0
  60. retab/_utils/_model_cards/anthropic.yaml +0 -59
  61. retab/_utils/_model_cards/auto.yaml +0 -43
  62. retab/_utils/_model_cards/gemini.yaml +0 -117
  63. retab/_utils/_model_cards/openai.yaml +0 -301
  64. retab/_utils/_model_cards/xai.yaml +0 -28
  65. retab/_utils/ai_models.py +0 -138
  66. retab/_utils/benchmarking.py +0 -484
  67. retab/_utils/chat.py +0 -327
  68. retab/_utils/display.py +0 -440
  69. retab/_utils/json_schema.py +0 -2156
  70. retab/_utils/mime.py +0 -165
  71. retab/_utils/responses.py +0 -169
  72. retab/_utils/stream_context_managers.py +0 -52
  73. retab/_utils/usage/__init__.py +0 -0
  74. retab/_utils/usage/usage.py +0 -301
  75. {retab-0.0.42.dist-info → retab-0.0.44.dist-info}/WHEEL +0 -0
  76. {retab-0.0.42.dist-info → retab-0.0.44.dist-info}/top_level.txt +0 -0
retab/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from .client import AsyncRetab, Retab
2
2
  from .types.schemas.object import Schema
3
+ from . import utils
3
4
 
4
- __all__ = ["Retab", "AsyncRetab", "Schema"]
5
+ __all__ = ["Retab", "AsyncRetab", "Schema", "utils"]
retab/client.py CHANGED
@@ -7,10 +7,9 @@ import backoff
7
7
  import backoff.types
8
8
  import httpx
9
9
  import truststore
10
- from pydantic_core import PydanticUndefined
11
10
 
12
- from .resources import consensus, documents, evals, files, finetuning, models, processors, schemas, secrets, usage, evaluations
13
- from .types.standards import PreparedRequest
11
+ from .resources import consensus, deployments, documents, files, finetuning, models, processors, schemas, secrets, usage, projects
12
+ from .types.standards import PreparedRequest, FieldUnset
14
13
 
15
14
 
16
15
  class MaxRetriesExceeded(Exception):
@@ -43,20 +42,15 @@ class BaseRetab:
43
42
  ValueError: If no API key is provided through arguments or environment variables
44
43
  """
45
44
 
46
- # claude_api_key (str, optional): Claude API key. Will look for CLAUDE_API_KEY env variable if not provided
47
- # xai_api_key (str, optional): XAI API key. Will look for XAI_API_KEY env variable if not provided
48
- # gemini_api_key (str, optional): Gemini API key. Will look for GEMINI_API_KEY env variable if not provided
49
-
50
45
  def __init__(
51
46
  self,
52
47
  api_key: Optional[str] = None,
53
48
  base_url: Optional[str] = None,
54
49
  timeout: float = 240.0,
55
50
  max_retries: int = 3,
56
- openai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
57
- gemini_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
58
- # claude_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
59
- xai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
51
+ openai_api_key: Optional[str] = FieldUnset,
52
+ gemini_api_key: Optional[str] = FieldUnset,
53
+ xai_api_key: Optional[str] = FieldUnset,
60
54
  ) -> None:
61
55
  if api_key is None:
62
56
  api_key = os.environ.get("RETAB_API_KEY")
@@ -80,30 +74,21 @@ class BaseRetab:
80
74
  "Content-Type": "application/json",
81
75
  }
82
76
 
83
- # Only check environment variables if the value is PydanticUndefined
84
- if openai_api_key is PydanticUndefined:
77
+ # Only check environment variables if the value is FieldUnset
78
+ if openai_api_key is FieldUnset:
85
79
  openai_api_key = os.environ.get("OPENAI_API_KEY")
86
80
 
87
- # if claude_api_key is PydanticUndefined:
88
- # claude_api_key = os.environ.get("CLAUDE_API_KEY")
89
-
90
- # if xai_api_key is PydanticUndefined:
91
- # xai_api_key = os.environ.get("XAI_API_KEY")
92
-
93
- if gemini_api_key is PydanticUndefined:
81
+ if gemini_api_key is FieldUnset:
94
82
  gemini_api_key = os.environ.get("GEMINI_API_KEY")
95
83
 
96
- # Only add headers if the values are actual strings (not None or PydanticUndefined)
97
- if openai_api_key and openai_api_key is not PydanticUndefined:
84
+ # Only add headers if the values are actual strings (not None or FieldUnset)
85
+ if openai_api_key and openai_api_key is not FieldUnset:
98
86
  self.headers["OpenAI-Api-Key"] = openai_api_key
99
87
 
100
- # if claude_api_key and claude_api_key is not PydanticUndefined:
101
- # self.headers["Anthropic-Api-Key"] = claude_api_key
102
-
103
- if xai_api_key and xai_api_key is not PydanticUndefined:
88
+ if xai_api_key and xai_api_key is not FieldUnset:
104
89
  self.headers["XAI-Api-Key"] = xai_api_key
105
90
 
106
- if gemini_api_key and gemini_api_key is not PydanticUndefined:
91
+ if gemini_api_key and gemini_api_key is not FieldUnset:
107
92
  self.headers["Gemini-Api-Key"] = gemini_api_key
108
93
 
109
94
  def _prepare_url(self, endpoint: str) -> str:
@@ -150,7 +135,7 @@ class Retab(BaseRetab):
150
135
  """Synchronous client for interacting with the Retab API.
151
136
 
152
137
  This client provides synchronous access to all Retab API resources including files, fine-tuning,
153
- prompt optimization, documents, models, datasets, and schemas.
138
+ prompt optimization, documents, models, processors, deployments, and schemas.
154
139
 
155
140
  Args:
156
141
  api_key (str, optional): Retab API key. If not provided, will look for RETAB_API_KEY env variable.
@@ -158,8 +143,6 @@ class Retab(BaseRetab):
158
143
  timeout (float): Request timeout in seconds. Defaults to 240.0
159
144
  max_retries (int): Maximum number of retries for failed requests. Defaults to 3
160
145
  openai_api_key (str, optional): OpenAI API key. Will look for OPENAI_API_KEY env variable if not provided
161
- claude_api_key (str, optional): Claude API key. Will look for CLAUDE_API_KEY env variable if not provided
162
- xai_api_key (str, optional): XAI API key. Will look for XAI_API_KEY env variable if not provided
163
146
  gemini_api_key (str, optional): Gemini API key. Will look for GEMINI_API_KEY env variable if not provided
164
147
 
165
148
  Attributes:
@@ -168,7 +151,8 @@ class Retab(BaseRetab):
168
151
  prompt_optimization: Access to prompt optimization operations
169
152
  documents: Access to document operations
170
153
  models: Access to model operations
171
- datasets: Access to dataset operations
154
+ processors: Access to processor operations
155
+ deployments: Access to deployment operations
172
156
  schemas: Access to schema operations
173
157
  responses: Access to responses API (OpenAI Responses API compatible interface)
174
158
  """
@@ -179,10 +163,8 @@ class Retab(BaseRetab):
179
163
  base_url: Optional[str] = None,
180
164
  timeout: float = 240.0,
181
165
  max_retries: int = 3,
182
- openai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
183
- gemini_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
184
- # claude_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
185
- # xai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
166
+ openai_api_key: Optional[str] = FieldUnset,
167
+ gemini_api_key: Optional[str] = FieldUnset,
186
168
  ) -> None:
187
169
  super().__init__(
188
170
  api_key=api_key,
@@ -191,20 +173,17 @@ class Retab(BaseRetab):
191
173
  max_retries=max_retries,
192
174
  openai_api_key=openai_api_key,
193
175
  gemini_api_key=gemini_api_key,
194
- # claude_api_key=claude_api_key,
195
- # xai_api_key=xai_api_key,
196
176
  )
197
177
 
198
178
  self.client = httpx.Client(timeout=self.timeout)
199
- self.evals = evals.Evals(client=self)
200
- self.evaluations = evaluations.Evaluations(client=self)
179
+ self.projects = projects.Projects(client=self)
201
180
  self.files = files.Files(client=self)
202
181
  self.fine_tuning = finetuning.FineTuning(client=self)
203
- # self.prompt_optimization = prompt_optimization.PromptOptimization(client=self)
204
182
  self.documents = documents.Documents(client=self)
205
183
  self.models = models.Models(client=self)
206
184
  self.schemas = schemas.Schemas(client=self)
207
185
  self.processors = processors.Processors(client=self)
186
+ self.deployments = deployments.Deployments(client=self)
208
187
  self.secrets = secrets.Secrets(client=self)
209
188
  self.usage = usage.Usage(client=self)
210
189
  self.consensus = consensus.Consensus(client=self)
@@ -418,7 +397,7 @@ class AsyncRetab(BaseRetab):
418
397
  """Asynchronous client for interacting with the Retab API.
419
398
 
420
399
  This client provides asynchronous access to all Retab API resources including files, fine-tuning,
421
- prompt optimization, documents, models, datasets, and schemas.
400
+ prompt optimization, documents, models, processors, deployments, and schemas.
422
401
 
423
402
  Args:
424
403
  api_key (str, optional): Retab API key. If not provided, will look for RETAB_API_KEY env variable.
@@ -436,7 +415,8 @@ class AsyncRetab(BaseRetab):
436
415
  prompt_optimization: Access to asynchronous prompt optimization operations
437
416
  documents: Access to asynchronous document operations
438
417
  models: Access to asynchronous model operations
439
- datasets: Access to asynchronous dataset operations
418
+ processors: Access to asynchronous processor operations
419
+ deployments: Access to asynchronous deployment operations
440
420
  schemas: Access to asynchronous schema operations
441
421
  responses: Access to responses API (OpenAI Responses API compatible interface)
442
422
  """
@@ -447,10 +427,8 @@ class AsyncRetab(BaseRetab):
447
427
  base_url: Optional[str] = None,
448
428
  timeout: float = 240.0,
449
429
  max_retries: int = 3,
450
- openai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
451
- gemini_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
452
- # claude_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
453
- # xai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
430
+ openai_api_key: Optional[str] = FieldUnset,
431
+ gemini_api_key: Optional[str] = FieldUnset,
454
432
  ) -> None:
455
433
  super().__init__(
456
434
  api_key=api_key,
@@ -459,21 +437,18 @@ class AsyncRetab(BaseRetab):
459
437
  max_retries=max_retries,
460
438
  openai_api_key=openai_api_key,
461
439
  gemini_api_key=gemini_api_key,
462
- # claude_api_key=claude_api_key,
463
- # xai_api_key=xai_api_key,
464
440
  )
465
441
 
466
442
  self.client = httpx.AsyncClient(timeout=self.timeout)
467
443
 
468
- self.evals = evals.AsyncEvals(client=self)
469
- self.evaluations = evaluations.AsyncEvaluations(client=self)
444
+ self.projects = projects.AsyncProjects(client=self)
470
445
  self.files = files.AsyncFiles(client=self)
471
446
  self.fine_tuning = finetuning.AsyncFineTuning(client=self)
472
- # self.prompt_optimization = prompt_optimization.AsyncPromptOptimization(client=self)
473
447
  self.documents = documents.AsyncDocuments(client=self)
474
448
  self.models = models.AsyncModels(client=self)
475
449
  self.schemas = schemas.AsyncSchemas(client=self)
476
450
  self.processors = processors.AsyncProcessors(client=self)
451
+ self.deployments = deployments.AsyncDeployments(client=self)
477
452
  self.secrets = secrets.AsyncSecrets(client=self)
478
453
  self.usage = usage.AsyncUsage(client=self)
479
454
  self.consensus = consensus.AsyncConsensus(client=self)
@@ -0,0 +1,180 @@
1
+ import collections.abc
2
+ import json
3
+ import os
4
+ import types
5
+ import typing
6
+ import enum
7
+ import sys
8
+ import inspect
9
+ from datetime import datetime, date
10
+ from typing import Any, Type, get_args, get_origin, Union, Literal, is_typeddict
11
+ from typing_extensions import is_typeddict as is_typeddict_ext
12
+ import typing_extensions
13
+ from pydantic_core import PydanticUndefined
14
+ from pydantic import BaseModel, EmailStr
15
+ import PIL.Image
16
+
17
+ to_compile: list[tuple[str, Type, bool]] = []
18
+
19
+ def is_base_model(field_type: Type) -> bool:
20
+ return getattr(field_type, "__name__", None) in ["BaseModel", "GenericModel", "ConfigDict", "Generic"]
21
+
22
+ def type_to_zod(field_type: Any, put_names: bool = True, ts: bool = False) -> str:
23
+ origin = get_origin(field_type) or field_type
24
+ optional = False
25
+
26
+ def make_union(args):
27
+ return args[0] if len(args) <= 1 else "z.union([" + ", ".join(args) + "])"
28
+
29
+ def make_ts_union(args):
30
+ return args[0] if len(args) <= 1 else " | ".join(args)
31
+
32
+ if isinstance(field_type, typing.ForwardRef):
33
+ return type_to_zod(typing._eval_type(field_type, globals(), locals(), []), ts=ts)
34
+ elif origin is typing.Annotated or origin is typing.Required or origin is typing_extensions.Required:
35
+ return type_to_zod(get_args(field_type)[0], put_names, ts=ts)
36
+ if origin is Union or origin is types.UnionType:
37
+ args = [x for x in get_args(field_type)]
38
+ if types.NoneType in args:
39
+ args.remove(types.NoneType)
40
+ optional = True
41
+ typename = make_union([type_to_zod(x) for x in args])
42
+ ts_typename = make_ts_union([type_to_zod(x, ts=True) for x in args])
43
+ elif issubclass(origin, BaseModel) or is_typeddict(origin) or is_typeddict_ext(origin):
44
+ if put_names:
45
+ typename = "Z" + origin.__name__
46
+ ts_typename = origin.__name__
47
+ to_compile.append((origin.__name__, field_type, True))
48
+ else:
49
+ excluded_fields = set()
50
+ typename = "z.object({\n"
51
+ ts_typename = "{\n"
52
+ props = [(n, f.annotation, f.default) for n, f in origin.model_fields.items()] if issubclass(origin, BaseModel) else \
53
+ [(n, f, PydanticUndefined) for n, f in origin.__annotations__.items()]
54
+
55
+ for field_name, field, default in props:
56
+ if field_name in excluded_fields:
57
+ continue
58
+ ts_compiled = type_to_zod(field, ts=True)
59
+ default_str = ""
60
+ if default is not PydanticUndefined and default is not None:
61
+ if isinstance(default, BaseModel):
62
+ default_str = f".default({json.dumps(default.model_dump(mode="json", exclude_unset=True))})"
63
+ else:
64
+ default_str = f".default({json.dumps(default)})"
65
+ typename += f" {field_name}: {type_to_zod(field)}{default_str},\n"
66
+ ts_typename += f" {field_name}{"?" if ts_compiled.endswith(" | undefined") or default is not PydanticUndefined else ""}: {ts_compiled},\n"
67
+ typename += "})"
68
+ ts_typename += "}"
69
+
70
+ based = origin.__bases__
71
+ for i in range(0, len(based)):
72
+ if is_base_model(based[i]) or based[i] is dict:
73
+ break
74
+ if issubclass(based[i], BaseModel):
75
+ excluded_fields.update(based[i].model_fields.keys())
76
+ typename += ".merge(Z" + based[i].__name__ + ".schema)"
77
+ ts_typename += " & " + based[i].__name__
78
+ elif origin is list or origin is typing.List or origin is collections.abc.Sequence or origin is collections.abc.Iterable:
79
+ typename = "z.array(" + type_to_zod(get_args(field_type)[0]) + ")"
80
+ ts_typename = "Array<" + type_to_zod(get_args(field_type)[0], ts=True) + ">"
81
+ elif origin is tuple:
82
+ args = get_args(field_type)
83
+ typename = "z.tuple([" + ", ".join([type_to_zod(x) for x in args]) + "])"
84
+ ts_typename = "[" + ", ".join([type_to_zod(x, ts=True) for x in args]) + "]"
85
+ elif origin is dict:
86
+ if len(get_args(field_type)) == 2:
87
+ typename = "z.record(" + type_to_zod(get_args(field_type)[0]) + ", " + type_to_zod(get_args(field_type)[1]) + ")"
88
+ ts_typename = "{[key: " + type_to_zod(get_args(field_type)[0], ts=True) + "]: " + type_to_zod(get_args(field_type)[1], ts=True) + "}"
89
+ else:
90
+ typename = "z.record(z.any())"
91
+ ts_typename = "{[key: string]: any}"
92
+ elif origin is Literal:
93
+ typename = make_union(["z.literal(" + json.dumps(x) + ")" for x in get_args(field_type)])
94
+ ts_typename = make_ts_union([json.dumps(x) for x in get_args(field_type)])
95
+ elif isinstance(field_type, typing.TypeVar):
96
+ typename = "z.any()"
97
+ ts_typename = "any"
98
+ elif isinstance(field_type, type) and issubclass(field_type, enum.Enum):
99
+ typename = "z.any()"
100
+ ts_typename = "any"
101
+ elif field_type is str or field_type is date or field_type is datetime:
102
+ typename = "z.string()"
103
+ ts_typename = "string"
104
+ elif field_type is int or field_type is float:
105
+ typename = "z.number()"
106
+ ts_typename = "number"
107
+ elif field_type is bool:
108
+ typename = "z.boolean()"
109
+ ts_typename = "boolean"
110
+ elif field_type is typing.Any:
111
+ typename = "z.any()"
112
+ ts_typename = "any"
113
+ elif field_type is bytes or field_type is PIL.Image.Image or field_type is typing.BinaryIO or origin is typing.IO or origin is typing_extensions.IO:
114
+ typename = "z.instanceof(Uint8Array)"
115
+ ts_typename = "Uint8Array"
116
+ elif field_type is EmailStr:
117
+ typename = "z.string().email()"
118
+ ts_typename = "string"
119
+ elif field_type is os.PathLike:
120
+ typename = "z.string()"
121
+ ts_typename = "string"
122
+ elif field_type is object:
123
+ typename = "z.object({}).passthrough()"
124
+ ts_typename = "object"
125
+ else:
126
+ raise ValueError(f"Unsupported type: {field_type} ({origin})")
127
+ if ts:
128
+ return ts_typename if not optional else ts_typename + " | null | undefined"
129
+ else:
130
+ return typename if not optional else typename + ".nullable().optional()"
131
+
132
+
133
+ # SET of names of python builtin types starting with a capital
134
+ builtin_types = {
135
+ "Any",
136
+ "BaseModel",
137
+ "NoneType",
138
+ "Literal",
139
+ "Union",
140
+ "List",
141
+ "Sequence",
142
+ "ConfigDict",
143
+ "Optional",
144
+ }
145
+
146
+ if __name__ == "__main__":
147
+ modules = []
148
+ for root, dirs, files in os.walk("retab/types"):
149
+ for module in files:
150
+ if module[-3:] != '.py':
151
+ continue
152
+ full_name = os.path.join(root, module[:-3]).replace(os.path.sep, '.')
153
+ __import__(full_name, locals(), globals())
154
+ modules.append(full_name)
155
+
156
+
157
+ for module_name in modules:
158
+ for name, obj in inspect.getmembers(sys.modules[module_name]):
159
+ if name[0] != name[0].lower() and name not in builtin_types:
160
+ to_compile.append((name, obj, False))
161
+
162
+ print("import * as z from 'zod';\n")
163
+
164
+ defined = {}
165
+ while len(to_compile) > 0:
166
+ name, model, necessary = to_compile.pop(0)
167
+ if name in defined: continue
168
+ defined[name] = True
169
+ try:
170
+ compiled = type_to_zod(model, False)
171
+ compiled_ts = type_to_zod(model, False, ts=True)
172
+ except Exception as e:
173
+ if not necessary:
174
+ print(f"Skipping {name} {model} due to error: {e}", file=sys.stderr)
175
+ continue
176
+ print(f"Error compiling {name} {model}", file=sys.stderr)
177
+ raise e
178
+ print("export const Z" + name + " = z.lazy(() => " + compiled + ");")
179
+ print("export type " + name + " = z.infer<typeof Z" + name + ">;\n")
180
+
@@ -21,7 +21,7 @@ class BaseConsensusMixin:
21
21
  mode=mode,
22
22
  )
23
23
 
24
- return PreparedRequest(method="POST", url="/v1/consensus/reconcile", data=request.model_dump(), idempotency_key=idempotency_key)
24
+ return PreparedRequest(method="POST", url="/v1/consensus/reconcile", data=request.model_dump(mode="json", exclude_unset=True), idempotency_key=idempotency_key)
25
25
 
26
26
 
27
27
  class Consensus(SyncAPIResource, BaseConsensusMixin):
@@ -55,7 +55,7 @@ class BaseResponsesMixin:
55
55
  instructions=instructions,
56
56
  )
57
57
 
58
- return PreparedRequest(method="POST", url="/v1/responses", data=request.model_dump(), idempotency_key=idempotency_key)
58
+ return PreparedRequest(method="POST", url="/v1/responses", data=request.model_dump(mode="json", exclude_unset=True), idempotency_key=idempotency_key)
59
59
 
60
60
  def prepare_parse(
61
61
  self,
@@ -0,0 +1,3 @@
1
+ from .client import AsyncDeployments, Deployments
2
+
3
+ __all__ = ["Deployments", "AsyncDeployments"]
@@ -0,0 +1,9 @@
1
+ from .client import (
2
+ AsyncAutomations,
3
+ Automations,
4
+ )
5
+
6
+ __all__ = [
7
+ "Automations",
8
+ "AsyncAutomations",
9
+ ]
@@ -0,0 +1,244 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ from typing import Any, Literal, Optional, Union
5
+
6
+ from ...._resource import AsyncAPIResource, SyncAPIResource
7
+ from ....types.automations.endpoints import Endpoint, UpdateEndpointRequest
8
+ from ....types.automations.links import Link, UpdateLinkRequest
9
+ from ....types.automations.mailboxes import Mailbox, UpdateMailboxRequest
10
+ from ....types.automations.outlook import Outlook, UpdateOutlookRequest
11
+ from ....types.standards import PreparedRequest
12
+ from .endpoints import AsyncEndpoints, Endpoints
13
+ from .links import AsyncLinks, Links
14
+ from .logs import AsyncLogs, Logs
15
+ from .mailboxes import AsyncMailboxes, Mailboxes
16
+ from .outlook import AsyncOutlooks, Outlooks
17
+ from .tests import AsyncTests, Tests
18
+
19
+
20
+ class SignatureVerificationError(Exception):
21
+ """Raised when webhook signature verification fails."""
22
+
23
+ pass
24
+
25
+
26
+ class AutomationsMixin:
27
+ def _verify_event(self, event_body: bytes, event_signature: str, secret: str) -> Any:
28
+ """
29
+ Verify the signature of a webhook event.
30
+
31
+ Args:
32
+ body: The raw request body
33
+ signature: The signature header
34
+ secret: The secret key used for signing
35
+
36
+ Returns:
37
+ The parsed event payload
38
+
39
+ Raises:
40
+ SignatureVerificationError: If the signature verification fails
41
+ """
42
+ expected_signature = hmac.new(secret.encode(), event_body, hashlib.sha256).hexdigest()
43
+
44
+ if not hmac.compare_digest(event_signature, expected_signature):
45
+ raise SignatureVerificationError("Invalid signature")
46
+
47
+ return json.loads(event_body.decode("utf-8"))
48
+
49
+ def prepare_list(
50
+ self,
51
+ processor_id: str,
52
+ before: Optional[str] = None,
53
+ after: Optional[str] = None,
54
+ limit: Optional[int] = 10,
55
+ order: Optional[Literal["asc", "desc"]] = "desc",
56
+ automation_id: Optional[str] = None,
57
+ webhook_url: Optional[str] = None,
58
+ name: Optional[str] = None,
59
+ ) -> PreparedRequest:
60
+ params = {
61
+ "before": before,
62
+ "after": after,
63
+ "limit": limit,
64
+ "order": order,
65
+ "id": automation_id,
66
+ "webhook_url": webhook_url,
67
+ "name": name,
68
+ }
69
+ params = {k: v for k, v in params.items() if v is not None}
70
+ return PreparedRequest(method="GET", url=f"/v1/processors/{processor_id}/automations", params=params)
71
+
72
+ def prepare_get(self, processor_id: str, automation_id: str) -> PreparedRequest:
73
+ return PreparedRequest(method="GET", url=f"/v1/processors/{processor_id}/automations/{automation_id}")
74
+
75
+ def prepare_update(
76
+ self,
77
+ processor_id: str,
78
+ automation_id: str,
79
+ automation_data: Union[UpdateLinkRequest, UpdateMailboxRequest, UpdateEndpointRequest, UpdateOutlookRequest],
80
+ ) -> PreparedRequest:
81
+ return PreparedRequest(method="PUT", url=f"/v1/processors/{processor_id}/automations/{automation_id}", data=automation_data.model_dump(mode="json"))
82
+
83
+ def prepare_delete(self, processor_id: str, automation_id: str) -> PreparedRequest:
84
+ return PreparedRequest(method="DELETE", url=f"/v1/processors/{processor_id}/automations/{automation_id}")
85
+
86
+
87
+ class Automations(SyncAPIResource, AutomationsMixin):
88
+ """Automations API wrapper"""
89
+
90
+ def __init__(self, client: Any) -> None:
91
+ super().__init__(client=client)
92
+ self.mailboxes = Mailboxes(client=client)
93
+ self.links = Links(client=client)
94
+ self.outlook = Outlooks(client=client)
95
+ self.endpoints = Endpoints(client=client)
96
+ self.tests = Tests(client=client)
97
+ self.logs = Logs(client=client)
98
+
99
+ def verify_event(self, event_body: bytes, event_signature: str, secret: str) -> Any:
100
+ """
101
+ Verify the signature of a webhook event.
102
+ """
103
+ return self._verify_event(event_body, event_signature, secret)
104
+
105
+ def list_automations(
106
+ self,
107
+ processor_id: str,
108
+ before: Optional[str] = None,
109
+ after: Optional[str] = None,
110
+ limit: Optional[int] = 10,
111
+ order: Optional[Literal["asc", "desc"]] = "desc",
112
+ automation_id: Optional[str] = None,
113
+ webhook_url: Optional[str] = None,
114
+ name: Optional[str] = None,
115
+ ):
116
+ """List automations attached to this processor."""
117
+ request = self.prepare_list(processor_id, before, after, limit, order, automation_id, webhook_url, name)
118
+ response = self._client._prepared_request(request)
119
+ return response
120
+
121
+ def get_automation(self, processor_id: str, automation_id: str) -> Union[Link, Mailbox, Endpoint, Outlook]:
122
+ """Get a specific automation attached to this processor."""
123
+ request = self.prepare_get(processor_id, automation_id)
124
+ response = self._client._prepared_request(request)
125
+
126
+ # Return the appropriate model based on the automation type
127
+ if response["object"] == "automation.link":
128
+ return Link.model_validate(response)
129
+ elif response["object"] == "automation.mailbox":
130
+ return Mailbox.model_validate(response)
131
+ elif response["object"] == "automation.endpoint":
132
+ return Endpoint.model_validate(response)
133
+ elif response["object"] == "automation.outlook":
134
+ return Outlook.model_validate(response)
135
+ else:
136
+ raise ValueError(f"Unknown automation type: {response.get('object')}")
137
+
138
+ def update_automation(
139
+ self,
140
+ processor_id: str,
141
+ automation_id: str,
142
+ automation_data: Union[UpdateLinkRequest, UpdateMailboxRequest, UpdateEndpointRequest, UpdateOutlookRequest],
143
+ ) -> Union[Link, Mailbox, Endpoint, Outlook]:
144
+ """Update an automation attached to this processor."""
145
+ request = self.prepare_update(processor_id, automation_id, automation_data)
146
+ response = self._client._prepared_request(request)
147
+
148
+ # Return the appropriate model based on the automation type
149
+ if response["object"] == "automation.link":
150
+ return Link.model_validate(response)
151
+ elif response["object"] == "automation.mailbox":
152
+ return Mailbox.model_validate(response)
153
+ elif response["object"] == "automation.endpoint":
154
+ return Endpoint.model_validate(response)
155
+ elif response["object"] == "automation.outlook":
156
+ return Outlook.model_validate(response)
157
+ else:
158
+ raise ValueError(f"Unknown automation type: {response.get('object')}")
159
+
160
+ def delete_automation(self, processor_id: str, automation_id: str) -> None:
161
+ """Delete an automation attached to this processor."""
162
+ request = self.prepare_delete(processor_id, automation_id)
163
+ self._client._prepared_request(request)
164
+ print(f"Automation {automation_id} deleted from processor {processor_id}")
165
+
166
+
167
+ class AsyncAutomations(AsyncAPIResource, AutomationsMixin):
168
+ """Async Automations API wrapper"""
169
+
170
+ def __init__(self, client: Any) -> None:
171
+ super().__init__(client=client)
172
+ self.mailboxes = AsyncMailboxes(client=client)
173
+ self.links = AsyncLinks(client=client)
174
+ self.outlook = AsyncOutlooks(client=client)
175
+ self.endpoints = AsyncEndpoints(client=client)
176
+ self.tests = AsyncTests(client=client)
177
+ self.logs = AsyncLogs(client=client)
178
+
179
+ async def verify_event(self, event_body: bytes, event_signature: str, secret: str) -> Any:
180
+ """
181
+ Verify the signature of a webhook event.
182
+ """
183
+ return self._verify_event(event_body, event_signature, secret)
184
+
185
+ async def list(
186
+ self,
187
+ processor_id: str,
188
+ before: Optional[str] = None,
189
+ after: Optional[str] = None,
190
+ limit: Optional[int] = 10,
191
+ order: Optional[Literal["asc", "desc"]] = "desc",
192
+ automation_id: Optional[str] = None,
193
+ webhook_url: Optional[str] = None,
194
+ name: Optional[str] = None,
195
+ ):
196
+ """List automations attached to this processor."""
197
+ request = self.prepare_list(processor_id, before, after, limit, order, automation_id, webhook_url, name)
198
+ response = await self._client._prepared_request(request)
199
+ return response
200
+
201
+ async def get(self, processor_id: str, automation_id: str) -> Union[Link, Mailbox, Endpoint, Outlook]:
202
+ """Get a specific automation attached to this processor."""
203
+ request = self.prepare_get(processor_id, automation_id)
204
+ response = await self._client._prepared_request(request)
205
+
206
+ # Return the appropriate model based on the automation type
207
+ if response["object"] == "automation.link":
208
+ return Link.model_validate(response)
209
+ elif response["object"] == "automation.mailbox":
210
+ return Mailbox.model_validate(response)
211
+ elif response["object"] == "automation.endpoint":
212
+ return Endpoint.model_validate(response)
213
+ elif response["object"] == "automation.outlook":
214
+ return Outlook.model_validate(response)
215
+ else:
216
+ raise ValueError(f"Unknown automation type: {response.get('object')}")
217
+
218
+ async def update(
219
+ self,
220
+ processor_id: str,
221
+ automation_id: str,
222
+ automation_data: Union[UpdateLinkRequest, UpdateMailboxRequest, UpdateEndpointRequest, UpdateOutlookRequest],
223
+ ) -> Union[Link, Mailbox, Endpoint, Outlook]:
224
+ """Update an automation attached to this processor."""
225
+ request = self.prepare_update(processor_id, automation_id, automation_data)
226
+ response = await self._client._prepared_request(request)
227
+
228
+ # Return the appropriate model based on the automation type
229
+ if response["object"] == "automation.link":
230
+ return Link.model_validate(response)
231
+ elif response["object"] == "automation.mailbox":
232
+ return Mailbox.model_validate(response)
233
+ elif response["object"] == "automation.endpoint":
234
+ return Endpoint.model_validate(response)
235
+ elif response["object"] == "automation.outlook":
236
+ return Outlook.model_validate(response)
237
+ else:
238
+ raise ValueError(f"Unknown automation type: {response.get('object')}")
239
+
240
+ async def delete(self, processor_id: str, automation_id: str) -> None:
241
+ """Delete an automation attached to this processor."""
242
+ request = self.prepare_delete(processor_id, automation_id)
243
+ await self._client._prepared_request(request)
244
+ print(f"Automation {automation_id} deleted from processor {processor_id}")