unique_toolkit 1.8.1__py3-none-any.whl → 1.23.0__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 unique_toolkit might be problematic. Click here for more details.
- unique_toolkit/__init__.py +20 -0
- unique_toolkit/_common/api_calling/human_verification_manager.py +121 -28
- unique_toolkit/_common/chunk_relevancy_sorter/config.py +3 -3
- unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +2 -5
- unique_toolkit/_common/default_language_model.py +9 -3
- unique_toolkit/_common/docx_generator/__init__.py +7 -0
- unique_toolkit/_common/docx_generator/config.py +12 -0
- unique_toolkit/_common/docx_generator/schemas.py +80 -0
- unique_toolkit/_common/docx_generator/service.py +252 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +138 -117
- unique_toolkit/_common/endpoint_requestor.py +240 -14
- unique_toolkit/_common/exception.py +20 -0
- unique_toolkit/_common/feature_flags/schema.py +1 -5
- unique_toolkit/_common/referencing.py +53 -0
- unique_toolkit/_common/string_utilities.py +52 -1
- unique_toolkit/_common/tests/test_referencing.py +521 -0
- unique_toolkit/_common/tests/test_string_utilities.py +506 -0
- unique_toolkit/_common/utils/files.py +43 -0
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +16 -6
- unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
- unique_toolkit/agentic/evaluation/config.py +3 -2
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +2 -2
- unique_toolkit/agentic/evaluation/evaluation_manager.py +9 -5
- unique_toolkit/agentic/evaluation/hallucination/constants.py +1 -1
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +26 -3
- unique_toolkit/agentic/history_manager/history_manager.py +14 -11
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +3 -4
- unique_toolkit/agentic/history_manager/utils.py +10 -87
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +107 -16
- unique_toolkit/agentic/reference_manager/reference_manager.py +1 -1
- unique_toolkit/agentic/responses_api/__init__.py +19 -0
- unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
- unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
- unique_toolkit/agentic/tools/a2a/__init__.py +18 -2
- unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +2 -0
- unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +3 -3
- unique_toolkit/agentic/tools/a2a/evaluation/config.py +1 -1
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +143 -91
- unique_toolkit/agentic/tools/a2a/manager.py +7 -1
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +11 -3
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
- unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +15 -5
- unique_toolkit/agentic/tools/a2a/tool/service.py +69 -36
- unique_toolkit/agentic/tools/config.py +16 -2
- unique_toolkit/agentic/tools/factory.py +4 -0
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py +7 -35
- unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
- unique_toolkit/agentic/tools/test/test_mcp_manager.py +95 -7
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +240 -0
- unique_toolkit/agentic/tools/tool.py +0 -11
- unique_toolkit/agentic/tools/tool_manager.py +337 -122
- unique_toolkit/agentic/tools/tool_progress_reporter.py +81 -15
- unique_toolkit/agentic/tools/utils/__init__.py +18 -0
- unique_toolkit/agentic/tools/utils/execution/execution.py +8 -4
- unique_toolkit/agentic/tools/utils/source_handling/schema.py +1 -1
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +54 -40
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +1 -1
- unique_toolkit/chat/service.py +96 -1569
- unique_toolkit/content/functions.py +116 -1
- unique_toolkit/content/schemas.py +59 -0
- unique_toolkit/content/service.py +5 -37
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/framework_utilities/langchain/client.py +27 -3
- unique_toolkit/framework_utilities/openai/client.py +12 -1
- unique_toolkit/framework_utilities/openai/message_builder.py +85 -1
- unique_toolkit/language_model/default_language_model.py +3 -0
- unique_toolkit/language_model/functions.py +25 -9
- unique_toolkit/language_model/infos.py +72 -4
- unique_toolkit/language_model/schemas.py +246 -40
- unique_toolkit/protocols/support.py +91 -9
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1630 -0
- unique_toolkit/services/knowledge_base.py +861 -0
- unique_toolkit/smart_rules/compile.py +56 -301
- unique_toolkit/test_utilities/events.py +197 -0
- {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +173 -3
- {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/RECORD +99 -67
- unique_toolkit/agentic/tools/a2a/postprocessing/_display.py +0 -122
- unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py +0 -19
- unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +0 -230
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py +0 -665
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +0 -391
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py +0 -256
- {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from docxtpl import DocxTemplate
|
|
7
|
+
from markdown_it import MarkdownIt
|
|
8
|
+
|
|
9
|
+
from unique_toolkit._common.docx_generator.config import DocxGeneratorConfig
|
|
10
|
+
from unique_toolkit._common.docx_generator.schemas import (
|
|
11
|
+
ContentField,
|
|
12
|
+
HeadingField,
|
|
13
|
+
# ImageField,
|
|
14
|
+
ParagraphField,
|
|
15
|
+
RunField,
|
|
16
|
+
RunsField,
|
|
17
|
+
)
|
|
18
|
+
from unique_toolkit.chat.service import ChatService
|
|
19
|
+
from unique_toolkit.services import KnowledgeBaseService
|
|
20
|
+
|
|
21
|
+
generator_dir_path = Path(__file__).resolve().parent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_LOGGER = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DocxGeneratorService:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
chat_service: ChatService,
|
|
31
|
+
knowledge_base_service: KnowledgeBaseService,
|
|
32
|
+
config: DocxGeneratorConfig,
|
|
33
|
+
):
|
|
34
|
+
self._knowledge_base_service = knowledge_base_service
|
|
35
|
+
self._config = config
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def parse_markdown_to_list_content_fields(
|
|
39
|
+
markdown: str, offset_header_lvl: int = 0
|
|
40
|
+
) -> list[HeadingField | ParagraphField | RunsField]:
|
|
41
|
+
# Initialize markdown-it parser
|
|
42
|
+
md = MarkdownIt()
|
|
43
|
+
|
|
44
|
+
# Preprocess markdown.
|
|
45
|
+
# - Replace all headings with the correct heading level
|
|
46
|
+
# - Remove "Relevant sources" heading
|
|
47
|
+
# - Replace "# Proposed answer" with "#### Proposed answer"
|
|
48
|
+
markdown = re.sub(r"(?m)^\s*## ", "#### ", markdown)
|
|
49
|
+
markdown = re.sub(r"(?m)^\s*### ", "##### ", markdown)
|
|
50
|
+
markdown = markdown.replace("# Relevant sources", "")
|
|
51
|
+
markdown = markdown.replace("# Proposed answer", "#### Proposed answer")
|
|
52
|
+
|
|
53
|
+
tokens = md.parse(markdown)
|
|
54
|
+
|
|
55
|
+
elements = []
|
|
56
|
+
current_section = {}
|
|
57
|
+
in_list = False
|
|
58
|
+
bullet_list_indent_level = 0
|
|
59
|
+
list_item_open = False
|
|
60
|
+
|
|
61
|
+
for token in tokens:
|
|
62
|
+
if token.type == "bullet_list_open":
|
|
63
|
+
in_list = True
|
|
64
|
+
bullet_list_indent_level = int(token.level / 2)
|
|
65
|
+
|
|
66
|
+
elif token.type == "bullet_list_close":
|
|
67
|
+
in_list = False
|
|
68
|
+
bullet_list_indent_level = 0
|
|
69
|
+
|
|
70
|
+
elif token.type == "list_item_open":
|
|
71
|
+
if list_item_open:
|
|
72
|
+
elements.append(current_section)
|
|
73
|
+
list_item_open = True
|
|
74
|
+
list_level = token.level - bullet_list_indent_level
|
|
75
|
+
current_section = {
|
|
76
|
+
"type": RunsField,
|
|
77
|
+
"runs": [],
|
|
78
|
+
"is_list_item": True,
|
|
79
|
+
"level": list_level,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
elif token.type == "list_item_close":
|
|
83
|
+
if current_section and current_section.get("runs"):
|
|
84
|
+
elements.append(current_section)
|
|
85
|
+
current_section = {}
|
|
86
|
+
list_item_open = False
|
|
87
|
+
|
|
88
|
+
if token.type == "heading_open":
|
|
89
|
+
# Heading start, token.tag gives the level (e.g., 'h1', 'h2', etc.)
|
|
90
|
+
header_lvl = int(token.tag[1]) # Extract the level number from tag
|
|
91
|
+
current_section = {
|
|
92
|
+
"type": HeadingField,
|
|
93
|
+
"text": "",
|
|
94
|
+
"level": header_lvl + offset_header_lvl,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
elif token.type == "heading_close":
|
|
98
|
+
if current_section:
|
|
99
|
+
elements.append(current_section)
|
|
100
|
+
current_section = {}
|
|
101
|
+
|
|
102
|
+
elif token.type == "paragraph_open":
|
|
103
|
+
if not in_list: # Only create new paragraph if not in a list
|
|
104
|
+
current_section = {"type": RunsField, "runs": []}
|
|
105
|
+
|
|
106
|
+
elif token.type == "paragraph_close":
|
|
107
|
+
if not in_list and current_section: # Only append if not in a list
|
|
108
|
+
elements.append(current_section)
|
|
109
|
+
current_section = {}
|
|
110
|
+
|
|
111
|
+
elif token.type == "inline":
|
|
112
|
+
if current_section.get("type") == HeadingField:
|
|
113
|
+
content = token.content
|
|
114
|
+
if content.startswith("_page"):
|
|
115
|
+
# replace "_pageXXXX_" with "PageXXXX", where XXXX can be any characters and numbers
|
|
116
|
+
content = re.sub(
|
|
117
|
+
r"^_page([a-zA-Z0-9\s-]+)_(.*?)",
|
|
118
|
+
r"Page\1",
|
|
119
|
+
content,
|
|
120
|
+
)
|
|
121
|
+
bold = True
|
|
122
|
+
current_section["text"] += content
|
|
123
|
+
elif "runs" in current_section:
|
|
124
|
+
bold = False
|
|
125
|
+
italic = False
|
|
126
|
+
runs = []
|
|
127
|
+
if token.children:
|
|
128
|
+
for child in token.children:
|
|
129
|
+
content = child.content
|
|
130
|
+
if child.type == "strong_open":
|
|
131
|
+
bold = True
|
|
132
|
+
elif child.type == "strong_close":
|
|
133
|
+
bold = False
|
|
134
|
+
elif child.type == "em_open":
|
|
135
|
+
italic = True
|
|
136
|
+
elif child.type == "em_close":
|
|
137
|
+
italic = False
|
|
138
|
+
if child.type == "softbreak":
|
|
139
|
+
content += "\n"
|
|
140
|
+
if content: # Only add non-empty content
|
|
141
|
+
runs.append(
|
|
142
|
+
RunField(
|
|
143
|
+
text=content,
|
|
144
|
+
bold=bold,
|
|
145
|
+
italic=italic,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
runs.append(
|
|
150
|
+
RunField(
|
|
151
|
+
text=token.content,
|
|
152
|
+
bold=bold,
|
|
153
|
+
italic=italic,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
current_section["runs"].extend(runs)
|
|
158
|
+
|
|
159
|
+
# Process remaining elements
|
|
160
|
+
contents = []
|
|
161
|
+
for element in elements:
|
|
162
|
+
if not element:
|
|
163
|
+
continue
|
|
164
|
+
if element["type"] == HeadingField:
|
|
165
|
+
contents.append(
|
|
166
|
+
HeadingField(
|
|
167
|
+
text=element["text"],
|
|
168
|
+
level=element["level"],
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
elif element["type"] == RunsField:
|
|
172
|
+
if element.get("is_list_item", False):
|
|
173
|
+
level: int = min(element.get("level", 1), 5)
|
|
174
|
+
if level > 1:
|
|
175
|
+
style = "List Bullet " + str(level)
|
|
176
|
+
else:
|
|
177
|
+
style = "List Bullet"
|
|
178
|
+
contents.append(RunsField(style=style, runs=element["runs"]))
|
|
179
|
+
else:
|
|
180
|
+
contents.append(RunsField(runs=element["runs"]))
|
|
181
|
+
|
|
182
|
+
return contents
|
|
183
|
+
|
|
184
|
+
def generate_from_template(
|
|
185
|
+
self,
|
|
186
|
+
subdoc_content: list[HeadingField | ParagraphField | RunsField],
|
|
187
|
+
fields: dict | None = None,
|
|
188
|
+
):
|
|
189
|
+
"""
|
|
190
|
+
Generate a docx file from a template with the given content.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
subdoc_content (list[HeadingField | ParagraphField | RunsField]): The content to be added to the docx file.
|
|
194
|
+
fields (dict): Other fields to be added to the docx file. Defaults to None.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
docx_template_object = self._get_template(self._config.template_content_id)
|
|
198
|
+
|
|
199
|
+
doc = DocxTemplate(io.BytesIO(docx_template_object))
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
content = {}
|
|
203
|
+
content["body"] = ContentField(contents=subdoc_content)
|
|
204
|
+
|
|
205
|
+
if fields:
|
|
206
|
+
content.update(fields)
|
|
207
|
+
|
|
208
|
+
for key, value in content.items():
|
|
209
|
+
if isinstance(value, ContentField):
|
|
210
|
+
content[key] = value.add(doc)
|
|
211
|
+
|
|
212
|
+
doc.render(content)
|
|
213
|
+
docx_rendered_object = io.BytesIO()
|
|
214
|
+
|
|
215
|
+
doc.save(docx_rendered_object)
|
|
216
|
+
docx_rendered_object.seek(0)
|
|
217
|
+
|
|
218
|
+
return docx_rendered_object.getvalue()
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
_LOGGER.error(f"Error generating docx: {e}")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def _get_template(self, template_content_id: str):
|
|
225
|
+
try:
|
|
226
|
+
if template_content_id:
|
|
227
|
+
_LOGGER.info(
|
|
228
|
+
f"Downloading template from content ID: {template_content_id}"
|
|
229
|
+
)
|
|
230
|
+
file_content = self._knowledge_base_service.download_content_to_bytes(
|
|
231
|
+
content_id=template_content_id
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
_LOGGER.info("No template content ID provided. Using default template.")
|
|
235
|
+
file_content = self._get_default_template()
|
|
236
|
+
except Exception as e:
|
|
237
|
+
_LOGGER.warning(
|
|
238
|
+
f"An error occurred while downloading the template {e}. Make sure the template content ID is valid. Falling back to default template."
|
|
239
|
+
)
|
|
240
|
+
file_content = self._get_default_template()
|
|
241
|
+
|
|
242
|
+
return file_content
|
|
243
|
+
|
|
244
|
+
def _get_default_template(self):
|
|
245
|
+
generator_dir_path = Path(__file__).resolve().parent
|
|
246
|
+
path = generator_dir_path / "template" / "Doc Template.docx"
|
|
247
|
+
|
|
248
|
+
file_content = path.read_bytes()
|
|
249
|
+
|
|
250
|
+
_LOGGER.info("Template downloaded from default template")
|
|
251
|
+
|
|
252
|
+
return file_content
|
|
Binary file
|
|
@@ -3,8 +3,9 @@ This module provides a minimal framework for building endpoint classes such that
|
|
|
3
3
|
the endpoints without having to know the details of the endpoints.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import warnings
|
|
6
7
|
from enum import StrEnum
|
|
7
|
-
from string import
|
|
8
|
+
from string import Template
|
|
8
9
|
from typing import (
|
|
9
10
|
Any,
|
|
10
11
|
Callable,
|
|
@@ -15,8 +16,7 @@ from typing import (
|
|
|
15
16
|
cast,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
from pydantic import BaseModel
|
|
19
|
-
from typing_extensions import deprecated
|
|
19
|
+
from pydantic import BaseModel, ValidationError
|
|
20
20
|
|
|
21
21
|
# Paramspecs
|
|
22
22
|
PayloadParamSpec = ParamSpec("PayloadParamSpec")
|
|
@@ -47,6 +47,42 @@ class HttpMethods(StrEnum):
|
|
|
47
47
|
EndpointMethods = HttpMethods
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
class ResponseValidationException(Exception):
|
|
51
|
+
"""
|
|
52
|
+
This exception is raised when the response validation fails.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self, operation_name: str, response: dict[str, Any], pydantic_error: str
|
|
57
|
+
):
|
|
58
|
+
super().__init__(
|
|
59
|
+
f"Response validation failed for operation {operation_name}\n"
|
|
60
|
+
f"Response: {response}\n"
|
|
61
|
+
f"Pydantic error: {pydantic_error}"
|
|
62
|
+
)
|
|
63
|
+
self._operation_name = operation_name
|
|
64
|
+
self._response = response
|
|
65
|
+
self._pydantic_error = pydantic_error
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def operation_name(self) -> str:
|
|
69
|
+
return self._operation_name
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def response(self) -> dict[str, Any]:
|
|
73
|
+
return self._response
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def pydantic_error(self) -> str:
|
|
77
|
+
return self._pydantic_error
|
|
78
|
+
|
|
79
|
+
def __str__(self):
|
|
80
|
+
return (
|
|
81
|
+
f"Response validation failed for {self._operation_name}\n"
|
|
82
|
+
+ f"Response: {self._response}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
50
86
|
class ApiOperationProtocol(
|
|
51
87
|
Protocol,
|
|
52
88
|
Generic[
|
|
@@ -57,22 +93,33 @@ class ApiOperationProtocol(
|
|
|
57
93
|
ResponseType,
|
|
58
94
|
],
|
|
59
95
|
):
|
|
96
|
+
@staticmethod
|
|
97
|
+
def path_dump_options() -> dict[str, Any]: ...
|
|
98
|
+
|
|
60
99
|
@staticmethod
|
|
61
100
|
def path_params_model() -> type[PathParamsType]: ...
|
|
62
101
|
|
|
102
|
+
@staticmethod
|
|
103
|
+
def payload_dump_options() -> dict[str, Any]: ...
|
|
104
|
+
|
|
63
105
|
@staticmethod
|
|
64
106
|
def payload_model() -> type[PayloadType]: ...
|
|
65
107
|
|
|
108
|
+
@staticmethod
|
|
109
|
+
def response_validate_options() -> dict[str, Any]: ...
|
|
110
|
+
|
|
66
111
|
@staticmethod
|
|
67
112
|
def response_model() -> type[ResponseType]: ...
|
|
68
113
|
|
|
69
114
|
@staticmethod
|
|
70
|
-
def
|
|
115
|
+
def create_path(
|
|
71
116
|
*args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
|
|
72
117
|
) -> str: ...
|
|
73
118
|
|
|
74
119
|
@staticmethod
|
|
75
|
-
def
|
|
120
|
+
def create_path_from_model(
|
|
121
|
+
path_params: PathParamsType, *, model_dump_options: dict | None = None
|
|
122
|
+
) -> str: ...
|
|
76
123
|
|
|
77
124
|
@staticmethod
|
|
78
125
|
def create_payload(
|
|
@@ -80,10 +127,14 @@ class ApiOperationProtocol(
|
|
|
80
127
|
) -> dict[str, Any]: ...
|
|
81
128
|
|
|
82
129
|
@staticmethod
|
|
83
|
-
def create_payload_from_model(
|
|
130
|
+
def create_payload_from_model(
|
|
131
|
+
payload: PayloadType, *, model_dump_options: dict | None = None
|
|
132
|
+
) -> dict[str, Any]: ...
|
|
84
133
|
|
|
85
134
|
@staticmethod
|
|
86
|
-
def handle_response(
|
|
135
|
+
def handle_response(
|
|
136
|
+
response: dict[str, Any], *, model_validate_options: dict | None = None
|
|
137
|
+
) -> ResponseType: ...
|
|
87
138
|
|
|
88
139
|
@staticmethod
|
|
89
140
|
def request_method() -> EndpointMethods: ...
|
|
@@ -98,11 +149,14 @@ class ApiOperationProtocol(
|
|
|
98
149
|
def build_api_operation(
|
|
99
150
|
*,
|
|
100
151
|
method: HttpMethods,
|
|
101
|
-
|
|
152
|
+
path_template: Template,
|
|
102
153
|
path_params_constructor: Callable[PathParamsSpec, PathParamsType],
|
|
103
154
|
payload_constructor: Callable[PayloadParamSpec, PayloadType],
|
|
104
155
|
response_model_type: type[ResponseType],
|
|
105
|
-
|
|
156
|
+
payload_dump_options: dict | None = None,
|
|
157
|
+
path_dump_options: dict | None = None,
|
|
158
|
+
response_validate_options: dict | None = None,
|
|
159
|
+
dump_options: dict | None = None, # Deprecated
|
|
106
160
|
) -> type[
|
|
107
161
|
ApiOperationProtocol[
|
|
108
162
|
PathParamsSpec,
|
|
@@ -128,64 +182,109 @@ def build_api_operation(
|
|
|
128
182
|
"by_alias": True,
|
|
129
183
|
"exclude_defaults": True,
|
|
130
184
|
}
|
|
185
|
+
else:
|
|
186
|
+
warnings.warn(
|
|
187
|
+
"dump_options is deprecated. Use payload_dump_options instead.",
|
|
188
|
+
DeprecationWarning,
|
|
189
|
+
)
|
|
131
190
|
|
|
132
191
|
class Operation(ApiOperationProtocol):
|
|
192
|
+
@staticmethod
|
|
193
|
+
def path_dump_options() -> dict[str, Any]:
|
|
194
|
+
return path_dump_options or {}
|
|
195
|
+
|
|
133
196
|
@staticmethod
|
|
134
197
|
def path_params_model() -> type[PathParamsType]:
|
|
135
198
|
return cast(type[PathParamsType], path_params_constructor)
|
|
136
199
|
|
|
200
|
+
@staticmethod
|
|
201
|
+
def payload_dump_options() -> dict[str, Any]:
|
|
202
|
+
return payload_dump_options or {}
|
|
203
|
+
|
|
137
204
|
@staticmethod
|
|
138
205
|
def payload_model() -> type[PayloadType]:
|
|
139
206
|
return cast(type[PayloadType], payload_constructor)
|
|
140
207
|
|
|
208
|
+
@staticmethod
|
|
209
|
+
def response_validate_options() -> dict[str, Any]:
|
|
210
|
+
return response_validate_options or {}
|
|
211
|
+
|
|
141
212
|
@staticmethod
|
|
142
213
|
def response_model() -> type[ResponseType]:
|
|
143
214
|
return response_model_type
|
|
144
215
|
|
|
145
216
|
@staticmethod
|
|
146
|
-
def
|
|
147
|
-
|
|
148
|
-
|
|
217
|
+
def path_template() -> Template:
|
|
218
|
+
return path_template
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def create_path_from_model(
|
|
222
|
+
path_params: PathParamsType, *, model_dump_options: dict | None = None
|
|
149
223
|
) -> str:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
]
|
|
160
|
-
|
|
161
|
-
# Verify all required path parameters are present
|
|
162
|
-
missing_params = [
|
|
163
|
-
param for param in template_params if param not in path_dict
|
|
164
|
-
]
|
|
165
|
-
if missing_params:
|
|
166
|
-
raise ValueError(f"Missing path parameters: {missing_params}")
|
|
167
|
-
|
|
168
|
-
return url_template.substitute(**path_dict)
|
|
224
|
+
if model_dump_options is None:
|
|
225
|
+
if path_dump_options is None:
|
|
226
|
+
model_dump_options = dump_options
|
|
227
|
+
else:
|
|
228
|
+
model_dump_options = path_dump_options
|
|
229
|
+
|
|
230
|
+
return path_template.substitute(
|
|
231
|
+
**path_params.model_dump(**model_dump_options)
|
|
232
|
+
)
|
|
169
233
|
|
|
170
234
|
@staticmethod
|
|
171
|
-
def
|
|
172
|
-
|
|
235
|
+
def create_path(
|
|
236
|
+
*args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
|
|
237
|
+
) -> str:
|
|
238
|
+
model = Operation.path_params_model()(*args, **kwargs)
|
|
239
|
+
return Operation.create_path_from_model(model)
|
|
173
240
|
|
|
174
241
|
@staticmethod
|
|
175
242
|
def create_payload(
|
|
176
|
-
*args: PayloadParamSpec.args,
|
|
243
|
+
*args: PayloadParamSpec.args,
|
|
244
|
+
**kwargs: PayloadParamSpec.kwargs,
|
|
177
245
|
) -> dict[str, Any]:
|
|
178
246
|
"""Create request body payload."""
|
|
247
|
+
if payload_dump_options is None:
|
|
248
|
+
model_dump_options = dump_options
|
|
249
|
+
else:
|
|
250
|
+
model_dump_options = payload_dump_options
|
|
251
|
+
|
|
179
252
|
request_model = Operation.payload_model()(*args, **kwargs)
|
|
180
|
-
return request_model.model_dump(**
|
|
253
|
+
return request_model.model_dump(**model_dump_options)
|
|
181
254
|
|
|
182
255
|
@staticmethod
|
|
183
|
-
def create_payload_from_model(
|
|
184
|
-
|
|
256
|
+
def create_payload_from_model(
|
|
257
|
+
payload: PayloadType, *, model_dump_options: dict | None = None
|
|
258
|
+
) -> dict[str, Any]:
|
|
259
|
+
if model_dump_options is None:
|
|
260
|
+
if payload_dump_options is None:
|
|
261
|
+
model_dump_options = dump_options
|
|
262
|
+
else:
|
|
263
|
+
model_dump_options = payload_dump_options
|
|
264
|
+
|
|
265
|
+
return payload.model_dump(**model_dump_options)
|
|
185
266
|
|
|
186
267
|
@staticmethod
|
|
187
|
-
def handle_response(
|
|
188
|
-
|
|
268
|
+
def handle_response(
|
|
269
|
+
response: dict[str, Any],
|
|
270
|
+
*,
|
|
271
|
+
model_validate_options: dict[str, Any] | None = None,
|
|
272
|
+
) -> ResponseType:
|
|
273
|
+
if model_validate_options is None:
|
|
274
|
+
if response_validate_options is None:
|
|
275
|
+
model_validate_options = {}
|
|
276
|
+
else:
|
|
277
|
+
model_validate_options = response_validate_options
|
|
278
|
+
try:
|
|
279
|
+
return Operation.response_model().model_validate(
|
|
280
|
+
response, **model_validate_options
|
|
281
|
+
)
|
|
282
|
+
except ValidationError as e:
|
|
283
|
+
raise ResponseValidationException(
|
|
284
|
+
operation_name=Operation.__name__,
|
|
285
|
+
response=response,
|
|
286
|
+
pydantic_error=str(e),
|
|
287
|
+
) from e
|
|
189
288
|
|
|
190
289
|
@staticmethod
|
|
191
290
|
def request_method() -> HttpMethods:
|
|
@@ -204,81 +303,3 @@ def build_api_operation(
|
|
|
204
303
|
return path_params, payload
|
|
205
304
|
|
|
206
305
|
return Operation
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@deprecated("Use ApiOperationProtocol instead")
|
|
210
|
-
class EndpointClassProtocol(ApiOperationProtocol):
|
|
211
|
-
pass
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@deprecated("Use build_api_operation instead")
|
|
215
|
-
def build_endpoint_class(
|
|
216
|
-
*,
|
|
217
|
-
method: HttpMethods,
|
|
218
|
-
url_template: Template,
|
|
219
|
-
path_params_constructor: Callable[PathParamsSpec, PathParamsType],
|
|
220
|
-
payload_constructor: Callable[PayloadParamSpec, PayloadType],
|
|
221
|
-
response_model_type: type[ResponseType],
|
|
222
|
-
dump_options: dict | None = None,
|
|
223
|
-
) -> type[
|
|
224
|
-
ApiOperationProtocol[
|
|
225
|
-
PathParamsSpec,
|
|
226
|
-
PathParamsType,
|
|
227
|
-
PayloadParamSpec,
|
|
228
|
-
PayloadType,
|
|
229
|
-
ResponseType,
|
|
230
|
-
]
|
|
231
|
-
]:
|
|
232
|
-
return build_api_operation(
|
|
233
|
-
method=method,
|
|
234
|
-
url_template=url_template,
|
|
235
|
-
path_params_constructor=path_params_constructor,
|
|
236
|
-
payload_constructor=payload_constructor,
|
|
237
|
-
response_model_type=response_model_type,
|
|
238
|
-
dump_options=dump_options,
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if __name__ == "__main__":
|
|
243
|
-
# Example models
|
|
244
|
-
class GetUserPathParams(BaseModel):
|
|
245
|
-
"""Path parameters for the user endpoint."""
|
|
246
|
-
|
|
247
|
-
user_id: int
|
|
248
|
-
|
|
249
|
-
class GetUserRequestBody(BaseModel):
|
|
250
|
-
"""Request body/query parameters for the user endpoint."""
|
|
251
|
-
|
|
252
|
-
include_profile: bool = False
|
|
253
|
-
|
|
254
|
-
class UserResponse(BaseModel):
|
|
255
|
-
"""Response model for user data."""
|
|
256
|
-
|
|
257
|
-
id: int
|
|
258
|
-
name: str
|
|
259
|
-
|
|
260
|
-
# Example usage of make_endpoint_class
|
|
261
|
-
UserOperation = build_endpoint_class(
|
|
262
|
-
url_template=Template("/users/${user_id}"),
|
|
263
|
-
path_params_constructor=GetUserPathParams,
|
|
264
|
-
payload_constructor=GetUserRequestBody,
|
|
265
|
-
response_model_type=UserResponse,
|
|
266
|
-
method=EndpointMethods.GET,
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
# Create URL from path parameters
|
|
270
|
-
url = UserOperation.create_url(user_id=123)
|
|
271
|
-
print(f"URL: {url}")
|
|
272
|
-
|
|
273
|
-
# Create payload from request body parameters
|
|
274
|
-
payload = UserOperation.create_payload(include_profile=True)
|
|
275
|
-
print(f"Payload: {payload}")
|
|
276
|
-
|
|
277
|
-
# Create response from endpoint
|
|
278
|
-
response = UserOperation.handle_response(
|
|
279
|
-
{
|
|
280
|
-
"id": 123,
|
|
281
|
-
"name": "John Doe",
|
|
282
|
-
}
|
|
283
|
-
)
|
|
284
|
-
print(f"Response: {response}")
|