pyopenapi-gen 0.14.0__py3-none-any.whl → 0.14.2__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/cli.py +3 -3
- pyopenapi_gen/context/import_collector.py +10 -10
- pyopenapi_gen/context/render_context.py +13 -13
- pyopenapi_gen/core/auth/plugins.py +7 -7
- pyopenapi_gen/core/http_status_codes.py +2 -4
- pyopenapi_gen/core/http_transport.py +19 -19
- pyopenapi_gen/core/loader/operations/parser.py +2 -2
- pyopenapi_gen/core/loader/operations/request_body.py +3 -3
- pyopenapi_gen/core/loader/parameters/parser.py +3 -3
- pyopenapi_gen/core/loader/responses/parser.py +2 -2
- pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
- pyopenapi_gen/core/pagination.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
- pyopenapi_gen/core/parsing/context.py +10 -10
- pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
- pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
- pyopenapi_gen/core/parsing/schema_parser.py +44 -25
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
- pyopenapi_gen/core/schemas.py +10 -10
- pyopenapi_gen/core/streaming_helpers.py +5 -7
- pyopenapi_gen/core/telemetry.py +4 -4
- pyopenapi_gen/core/utils.py +7 -7
- pyopenapi_gen/core/writers/code_writer.py +2 -2
- pyopenapi_gen/core/writers/documentation_writer.py +18 -18
- pyopenapi_gen/core/writers/line_writer.py +3 -3
- pyopenapi_gen/core/writers/python_construct_renderer.py +10 -10
- pyopenapi_gen/emit/models_emitter.py +2 -2
- pyopenapi_gen/emitters/core_emitter.py +3 -5
- pyopenapi_gen/emitters/endpoints_emitter.py +24 -16
- pyopenapi_gen/emitters/exceptions_emitter.py +4 -3
- pyopenapi_gen/emitters/models_emitter.py +6 -6
- pyopenapi_gen/generator/client_generator.py +6 -6
- pyopenapi_gen/helpers/endpoint_utils.py +16 -18
- pyopenapi_gen/helpers/type_cleaner.py +66 -53
- pyopenapi_gen/helpers/type_helper.py +7 -7
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
- pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
- pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
- pyopenapi_gen/ir.py +32 -34
- pyopenapi_gen/types/contracts/protocols.py +5 -5
- pyopenapi_gen/types/contracts/types.py +2 -3
- pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
- pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
- pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
- pyopenapi_gen/types/services/type_service.py +55 -9
- pyopenapi_gen/types/strategies/response_strategy.py +6 -7
- pyopenapi_gen/visit/client_visitor.py +5 -7
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +38 -17
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
- pyopenapi_gen/visit/model/alias_generator.py +1 -4
- pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
- pyopenapi_gen/visit/model/model_visitor.py +2 -3
- pyopenapi_gen/visit/visitor.py +3 -3
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/METADATA +1 -1
- pyopenapi_gen-0.14.2.dist-info/RECORD +132 -0
- pyopenapi_gen-0.14.0.dist-info/RECORD +0 -132
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/licenses/LICENSE +0 -0
pyopenapi_gen/core/telemetry.py
CHANGED
@@ -8,7 +8,7 @@ usage telemetry for PyOpenAPI Generator. Telemetry is opt-in only.
|
|
8
8
|
import json
|
9
9
|
import os
|
10
10
|
import time
|
11
|
-
from typing import Any
|
11
|
+
from typing import Any
|
12
12
|
|
13
13
|
|
14
14
|
class TelemetryClient:
|
@@ -24,7 +24,7 @@ class TelemetryClient:
|
|
24
24
|
enabled: Whether telemetry is currently enabled
|
25
25
|
"""
|
26
26
|
|
27
|
-
def __init__(self, enabled:
|
27
|
+
def __init__(self, enabled: bool | None = None) -> None:
|
28
28
|
"""
|
29
29
|
Initialize a new TelemetryClient.
|
30
30
|
|
@@ -38,7 +38,7 @@ class TelemetryClient:
|
|
38
38
|
else:
|
39
39
|
self.enabled = enabled
|
40
40
|
|
41
|
-
def track_event(self, event: str, properties:
|
41
|
+
def track_event(self, event: str, properties: dict[str, Any] | None = None) -> None:
|
42
42
|
"""
|
43
43
|
Track a telemetry event if telemetry is enabled.
|
44
44
|
|
@@ -52,7 +52,7 @@ class TelemetryClient:
|
|
52
52
|
if not self.enabled:
|
53
53
|
return
|
54
54
|
|
55
|
-
data:
|
55
|
+
data: dict[str, Any] = {
|
56
56
|
"event": event,
|
57
57
|
"properties": properties or {},
|
58
58
|
"timestamp": time.time(),
|
pyopenapi_gen/core/utils.py
CHANGED
@@ -8,7 +8,7 @@ import keyword
|
|
8
8
|
import logging
|
9
9
|
import re
|
10
10
|
from datetime import datetime
|
11
|
-
from typing import Any,
|
11
|
+
from typing import Any, Set, Type, TypeVar, cast
|
12
12
|
|
13
13
|
logger = logging.getLogger(__name__)
|
14
14
|
|
@@ -228,7 +228,7 @@ class ParamSubstitutor:
|
|
228
228
|
"""Helper for rendering path templates with path parameters."""
|
229
229
|
|
230
230
|
@staticmethod
|
231
|
-
def render_path(template: str, values:
|
231
|
+
def render_path(template: str, values: dict[str, Any]) -> str:
|
232
232
|
"""Replace placeholders in a URL path template using provided values."""
|
233
233
|
rendered = template
|
234
234
|
for key, val in values.items():
|
@@ -240,7 +240,7 @@ class KwargsBuilder:
|
|
240
240
|
"""Builder for assembling HTTP request keyword arguments."""
|
241
241
|
|
242
242
|
def __init__(self) -> None:
|
243
|
-
self._kwargs:
|
243
|
+
self._kwargs: dict[str, Any] = {}
|
244
244
|
|
245
245
|
def with_params(self, **params: Any) -> "KwargsBuilder":
|
246
246
|
"""Add query parameters, skipping None values."""
|
@@ -254,7 +254,7 @@ class KwargsBuilder:
|
|
254
254
|
self._kwargs["json"] = body
|
255
255
|
return self
|
256
256
|
|
257
|
-
def build(self) ->
|
257
|
+
def build(self) -> dict[str, Any]:
|
258
258
|
"""Return the assembled kwargs dictionary."""
|
259
259
|
return self._kwargs
|
260
260
|
|
@@ -263,10 +263,10 @@ class Formatter:
|
|
263
263
|
"""Helper to format code using Black, falling back to unformatted content if Black is unavailable or errors."""
|
264
264
|
|
265
265
|
def __init__(self) -> None:
|
266
|
-
from typing import Any, Callable
|
266
|
+
from typing import Any, Callable
|
267
267
|
|
268
|
-
self._file_mode:
|
269
|
-
self._format_str:
|
268
|
+
self._file_mode: Any | None = None
|
269
|
+
self._format_str: Callable[..., str] | None = None
|
270
270
|
try:
|
271
271
|
from black import FileMode, format_str
|
272
272
|
|
@@ -6,7 +6,7 @@ writing lines and blocks, and supporting wrapped output for code and docstrings.
|
|
6
6
|
to be used by code generation visitors and emitters to ensure consistent, readable output.
|
7
7
|
"""
|
8
8
|
|
9
|
-
from typing import List
|
9
|
+
from typing import List
|
10
10
|
|
11
11
|
from .line_writer import LineWriter
|
12
12
|
|
@@ -88,7 +88,7 @@ class CodeWriter:
|
|
88
88
|
self.writer.max_width = old_width
|
89
89
|
|
90
90
|
def write_function_signature(
|
91
|
-
self, name: str, args: List[str], return_type:
|
91
|
+
self, name: str, args: List[str], return_type: str | None = None, async_: bool = False
|
92
92
|
) -> None:
|
93
93
|
"""
|
94
94
|
Write a function or method signature, with each argument on its own line and correct indentation.
|
@@ -6,7 +6,7 @@ for building comprehensive, type-rich docstrings for generated Python code. It s
|
|
6
6
|
alignment, line wrapping, and section formatting for Args, Returns, and Raises.
|
7
7
|
"""
|
8
8
|
|
9
|
-
from typing import List,
|
9
|
+
from typing import List, Tuple, Union
|
10
10
|
|
11
11
|
from .line_writer import LineWriter
|
12
12
|
|
@@ -16,36 +16,36 @@ class DocumentationBlock:
|
|
16
16
|
Data container for docstring content.
|
17
17
|
|
18
18
|
Attributes:
|
19
|
-
summary (
|
20
|
-
description (
|
19
|
+
summary (str | None): The summary line for the docstring.
|
20
|
+
description (str | None): The detailed description.
|
21
21
|
args (Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]]):
|
22
22
|
List of arguments as (name, type, desc) or (type, desc) tuples.
|
23
|
-
returns (
|
24
|
-
raises (
|
23
|
+
returns (Tuple[str, str] | None): The return type and description.
|
24
|
+
raises (List[Tuple[str, str]] | None): List of (exception type, description) tuples.
|
25
25
|
"""
|
26
26
|
|
27
27
|
def __init__(
|
28
28
|
self,
|
29
|
-
summary:
|
30
|
-
description:
|
31
|
-
args:
|
32
|
-
returns:
|
33
|
-
raises:
|
29
|
+
summary: str | None = None,
|
30
|
+
description: str | None = None,
|
31
|
+
args: List[Union[Tuple[str, str, str], Tuple[str, str]]] | None = None,
|
32
|
+
returns: Tuple[str, str] | None = None,
|
33
|
+
raises: List[Tuple[str, str]] | None = None,
|
34
34
|
) -> None:
|
35
35
|
"""
|
36
36
|
Initialize a DocumentationBlock.
|
37
37
|
|
38
38
|
Args:
|
39
|
-
summary (
|
40
|
-
description (
|
39
|
+
summary (str | None): The summary line.
|
40
|
+
description (str | None): The detailed description.
|
41
41
|
args (Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]]): Arguments.
|
42
|
-
returns (
|
43
|
-
raises (
|
42
|
+
returns (Tuple[str, str] | None): Return type and description.
|
43
|
+
raises (List[Tuple[str, str]] | None): Exceptions.
|
44
44
|
"""
|
45
|
-
self.summary:
|
46
|
-
self.description:
|
45
|
+
self.summary: str | None = summary
|
46
|
+
self.description: str | None = description
|
47
47
|
self.args: List[Union[Tuple[str, str, str], Tuple[str, str]]] = args or []
|
48
|
-
self.returns:
|
48
|
+
self.returns: Tuple[str, str] | None = returns
|
49
49
|
self.raises: List[Tuple[str, str]] = raises or []
|
50
50
|
|
51
51
|
|
@@ -58,7 +58,7 @@ class DocumentationFormatter:
|
|
58
58
|
self.width: int = width
|
59
59
|
self.min_desc_col: int = min_desc_col
|
60
60
|
|
61
|
-
def wrap(self, text: str, indent: int, prefix:
|
61
|
+
def wrap(self, text: str, indent: int, prefix: str | None = None) -> List[str]:
|
62
62
|
if not text:
|
63
63
|
return []
|
64
64
|
writer = LineWriter(max_width=self.width)
|
@@ -5,7 +5,7 @@ This class is designed for use in both code and documentation generation, provid
|
|
5
5
|
new lines, and query the current line's width.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from typing import List
|
8
|
+
from typing import List
|
9
9
|
|
10
10
|
|
11
11
|
class LineWriter:
|
@@ -169,7 +169,7 @@ class LineWriter:
|
|
169
169
|
self.lines[-1] += " " * (col - current - 1)
|
170
170
|
# If already at or past col, do nothing
|
171
171
|
|
172
|
-
def append_wrapped_at_column(self, text: str, width: int, col:
|
172
|
+
def append_wrapped_at_column(self, text: str, width: int, col: int | None = None) -> None:
|
173
173
|
"""
|
174
174
|
Append text, wrapping as needed, so that the first line continues from the current position,
|
175
175
|
and all subsequent lines start at column `col`.
|
@@ -180,7 +180,7 @@ class LineWriter:
|
|
180
180
|
Args:
|
181
181
|
text (str) : The text to append and wrap.
|
182
182
|
width (int) : The maximum line width.
|
183
|
-
col (
|
183
|
+
col (int | None) : The column at which to start wrapped lines. If None, uses current
|
184
184
|
line width.
|
185
185
|
"""
|
186
186
|
import textwrap
|
@@ -7,7 +7,7 @@ It handles all the details of formatting, import registration, and docstring gen
|
|
7
7
|
for these constructs.
|
8
8
|
"""
|
9
9
|
|
10
|
-
from typing import
|
10
|
+
from typing import List, Tuple
|
11
11
|
|
12
12
|
from pyopenapi_gen.context.render_context import RenderContext
|
13
13
|
|
@@ -35,7 +35,7 @@ class PythonConstructRenderer:
|
|
35
35
|
self,
|
36
36
|
alias_name: str,
|
37
37
|
target_type: str,
|
38
|
-
description:
|
38
|
+
description: str | None,
|
39
39
|
context: RenderContext,
|
40
40
|
) -> str:
|
41
41
|
"""
|
@@ -80,7 +80,7 @@ class PythonConstructRenderer:
|
|
80
80
|
enum_name: str,
|
81
81
|
base_type: str, # 'str' or 'int'
|
82
82
|
values: List[Tuple[str, str | int]], # List of (MEMBER_NAME, value)
|
83
|
-
description:
|
83
|
+
description: str | None,
|
84
84
|
context: RenderContext,
|
85
85
|
) -> str:
|
86
86
|
"""
|
@@ -143,10 +143,10 @@ class PythonConstructRenderer:
|
|
143
143
|
def render_dataclass(
|
144
144
|
self,
|
145
145
|
class_name: str,
|
146
|
-
fields: List[Tuple[str, str,
|
147
|
-
description:
|
146
|
+
fields: List[Tuple[str, str, str | None, str | None]], # name, type_hint, default_expr, description
|
147
|
+
description: str | None,
|
148
148
|
context: RenderContext,
|
149
|
-
field_mappings:
|
149
|
+
field_mappings: dict[str, str] | None = None,
|
150
150
|
) -> str:
|
151
151
|
"""
|
152
152
|
Render a dataclass as Python code with BaseSchema support.
|
@@ -168,7 +168,7 @@ class PythonConstructRenderer:
|
|
168
168
|
\"\"\"User information with automatic JSON field mapping.\"\"\"
|
169
169
|
id_: str
|
170
170
|
first_name: str
|
171
|
-
email:
|
171
|
+
email: str | None = None
|
172
172
|
is_active: bool = True
|
173
173
|
|
174
174
|
class Meta:
|
@@ -274,9 +274,9 @@ class PythonConstructRenderer:
|
|
274
274
|
def render_class(
|
275
275
|
self,
|
276
276
|
class_name: str,
|
277
|
-
base_classes:
|
278
|
-
docstring:
|
279
|
-
body_lines:
|
277
|
+
base_classes: List[str] | None,
|
278
|
+
docstring: str | None,
|
279
|
+
body_lines: List[str] | None,
|
280
280
|
context: RenderContext,
|
281
281
|
) -> str:
|
282
282
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import logging
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import List
|
3
|
+
from typing import List
|
4
4
|
|
5
5
|
from pyopenapi_gen.context.render_context import RenderContext
|
6
6
|
from pyopenapi_gen.core.utils import NameSanitizer
|
@@ -20,7 +20,7 @@ class ModelsEmitter:
|
|
20
20
|
# self.writer an instance CodeWriter() here seems unused globally for this emitter.
|
21
21
|
# Each file generation part either writes directly or uses a local CodeWriter.
|
22
22
|
|
23
|
-
def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) ->
|
23
|
+
def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> str | None:
|
24
24
|
"""Generates a single Python file for a given IRSchema. Returns file path if generated."""
|
25
25
|
if not schema_ir.name:
|
26
26
|
logger.warning(f"Skipping model generation for schema without a name: {schema_ir}")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import importlib.resources
|
2
2
|
import os
|
3
|
-
from typing import List
|
3
|
+
from typing import List
|
4
4
|
|
5
5
|
from pyopenapi_gen.context.file_manager import FileManager
|
6
6
|
|
@@ -22,12 +22,10 @@ CORE_README_TEMPLATE_FILENAME = "README.md"
|
|
22
22
|
|
23
23
|
CONFIG_TEMPLATE = """
|
24
24
|
from dataclasses import dataclass
|
25
|
-
from typing import Optional
|
26
|
-
|
27
25
|
@dataclass
|
28
26
|
class ClientConfig:
|
29
27
|
base_url: str
|
30
|
-
timeout:
|
28
|
+
timeout: float | None = 30.0
|
31
29
|
"""
|
32
30
|
|
33
31
|
|
@@ -35,7 +33,7 @@ class CoreEmitter:
|
|
35
33
|
"""Copies all required runtime files into the generated core module."""
|
36
34
|
|
37
35
|
def __init__(
|
38
|
-
self, core_dir: str = "core", core_package: str = "core", exception_alias_names:
|
36
|
+
self, core_dir: str = "core", core_package: str = "core", exception_alias_names: List[str] | None = None
|
39
37
|
):
|
40
38
|
# core_dir is the relative path WITHIN the output package, e.g., "core" or "shared/core"
|
41
39
|
# core_package is the Python import name, e.g., "core" or "shared.core"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import logging
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import
|
3
|
+
from typing import List, Tuple
|
4
4
|
|
5
5
|
from pyopenapi_gen import IROperation, IRParameter, IRRequestBody
|
6
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -17,7 +17,7 @@ PARAM_TYPE_MAPPING = {
|
|
17
17
|
"boolean": "bool",
|
18
18
|
"string": "str",
|
19
19
|
"array": "List",
|
20
|
-
"object": "
|
20
|
+
"object": "dict[str, Any]",
|
21
21
|
}
|
22
22
|
# Format-specific overrides
|
23
23
|
PARAM_FORMAT_MAPPING = {
|
@@ -47,7 +47,7 @@ def schema_to_type(schema: IRParameter) -> str:
|
|
47
47
|
# Array handling
|
48
48
|
elif s.type == "array" and s.items:
|
49
49
|
# For array items, we recursively call schema_to_type.
|
50
|
-
# The nullability of the item_type itself (e.g. List[
|
50
|
+
# The nullability of the item_type itself (e.g. List[int | None])
|
51
51
|
# will be handled by the recursive call based on s.items.is_nullable.
|
52
52
|
item_schema_as_param = IRParameter(name="_item", param_in="_internal", required=False, schema=s.items)
|
53
53
|
item_type_str = schema_to_type(item_schema_as_param)
|
@@ -70,8 +70,8 @@ def schema_to_type(schema: IRParameter) -> str:
|
|
70
70
|
# 2. Apply nullability based on IRSchema's is_nullable field
|
71
71
|
# This s.is_nullable should be the source of truth from the IR after parsing.
|
72
72
|
if s.is_nullable:
|
73
|
-
# Ensure "Any" also gets wrapped, e.g.
|
74
|
-
py_type = f"
|
73
|
+
# Ensure "Any" also gets wrapped, e.g. Any | None
|
74
|
+
py_type = f"{py_type} | None"
|
75
75
|
|
76
76
|
return py_type
|
77
77
|
|
@@ -82,7 +82,7 @@ def _get_request_body_type(body: IRRequestBody) -> str:
|
|
82
82
|
if "json" in mt.lower():
|
83
83
|
return schema_to_type(IRParameter(name="body", param_in="body", required=body.required, schema=sch))
|
84
84
|
# Fallback to generic dict
|
85
|
-
return "
|
85
|
+
return "dict[str, Any]"
|
86
86
|
|
87
87
|
|
88
88
|
def _deduplicate_tag_clients(client_classes: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
|
@@ -106,16 +106,20 @@ class EndpointsEmitter:
|
|
106
106
|
def __init__(self, context: RenderContext) -> None:
|
107
107
|
self.context = context
|
108
108
|
self.formatter = Formatter()
|
109
|
-
self.visitor:
|
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
|
-
seen_methods:
|
122
|
+
seen_methods: dict[str, int] = {}
|
119
123
|
for op in operations:
|
120
124
|
method_name = NameSanitizer.sanitize_method_name(op.operation_id)
|
121
125
|
if method_name in seen_methods:
|
@@ -144,7 +148,7 @@ class EndpointsEmitter:
|
|
144
148
|
self.context.file_manager.write_file(str(file_path), content)
|
145
149
|
|
146
150
|
# Ensure parsed_schemas is at least an empty dict if None,
|
147
|
-
# as EndpointVisitor expects
|
151
|
+
# as EndpointVisitor expects dict[str, IRSchema]
|
148
152
|
current_parsed_schemas = self.context.parsed_schemas
|
149
153
|
if current_parsed_schemas is None:
|
150
154
|
logger.warning(
|
@@ -156,8 +160,12 @@ class EndpointsEmitter:
|
|
156
160
|
if self.visitor is None:
|
157
161
|
self.visitor = EndpointVisitor(current_parsed_schemas) # Pass the (potentially defaulted) dict
|
158
162
|
|
159
|
-
|
160
|
-
|
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
|
+
|
167
|
+
tag_key_to_ops: dict[str, List[IROperation]] = {}
|
168
|
+
tag_key_to_candidates: dict[str, List[str]] = {}
|
161
169
|
for op in operations:
|
162
170
|
tags = op.tags or [DEFAULT_TAG]
|
163
171
|
for tag in tags:
|
@@ -175,7 +183,7 @@ class EndpointsEmitter:
|
|
175
183
|
upper = sum(1 for c in t if c.isupper())
|
176
184
|
return (is_pascal, word_count, upper, t)
|
177
185
|
|
178
|
-
tag_map:
|
186
|
+
tag_map: dict[str, str] = {}
|
179
187
|
for key, candidates in tag_key_to_candidates.items():
|
180
188
|
best_tag_for_key = DEFAULT_TAG # Default if no candidates somehow
|
181
189
|
if candidates:
|
@@ -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:
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
import os
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import Optional
|
5
4
|
|
6
5
|
from pyopenapi_gen import IRSpec
|
7
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -22,13 +21,13 @@ class ExceptionsEmitter:
|
|
22
21
|
exceptions are available.
|
23
22
|
"""
|
24
23
|
|
25
|
-
def __init__(self, core_package_name: str = "core", overall_project_root:
|
24
|
+
def __init__(self, core_package_name: str = "core", overall_project_root: str | None = None) -> None:
|
26
25
|
self.visitor = ExceptionVisitor()
|
27
26
|
self.core_package_name = core_package_name
|
28
27
|
self.overall_project_root = overall_project_root
|
29
28
|
|
30
29
|
def emit(
|
31
|
-
self, spec: IRSpec, output_dir: str, client_package_name:
|
30
|
+
self, spec: IRSpec, output_dir: str, client_package_name: str | None = None
|
32
31
|
) -> tuple[list[str], list[str]]:
|
33
32
|
"""Generate exception aliases for the given spec.
|
34
33
|
|
@@ -61,6 +60,8 @@ class ExceptionsEmitter:
|
|
61
60
|
|
62
61
|
generated_imports = context.render_imports()
|
63
62
|
|
63
|
+
alias_names.sort()
|
64
|
+
|
64
65
|
# Add __all__ list with proper spacing (2 blank lines after last class - Ruff E305)
|
65
66
|
if alias_names:
|
66
67
|
all_list_str = ", ".join([f'"{name}"' for name in alias_names])
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import logging
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import
|
3
|
+
from typing import List, Set
|
4
4
|
|
5
5
|
from pyopenapi_gen import IRSchema, IRSpec
|
6
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -22,15 +22,15 @@ class ModelsEmitter:
|
|
22
22
|
Handles creation of __init__.py and py.typed files.
|
23
23
|
"""
|
24
24
|
|
25
|
-
def __init__(self, context: RenderContext, parsed_schemas:
|
25
|
+
def __init__(self, context: RenderContext, parsed_schemas: dict[str, IRSchema]):
|
26
26
|
self.context: RenderContext = context
|
27
27
|
# Store a reference to the schemas that were passed in.
|
28
28
|
# These schemas will have their .generation_name and .final_module_stem updated.
|
29
|
-
self.parsed_schemas:
|
29
|
+
self.parsed_schemas: dict[str, IRSchema] = parsed_schemas
|
30
30
|
self.import_collector = self.context.import_collector
|
31
31
|
self.writer = CodeWriter()
|
32
32
|
|
33
|
-
def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) ->
|
33
|
+
def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> str | None:
|
34
34
|
"""Generates a single Python file for a given IRSchema."""
|
35
35
|
if not schema_ir.name: # Original name, used for logging/initial identification
|
36
36
|
logger.warning(f"Skipping model generation for schema without an original name: {schema_ir}")
|
@@ -168,7 +168,7 @@ class ModelsEmitter:
|
|
168
168
|
generated_content = init_writer.get_code()
|
169
169
|
return generated_content
|
170
170
|
|
171
|
-
def emit(self, spec: IRSpec, output_root: str) ->
|
171
|
+
def emit(self, spec: IRSpec, output_root: str) -> dict[str, List[str]]:
|
172
172
|
"""Emits all model files derived from IR schemas.
|
173
173
|
|
174
174
|
Contracts:
|
@@ -352,7 +352,7 @@ class ModelsEmitter:
|
|
352
352
|
|
353
353
|
# Fetch the schema_ir object using the key from all_schemas_for_generation
|
354
354
|
# This ensures we are working with the potentially newly created & named schemas.
|
355
|
-
current_schema_ir_obj:
|
355
|
+
current_schema_ir_obj: IRSchema | None = all_schemas_for_generation.get(schema_key)
|
356
356
|
|
357
357
|
if not current_schema_ir_obj:
|
358
358
|
logger.warning(f"Schema key '{schema_key}' from all_schemas_for_generation not found. Skipping.")
|
@@ -9,7 +9,7 @@ import tempfile
|
|
9
9
|
import time
|
10
10
|
from datetime import datetime
|
11
11
|
from pathlib import Path
|
12
|
-
from typing import Any,
|
12
|
+
from typing import Any, List
|
13
13
|
|
14
14
|
from pyopenapi_gen.context.render_context import RenderContext
|
15
15
|
from pyopenapi_gen.core.loader.loader import load_ir_from_spec
|
@@ -47,9 +47,9 @@ class ClientGenerator:
|
|
47
47
|
"""
|
48
48
|
self.verbose = verbose
|
49
49
|
self.start_time = time.time()
|
50
|
-
self.timings:
|
50
|
+
self.timings: dict[str, float] = {}
|
51
51
|
|
52
|
-
def _log_progress(self, message: str, stage:
|
52
|
+
def _log_progress(self, message: str, stage: str | None = None) -> None:
|
53
53
|
"""
|
54
54
|
Log a progress message with timestamp.
|
55
55
|
|
@@ -89,7 +89,7 @@ class ClientGenerator:
|
|
89
89
|
output_package: str,
|
90
90
|
force: bool = False,
|
91
91
|
no_postprocess: bool = False,
|
92
|
-
core_package:
|
92
|
+
core_package: str | None = None,
|
93
93
|
) -> List[Path]:
|
94
94
|
"""
|
95
95
|
Generate the client code from the OpenAPI spec.
|
@@ -99,10 +99,10 @@ class ClientGenerator:
|
|
99
99
|
project_root (Path): Path to the root of the Python project (absolute or relative).
|
100
100
|
output_package (str): Python package path for the generated client (e.g., 'pyapis.my_api_client').
|
101
101
|
force (bool): Overwrite output without diff check.
|
102
|
-
name (
|
102
|
+
name (str | None): Custom client package name (not used).
|
103
103
|
docs (bool): Kept for interface compatibility.
|
104
104
|
telemetry (bool): Kept for interface compatibility.
|
105
|
-
auth (
|
105
|
+
auth (str | None): Kept for interface compatibility.
|
106
106
|
no_postprocess (bool): Skip post-processing (type checking, etc.).
|
107
107
|
core_package (str): Python package path for the core package.
|
108
108
|
|