pyopenapi-gen 0.8.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.
- pyopenapi_gen/__init__.py +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +30 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,630 @@
|
|
1
|
+
"""
|
2
|
+
RenderContext: Central context manager for Python code generation.
|
3
|
+
|
4
|
+
This module provides the RenderContext class, which serves as the central state
|
5
|
+
management object during code generation. It tracks imports, generated modules,
|
6
|
+
and the current file being processed, ensuring proper relative/absolute import
|
7
|
+
handling and maintaining consistent state across the generation process.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import logging
|
11
|
+
import os
|
12
|
+
import re
|
13
|
+
import sys
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import Dict, Optional, Set
|
16
|
+
|
17
|
+
from pyopenapi_gen import IRSchema
|
18
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
19
|
+
|
20
|
+
from .file_manager import FileManager
|
21
|
+
from .import_collector import ImportCollector
|
22
|
+
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
class RenderContext:
|
27
|
+
"""
|
28
|
+
Central context manager for tracking state during code generation.
|
29
|
+
|
30
|
+
This class serves as the primary state container during code generation,
|
31
|
+
managing imports, tracking generated modules, and calculating import paths.
|
32
|
+
All imports are stored as absolute (package-root-relative) module paths internally
|
33
|
+
and converted to appropriate relative or absolute imports at render time.
|
34
|
+
|
35
|
+
Attributes:
|
36
|
+
file_manager: Utility for writing files to disk
|
37
|
+
import_collector: Manages imports for the current file being rendered
|
38
|
+
generated_modules: Set of absolute paths to modules generated in this run
|
39
|
+
current_file: Absolute path of the file currently being rendered
|
40
|
+
core_package_name: The full Python import path of the core package (e.g., "custom_core", "shared.my_core").
|
41
|
+
package_root_for_generated_code: Absolute path to the root of the *currently emitting* package
|
42
|
+
(e.g., project_root/client_api or project_root/custom_core if emitting core
|
43
|
+
itself). Used for calculating relative paths *within* this package.
|
44
|
+
overall_project_root: Absolute path to the top-level project.
|
45
|
+
Used as the base for resolving absolute Python import paths,
|
46
|
+
especially for an external core_package.
|
47
|
+
parsed_schemas: Optional dictionary of all parsed IRSchema objects, keyed by their original names.
|
48
|
+
conditional_imports: Dictionary of conditional imports (e.g., under TYPE_CHECKING)
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
file_manager: Optional[FileManager] = None,
|
54
|
+
core_package_name: str = "core",
|
55
|
+
package_root_for_generated_code: Optional[str] = None,
|
56
|
+
overall_project_root: Optional[str] = None,
|
57
|
+
parsed_schemas: Optional[Dict[str, IRSchema]] = None,
|
58
|
+
use_absolute_imports: bool = True,
|
59
|
+
output_package_name: Optional[str] = None,
|
60
|
+
) -> None:
|
61
|
+
"""
|
62
|
+
Initialize a new RenderContext.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
file_manager: Utility for file operations (defaults to a new FileManager)
|
66
|
+
core_package_name: The full Python import path of the core package (e.g., "custom_core", "shared.my_core").
|
67
|
+
package_root_for_generated_code: Absolute path to the root of the *currently emitting* package
|
68
|
+
(e.g., project_root/client_api or project_root/custom_core if emitting core
|
69
|
+
itself). Used for calculating relative paths *within* this package.
|
70
|
+
overall_project_root: Absolute path to the top-level project.
|
71
|
+
Used as the base for resolving absolute Python import paths,
|
72
|
+
especially for an external core_package.
|
73
|
+
parsed_schemas: Optional dictionary of all parsed IRSchema objects.
|
74
|
+
use_absolute_imports: Whether to use absolute imports instead of relative imports for internal modules.
|
75
|
+
output_package_name: The full output package name (e.g., "pyapis.business") for generating absolute imports.
|
76
|
+
"""
|
77
|
+
self.file_manager = file_manager or FileManager()
|
78
|
+
self.import_collector = ImportCollector()
|
79
|
+
self.generated_modules: Set[str] = set()
|
80
|
+
self.current_file: Optional[str] = None
|
81
|
+
self.core_package_name: str = core_package_name
|
82
|
+
self.package_root_for_generated_code: Optional[str] = package_root_for_generated_code
|
83
|
+
self.overall_project_root: Optional[str] = overall_project_root or os.getcwd()
|
84
|
+
self.parsed_schemas: Optional[Dict[str, IRSchema]] = parsed_schemas
|
85
|
+
self.use_absolute_imports: bool = use_absolute_imports
|
86
|
+
self.output_package_name: Optional[str] = output_package_name
|
87
|
+
# Dictionary to store conditional imports, keyed by condition
|
88
|
+
self.conditional_imports: Dict[str, Dict[str, Set[str]]] = {}
|
89
|
+
|
90
|
+
def set_current_file(self, abs_path: str) -> None:
|
91
|
+
"""
|
92
|
+
Set the absolute path of the file currently being rendered.
|
93
|
+
|
94
|
+
This method also resets the import collector and immediately re-initializes
|
95
|
+
its file context for subsequent import additions.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
abs_path: The absolute path of the file to set as current
|
99
|
+
"""
|
100
|
+
self.current_file = abs_path
|
101
|
+
# Reset the import collector for each new file to ensure isolation
|
102
|
+
self.import_collector.reset()
|
103
|
+
|
104
|
+
# Immediately set the new context on the import_collector
|
105
|
+
current_module_dot_path = self.get_current_module_dot_path()
|
106
|
+
package_root_for_collector = self.get_current_package_name_for_generated_code()
|
107
|
+
|
108
|
+
self.import_collector.set_current_file_context_for_rendering(
|
109
|
+
current_module_dot_path=current_module_dot_path,
|
110
|
+
package_root=package_root_for_collector,
|
111
|
+
core_package_name_for_absolute_treatment=self.core_package_name,
|
112
|
+
)
|
113
|
+
|
114
|
+
def add_import(self, logical_module: str, name: Optional[str] = None, is_typing_import: bool = False) -> None:
|
115
|
+
"""
|
116
|
+
Add an import to the collector.
|
117
|
+
|
118
|
+
- Core package imports are always absolute using `core_package_name`.
|
119
|
+
- Standard library imports are absolute.
|
120
|
+
- Other internal package imports are made relative if possible.
|
121
|
+
- Unknown modules are treated as absolute external imports.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
logical_module: The logical module path to import from (e.g., "typing",
|
125
|
+
"shared_core.http_transport", "generated_client.models.mymodel",
|
126
|
+
"some_external_lib.api").
|
127
|
+
For internal modules, this should be the fully qualified path from project root.
|
128
|
+
name: The name to import from the module
|
129
|
+
is_typing_import: Whether the import is a typing import
|
130
|
+
"""
|
131
|
+
if not logical_module:
|
132
|
+
return
|
133
|
+
|
134
|
+
# Fix incomplete module paths for absolute imports
|
135
|
+
if self.use_absolute_imports and self.output_package_name:
|
136
|
+
# Detect incomplete paths like "business.models.agent" that should be "pyapis.business.models.agent"
|
137
|
+
root_package = self.output_package_name.split(".")[0] # "pyapis" from "pyapis.business"
|
138
|
+
package_suffix = ".".join(self.output_package_name.split(".")[1:]) # "business" from "pyapis.business"
|
139
|
+
|
140
|
+
# Check if this is an incomplete internal module path
|
141
|
+
if package_suffix and logical_module.startswith(f"{package_suffix}."):
|
142
|
+
# This is an incomplete path like "business.models.agent"
|
143
|
+
# Convert to complete path like "pyapis.business.models.agent"
|
144
|
+
logical_module = f"{root_package}.{logical_module}"
|
145
|
+
|
146
|
+
# 1. Special handling for typing imports if is_typing_import is True
|
147
|
+
if is_typing_import and logical_module == "typing" and name:
|
148
|
+
self.import_collector.add_typing_import(name)
|
149
|
+
return
|
150
|
+
|
151
|
+
# 2. Core module import?
|
152
|
+
is_target_in_core_pkg_namespace = logical_module == self.core_package_name or logical_module.startswith(
|
153
|
+
self.core_package_name + "."
|
154
|
+
)
|
155
|
+
if is_target_in_core_pkg_namespace:
|
156
|
+
if name:
|
157
|
+
self.import_collector.add_import(module=logical_module, name=name)
|
158
|
+
else:
|
159
|
+
self.import_collector.add_plain_import(module=logical_module) # Core plain import
|
160
|
+
return
|
161
|
+
|
162
|
+
# 3. Stdlib/Builtin?
|
163
|
+
COMMON_STDLIB = {
|
164
|
+
"typing",
|
165
|
+
"os",
|
166
|
+
"sys",
|
167
|
+
"re",
|
168
|
+
"json",
|
169
|
+
"collections",
|
170
|
+
"datetime",
|
171
|
+
"enum",
|
172
|
+
"pathlib",
|
173
|
+
"abc",
|
174
|
+
"contextlib",
|
175
|
+
"functools",
|
176
|
+
"itertools",
|
177
|
+
"logging",
|
178
|
+
"math",
|
179
|
+
"decimal",
|
180
|
+
"dataclasses",
|
181
|
+
"asyncio",
|
182
|
+
"tempfile",
|
183
|
+
"subprocess",
|
184
|
+
"textwrap",
|
185
|
+
}
|
186
|
+
top_level_module = logical_module.split(".")[0]
|
187
|
+
if (
|
188
|
+
logical_module in sys.builtin_module_names
|
189
|
+
or logical_module in COMMON_STDLIB
|
190
|
+
or top_level_module in COMMON_STDLIB
|
191
|
+
):
|
192
|
+
if name:
|
193
|
+
self.import_collector.add_import(module=logical_module, name=name)
|
194
|
+
else:
|
195
|
+
self.import_collector.add_plain_import(module=logical_module) # Stdlib plain import
|
196
|
+
return
|
197
|
+
|
198
|
+
# 4. Known third-party?
|
199
|
+
KNOWN_THIRD_PARTY = {"httpx", "pydantic"}
|
200
|
+
if logical_module in KNOWN_THIRD_PARTY or top_level_module in KNOWN_THIRD_PARTY:
|
201
|
+
if name:
|
202
|
+
self.import_collector.add_import(module=logical_module, name=name)
|
203
|
+
else:
|
204
|
+
self.import_collector.add_plain_import(module=logical_module) # Third-party plain import
|
205
|
+
return
|
206
|
+
|
207
|
+
# 5. Internal to current generated package?
|
208
|
+
current_gen_package_name_str = self.get_current_package_name_for_generated_code()
|
209
|
+
|
210
|
+
is_internal_module_candidate = False # Initialize here
|
211
|
+
if current_gen_package_name_str:
|
212
|
+
if logical_module == current_gen_package_name_str: # e.g. importing current_gen_package_name_str itself
|
213
|
+
is_internal_module_candidate = True
|
214
|
+
elif logical_module.startswith(current_gen_package_name_str + "."):
|
215
|
+
is_internal_module_candidate = True
|
216
|
+
|
217
|
+
if is_internal_module_candidate:
|
218
|
+
# It looks like an internal module.
|
219
|
+
# First, check if it's a direct self-import of the full logical path.
|
220
|
+
current_full_module_path = self.get_current_module_dot_path()
|
221
|
+
if current_full_module_path == logical_module:
|
222
|
+
return # Skip if it's a direct self-import
|
223
|
+
|
224
|
+
# Determine module path relative to current generated package root
|
225
|
+
module_relative_to_gen_pkg_root: str
|
226
|
+
if logical_module == current_gen_package_name_str: # Importing the root package itself
|
227
|
+
# This case should likely be handled by calculate_relative_path based on current file
|
228
|
+
# For now, let's treat it as a root module, and calculate_relative_path will see if it needs dots
|
229
|
+
module_relative_to_gen_pkg_root = logical_module # This might be too simplistic for root pkg itself
|
230
|
+
elif current_gen_package_name_str: # Should be true due to is_internal_module_candidate
|
231
|
+
module_relative_to_gen_pkg_root = logical_module[len(current_gen_package_name_str) + 1 :]
|
232
|
+
else: # Should not happen if current_gen_package_name_str was required for is_internal_module_candidate
|
233
|
+
module_relative_to_gen_pkg_root = logical_module
|
234
|
+
|
235
|
+
# For internal modules, try to use relative imports when appropriate
|
236
|
+
relative_path = self.calculate_relative_path_for_internal_module(module_relative_to_gen_pkg_root)
|
237
|
+
|
238
|
+
if relative_path:
|
239
|
+
# Use relative imports for internal modules when possible
|
240
|
+
if name is None:
|
241
|
+
return
|
242
|
+
self.import_collector.add_relative_import(relative_path, name)
|
243
|
+
return
|
244
|
+
else:
|
245
|
+
# Fall back to absolute imports for internal modules that can't use relative paths
|
246
|
+
if name:
|
247
|
+
self.import_collector.add_import(module=logical_module, name=name)
|
248
|
+
else:
|
249
|
+
self.import_collector.add_plain_import(module=logical_module)
|
250
|
+
return
|
251
|
+
|
252
|
+
# 6. Default: External library, add as absolute.
|
253
|
+
if name:
|
254
|
+
self.import_collector.add_import(module=logical_module, name=name)
|
255
|
+
else:
|
256
|
+
# If name is None, it's a plain import like 'import os'
|
257
|
+
self.import_collector.add_plain_import(module=logical_module)
|
258
|
+
|
259
|
+
def mark_generated_module(self, abs_module_path: str) -> None:
|
260
|
+
"""
|
261
|
+
Mark a module as generated in this run.
|
262
|
+
This helps in determining if an import is for a locally generated module.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
abs_module_path: The absolute path of the generated module
|
266
|
+
"""
|
267
|
+
self.generated_modules.add(abs_module_path)
|
268
|
+
|
269
|
+
def add_conditional_import(self, condition: str, module: str, name: str) -> None:
|
270
|
+
"""
|
271
|
+
Add a conditional import (e.g., under TYPE_CHECKING).
|
272
|
+
|
273
|
+
Args:
|
274
|
+
condition: The condition for the import (e.g., "TYPE_CHECKING")
|
275
|
+
module: The module to import from
|
276
|
+
name: The name to import
|
277
|
+
"""
|
278
|
+
# Apply the same unified import path correction logic as add_import
|
279
|
+
logical_module = module
|
280
|
+
|
281
|
+
# Fix incomplete module paths for absolute imports
|
282
|
+
if self.use_absolute_imports and self.output_package_name:
|
283
|
+
# Detect incomplete paths like "business.models.agent" that should be "pyapis.business.models.agent"
|
284
|
+
root_package = self.output_package_name.split(".")[0] # "pyapis" from "pyapis.business"
|
285
|
+
package_suffix = ".".join(self.output_package_name.split(".")[1:]) # "business" from "pyapis.business"
|
286
|
+
|
287
|
+
# Check if this is an incomplete internal module path
|
288
|
+
if package_suffix and logical_module.startswith(f"{package_suffix}."):
|
289
|
+
# This is an incomplete path like "business.models.agent"
|
290
|
+
# Convert to complete path like "pyapis.business.models.agent"
|
291
|
+
logical_module = f"{root_package}.{logical_module}"
|
292
|
+
|
293
|
+
if condition not in self.conditional_imports:
|
294
|
+
self.conditional_imports[condition] = {}
|
295
|
+
if logical_module not in self.conditional_imports[condition]:
|
296
|
+
self.conditional_imports[condition][logical_module] = set()
|
297
|
+
self.conditional_imports[condition][logical_module].add(name)
|
298
|
+
|
299
|
+
def render_imports(self) -> str:
|
300
|
+
"""
|
301
|
+
Render all imports for the current file, including conditional imports.
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
A string containing all import statements.
|
305
|
+
"""
|
306
|
+
# Get standard imports
|
307
|
+
regular_imports = self.import_collector.get_formatted_imports()
|
308
|
+
|
309
|
+
# Handle conditional imports
|
310
|
+
conditional_imports = []
|
311
|
+
has_type_checking_imports = False
|
312
|
+
|
313
|
+
for condition, imports in self.conditional_imports.items():
|
314
|
+
if imports:
|
315
|
+
# Check if this uses TYPE_CHECKING
|
316
|
+
if condition == "TYPE_CHECKING":
|
317
|
+
has_type_checking_imports = True
|
318
|
+
|
319
|
+
# Start the conditional block
|
320
|
+
conditional_block = [f"\nif {condition}:"]
|
321
|
+
|
322
|
+
# Add each import under the condition
|
323
|
+
for module, names in sorted(imports.items()):
|
324
|
+
names_str = ", ".join(sorted(names))
|
325
|
+
conditional_block.append(f" from {module} import {names_str}")
|
326
|
+
|
327
|
+
conditional_imports.append("\n".join(conditional_block))
|
328
|
+
|
329
|
+
# Add TYPE_CHECKING import if needed but not already present
|
330
|
+
if has_type_checking_imports and not self.import_collector.has_import("typing", "TYPE_CHECKING"):
|
331
|
+
self.import_collector.add_typing_import("TYPE_CHECKING")
|
332
|
+
# Re-generate regular imports to include TYPE_CHECKING
|
333
|
+
regular_imports = self.import_collector.get_formatted_imports()
|
334
|
+
|
335
|
+
# Combine all imports
|
336
|
+
all_imports = regular_imports
|
337
|
+
if conditional_imports:
|
338
|
+
all_imports += "\n" + "\n".join(conditional_imports)
|
339
|
+
|
340
|
+
return all_imports
|
341
|
+
|
342
|
+
def add_typing_imports_for_type(self, type_str: str) -> None:
|
343
|
+
"""
|
344
|
+
Add necessary typing imports for a given type string.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
type_str: The type string to parse for typing imports
|
348
|
+
"""
|
349
|
+
# Handle datetime.date and datetime.datetime explicitly
|
350
|
+
# Regex to find "datetime.date" or "datetime.datetime" as whole words
|
351
|
+
datetime_specific_matches = re.findall(r"\b(datetime\.(?:date|datetime))\b", type_str)
|
352
|
+
for dt_match in datetime_specific_matches:
|
353
|
+
module_name, class_name = dt_match.split(".")
|
354
|
+
self.add_import(module_name, class_name, is_typing_import=False)
|
355
|
+
|
356
|
+
# Remove datetime.xxx parts to avoid matching 'date' or 'datetime' as typing members
|
357
|
+
type_str_for_typing_search = re.sub(r"\bdatetime\.(?:date|datetime)\b", "", type_str)
|
358
|
+
|
359
|
+
# General regex for other potential typing names (words)
|
360
|
+
all_words_in_type_str = re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\b", type_str_for_typing_search)
|
361
|
+
potential_names_to_import = set(all_words_in_type_str)
|
362
|
+
|
363
|
+
# Names that were part of datetime.date or datetime.datetime (e.g., "date", "datetime")
|
364
|
+
# These should not be re-imported from typing or as models if already handled by specific datetime import
|
365
|
+
handled_datetime_parts = set()
|
366
|
+
for dt_match in datetime_specific_matches: # e.g., "datetime.date"
|
367
|
+
parts = dt_match.split(".") # ["datetime", "date"]
|
368
|
+
handled_datetime_parts.update(parts)
|
369
|
+
|
370
|
+
known_typing_constructs = {
|
371
|
+
"List",
|
372
|
+
"Optional",
|
373
|
+
"Dict",
|
374
|
+
"Set",
|
375
|
+
"Tuple",
|
376
|
+
"Union",
|
377
|
+
"Any",
|
378
|
+
"AsyncIterator",
|
379
|
+
"Iterator",
|
380
|
+
"Sequence",
|
381
|
+
"Mapping",
|
382
|
+
"Type",
|
383
|
+
"Literal",
|
384
|
+
"TypedDict",
|
385
|
+
"DefaultDict",
|
386
|
+
"Deque",
|
387
|
+
"Counter",
|
388
|
+
"ChainMap",
|
389
|
+
"NoReturn",
|
390
|
+
"Generator",
|
391
|
+
"Awaitable",
|
392
|
+
"Callable",
|
393
|
+
"Protocol",
|
394
|
+
"runtime_checkable",
|
395
|
+
"Self",
|
396
|
+
"ClassVar",
|
397
|
+
"Final",
|
398
|
+
"Required",
|
399
|
+
"NotRequired",
|
400
|
+
"Annotated",
|
401
|
+
"TypeGuard",
|
402
|
+
"SupportsIndex",
|
403
|
+
"SupportsAbs",
|
404
|
+
"SupportsBytes",
|
405
|
+
"SupportsComplex",
|
406
|
+
"SupportsFloat",
|
407
|
+
"SupportsInt",
|
408
|
+
"SupportsRound",
|
409
|
+
"TypeAlias",
|
410
|
+
}
|
411
|
+
|
412
|
+
for name in potential_names_to_import:
|
413
|
+
if name in handled_datetime_parts and name in {"date", "datetime"}:
|
414
|
+
# If 'date' or 'datetime' were part of 'datetime.date' or 'datetime.datetime'
|
415
|
+
# they were handled by add_import(module_name, class_name) earlier. Skip further processing.
|
416
|
+
continue
|
417
|
+
|
418
|
+
if name in known_typing_constructs:
|
419
|
+
self.add_import("typing", name, is_typing_import=True)
|
420
|
+
continue # Successfully handled as a typing import
|
421
|
+
|
422
|
+
# Check if 'name' is a known schema (model)
|
423
|
+
if self.parsed_schemas:
|
424
|
+
found_schema_obj = None
|
425
|
+
# schema_obj.name is the Python class name (e.g., VectorDatabase)
|
426
|
+
for schema_obj in self.parsed_schemas.values():
|
427
|
+
if schema_obj.name == name:
|
428
|
+
found_schema_obj = schema_obj
|
429
|
+
break
|
430
|
+
|
431
|
+
if found_schema_obj:
|
432
|
+
# Use the generation_name if available, fallback to name
|
433
|
+
schema_class_name = found_schema_obj.generation_name or found_schema_obj.name
|
434
|
+
if schema_class_name is None:
|
435
|
+
logger.warning(f"Skipping import generation for an unnamed schema: {found_schema_obj}")
|
436
|
+
continue # Skip to the next name if schema_class_name is None
|
437
|
+
|
438
|
+
# Use the final_module_stem if available, otherwise sanitize the class name
|
439
|
+
if found_schema_obj.final_module_stem:
|
440
|
+
schema_file_name_segment = found_schema_obj.final_module_stem
|
441
|
+
else:
|
442
|
+
# Fallback to sanitizing the class name
|
443
|
+
schema_file_name_segment = NameSanitizer.sanitize_filename(schema_class_name, suffix="")
|
444
|
+
|
445
|
+
current_gen_pkg_base_name = self.get_current_package_name_for_generated_code()
|
446
|
+
|
447
|
+
if current_gen_pkg_base_name:
|
448
|
+
# Models are typically in <current_gen_pkg_base_name>.models.<schema_file_name>
|
449
|
+
model_module_logical_path = f"{current_gen_pkg_base_name}.models.{schema_file_name_segment}"
|
450
|
+
|
451
|
+
current_rendering_module_logical_path = self.get_current_module_dot_path()
|
452
|
+
|
453
|
+
# Avoid self-importing if the model is defined in the current file being rendered
|
454
|
+
if current_rendering_module_logical_path != model_module_logical_path:
|
455
|
+
self.add_import(logical_module=model_module_logical_path, name=schema_class_name)
|
456
|
+
# else:
|
457
|
+
# logger.debug(f"Skipping import of {schema_class_name} from "
|
458
|
+
# f"{model_module_logical_path} as it's the current module.")
|
459
|
+
continue # Successfully handled (or skipped self-import) as a model import
|
460
|
+
else:
|
461
|
+
logger.warning(
|
462
|
+
f"Cannot determine current generated package name for schema '{schema_class_name}'. "
|
463
|
+
f"Import for it might be missing in {self.current_file}."
|
464
|
+
)
|
465
|
+
# Fall-through: if name is not a typing construct, not datetime part, and not a parsed schema,
|
466
|
+
# it will be ignored by this function. Other import mechanisms might handle it (e.g. primitives like 'str').
|
467
|
+
|
468
|
+
def add_plain_import(self, module: str) -> None:
|
469
|
+
"""Add a plain import statement (e.g., `import os`)."""
|
470
|
+
self.import_collector.add_plain_import(module)
|
471
|
+
|
472
|
+
def calculate_relative_path_for_internal_module(
|
473
|
+
self,
|
474
|
+
target_logical_module_relative_to_gen_pkg_root: str,
|
475
|
+
) -> str | None:
|
476
|
+
"""
|
477
|
+
Calculates a relative Python import path for a target module within the
|
478
|
+
currently generated package, given the current file being rendered.
|
479
|
+
|
480
|
+
Example:
|
481
|
+
current_file: /project/out_pkg/endpoints/tags_api.py
|
482
|
+
package_root_for_generated_code: /project/out_pkg
|
483
|
+
target_logical_module_relative_to_gen_pkg_root: "models.tag_model"
|
484
|
+
Returns: "..models.tag_model"
|
485
|
+
|
486
|
+
Args:
|
487
|
+
target_logical_module_relative_to_gen_pkg_root: The dot-separated path of the target module,
|
488
|
+
relative to the `package_root_for_generated_code` (e.g., "models.user").
|
489
|
+
|
490
|
+
Returns:
|
491
|
+
The relative import string (e.g., ".sibling", "..models.user"), or None if a relative path
|
492
|
+
cannot be determined (e.g., context not fully set, or target is current file).
|
493
|
+
"""
|
494
|
+
if not self.current_file or not self.package_root_for_generated_code:
|
495
|
+
return None
|
496
|
+
|
497
|
+
try:
|
498
|
+
current_file_abs = os.path.abspath(self.current_file)
|
499
|
+
package_root_abs = os.path.abspath(self.package_root_for_generated_code)
|
500
|
+
current_dir_abs = os.path.dirname(current_file_abs)
|
501
|
+
except Exception: # Was error logging here
|
502
|
+
return None
|
503
|
+
|
504
|
+
target_parts = target_logical_module_relative_to_gen_pkg_root.split(".")
|
505
|
+
|
506
|
+
# Construct potential absolute paths for the target (as a directory/package or as a .py file)
|
507
|
+
target_as_dir_abs = os.path.join(package_root_abs, *target_parts)
|
508
|
+
target_as_file_abs = os.path.join(package_root_abs, *target_parts) + ".py"
|
509
|
+
|
510
|
+
target_abs_path: str
|
511
|
+
is_target_package: bool # True if target is a package (directory), False if a module (.py file)
|
512
|
+
|
513
|
+
if os.path.isdir(target_as_dir_abs):
|
514
|
+
target_abs_path = target_as_dir_abs
|
515
|
+
is_target_package = True
|
516
|
+
elif os.path.isfile(target_as_file_abs):
|
517
|
+
target_abs_path = target_as_file_abs
|
518
|
+
is_target_package = False
|
519
|
+
else:
|
520
|
+
# Target does not exist. Assume it WILL be a .py module for path calculation.
|
521
|
+
target_abs_path = target_as_file_abs
|
522
|
+
is_target_package = False # Assume it's a module if it doesn't exist
|
523
|
+
|
524
|
+
# Self-import check: if the resolved target_abs_path is the same as the current_file_abs.
|
525
|
+
if current_file_abs == target_abs_path:
|
526
|
+
return None
|
527
|
+
|
528
|
+
try:
|
529
|
+
relative_file_path = os.path.relpath(target_abs_path, start=current_dir_abs)
|
530
|
+
except ValueError: # Was warning logging here
|
531
|
+
return None
|
532
|
+
|
533
|
+
# If the target is a module file (not a package/directory), and the relative path ends with .py, remove it.
|
534
|
+
if not is_target_package and relative_file_path.endswith(".py"):
|
535
|
+
relative_file_path = relative_file_path[:-3]
|
536
|
+
|
537
|
+
path_components = relative_file_path.split(os.sep)
|
538
|
+
level = 0
|
539
|
+
parts_after_pardir = []
|
540
|
+
pardir_found_and_processed = False
|
541
|
+
|
542
|
+
for part in path_components:
|
543
|
+
if part == os.pardir:
|
544
|
+
if not pardir_found_and_processed:
|
545
|
+
level += 1
|
546
|
+
elif part == os.curdir:
|
547
|
+
# If current dir '.' is the first part, it implies sibling, level remains 0.
|
548
|
+
# If it appears after '..', it's unusual but we just ignore it.
|
549
|
+
if not path_components[0] == os.curdir and not pardir_found_and_processed:
|
550
|
+
# This case ('.' after some actual path part) means we treat it as a path segment
|
551
|
+
parts_after_pardir.append(part)
|
552
|
+
else: # Actual path segment
|
553
|
+
pardir_found_and_processed = True # Stop counting '..' for level once a real segment is found
|
554
|
+
parts_after_pardir.append(part)
|
555
|
+
|
556
|
+
# If the path started with '..' (e.g. '../../foo'), level is correct.
|
557
|
+
# If it started with 'foo' (sibling dir or file), level is 0.
|
558
|
+
# num_dots_for_prefix: 0 for current dir, 1 for ./foo, 2 for ../foo, 3 for ../../foo
|
559
|
+
if relative_file_path == os.curdir or not path_components or path_components == [os.curdir]:
|
560
|
+
# This means target is current_dir itself (e.g. importing __init__.py)
|
561
|
+
# This should ideally be caught by self-import if current_file is __init__.py itself
|
562
|
+
# Or if target is __init__.py in current_dir. For this, we need just "."
|
563
|
+
num_dots_for_prefix = 1
|
564
|
+
parts_after_pardir = [] # No suffix needed for just "."
|
565
|
+
elif level == 0 and (not parts_after_pardir or parts_after_pardir == [os.curdir]):
|
566
|
+
# This condition implies relative_file_path was something like "." or empty after processing,
|
567
|
+
# meaning it's in the current directory. If os.curdir was the only thing, it's handled above.
|
568
|
+
# This is for safety, might be redundant with the direct os.curdir check.
|
569
|
+
num_dots_for_prefix = 1
|
570
|
+
parts_after_pardir = [
|
571
|
+
comp for comp in parts_after_pardir if comp != os.curdir
|
572
|
+
] # clean out curdir if it's there
|
573
|
+
else:
|
574
|
+
num_dots_for_prefix = level + 1
|
575
|
+
|
576
|
+
leading_dots_str = "." * num_dots_for_prefix
|
577
|
+
module_name_suffix = ".".join(p for p in parts_after_pardir if p) # Filter out empty strings
|
578
|
+
|
579
|
+
if module_name_suffix:
|
580
|
+
final_relative_path = leading_dots_str + module_name_suffix
|
581
|
+
else:
|
582
|
+
# This happens if only dots are needed, e.g. `from .. import foo` (suffix is empty, path is just dots)
|
583
|
+
# or `from . import bar`
|
584
|
+
final_relative_path = leading_dots_str
|
585
|
+
|
586
|
+
return final_relative_path
|
587
|
+
|
588
|
+
def get_current_package_name_for_generated_code(self) -> str | None:
|
589
|
+
"""
|
590
|
+
Get the current package name for the generated code.
|
591
|
+
|
592
|
+
Returns:
|
593
|
+
The current package name for the generated code, or None if not set.
|
594
|
+
"""
|
595
|
+
# If we have the full output package name, use it for absolute imports
|
596
|
+
if self.output_package_name:
|
597
|
+
return self.output_package_name
|
598
|
+
|
599
|
+
# Fallback to deriving from filesystem path (legacy behavior)
|
600
|
+
return self.package_root_for_generated_code.split(os.sep)[-1] if self.package_root_for_generated_code else None
|
601
|
+
|
602
|
+
def get_current_module_dot_path(self) -> str | None:
|
603
|
+
"""
|
604
|
+
Get the current module dot path relative to the overall project root.
|
605
|
+
Example: if current_file is /project/pkg/sub/mod.py and package_root_for_generated_code is /project/pkg,
|
606
|
+
and overall_project_root is /project, this should attempt to return pkg.sub.mod
|
607
|
+
"""
|
608
|
+
if not self.current_file or not self.overall_project_root:
|
609
|
+
return None
|
610
|
+
|
611
|
+
try:
|
612
|
+
abs_current_file = Path(self.current_file).resolve()
|
613
|
+
abs_overall_project_root = Path(self.overall_project_root).resolve()
|
614
|
+
|
615
|
+
# Get the relative path of the current file from the overall project root
|
616
|
+
relative_path_from_project_root = abs_current_file.relative_to(abs_overall_project_root)
|
617
|
+
|
618
|
+
# Remove .py extension
|
619
|
+
module_parts = list(relative_path_from_project_root.parts)
|
620
|
+
if module_parts[-1].endswith(".py"):
|
621
|
+
module_parts[-1] = module_parts[-1][:-3]
|
622
|
+
|
623
|
+
# Handle __init__.py cases: if the last part is __init__, it refers to the directory itself as the module
|
624
|
+
if module_parts[-1] == "__init__":
|
625
|
+
module_parts.pop()
|
626
|
+
|
627
|
+
return ".".join(module_parts)
|
628
|
+
|
629
|
+
except ValueError: # If current_file is not under overall_project_root
|
630
|
+
return None
|
File without changes
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from typing import Any, Protocol # noqa: F401
|
2
|
+
|
3
|
+
|
4
|
+
class BaseAuth(Protocol):
|
5
|
+
"""Protocol for authentication plugins."""
|
6
|
+
|
7
|
+
async def authenticate_request(self, request_args: dict[str, Any]) -> dict[str, Any]:
|
8
|
+
"""Modify or augment the request arguments for authentication."""
|
9
|
+
# Default stub returns the input unchanged
|
10
|
+
return request_args
|
11
|
+
|
12
|
+
|
13
|
+
class CompositeAuth(BaseAuth):
|
14
|
+
"""Compose multiple BaseAuth plugins, applying each in sequence to the request."""
|
15
|
+
|
16
|
+
def __init__(self, *plugins: BaseAuth):
|
17
|
+
self.plugins = plugins
|
18
|
+
|
19
|
+
async def authenticate_request(self, request_args: dict[str, Any]) -> dict[str, Any]:
|
20
|
+
for plugin in self.plugins:
|
21
|
+
request_args = await plugin.authenticate_request(request_args)
|
22
|
+
return request_args
|