unique_toolkit 0.7.9__py3-none-any.whl → 1.33.3__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 (190) hide show
  1. unique_toolkit/__init__.py +36 -3
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +357 -0
  3. unique_toolkit/_common/base_model_type_attribute.py +303 -0
  4. unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
  5. unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
  6. unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
  7. unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
  8. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
  9. unique_toolkit/_common/default_language_model.py +12 -0
  10. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  11. unique_toolkit/_common/docx_generator/config.py +12 -0
  12. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  13. unique_toolkit/_common/docx_generator/service.py +225 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +368 -0
  16. unique_toolkit/_common/endpoint_requestor.py +480 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/experimental/endpoint_builder.py +368 -0
  19. unique_toolkit/_common/experimental/endpoint_requestor.py +488 -0
  20. unique_toolkit/_common/feature_flags/schema.py +9 -0
  21. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  22. unique_toolkit/_common/pydantic_helpers.py +174 -0
  23. unique_toolkit/_common/referencing.py +53 -0
  24. unique_toolkit/_common/string_utilities.py +140 -0
  25. unique_toolkit/_common/tests/test_referencing.py +521 -0
  26. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  27. unique_toolkit/_common/token/image_token_counting.py +67 -0
  28. unique_toolkit/_common/token/token_counting.py +204 -0
  29. unique_toolkit/_common/utils/__init__.py +1 -0
  30. unique_toolkit/_common/utils/files.py +43 -0
  31. unique_toolkit/_common/utils/image/encode.py +25 -0
  32. unique_toolkit/_common/utils/jinja/helpers.py +10 -0
  33. unique_toolkit/_common/utils/jinja/render.py +18 -0
  34. unique_toolkit/_common/utils/jinja/schema.py +65 -0
  35. unique_toolkit/_common/utils/jinja/utils.py +80 -0
  36. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  37. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  38. unique_toolkit/_common/utils/write_configuration.py +51 -0
  39. unique_toolkit/_common/validators.py +101 -4
  40. unique_toolkit/agentic/__init__.py +1 -0
  41. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  42. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  43. unique_toolkit/agentic/evaluation/config.py +36 -0
  44. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  45. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  46. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  47. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  48. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  49. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +112 -0
  50. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  51. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +20 -16
  52. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +32 -21
  53. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  54. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  55. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  56. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  57. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +298 -0
  58. unique_toolkit/agentic/history_manager/history_manager.py +241 -0
  59. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  60. unique_toolkit/agentic/history_manager/utils.py +96 -0
  61. unique_toolkit/agentic/message_log_manager/__init__.py +5 -0
  62. unique_toolkit/agentic/message_log_manager/service.py +93 -0
  63. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  64. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  65. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  66. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +71 -0
  67. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +297 -0
  68. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  69. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  70. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  71. unique_toolkit/agentic/tools/__init__.py +1 -0
  72. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  73. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  74. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  75. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  76. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  77. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  78. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  79. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  80. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  81. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +240 -0
  82. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +84 -0
  83. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +78 -0
  84. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +264 -0
  85. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  86. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  87. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +2103 -0
  88. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  89. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  90. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  91. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  92. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  93. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  94. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  95. unique_toolkit/agentic/tools/a2a/tool/config.py +158 -0
  96. unique_toolkit/agentic/tools/a2a/tool/service.py +393 -0
  97. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  98. unique_toolkit/agentic/tools/config.py +128 -0
  99. unique_toolkit/agentic/tools/factory.py +44 -0
  100. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  101. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  102. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  103. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  104. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  105. unique_toolkit/agentic/tools/openai_builtin/base.py +46 -0
  106. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  107. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +88 -0
  108. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +250 -0
  109. unique_toolkit/agentic/tools/openai_builtin/manager.py +79 -0
  110. unique_toolkit/agentic/tools/schemas.py +145 -0
  111. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  112. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  113. unique_toolkit/agentic/tools/tool.py +187 -0
  114. unique_toolkit/agentic/tools/tool_manager.py +492 -0
  115. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  116. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  117. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  118. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  119. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  120. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  121. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  122. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  123. unique_toolkit/app/__init__.py +9 -0
  124. unique_toolkit/app/dev_util.py +180 -0
  125. unique_toolkit/app/fast_api_factory.py +131 -0
  126. unique_toolkit/app/init_sdk.py +32 -1
  127. unique_toolkit/app/schemas.py +206 -31
  128. unique_toolkit/app/unique_settings.py +367 -0
  129. unique_toolkit/app/webhook.py +77 -0
  130. unique_toolkit/chat/__init__.py +8 -1
  131. unique_toolkit/chat/deprecated/service.py +232 -0
  132. unique_toolkit/chat/functions.py +648 -78
  133. unique_toolkit/chat/rendering.py +34 -0
  134. unique_toolkit/chat/responses_api.py +461 -0
  135. unique_toolkit/chat/schemas.py +134 -2
  136. unique_toolkit/chat/service.py +115 -767
  137. unique_toolkit/content/functions.py +353 -8
  138. unique_toolkit/content/schemas.py +128 -15
  139. unique_toolkit/content/service.py +321 -45
  140. unique_toolkit/content/smart_rules.py +301 -0
  141. unique_toolkit/content/utils.py +10 -3
  142. unique_toolkit/data_extraction/README.md +96 -0
  143. unique_toolkit/data_extraction/__init__.py +11 -0
  144. unique_toolkit/data_extraction/augmented/__init__.py +5 -0
  145. unique_toolkit/data_extraction/augmented/service.py +93 -0
  146. unique_toolkit/data_extraction/base.py +25 -0
  147. unique_toolkit/data_extraction/basic/__init__.py +11 -0
  148. unique_toolkit/data_extraction/basic/config.py +18 -0
  149. unique_toolkit/data_extraction/basic/prompt.py +13 -0
  150. unique_toolkit/data_extraction/basic/service.py +55 -0
  151. unique_toolkit/embedding/service.py +103 -12
  152. unique_toolkit/framework_utilities/__init__.py +1 -0
  153. unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  154. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  155. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  156. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  157. unique_toolkit/framework_utilities/openai/client.py +84 -0
  158. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  159. unique_toolkit/framework_utilities/utils.py +23 -0
  160. unique_toolkit/language_model/__init__.py +3 -0
  161. unique_toolkit/language_model/_responses_api_utils.py +93 -0
  162. unique_toolkit/language_model/builder.py +27 -11
  163. unique_toolkit/language_model/default_language_model.py +3 -0
  164. unique_toolkit/language_model/functions.py +345 -43
  165. unique_toolkit/language_model/infos.py +1288 -46
  166. unique_toolkit/language_model/reference.py +242 -0
  167. unique_toolkit/language_model/schemas.py +481 -49
  168. unique_toolkit/language_model/service.py +229 -28
  169. unique_toolkit/protocols/support.py +145 -0
  170. unique_toolkit/services/__init__.py +7 -0
  171. unique_toolkit/services/chat_service.py +1631 -0
  172. unique_toolkit/services/knowledge_base.py +1094 -0
  173. unique_toolkit/short_term_memory/service.py +178 -41
  174. unique_toolkit/smart_rules/__init__.py +0 -0
  175. unique_toolkit/smart_rules/compile.py +56 -0
  176. unique_toolkit/test_utilities/events.py +197 -0
  177. unique_toolkit-1.33.3.dist-info/METADATA +1145 -0
  178. unique_toolkit-1.33.3.dist-info/RECORD +205 -0
  179. unique_toolkit/evaluators/__init__.py +0 -1
  180. unique_toolkit/evaluators/config.py +0 -35
  181. unique_toolkit/evaluators/constants.py +0 -1
  182. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  183. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  184. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  185. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  186. unique_toolkit-0.7.9.dist-info/METADATA +0 -413
  187. unique_toolkit-0.7.9.dist-info/RECORD +0 -64
  188. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  189. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
  190. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,80 @@
1
+ from docx.document import Document as DocumentObject
2
+ from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
3
+ from docxtpl import DocxTemplate
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class HeadingField(BaseModel):
8
+ text: str
9
+ level: int = 4
10
+ alignment: WD_PARAGRAPH_ALIGNMENT = WD_PARAGRAPH_ALIGNMENT.LEFT
11
+
12
+ def add(self, doc: DocumentObject):
13
+ p = doc.add_heading(self.text, level=self.level)
14
+ p.alignment = self.alignment
15
+ return p
16
+
17
+ def __str__(self):
18
+ return f"HeadingField(text={self.text}, level={self.level}, alignment={self.alignment})"
19
+
20
+
21
+ class ParagraphField(BaseModel):
22
+ text: str
23
+ style: str | None = None
24
+ alignment: WD_PARAGRAPH_ALIGNMENT = WD_PARAGRAPH_ALIGNMENT.LEFT
25
+
26
+ def add(self, doc: DocumentObject):
27
+ p = doc.add_paragraph(self.text, style=self.style)
28
+ p.alignment = self.alignment
29
+ return p
30
+
31
+ def __str__(self):
32
+ return f"ParagraphField(text={self.text}, style={self.style}, alignment={self.alignment})"
33
+
34
+
35
+ class RunField(BaseModel):
36
+ text: str
37
+ italic: bool | None = False
38
+ bold: bool | None = False
39
+ alignment: WD_PARAGRAPH_ALIGNMENT = WD_PARAGRAPH_ALIGNMENT.LEFT
40
+
41
+ def __str__(self):
42
+ return f"RunField(text={self.text}, italic={self.italic}, alignment={self.alignment})"
43
+
44
+
45
+ class RunsField(BaseModel):
46
+ runs: list[RunField]
47
+ style: str | None = None
48
+ alignment: WD_PARAGRAPH_ALIGNMENT = WD_PARAGRAPH_ALIGNMENT.LEFT
49
+
50
+ def add(self, doc: DocumentObject):
51
+ if not self.runs:
52
+ return None
53
+ p = doc.add_paragraph(style=self.style)
54
+ for run in self.runs:
55
+ r = p.add_run(run.text)
56
+ if run.italic:
57
+ r.italic = True
58
+ if run.bold:
59
+ r.bold = True
60
+ return p
61
+
62
+ def __str__(self):
63
+ return f"RunsField(runs={self.runs}, style={self.style}, alignment={self.alignment})"
64
+
65
+
66
+ class ContentField(BaseModel):
67
+ contents: list[HeadingField | ParagraphField | RunsField]
68
+
69
+ def add(self, doc: DocxTemplate):
70
+ sd = doc.new_subdoc()
71
+ for content in self.contents:
72
+ # if isinstance(content, ImageField):
73
+ # content.download_image(self.download_path)
74
+ # content.add(sd) # type: ignore
75
+ # else:
76
+ content.add(sd) # type: ignore
77
+ return sd
78
+
79
+ def __str__(self):
80
+ return f"ContentField(contents={self.contents})"
@@ -0,0 +1,225 @@
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
+ ParagraphField,
14
+ RunField,
15
+ RunsField,
16
+ )
17
+
18
+ generator_dir_path = Path(__file__).resolve().parent
19
+
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+
24
+ class DocxGeneratorService:
25
+ def __init__(self, config: DocxGeneratorConfig, *, template: bytes | None = None):
26
+ self._config = config
27
+ self._template = template
28
+
29
+ @staticmethod
30
+ def parse_markdown_to_list_content_fields(
31
+ markdown: str, offset_header_lvl: int = 0
32
+ ) -> list[HeadingField | ParagraphField | RunsField]:
33
+ # Initialize markdown-it parser
34
+ md = MarkdownIt()
35
+
36
+ # Preprocess markdown.
37
+ # - Replace all headings with the correct heading level
38
+ # - Remove "Relevant sources" heading
39
+ # - Replace "# Proposed answer" with "#### Proposed answer"
40
+ markdown = re.sub(r"(?m)^\s*## ", "#### ", markdown)
41
+ markdown = re.sub(r"(?m)^\s*### ", "##### ", markdown)
42
+ markdown = markdown.replace("# Relevant sources", "")
43
+ markdown = markdown.replace("# Proposed answer", "#### Proposed answer")
44
+
45
+ tokens = md.parse(markdown)
46
+
47
+ elements = []
48
+ current_section = {}
49
+ in_list = False
50
+ bullet_list_indent_level = 0
51
+ list_item_open = False
52
+
53
+ for token in tokens:
54
+ if token.type == "bullet_list_open":
55
+ in_list = True
56
+ bullet_list_indent_level = int(token.level / 2)
57
+
58
+ elif token.type == "bullet_list_close":
59
+ in_list = False
60
+ bullet_list_indent_level = 0
61
+
62
+ elif token.type == "list_item_open":
63
+ if list_item_open:
64
+ elements.append(current_section)
65
+ list_item_open = True
66
+ list_level = token.level - bullet_list_indent_level
67
+ current_section = {
68
+ "type": RunsField,
69
+ "runs": [],
70
+ "is_list_item": True,
71
+ "level": list_level,
72
+ }
73
+
74
+ elif token.type == "list_item_close":
75
+ if current_section and current_section.get("runs"):
76
+ elements.append(current_section)
77
+ current_section = {}
78
+ list_item_open = False
79
+
80
+ if token.type == "heading_open":
81
+ # Heading start, token.tag gives the level (e.g., 'h1', 'h2', etc.)
82
+ header_lvl = int(token.tag[1]) # Extract the level number from tag
83
+ current_section = {
84
+ "type": HeadingField,
85
+ "text": "",
86
+ "level": header_lvl + offset_header_lvl,
87
+ }
88
+
89
+ elif token.type == "heading_close":
90
+ if current_section:
91
+ elements.append(current_section)
92
+ current_section = {}
93
+
94
+ elif token.type == "paragraph_open":
95
+ if not in_list: # Only create new paragraph if not in a list
96
+ current_section = {"type": RunsField, "runs": []}
97
+
98
+ elif token.type == "paragraph_close":
99
+ if not in_list and current_section: # Only append if not in a list
100
+ elements.append(current_section)
101
+ current_section = {}
102
+
103
+ elif token.type == "inline":
104
+ if current_section.get("type") == HeadingField:
105
+ content = token.content
106
+ if content.startswith("_page"):
107
+ # replace "_pageXXXX_" with "PageXXXX", where XXXX can be any characters and numbers
108
+ content = re.sub(
109
+ r"^_page([a-zA-Z0-9\s-]+)_(.*?)",
110
+ r"Page\1",
111
+ content,
112
+ )
113
+ bold = True
114
+ current_section["text"] += content
115
+ elif "runs" in current_section:
116
+ bold = False
117
+ italic = False
118
+ runs = []
119
+ if token.children:
120
+ for child in token.children:
121
+ content = child.content
122
+ if child.type == "strong_open":
123
+ bold = True
124
+ elif child.type == "strong_close":
125
+ bold = False
126
+ elif child.type == "em_open":
127
+ italic = True
128
+ elif child.type == "em_close":
129
+ italic = False
130
+ if child.type == "softbreak":
131
+ content += "\n"
132
+ if content: # Only add non-empty content
133
+ runs.append(
134
+ RunField(
135
+ text=content,
136
+ bold=bold,
137
+ italic=italic,
138
+ )
139
+ )
140
+ else:
141
+ runs.append(
142
+ RunField(
143
+ text=token.content,
144
+ bold=bold,
145
+ italic=italic,
146
+ )
147
+ )
148
+
149
+ current_section["runs"].extend(runs)
150
+
151
+ # Process remaining elements
152
+ contents = []
153
+ for element in elements:
154
+ if not element:
155
+ continue
156
+ if element["type"] == HeadingField:
157
+ contents.append(
158
+ HeadingField(
159
+ text=element["text"],
160
+ level=element["level"],
161
+ )
162
+ )
163
+ elif element["type"] == RunsField:
164
+ if element.get("is_list_item", False):
165
+ level: int = min(element.get("level", 1), 5)
166
+ if level > 1:
167
+ style = "List Bullet " + str(level)
168
+ else:
169
+ style = "List Bullet"
170
+ contents.append(RunsField(style=style, runs=element["runs"]))
171
+ else:
172
+ contents.append(RunsField(runs=element["runs"]))
173
+
174
+ return contents
175
+
176
+ def generate_from_template(
177
+ self,
178
+ subdoc_content: list[HeadingField | ParagraphField | RunsField],
179
+ fields: dict | None = None,
180
+ ):
181
+ """
182
+ Generate a docx file from a template with the given content.
183
+
184
+ Args:
185
+ subdoc_content (list[HeadingField | ParagraphField | RunsField]): The content to be added to the docx file.
186
+ fields (dict): Other fields to be added to the docx file. Defaults to None.
187
+ """
188
+ doc = DocxTemplate(io.BytesIO(self.template))
189
+
190
+ try:
191
+ content = {}
192
+ content["body"] = ContentField(contents=subdoc_content)
193
+
194
+ if fields:
195
+ content.update(fields)
196
+
197
+ for key, value in content.items():
198
+ if isinstance(value, ContentField):
199
+ content[key] = value.add(doc)
200
+
201
+ doc.render(content)
202
+ docx_rendered_object = io.BytesIO()
203
+
204
+ doc.save(docx_rendered_object)
205
+ docx_rendered_object.seek(0)
206
+
207
+ return docx_rendered_object.getvalue()
208
+
209
+ except Exception as e:
210
+ _LOGGER.error(f"Error generating docx: {e}")
211
+ return None
212
+
213
+ def _get_default_template(self) -> bytes:
214
+ generator_dir_path = Path(__file__).resolve().parent
215
+ path = generator_dir_path / "template" / "Doc Template.docx"
216
+
217
+ file_content = path.read_bytes()
218
+
219
+ _LOGGER.info("Template downloaded from default template")
220
+
221
+ return file_content
222
+
223
+ @property
224
+ def template(self) -> bytes:
225
+ return self._template or self._get_default_template()
@@ -0,0 +1,368 @@
1
+ """
2
+ This module provides a minimal framework for building endpoint classes such that a client can use
3
+ the endpoints without having to know the details of the endpoints.
4
+ """
5
+
6
+ import warnings
7
+ from enum import StrEnum
8
+ from string import Template
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Generic,
13
+ ParamSpec,
14
+ Protocol,
15
+ TypeVar,
16
+ cast,
17
+ )
18
+
19
+ from pydantic import BaseModel, ValidationError
20
+
21
+ # Paramspecs
22
+ PayloadParamSpec = ParamSpec("PayloadParamSpec")
23
+ PathParamsSpec = ParamSpec("PathParamsSpec")
24
+ QueryParamsSpec = ParamSpec("QueryParamsSpec")
25
+
26
+ # Type variables
27
+ ResponseType = TypeVar("ResponseType", bound=BaseModel, covariant=True)
28
+ PathParamsType = TypeVar("PathParamsType", bound=BaseModel)
29
+ PayloadType = TypeVar("PayloadType", bound=BaseModel)
30
+ QueryParamsType = TypeVar("QueryParamsType", bound=BaseModel)
31
+
32
+ # Helper type to extract constructor parameters
33
+
34
+ # Type for the constructor of a Pydantic model
35
+ ModelConstructor = Callable[..., BaseModel]
36
+
37
+
38
+ class HttpMethods(StrEnum):
39
+ GET = "GET"
40
+ POST = "POST"
41
+ PUT = "PUT"
42
+ DELETE = "DELETE"
43
+ PATCH = "PATCH"
44
+ OPTIONS = "OPTIONS"
45
+ HEAD = "HEAD"
46
+
47
+
48
+ # Backward compatibility TODO: Remove in 2.0.0.
49
+ EndpointMethods = HttpMethods
50
+
51
+
52
+ class ResponseValidationException(Exception):
53
+ """
54
+ This exception is raised when the response validation fails.
55
+ """
56
+
57
+ def __init__(
58
+ self, operation_name: str, response: dict[str, Any], pydantic_error: str
59
+ ):
60
+ super().__init__(
61
+ f"Response validation failed for operation {operation_name}\n"
62
+ f"Response: {response}\n"
63
+ f"Pydantic error: {pydantic_error}"
64
+ )
65
+ self._operation_name = operation_name
66
+ self._response = response
67
+ self._pydantic_error = pydantic_error
68
+
69
+ @property
70
+ def operation_name(self) -> str:
71
+ return self._operation_name
72
+
73
+ @property
74
+ def response(self) -> dict[str, Any]:
75
+ return self._response
76
+
77
+ @property
78
+ def pydantic_error(self) -> str:
79
+ return self._pydantic_error
80
+
81
+ def __str__(self):
82
+ return (
83
+ f"Response validation failed for {self._operation_name}\n"
84
+ + f"Response: {self._response}"
85
+ )
86
+
87
+
88
+ class ApiOperationProtocol(
89
+ Protocol,
90
+ Generic[
91
+ PathParamsSpec,
92
+ PathParamsType,
93
+ PayloadParamSpec,
94
+ PayloadType,
95
+ QueryParamsSpec,
96
+ QueryParamsType,
97
+ ResponseType,
98
+ ],
99
+ ):
100
+ @staticmethod
101
+ def path_dump_options() -> dict[str, Any]: ...
102
+
103
+ @staticmethod
104
+ def path_params_model() -> type[PathParamsType]: ...
105
+
106
+ @staticmethod
107
+ def payload_dump_options() -> dict[str, Any]: ...
108
+
109
+ @staticmethod
110
+ def payload_model() -> type[PayloadType]: ...
111
+
112
+ @staticmethod
113
+ def response_validate_options() -> dict[str, Any]: ...
114
+
115
+ @staticmethod
116
+ def response_model() -> type[ResponseType]: ...
117
+
118
+ @staticmethod
119
+ def query_params_dump_options() -> dict[str, Any]: ...
120
+
121
+ @staticmethod
122
+ def query_params_model() -> type[QueryParamsType]: ...
123
+
124
+ @staticmethod
125
+ def create_path(
126
+ *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
127
+ ) -> str: ...
128
+
129
+ @staticmethod
130
+ def create_path_from_model(
131
+ path_params: PathParamsType, *, model_dump_options: dict | None = None
132
+ ) -> str: ...
133
+
134
+ @staticmethod
135
+ def create_payload(
136
+ *args: PayloadParamSpec.args, **kwargs: PayloadParamSpec.kwargs
137
+ ) -> dict[str, Any]: ...
138
+
139
+ @staticmethod
140
+ def create_payload_from_model(
141
+ payload: PayloadType, *, model_dump_options: dict | None = None
142
+ ) -> dict[str, Any]: ...
143
+
144
+ @staticmethod
145
+ def create_query_params(
146
+ *args: QueryParamsSpec.args, **kwargs: QueryParamsSpec.kwargs
147
+ ) -> dict[str, Any]: ...
148
+
149
+ @staticmethod
150
+ def create_query_params_from_model(
151
+ query_params: QueryParamsType, *, model_dump_options: dict | None = None
152
+ ) -> dict[str, Any]: ...
153
+
154
+ @staticmethod
155
+ def handle_response(
156
+ response: dict[str, Any], *, model_validate_options: dict | None = None
157
+ ) -> ResponseType: ...
158
+
159
+ @staticmethod
160
+ def request_method() -> EndpointMethods: ...
161
+
162
+ @staticmethod
163
+ def models_from_combined(
164
+ combined: dict[str, Any],
165
+ ) -> tuple[PathParamsType, PayloadType, QueryParamsType]: ...
166
+
167
+
168
+ class EmptyModel(BaseModel): ...
169
+
170
+
171
+ # Model for any client to implement
172
+ def build_api_operation(
173
+ *,
174
+ method: HttpMethods,
175
+ path_template: Template,
176
+ response_model_type: type[ResponseType],
177
+ path_params_constructor: Callable[PathParamsSpec, PathParamsType] = EmptyModel,
178
+ payload_dump_options: dict | None = None,
179
+ payload_constructor: Callable[PayloadParamSpec, PayloadType] = EmptyModel,
180
+ path_dump_options: dict | None = None,
181
+ query_params_constructor: Callable[QueryParamsSpec, QueryParamsType] = EmptyModel,
182
+ query_params_dump_options: dict | None = None,
183
+ response_validate_options: dict | None = None,
184
+ dump_options: dict | None = None, # Deprecated
185
+ ) -> type[
186
+ ApiOperationProtocol[
187
+ PathParamsSpec,
188
+ PathParamsType,
189
+ PayloadParamSpec,
190
+ PayloadType,
191
+ QueryParamsSpec,
192
+ QueryParamsType,
193
+ ResponseType,
194
+ ]
195
+ ]:
196
+ """Generate a class with static methods for endpoint handling.
197
+
198
+ Uses separate models for path parameters and request body for clean API design.
199
+
200
+ Returns a class with static methods:
201
+ - create_url: Creates URL from path parameters
202
+ - create_payload: Creates request body payload
203
+ """
204
+
205
+ # Verify that the path_params_constructor and payload_constructor are valid pydantic models
206
+ if not dump_options:
207
+ dump_options = {
208
+ "exclude_unset": True,
209
+ "by_alias": True,
210
+ "exclude_defaults": True,
211
+ }
212
+ else:
213
+ warnings.warn(
214
+ "dump_options is deprecated. Use payload_dump_options instead.",
215
+ DeprecationWarning,
216
+ )
217
+
218
+ class Operation(ApiOperationProtocol):
219
+ @staticmethod
220
+ def path_dump_options() -> dict[str, Any]:
221
+ return path_dump_options or {}
222
+
223
+ @staticmethod
224
+ def path_params_model() -> type[PathParamsType]:
225
+ return cast(type[PathParamsType], path_params_constructor)
226
+
227
+ @staticmethod
228
+ def payload_dump_options() -> dict[str, Any]:
229
+ return payload_dump_options or {}
230
+
231
+ @staticmethod
232
+ def payload_model() -> type[PayloadType]:
233
+ return cast(type[PayloadType], payload_constructor)
234
+
235
+ @staticmethod
236
+ def query_params_dump_options() -> dict[str, Any]:
237
+ return query_params_dump_options or {}
238
+
239
+ @staticmethod
240
+ def query_params_model() -> type[QueryParamsType]:
241
+ return cast(type[QueryParamsType], query_params_constructor)
242
+
243
+ @staticmethod
244
+ def response_validate_options() -> dict[str, Any]:
245
+ return response_validate_options or {}
246
+
247
+ @staticmethod
248
+ def response_model() -> type[ResponseType]:
249
+ return response_model_type
250
+
251
+ @staticmethod
252
+ def path_template() -> Template:
253
+ return path_template
254
+
255
+ @staticmethod
256
+ def create_path_from_model(
257
+ path_params: PathParamsType, *, model_dump_options: dict | None = None
258
+ ) -> str:
259
+ if model_dump_options is None:
260
+ if path_dump_options is None:
261
+ model_dump_options = dump_options
262
+ else:
263
+ model_dump_options = path_dump_options
264
+
265
+ return path_template.substitute(
266
+ **path_params.model_dump(**model_dump_options)
267
+ )
268
+
269
+ @staticmethod
270
+ def create_path(
271
+ *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
272
+ ) -> str:
273
+ model = Operation.path_params_model()(*args, **kwargs)
274
+ return Operation.create_path_from_model(model)
275
+
276
+ @staticmethod
277
+ def create_payload(
278
+ *args: PayloadParamSpec.args,
279
+ **kwargs: PayloadParamSpec.kwargs,
280
+ ) -> dict[str, Any]:
281
+ """Create request body payload."""
282
+ if payload_dump_options is None:
283
+ model_dump_options = dump_options
284
+ else:
285
+ model_dump_options = payload_dump_options
286
+
287
+ request_model = Operation.payload_model()(*args, **kwargs)
288
+ return request_model.model_dump(**model_dump_options)
289
+
290
+ @staticmethod
291
+ def create_payload_from_model(
292
+ payload: PayloadType, *, model_dump_options: dict | None = None
293
+ ) -> dict[str, Any]:
294
+ if model_dump_options is None:
295
+ if payload_dump_options is None:
296
+ model_dump_options = dump_options
297
+ else:
298
+ model_dump_options = payload_dump_options
299
+
300
+ return payload.model_dump(**model_dump_options)
301
+
302
+ @staticmethod
303
+ def create_query_params(
304
+ *args: QueryParamsSpec.args, **kwargs: QueryParamsSpec.kwargs
305
+ ) -> dict[str, Any]:
306
+ if query_params_dump_options is None:
307
+ model_dump_options = dump_options
308
+ else:
309
+ model_dump_options = query_params_dump_options
310
+
311
+ return query_params_constructor(*args, **kwargs).model_dump(
312
+ **model_dump_options
313
+ )
314
+
315
+ @staticmethod
316
+ def create_query_params_from_model(
317
+ query_params: QueryParamsType, *, model_dump_options: dict | None = None
318
+ ) -> dict[str, Any]:
319
+ if model_dump_options is None:
320
+ if query_params_dump_options is None:
321
+ model_dump_options = dump_options
322
+ else:
323
+ model_dump_options = query_params_dump_options
324
+
325
+ return query_params.model_dump(**model_dump_options)
326
+
327
+ @staticmethod
328
+ def handle_response(
329
+ response: dict[str, Any],
330
+ *,
331
+ model_validate_options: dict[str, Any] | None = None,
332
+ ) -> ResponseType:
333
+ if model_validate_options is None:
334
+ if response_validate_options is None:
335
+ model_validate_options = {}
336
+ else:
337
+ model_validate_options = response_validate_options
338
+ try:
339
+ return Operation.response_model().model_validate(
340
+ response, **model_validate_options
341
+ )
342
+ except ValidationError as e:
343
+ raise ResponseValidationException(
344
+ operation_name=Operation.__name__,
345
+ response=response,
346
+ pydantic_error=str(e),
347
+ ) from e
348
+
349
+ @staticmethod
350
+ def request_method() -> HttpMethods:
351
+ return method
352
+
353
+ @staticmethod
354
+ def models_from_combined(
355
+ combined: dict[str, Any],
356
+ ) -> tuple[PathParamsType, PayloadType, QueryParamsType]:
357
+ path_params = Operation.path_params_model().model_validate(
358
+ combined, by_alias=True, by_name=True
359
+ )
360
+ payload = Operation.payload_model().model_validate(
361
+ combined, by_alias=True, by_name=True
362
+ )
363
+ query_params = Operation.query_params_model().model_validate(
364
+ combined, by_alias=True, by_name=True
365
+ )
366
+ return path_params, payload, query_params
367
+
368
+ return Operation