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.

Files changed (105) hide show
  1. unique_toolkit/__init__.py +20 -0
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +121 -28
  3. unique_toolkit/_common/chunk_relevancy_sorter/config.py +3 -3
  4. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +2 -5
  5. unique_toolkit/_common/default_language_model.py +9 -3
  6. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  7. unique_toolkit/_common/docx_generator/config.py +12 -0
  8. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  9. unique_toolkit/_common/docx_generator/service.py +252 -0
  10. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  11. unique_toolkit/_common/endpoint_builder.py +138 -117
  12. unique_toolkit/_common/endpoint_requestor.py +240 -14
  13. unique_toolkit/_common/exception.py +20 -0
  14. unique_toolkit/_common/feature_flags/schema.py +1 -5
  15. unique_toolkit/_common/referencing.py +53 -0
  16. unique_toolkit/_common/string_utilities.py +52 -1
  17. unique_toolkit/_common/tests/test_referencing.py +521 -0
  18. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  19. unique_toolkit/_common/utils/files.py +43 -0
  20. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +16 -6
  21. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  22. unique_toolkit/agentic/evaluation/config.py +3 -2
  23. unique_toolkit/agentic/evaluation/context_relevancy/service.py +2 -2
  24. unique_toolkit/agentic/evaluation/evaluation_manager.py +9 -5
  25. unique_toolkit/agentic/evaluation/hallucination/constants.py +1 -1
  26. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +26 -3
  27. unique_toolkit/agentic/history_manager/history_manager.py +14 -11
  28. unique_toolkit/agentic/history_manager/loop_token_reducer.py +3 -4
  29. unique_toolkit/agentic/history_manager/utils.py +10 -87
  30. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +107 -16
  31. unique_toolkit/agentic/reference_manager/reference_manager.py +1 -1
  32. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  33. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  34. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  35. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  36. unique_toolkit/agentic/tools/a2a/__init__.py +18 -2
  37. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +2 -0
  38. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +3 -3
  39. unique_toolkit/agentic/tools/a2a/evaluation/config.py +1 -1
  40. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +143 -91
  41. unique_toolkit/agentic/tools/a2a/manager.py +7 -1
  42. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +11 -3
  43. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  44. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  45. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +21 -0
  46. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  47. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  48. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  49. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  50. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  51. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  52. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  53. unique_toolkit/agentic/tools/a2a/tool/config.py +15 -5
  54. unique_toolkit/agentic/tools/a2a/tool/service.py +69 -36
  55. unique_toolkit/agentic/tools/config.py +16 -2
  56. unique_toolkit/agentic/tools/factory.py +4 -0
  57. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +7 -35
  58. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  59. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  60. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  61. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  62. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  63. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  64. unique_toolkit/agentic/tools/test/test_mcp_manager.py +95 -7
  65. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +240 -0
  66. unique_toolkit/agentic/tools/tool.py +0 -11
  67. unique_toolkit/agentic/tools/tool_manager.py +337 -122
  68. unique_toolkit/agentic/tools/tool_progress_reporter.py +81 -15
  69. unique_toolkit/agentic/tools/utils/__init__.py +18 -0
  70. unique_toolkit/agentic/tools/utils/execution/execution.py +8 -4
  71. unique_toolkit/agentic/tools/utils/source_handling/schema.py +1 -1
  72. unique_toolkit/chat/__init__.py +8 -1
  73. unique_toolkit/chat/deprecated/service.py +232 -0
  74. unique_toolkit/chat/functions.py +54 -40
  75. unique_toolkit/chat/rendering.py +34 -0
  76. unique_toolkit/chat/responses_api.py +461 -0
  77. unique_toolkit/chat/schemas.py +1 -1
  78. unique_toolkit/chat/service.py +96 -1569
  79. unique_toolkit/content/functions.py +116 -1
  80. unique_toolkit/content/schemas.py +59 -0
  81. unique_toolkit/content/service.py +5 -37
  82. unique_toolkit/content/smart_rules.py +301 -0
  83. unique_toolkit/framework_utilities/langchain/client.py +27 -3
  84. unique_toolkit/framework_utilities/openai/client.py +12 -1
  85. unique_toolkit/framework_utilities/openai/message_builder.py +85 -1
  86. unique_toolkit/language_model/default_language_model.py +3 -0
  87. unique_toolkit/language_model/functions.py +25 -9
  88. unique_toolkit/language_model/infos.py +72 -4
  89. unique_toolkit/language_model/schemas.py +246 -40
  90. unique_toolkit/protocols/support.py +91 -9
  91. unique_toolkit/services/__init__.py +7 -0
  92. unique_toolkit/services/chat_service.py +1630 -0
  93. unique_toolkit/services/knowledge_base.py +861 -0
  94. unique_toolkit/smart_rules/compile.py +56 -301
  95. unique_toolkit/test_utilities/events.py +197 -0
  96. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +173 -3
  97. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/RECORD +99 -67
  98. unique_toolkit/agentic/tools/a2a/postprocessing/_display.py +0 -122
  99. unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py +0 -19
  100. unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +0 -230
  101. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py +0 -665
  102. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +0 -391
  103. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py +0 -256
  104. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  105. {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
@@ -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 Formatter, Template
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 create_url(
115
+ def create_path(
71
116
  *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
72
117
  ) -> str: ...
73
118
 
74
119
  @staticmethod
75
- def create_url_from_model(path_params: PathParamsType) -> str: ...
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(payload: PayloadType) -> dict[str, Any]: ...
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(response: dict[str, Any]) -> ResponseType: ...
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
- url_template: Template,
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
- dump_options: dict | None = None,
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 create_url(
147
- *args: PathParamsSpec.args,
148
- **kwargs: PathParamsSpec.kwargs,
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
- """Create URL from path parameters."""
151
- path_model = Operation.path_params_model()(*args, **kwargs)
152
- path_dict = path_model.model_dump(**dump_options)
153
-
154
- # Extract expected path parameters from template
155
- template_params = [
156
- fname
157
- for _, fname, _, _ in Formatter().parse(url_template.template)
158
- if fname is not None
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 create_url_from_model(path_params: PathParamsType) -> str:
172
- return url_template.substitute(**path_params.model_dump(**dump_options))
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, **kwargs: PayloadParamSpec.kwargs
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(**dump_options)
253
+ return request_model.model_dump(**model_dump_options)
181
254
 
182
255
  @staticmethod
183
- def create_payload_from_model(payload: PayloadType) -> dict[str, Any]:
184
- return payload.model_dump(**dump_options)
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(response: dict[str, Any]) -> ResponseType:
188
- return Operation.response_model().model_validate(response)
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}")