pyopenapi-gen 0.14.1__py3-none-any.whl → 0.14.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/context/render_context.py +97 -1
- pyopenapi_gen/core/writers/python_construct_renderer.py +2 -15
- pyopenapi_gen/emitters/endpoints_emitter.py +12 -4
- pyopenapi_gen/visit/model/dataclass_generator.py +2 -1
- {pyopenapi_gen-0.14.1.dist-info → pyopenapi_gen-0.14.3.dist-info}/METADATA +1 -1
- {pyopenapi_gen-0.14.1.dist-info → pyopenapi_gen-0.14.3.dist-info}/RECORD +9 -9
- {pyopenapi_gen-0.14.1.dist-info → pyopenapi_gen-0.14.3.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.14.1.dist-info → pyopenapi_gen-0.14.3.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.14.1.dist-info → pyopenapi_gen-0.14.3.dist-info}/licenses/LICENSE +0 -0
@@ -153,10 +153,25 @@ class RenderContext:
|
|
153
153
|
self.core_package_name + "."
|
154
154
|
)
|
155
155
|
if is_target_in_core_pkg_namespace:
|
156
|
+
# For root-level sibling packages (core and output package both at top level),
|
157
|
+
# we MUST use absolute imports because Python doesn't support relative imports
|
158
|
+
# that go beyond the top-level package.
|
159
|
+
#
|
160
|
+
# Example structure that requires absolute imports:
|
161
|
+
# output/ # NOT a package (no __init__.py)
|
162
|
+
# ├── core/ # top-level package
|
163
|
+
# └── businessapi/ # top-level package
|
164
|
+
#
|
165
|
+
# In this case, businessapi/client.py cannot use "from ..core import X"
|
166
|
+
# because that would try to go UP from businessapi to output/, which is not a package.
|
167
|
+
#
|
168
|
+
# Solution: Always use absolute imports for core when it's a root-level sibling.
|
169
|
+
|
170
|
+
# Always use absolute imports for core imports
|
156
171
|
if name:
|
157
172
|
self.import_collector.add_import(module=logical_module, name=name)
|
158
173
|
else:
|
159
|
-
self.import_collector.add_plain_import(module=logical_module)
|
174
|
+
self.import_collector.add_plain_import(module=logical_module)
|
160
175
|
return
|
161
176
|
|
162
177
|
# 3. Stdlib/Builtin?
|
@@ -628,3 +643,84 @@ class RenderContext:
|
|
628
643
|
|
629
644
|
except ValueError: # If current_file is not under overall_project_root
|
630
645
|
return None
|
646
|
+
|
647
|
+
def get_core_import_path(self, submodule: str) -> str:
|
648
|
+
"""
|
649
|
+
Calculate the correct import path to a core package submodule from the current file.
|
650
|
+
|
651
|
+
Handles both sibling core packages (core/, custom_core/) and external packages (api_sdks.my_core).
|
652
|
+
|
653
|
+
Args:
|
654
|
+
submodule: The submodule within core to import from (e.g., "schemas", "http_transport", "exceptions")
|
655
|
+
|
656
|
+
Returns:
|
657
|
+
The correct import path string (e.g., "...core.schemas", "..core.http_transport", "api_sdks.my_core.schemas")
|
658
|
+
|
659
|
+
Examples:
|
660
|
+
From businessapi/models/user.py → core/schemas.py returns "...core.schemas"
|
661
|
+
From businessapi/client.py → core/http_transport.py returns "..core.http_transport"
|
662
|
+
From businessapi/endpoints/auth.py → core/exceptions.py returns "...core.exceptions"
|
663
|
+
External core package (api_sdks.my_core) returns "api_sdks.my_core.schemas"
|
664
|
+
"""
|
665
|
+
# 1. Check if core_package_name contains dots or is already a relative import
|
666
|
+
if "." in self.core_package_name and not self.core_package_name.startswith("."):
|
667
|
+
# External package (e.g., "api_sdks.my_core") - use absolute import
|
668
|
+
return f"{self.core_package_name}.{submodule}"
|
669
|
+
|
670
|
+
if self.core_package_name.startswith(".."):
|
671
|
+
# Already a relative import path
|
672
|
+
return f"{self.core_package_name}.{submodule}"
|
673
|
+
|
674
|
+
# 2. Local core package (sibling) - calculate relative path
|
675
|
+
return self._calculate_relative_core_path(submodule)
|
676
|
+
|
677
|
+
def _calculate_relative_core_path(self, submodule: str) -> str:
|
678
|
+
"""Calculate relative import path to sibling core package."""
|
679
|
+
|
680
|
+
if not self.current_file or not self.package_root_for_generated_code or not self.overall_project_root:
|
681
|
+
# Fallback to absolute import if context not fully set
|
682
|
+
logger.warning(
|
683
|
+
f"Cannot calculate relative core path: context not fully set. "
|
684
|
+
f"current_file={self.current_file}, package_root={self.package_root_for_generated_code}, "
|
685
|
+
f"project_root={self.overall_project_root}. Using absolute import."
|
686
|
+
)
|
687
|
+
return f"{self.core_package_name}.{submodule}"
|
688
|
+
|
689
|
+
try:
|
690
|
+
# 1. Get current file's directory
|
691
|
+
current_file_abs = Path(self.current_file).resolve()
|
692
|
+
current_dir_abs = current_file_abs.parent
|
693
|
+
|
694
|
+
# 2. Determine core package location (sibling to output package)
|
695
|
+
project_root_abs = Path(self.overall_project_root).resolve()
|
696
|
+
|
697
|
+
# Core is sibling to the output package
|
698
|
+
core_abs = project_root_abs / self.core_package_name
|
699
|
+
target_abs = core_abs / submodule.replace(".", os.sep)
|
700
|
+
|
701
|
+
# 3. Calculate relative path from current directory to target
|
702
|
+
relative_path = os.path.relpath(target_abs, start=current_dir_abs)
|
703
|
+
|
704
|
+
# 4. Convert filesystem path to Python import path
|
705
|
+
# e.g., "../../core/schemas" → "...core.schemas"
|
706
|
+
path_parts = relative_path.split(os.sep)
|
707
|
+
|
708
|
+
dots = 0
|
709
|
+
module_parts = []
|
710
|
+
|
711
|
+
for part in path_parts:
|
712
|
+
if part == "..":
|
713
|
+
dots += 1
|
714
|
+
elif part != ".":
|
715
|
+
module_parts.append(part)
|
716
|
+
|
717
|
+
# Prefix with dots (add one more for Python relative imports)
|
718
|
+
prefix = "." * (dots + 1)
|
719
|
+
module_path = ".".join(module_parts)
|
720
|
+
|
721
|
+
return f"{prefix}{module_path}"
|
722
|
+
|
723
|
+
except Exception as e:
|
724
|
+
# Fallback to absolute import on any error
|
725
|
+
logger.warning(f"Failed to calculate relative core path: {e}. Using absolute import.")
|
726
|
+
return f"{self.core_package_name}.{submodule}"
|
@@ -183,21 +183,8 @@ class PythonConstructRenderer:
|
|
183
183
|
context.add_import("dataclasses", "dataclass")
|
184
184
|
|
185
185
|
# Always use self-contained BaseSchema for client independence with automatic field mapping
|
186
|
-
# Use
|
187
|
-
|
188
|
-
# Already a relative import
|
189
|
-
core_import_path = f"{context.core_package_name}.schemas"
|
190
|
-
elif "." in context.core_package_name:
|
191
|
-
# External core package with dots (e.g., api_sdks.my_core) - use absolute import
|
192
|
-
core_import_path = f"{context.core_package_name}.schemas"
|
193
|
-
elif context.core_package_name == "core":
|
194
|
-
# Default relative core package
|
195
|
-
core_import_path = "..core.schemas"
|
196
|
-
else:
|
197
|
-
# Simple external core package name (e.g., shared_core_pkg) - use absolute import
|
198
|
-
core_import_path = f"{context.core_package_name}.schemas"
|
199
|
-
|
200
|
-
context.add_import(core_import_path, "BaseSchema")
|
186
|
+
# Use absolute core import path - add_import will handle it correctly
|
187
|
+
context.add_import(f"{context.core_package_name}.schemas", "BaseSchema")
|
201
188
|
|
202
189
|
# Add __all__ export
|
203
190
|
writer.write_line(f'__all__ = ["{class_name}"]')
|
@@ -108,12 +108,16 @@ class EndpointsEmitter:
|
|
108
108
|
self.formatter = Formatter()
|
109
109
|
self.visitor: EndpointVisitor | None = None
|
110
110
|
|
111
|
-
def
|
111
|
+
def _deduplicate_operation_ids_globally(self, operations: List[IROperation]) -> None:
|
112
112
|
"""
|
113
|
-
Ensures all operations have unique method names
|
113
|
+
Ensures all operations have unique method names globally across all tags.
|
114
|
+
|
115
|
+
This prevents the bug where operations with multiple tags share the same
|
116
|
+
IROperation object reference, causing _deduplicate_operation_ids() to
|
117
|
+
modify the same object multiple times and accumulate _2_2 suffixes.
|
114
118
|
|
115
119
|
Args:
|
116
|
-
operations: List of operations
|
120
|
+
operations: List of all operations across all tags.
|
117
121
|
"""
|
118
122
|
seen_methods: dict[str, int] = {}
|
119
123
|
for op in operations:
|
@@ -156,6 +160,10 @@ class EndpointsEmitter:
|
|
156
160
|
if self.visitor is None:
|
157
161
|
self.visitor = EndpointVisitor(current_parsed_schemas) # Pass the (potentially defaulted) dict
|
158
162
|
|
163
|
+
# Deduplicate operation IDs globally BEFORE tag grouping to prevent
|
164
|
+
# multi-tag operations from accumulating _2_2 suffixes
|
165
|
+
self._deduplicate_operation_ids_globally(operations)
|
166
|
+
|
159
167
|
tag_key_to_ops: dict[str, List[IROperation]] = {}
|
160
168
|
tag_key_to_candidates: dict[str, List[str]] = {}
|
161
169
|
for op in operations:
|
@@ -194,7 +202,7 @@ class EndpointsEmitter:
|
|
194
202
|
# This will set current_file and reset+reinit import_collector's context
|
195
203
|
self.context.set_current_file(str(file_path))
|
196
204
|
|
197
|
-
|
205
|
+
# Deduplication now done globally before tag grouping (see above)
|
198
206
|
|
199
207
|
# EndpointVisitor must exist here due to check above
|
200
208
|
if self.visitor is None:
|
@@ -80,7 +80,8 @@ class DataclassGenerator:
|
|
80
80
|
context.add_import("dataclasses", "dataclass")
|
81
81
|
context.add_import("dataclasses", "field")
|
82
82
|
context.add_import("typing", "Any")
|
83
|
-
|
83
|
+
# Use absolute core import path - add_import will handle it correctly
|
84
|
+
context.add_import(f"{context.core_package_name}.schemas", "BaseSchema")
|
84
85
|
|
85
86
|
description = schema.description or "Generic JSON value object that preserves arbitrary data."
|
86
87
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pyopenapi-gen
|
3
|
-
Version: 0.14.
|
3
|
+
Version: 0.14.3
|
4
4
|
Summary: Modern, async-first Python client generator for OpenAPI specifications with advanced cycle detection and unified type resolution
|
5
5
|
Project-URL: Homepage, https://github.com/your-org/pyopenapi-gen
|
6
6
|
Project-URL: Documentation, https://github.com/your-org/pyopenapi-gen/blob/main/README.md
|
@@ -7,7 +7,7 @@ pyopenapi_gen/py.typed,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
|
|
7
7
|
pyopenapi_gen/context/CLAUDE.md,sha256=eUPvSY2ADQK21i52bWfzyBcDPVvvepErMiQrq6ndwlU,9004
|
8
8
|
pyopenapi_gen/context/file_manager.py,sha256=vpbRByO5SH6COdjb6C-pXkdSIRu7QFqrXxi69VLKBnM,1691
|
9
9
|
pyopenapi_gen/context/import_collector.py,sha256=5WzQ5fXxARHJFZmYZ_GjPG5YjjzRLZTdN7jO2cbbk88,15225
|
10
|
-
pyopenapi_gen/context/render_context.py,sha256=
|
10
|
+
pyopenapi_gen/context/render_context.py,sha256=fAq0u3rDMZ0_OEHegrRUd98ySxs390XJbdypqQX8bwI,34894
|
11
11
|
pyopenapi_gen/core/CLAUDE.md,sha256=bz48K-PSrhxCq5ScmiLiU9kfpVVzSWRKOA9RdKk_pbg,6482
|
12
12
|
pyopenapi_gen/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
pyopenapi_gen/core/exceptions.py,sha256=HYFiYdmzsZUl46vB8M3B6Vpp6m8iqjUcKDWdL4yEKHo,498
|
@@ -64,14 +64,14 @@ pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py,sha256=d9DZ1CV
|
|
64
64
|
pyopenapi_gen/core/writers/code_writer.py,sha256=5rWMPRdj5dWu3NRER6do4NEJEIa9qL68Hc7zhJ8jSdg,4763
|
65
65
|
pyopenapi_gen/core/writers/documentation_writer.py,sha256=ZnXJsRc9y9bUMsQR7g0qoBJZbBo2MCxHH_5KgxrPtyA,8645
|
66
66
|
pyopenapi_gen/core/writers/line_writer.py,sha256=-K2FaMtQa6hdzGZLcjQrT2ItEmfE-kquTCrn6R3I_QA,7743
|
67
|
-
pyopenapi_gen/core/writers/python_construct_renderer.py,sha256=
|
67
|
+
pyopenapi_gen/core/writers/python_construct_renderer.py,sha256=mJ8iCDQAJWg32ZaDI4U7eWRzym3bwzOrJ2VKAXOZds0,12066
|
68
68
|
pyopenapi_gen/core_package_template/README.md,sha256=8YP-MS0KxphRbCGBf7kV3dYIFLU9piOJ3IMm3K_0hcI,1488
|
69
69
|
pyopenapi_gen/emit/models_emitter.py,sha256=MBRq71UjtlZHrpf9QhDN0wXk_X-oGeGd6TAq3RKG7ko,7180
|
70
70
|
pyopenapi_gen/emitters/CLAUDE.md,sha256=iZYEZq1a1h033rxuh97cMpsKUElv72ysvTm3-QQUvrs,9323
|
71
71
|
pyopenapi_gen/emitters/client_emitter.py,sha256=kmMVnG-wAOJm7TUm0xOQ5YnSJfYxz1SwtpiyoUCbcCA,1939
|
72
72
|
pyopenapi_gen/emitters/core_emitter.py,sha256=YSuqDlYv3P687TsVT_Z9n7a6GZepGAvv3-N1Q2kK6Zg,7975
|
73
73
|
pyopenapi_gen/emitters/docs_emitter.py,sha256=aouKqhRdtVvYfGVsye_uqM80nONRy0SqN06cr1l3OgA,1137
|
74
|
-
pyopenapi_gen/emitters/endpoints_emitter.py,sha256=
|
74
|
+
pyopenapi_gen/emitters/endpoints_emitter.py,sha256=0DXOXu2Yb3DRTic_k63q2L-yKS6ZFdhbcBCK3DPZU3Y,10040
|
75
75
|
pyopenapi_gen/emitters/exceptions_emitter.py,sha256=PfbDQX7dfgg2htvxEh40t7FR7b3BrK8jeRd5INu_kjk,7547
|
76
76
|
pyopenapi_gen/emitters/models_emitter.py,sha256=wJwtvCGEmhy5yhojfoUW2CXNOQytGlN4J8-GcwoYIMY,22221
|
77
77
|
pyopenapi_gen/generator/CLAUDE.md,sha256=BS9KkmLvk2WD-Io-_apoWjGNeMU4q4LKy4UOxYF9WxM,10870
|
@@ -122,11 +122,11 @@ pyopenapi_gen/visit/endpoint/processors/import_analyzer.py,sha256=ou5pl3S6PXvHrh
|
|
122
122
|
pyopenapi_gen/visit/endpoint/processors/parameter_processor.py,sha256=E0VoygkhU8iCsvH0U-tA6ZZg2Nm3rfr4e17vxqQLH7c,7666
|
123
123
|
pyopenapi_gen/visit/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
124
124
|
pyopenapi_gen/visit/model/alias_generator.py,sha256=wEMHipPA1_CFxvQ6CS9j4qgXK93seI1bI_tFJvIrb70,3563
|
125
|
-
pyopenapi_gen/visit/model/dataclass_generator.py,sha256=
|
125
|
+
pyopenapi_gen/visit/model/dataclass_generator.py,sha256=L794s2yyYUO__akGd120NJMV7g2xqiPfQyZo8CduIVM,14426
|
126
126
|
pyopenapi_gen/visit/model/enum_generator.py,sha256=AXqKUFuWUUjUF_6_HqBKY8vB5GYu35Pb2C2WPFrOw1k,10061
|
127
127
|
pyopenapi_gen/visit/model/model_visitor.py,sha256=TC6pbxpQiy5FWhmQpfllLuXA3ImTYNMcrazkOFZCIyo,9470
|
128
|
-
pyopenapi_gen-0.14.
|
129
|
-
pyopenapi_gen-0.14.
|
130
|
-
pyopenapi_gen-0.14.
|
131
|
-
pyopenapi_gen-0.14.
|
132
|
-
pyopenapi_gen-0.14.
|
128
|
+
pyopenapi_gen-0.14.3.dist-info/METADATA,sha256=sKA5cB3FHhSkY7wbQs8AlGg9GJc21uitec8EjIXbWtE,14025
|
129
|
+
pyopenapi_gen-0.14.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
130
|
+
pyopenapi_gen-0.14.3.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
|
131
|
+
pyopenapi_gen-0.14.3.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
|
132
|
+
pyopenapi_gen-0.14.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|