pyopenapi-gen 2.7.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.
Files changed (137) hide show
  1. pyopenapi_gen/__init__.py +224 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +62 -0
  4. pyopenapi_gen/context/CLAUDE.md +284 -0
  5. pyopenapi_gen/context/file_manager.py +52 -0
  6. pyopenapi_gen/context/import_collector.py +382 -0
  7. pyopenapi_gen/context/render_context.py +726 -0
  8. pyopenapi_gen/core/CLAUDE.md +224 -0
  9. pyopenapi_gen/core/__init__.py +0 -0
  10. pyopenapi_gen/core/auth/base.py +22 -0
  11. pyopenapi_gen/core/auth/plugins.py +89 -0
  12. pyopenapi_gen/core/cattrs_converter.py +810 -0
  13. pyopenapi_gen/core/exceptions.py +20 -0
  14. pyopenapi_gen/core/http_status_codes.py +218 -0
  15. pyopenapi_gen/core/http_transport.py +222 -0
  16. pyopenapi_gen/core/loader/__init__.py +12 -0
  17. pyopenapi_gen/core/loader/loader.py +174 -0
  18. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  19. pyopenapi_gen/core/loader/operations/parser.py +161 -0
  20. pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
  21. pyopenapi_gen/core/loader/operations/request_body.py +90 -0
  22. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  23. pyopenapi_gen/core/loader/parameters/parser.py +186 -0
  24. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  25. pyopenapi_gen/core/loader/responses/parser.py +111 -0
  26. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  27. pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
  28. pyopenapi_gen/core/pagination.py +64 -0
  29. pyopenapi_gen/core/parsing/__init__.py +13 -0
  30. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  37. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  38. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  39. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  40. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  41. pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
  42. pyopenapi_gen/core/parsing/context.py +187 -0
  43. pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
  44. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  45. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
  46. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
  47. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
  48. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
  49. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  50. pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
  51. pyopenapi_gen/core/parsing/schema_parser.py +804 -0
  52. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  53. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  54. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
  55. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  56. pyopenapi_gen/core/postprocess_manager.py +260 -0
  57. pyopenapi_gen/core/spec_fetcher.py +148 -0
  58. pyopenapi_gen/core/streaming_helpers.py +84 -0
  59. pyopenapi_gen/core/telemetry.py +69 -0
  60. pyopenapi_gen/core/utils.py +456 -0
  61. pyopenapi_gen/core/warning_collector.py +83 -0
  62. pyopenapi_gen/core/writers/code_writer.py +135 -0
  63. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  64. pyopenapi_gen/core/writers/line_writer.py +217 -0
  65. pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
  66. pyopenapi_gen/core_package_template/README.md +21 -0
  67. pyopenapi_gen/emit/models_emitter.py +143 -0
  68. pyopenapi_gen/emitters/CLAUDE.md +286 -0
  69. pyopenapi_gen/emitters/client_emitter.py +51 -0
  70. pyopenapi_gen/emitters/core_emitter.py +181 -0
  71. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  72. pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
  73. pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
  74. pyopenapi_gen/emitters/mocks_emitter.py +185 -0
  75. pyopenapi_gen/emitters/models_emitter.py +426 -0
  76. pyopenapi_gen/generator/CLAUDE.md +352 -0
  77. pyopenapi_gen/generator/client_generator.py +567 -0
  78. pyopenapi_gen/generator/exceptions.py +7 -0
  79. pyopenapi_gen/helpers/CLAUDE.md +325 -0
  80. pyopenapi_gen/helpers/__init__.py +1 -0
  81. pyopenapi_gen/helpers/endpoint_utils.py +532 -0
  82. pyopenapi_gen/helpers/type_cleaner.py +334 -0
  83. pyopenapi_gen/helpers/type_helper.py +112 -0
  84. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  85. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  86. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  87. pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
  88. pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
  89. pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
  90. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
  91. pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
  92. pyopenapi_gen/helpers/url_utils.py +14 -0
  93. pyopenapi_gen/http_types.py +20 -0
  94. pyopenapi_gen/ir.py +165 -0
  95. pyopenapi_gen/py.typed +1 -0
  96. pyopenapi_gen/types/CLAUDE.md +140 -0
  97. pyopenapi_gen/types/__init__.py +11 -0
  98. pyopenapi_gen/types/contracts/__init__.py +13 -0
  99. pyopenapi_gen/types/contracts/protocols.py +106 -0
  100. pyopenapi_gen/types/contracts/types.py +28 -0
  101. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  102. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  103. pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
  104. pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
  105. pyopenapi_gen/types/services/__init__.py +5 -0
  106. pyopenapi_gen/types/services/type_service.py +165 -0
  107. pyopenapi_gen/types/strategies/__init__.py +5 -0
  108. pyopenapi_gen/types/strategies/response_strategy.py +310 -0
  109. pyopenapi_gen/visit/CLAUDE.md +272 -0
  110. pyopenapi_gen/visit/client_visitor.py +477 -0
  111. pyopenapi_gen/visit/docs_visitor.py +38 -0
  112. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  113. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
  114. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  115. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
  116. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
  117. pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
  118. pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
  119. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  120. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
  121. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
  122. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
  123. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  124. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
  125. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  126. pyopenapi_gen/visit/exception_visitor.py +90 -0
  127. pyopenapi_gen/visit/model/__init__.py +0 -0
  128. pyopenapi_gen/visit/model/alias_generator.py +93 -0
  129. pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
  130. pyopenapi_gen/visit/model/enum_generator.py +212 -0
  131. pyopenapi_gen/visit/model/model_visitor.py +198 -0
  132. pyopenapi_gen/visit/visitor.py +97 -0
  133. pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
  134. pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
  135. pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
  136. pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
  137. pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,224 @@
1
+ """pyopenapi_gen – Core package
2
+
3
+ This package provides the internal representation (IR) dataclasses that act as an
4
+ intermediate layer between the parsed OpenAPI specification and the code
5
+ emitters. The IR aims to be a *stable*, *fully‑typed* model that the rest of the
6
+ code‑generation pipeline can rely on.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ # Removed dataclasses, field, Enum, unique from here, they are in ir.py and http_types.py
14
+ from typing import (
15
+ TYPE_CHECKING,
16
+ Any,
17
+ List,
18
+ )
19
+
20
+ # Kept Any, List, Optional, TYPE_CHECKING for __getattr__ and __dir__
21
+ # Import HTTPMethod from its canonical location
22
+ from .http_types import HTTPMethod
23
+
24
+ # Import IR classes from their canonical location
25
+ from .ir import (
26
+ IROperation,
27
+ IRParameter,
28
+ IRRequestBody,
29
+ IRResponse,
30
+ IRSchema,
31
+ IRSpec,
32
+ )
33
+
34
+ __all__ = [
35
+ # Main API
36
+ "generate_client",
37
+ "ClientGenerator",
38
+ "GenerationError",
39
+ # IR classes
40
+ "HTTPMethod",
41
+ "IRParameter",
42
+ "IRResponse",
43
+ "IROperation",
44
+ "IRSchema",
45
+ "IRSpec",
46
+ "IRRequestBody",
47
+ # Utilities
48
+ "load_ir_from_spec",
49
+ "WarningCollector",
50
+ ]
51
+
52
+ # Semantic version of the generator core – automatically managed by semantic-release.
53
+ __version__: str = "2.7.2"
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Lazy-loading and autocompletion support (This part remains)
57
+ # ---------------------------------------------------------------------------
58
+ if TYPE_CHECKING:
59
+ # Imports for static analysis
60
+ from .core.loader.loader import load_ir_from_spec # noqa: F401
61
+ from .core.warning_collector import WarningCollector # noqa: F401
62
+ from .generator.client_generator import ClientGenerator, GenerationError # noqa: F401
63
+
64
+ # Expose loader and collector at package level
65
+ # __all__ is already updated above
66
+
67
+
68
+ def __getattr__(name: str) -> Any:
69
+ # Lazy-import attributes for runtime, supports IDE completion via TYPE_CHECKING
70
+ if name == "load_ir_from_spec":
71
+ from .core.loader.loader import load_ir_from_spec as _func
72
+
73
+ return _func
74
+ if name == "WarningCollector":
75
+ from .core.warning_collector import WarningCollector as _warning_cls
76
+
77
+ return _warning_cls
78
+ if name == "ClientGenerator":
79
+ from .generator.client_generator import ClientGenerator as _gen_cls
80
+
81
+ return _gen_cls
82
+ if name == "GenerationError":
83
+ from .generator.client_generator import GenerationError as _exc
84
+
85
+ return _exc
86
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
87
+
88
+
89
+ def __dir__() -> List[str]:
90
+ # Ensure dir() and completion shows all exports
91
+ return __all__.copy()
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Main Programmatic API
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ def generate_client(
100
+ spec_path: str,
101
+ project_root: str,
102
+ output_package: str,
103
+ core_package: str | None = None,
104
+ force: bool = False,
105
+ no_postprocess: bool = False,
106
+ verbose: bool = False,
107
+ ) -> List[Path]:
108
+ """Generate a Python client from an OpenAPI specification.
109
+
110
+ This is the main entry point for programmatic usage of pyopenapi_gen.
111
+ It provides a simple function-based API for generating OpenAPI clients
112
+ without needing to instantiate classes or understand internal structure.
113
+
114
+ Args:
115
+ spec_path: Path or URL to the OpenAPI specification (YAML or JSON).
116
+ Can be a relative/absolute file path or HTTP(S) URL.
117
+ Examples: "input/spec.yaml", "https://api.example.com/openapi.json"
118
+
119
+ project_root: Root directory of your Python project where the generated
120
+ client will be placed. This is the directory that contains
121
+ your top-level Python packages.
122
+
123
+ output_package: Python package name for the generated client.
124
+ Uses dot notation (e.g., 'pyapis.my_client').
125
+ The client will be generated at:
126
+ {project_root}/{output_package_as_path}/
127
+
128
+ core_package: Optional Python package name for shared core runtime.
129
+ If not specified, defaults to {output_package}.core.
130
+ Use this when generating multiple clients that share
131
+ common runtime code (auth, config, http transport, etc.).
132
+
133
+ force: If True, overwrites existing output without diff checking.
134
+ If False (default), compares with existing output and only
135
+ updates if changes are detected.
136
+
137
+ no_postprocess: If True, skips post-processing (Black formatting and
138
+ mypy type checking). Useful for faster iteration during
139
+ development.
140
+
141
+ verbose: If True, prints detailed progress information during generation.
142
+
143
+ Returns:
144
+ List of Path objects for all generated files.
145
+
146
+ Raises:
147
+ GenerationError: If generation fails due to invalid spec, file I/O
148
+ errors, or other issues during code generation.
149
+
150
+ Examples:
151
+ Basic usage with default settings:
152
+
153
+ >>> from pyopenapi_gen import generate_client
154
+ >>>
155
+ >>> files = generate_client(
156
+ ... spec_path="input/openapi.yaml",
157
+ ... project_root=".",
158
+ ... output_package="pyapis.my_client"
159
+ ... )
160
+ >>> print(f"Generated {len(files)} files")
161
+
162
+ Generate multiple clients sharing a common core package:
163
+
164
+ >>> from pyopenapi_gen import generate_client
165
+ >>>
166
+ >>> # Generate first client (creates shared core)
167
+ >>> generate_client(
168
+ ... spec_path="api_v1.yaml",
169
+ ... project_root=".",
170
+ ... output_package="pyapis.client_v1",
171
+ ... core_package="pyapis.core"
172
+ ... )
173
+ >>>
174
+ >>> # Generate second client (reuses core)
175
+ >>> generate_client(
176
+ ... spec_path="api_v2.yaml",
177
+ ... project_root=".",
178
+ ... output_package="pyapis.client_v2",
179
+ ... core_package="pyapis.core"
180
+ ... )
181
+
182
+ Handle generation errors:
183
+
184
+ >>> from pyopenapi_gen import generate_client, GenerationError
185
+ >>>
186
+ >>> try:
187
+ ... generate_client(
188
+ ... spec_path="openapi.yaml",
189
+ ... project_root=".",
190
+ ... output_package="pyapis.my_client",
191
+ ... verbose=True
192
+ ... )
193
+ ... except GenerationError as e:
194
+ ... print(f"Generation failed: {e}")
195
+
196
+ Force regeneration with verbose output:
197
+
198
+ >>> generate_client(
199
+ ... spec_path="openapi.yaml",
200
+ ... project_root=".",
201
+ ... output_package="pyapis.my_client",
202
+ ... force=True,
203
+ ... verbose=True
204
+ ... )
205
+
206
+ Notes:
207
+ - Generated clients are completely self-contained and require no
208
+ runtime dependency on pyopenapi_gen
209
+ - All generated code is automatically formatted with Black and
210
+ type-checked with mypy (unless no_postprocess=True)
211
+ - The generated client uses modern async/await patterns with httpx
212
+ - Type hints are included for all generated code
213
+ """
214
+ from .generator.client_generator import ClientGenerator
215
+
216
+ generator = ClientGenerator(verbose=verbose)
217
+ return generator.generate(
218
+ spec_path=spec_path,
219
+ project_root=Path(project_root),
220
+ output_package=output_package,
221
+ core_package=core_package,
222
+ force=force,
223
+ no_postprocess=no_postprocess,
224
+ )
@@ -0,0 +1,6 @@
1
+ """Main entry point for the pyopenapi_gen CLI."""
2
+
3
+ from .cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
pyopenapi_gen/cli.py ADDED
@@ -0,0 +1,62 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+
5
+ from .core.spec_fetcher import is_url
6
+ from .generator.client_generator import ClientGenerator, GenerationError
7
+
8
+
9
+ def main(
10
+ spec: str = typer.Argument(..., help="Path or URL to OpenAPI spec"),
11
+ project_root: Path = typer.Option(
12
+ ...,
13
+ "--project-root",
14
+ help=(
15
+ "Path to the directory containing your top-level Python packages. "
16
+ "Generated code will be placed at project-root + output-package path."
17
+ ),
18
+ ),
19
+ output_package: str = typer.Option(
20
+ ..., "--output-package", help="Python package path for the generated client (e.g., 'pyapis.my_api_client')."
21
+ ),
22
+ force: bool = typer.Option(False, "-f", "--force", help="Overwrite without diff check"),
23
+ no_postprocess: bool = typer.Option(False, "--no-postprocess", help="Skip post-processing (type checking, etc.)"),
24
+ core_package: str | None = typer.Option(
25
+ None,
26
+ "--core-package",
27
+ help=(
28
+ "Python package path for the core package (e.g., 'pyapis.core'). "
29
+ "If not set, defaults to <output-package>.core."
30
+ ),
31
+ ),
32
+ ) -> None:
33
+ """
34
+ Generate a Python OpenAPI client from a spec file or URL.
35
+ Only parses CLI arguments and delegates to ClientGenerator.
36
+ """
37
+ if core_package is None:
38
+ core_package = output_package + ".core"
39
+ generator = ClientGenerator()
40
+ # Handle both URLs (pass as-is) and file paths (resolve to absolute)
41
+ spec_path = spec if is_url(spec) else str(Path(spec).resolve())
42
+ try:
43
+ generator.generate(
44
+ spec_path=spec_path,
45
+ project_root=project_root,
46
+ output_package=output_package,
47
+ force=force,
48
+ no_postprocess=no_postprocess,
49
+ core_package=core_package,
50
+ )
51
+ typer.echo("Client generation complete.")
52
+ except GenerationError as e:
53
+ typer.echo(f"Generation failed: {e}", err=True)
54
+ raise typer.Exit(code=1)
55
+
56
+
57
+ app = typer.Typer(help="PyOpenAPI Generator CLI - Generate Python clients from OpenAPI specs.")
58
+ app.command()(main)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ app()
@@ -0,0 +1,284 @@
1
+ # context/ - Rendering Context Management
2
+
3
+ ## Why This Folder?
4
+ Manage stateful information during code generation: imports, templates, file paths, and rendering state. Provides clean interface between visitors and emitters.
5
+
6
+ ## Key Dependencies
7
+ - **Input**: Path configuration, package names, template data
8
+ - **Output**: Import statements, resolved file paths, template rendering
9
+ - **Used by**: All visitors and emitters for consistent code generation
10
+
11
+ ## Essential Architecture
12
+
13
+ ### 1. Context Lifecycle
14
+ ```python
15
+ # 1. Create context for generation session
16
+ context = RenderContext(project_root="/path/to/project", output_package="my_client")
17
+
18
+ # 2. Visitors use context for type resolution and imports
19
+ visitor.visit_schema(schema, context) # Registers imports
20
+
21
+ # 3. Emitters use context for file organization
22
+ emitter.emit_models(schemas, context) # Consumes imports
23
+ ```
24
+
25
+ ### 2. State Management
26
+ ```python
27
+ # render_context.py
28
+ class RenderContext:
29
+ def __init__(self, project_root: Path, output_package: str):
30
+ self.import_collector = ImportCollector()
31
+ self.file_manager = FileManager(project_root)
32
+ self.template_vars = {}
33
+ self.output_package = output_package
34
+ self.forward_refs = set()
35
+ ```
36
+
37
+ ## Critical Components
38
+
39
+ ### render_context.py
40
+ **Purpose**: Main context object passed through generation pipeline
41
+ ```python
42
+ class RenderContext:
43
+ def add_import(self, import_statement: str) -> None:
44
+ """Register import for current file being generated"""
45
+ self.import_collector.add_import(import_statement)
46
+
47
+ def get_imports(self) -> List[str]:
48
+ """Get sorted, deduplicated imports for current file"""
49
+ return self.import_collector.get_sorted_imports()
50
+
51
+ def clear_imports(self) -> None:
52
+ """Clear imports for next file generation"""
53
+ self.import_collector.clear()
54
+
55
+ def resolve_relative_import(self, from_package: str, to_package: str) -> str:
56
+ """Convert absolute import to relative import"""
57
+ return self.import_collector.make_relative_import(from_package, to_package)
58
+ ```
59
+
60
+ ### import_collector.py
61
+ **Purpose**: Collect and manage import statements during code generation
62
+ ```python
63
+ class ImportCollector:
64
+ def __init__(self):
65
+ self.imports: Set[str] = set()
66
+ self.from_imports: Dict[str, Set[str]] = defaultdict(set)
67
+
68
+ def add_import(self, import_statement: str) -> None:
69
+ """Add import statement, handling both 'import' and 'from' forms"""
70
+ if import_statement.startswith("from "):
71
+ self.parse_from_import(import_statement)
72
+ else:
73
+ self.imports.add(import_statement)
74
+
75
+ def get_sorted_imports(self) -> List[str]:
76
+ """Return sorted imports: stdlib, third-party, local"""
77
+ return self.sort_imports_by_category()
78
+ ```
79
+
80
+ ### file_manager.py
81
+ **Purpose**: Handle file operations and path resolution
82
+ ```python
83
+ class FileManager:
84
+ def __init__(self, project_root: Path):
85
+ self.project_root = project_root
86
+
87
+ def write_file(self, file_path: Path, content: str) -> None:
88
+ """Write file with proper directory creation"""
89
+ file_path.parent.mkdir(parents=True, exist_ok=True)
90
+ file_path.write_text(content)
91
+
92
+ def resolve_package_path(self, package_name: str) -> Path:
93
+ """Convert package.name to file system path"""
94
+ parts = package_name.split(".")
95
+ return self.project_root / Path(*parts)
96
+ ```
97
+
98
+ ## Import Management Patterns
99
+
100
+ ### 1. Import Categories
101
+ ```python
102
+ # import_collector.py
103
+ def categorize_import(self, import_statement: str) -> ImportCategory:
104
+ """Categorize imports for proper sorting"""
105
+ if self.is_stdlib_import(import_statement):
106
+ return ImportCategory.STDLIB
107
+ elif self.is_third_party_import(import_statement):
108
+ return ImportCategory.THIRD_PARTY
109
+ else:
110
+ return ImportCategory.LOCAL
111
+ ```
112
+
113
+ ### 2. From Import Consolidation
114
+ ```python
115
+ # Convert multiple from imports to single statement
116
+ # "from typing import List"
117
+ # "from typing import Dict"
118
+ # →
119
+ # "from typing import Dict, List"
120
+
121
+ def consolidate_from_imports(self) -> List[str]:
122
+ consolidated = []
123
+ for module, imports in self.from_imports.items():
124
+ sorted_imports = sorted(imports)
125
+ consolidated.append(f"from {module} import {', '.join(sorted_imports)}")
126
+ return consolidated
127
+ ```
128
+
129
+ ### 3. Relative Import Conversion
130
+ ```python
131
+ def make_relative_import(self, from_package: str, to_package: str) -> str:
132
+ """Convert absolute import to relative import"""
133
+ # from my_client.models.user import User
134
+ # →
135
+ # from ..models.user import User (when called from my_client.endpoints)
136
+
137
+ from_parts = from_package.split(".")
138
+ to_parts = to_package.split(".")
139
+
140
+ # Find common prefix
141
+ common_len = self.find_common_prefix_length(from_parts, to_parts)
142
+
143
+ # Calculate relative depth
144
+ relative_depth = len(from_parts) - common_len
145
+ prefix = "." * relative_depth
146
+
147
+ # Build relative import
148
+ remaining_path = ".".join(to_parts[common_len:])
149
+ return f"from {prefix}{remaining_path} import"
150
+ ```
151
+
152
+ ## Template Management
153
+
154
+ ### 1. Template Variables
155
+ ```python
156
+ # Store template variables for consistent rendering
157
+ context.template_vars.update({
158
+ "client_name": "MyAPIClient",
159
+ "base_url": "https://api.example.com",
160
+ "version": "1.0.0",
161
+ "auth_type": "bearer"
162
+ })
163
+ ```
164
+
165
+ ### 2. Template Rendering
166
+ ```python
167
+ def render_template(self, template: str, **kwargs) -> str:
168
+ """Render template with context variables"""
169
+ all_vars = {**self.template_vars, **kwargs}
170
+ return template.format(**all_vars)
171
+ ```
172
+
173
+ ## Dependencies on Other Systems
174
+
175
+ ### From types/
176
+ - Implements `TypeContext` protocol for type resolution
177
+ - Provides import registration for complex types
178
+
179
+ ### From visit/
180
+ - Receives import registration during code generation
181
+ - Provides path resolution for relative imports
182
+
183
+ ### From emitters/
184
+ - Provides file writing capabilities
185
+ - Supplies consolidated imports for file headers
186
+
187
+ ## Testing Requirements
188
+
189
+ ### Import Management Tests
190
+ ```python
191
+ def test_import_collector__multiple_from_imports__consolidates_correctly():
192
+ # Arrange
193
+ collector = ImportCollector()
194
+ collector.add_import("from typing import List")
195
+ collector.add_import("from typing import Dict")
196
+
197
+ # Act
198
+ imports = collector.get_sorted_imports()
199
+
200
+ # Assert
201
+ assert "from typing import Dict, List" in imports
202
+ ```
203
+
204
+ ### Path Resolution Tests
205
+ ```python
206
+ def test_file_manager__package_path__resolves_correctly():
207
+ # Test package name to file path conversion
208
+ manager = FileManager(Path("/project"))
209
+ path = manager.resolve_package_path("my_client.models")
210
+
211
+ assert path == Path("/project/my_client/models")
212
+ ```
213
+
214
+ ## Extension Points
215
+
216
+ ### Custom Import Sorting
217
+ ```python
218
+ class CustomImportCollector(ImportCollector):
219
+ def sort_imports_by_category(self) -> List[str]:
220
+ # Custom import sorting logic
221
+ # Example: Group all async imports together
222
+ pass
223
+ ```
224
+
225
+ ### Template System Integration
226
+ ```python
227
+ def add_template_engine(self, engine: TemplateEngine) -> None:
228
+ """Add custom template engine (Jinja2, etc.)"""
229
+ self.template_engine = engine
230
+
231
+ def render_template(self, template_name: str, **kwargs) -> str:
232
+ """Render template using custom engine"""
233
+ return self.template_engine.render(template_name, **kwargs)
234
+ ```
235
+
236
+ ## Critical Implementation Details
237
+
238
+ ### Thread Safety
239
+ ```python
240
+ # Context is NOT thread-safe by design
241
+ # Each generation session gets its own context instance
242
+ def create_context() -> RenderContext:
243
+ return RenderContext(project_root, output_package)
244
+ ```
245
+
246
+ ### Memory Management
247
+ ```python
248
+ # Clear context between files to prevent memory leaks
249
+ def emit_file(self, file_path: Path, generator_func: Callable) -> None:
250
+ self.context.clear_imports()
251
+
252
+ # Generate code
253
+ code = generator_func(self.context)
254
+
255
+ # Write file with imports
256
+ imports = self.context.get_imports()
257
+ final_code = self.combine_imports_and_code(imports, code)
258
+
259
+ self.file_manager.write_file(file_path, final_code)
260
+ ```
261
+
262
+ ### Error Context
263
+ ```python
264
+ # Always provide context in error messages
265
+ def add_import_with_context(self, import_statement: str, file_context: str) -> None:
266
+ try:
267
+ self.import_collector.add_import(import_statement)
268
+ except Exception as e:
269
+ raise ImportError(f"Failed to add import '{import_statement}' in {file_context}: {e}")
270
+ ```
271
+
272
+ ## Common Pitfalls
273
+
274
+ 1. **Import Leakage**: Not clearing imports between files
275
+ 2. **Path Confusion**: Using absolute paths instead of relative
276
+ 3. **State Mutation**: Modifying context from multiple threads
277
+ 4. **Memory Leaks**: Not cleaning up context after generation
278
+
279
+ ## Best Practices
280
+
281
+ 1. **One Context Per Session**: Create new context for each generation
282
+ 2. **Clear Between Files**: Always clear imports between file generations
283
+ 3. **Use Relative Imports**: Convert absolute imports to relative
284
+ 4. **Error Context**: Include file/operation context in errors
@@ -0,0 +1,52 @@
1
+ """
2
+ FileManager: Manages file operations for code generation.
3
+
4
+ This module provides utilities for creating directories and writing
5
+ generated Python code to files, with appropriate logging for debugging.
6
+ """
7
+
8
+ import os
9
+ import tempfile
10
+
11
+
12
+ class FileManager:
13
+ """
14
+ Manages file operations for the code generation process.
15
+
16
+ This class provides methods to write content to files and ensure directories
17
+ exist, with built-in logging for debugging purposes.
18
+ """
19
+
20
+ def write_file(self, path: str, content: str) -> None:
21
+ """
22
+ Write content to a file, ensuring the parent directory exists.
23
+
24
+ This method also logs the file path and first 10 lines of content
25
+ to a debug log for troubleshooting purposes.
26
+
27
+ Args:
28
+ path: The absolute path to the file to write
29
+ content: The string content to write to the file
30
+ """
31
+ self.ensure_dir(os.path.dirname(path))
32
+
33
+ # Log the file path and first 10 lines of content for debugging
34
+ debug_log_path = os.path.join(tempfile.gettempdir(), "pyopenapi_gen_file_write_debug.log")
35
+ with open(debug_log_path, "a") as debug_log:
36
+ debug_log.write(f"WRITE FILE: {path}\n")
37
+ for line in content.splitlines()[:10]:
38
+ debug_log.write(line + "\n")
39
+ debug_log.write("---\n")
40
+
41
+ # Write the content to the file
42
+ with open(path, "w") as f:
43
+ f.write(content)
44
+
45
+ def ensure_dir(self, path: str) -> None:
46
+ """
47
+ Ensure a directory exists, creating it if necessary.
48
+
49
+ Args:
50
+ path: The directory path to ensure exists
51
+ """
52
+ os.makedirs(path, exist_ok=True)