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,286 @@
1
+ # emitters/ - File Organization and Output
2
+
3
+ ## Why This Folder?
4
+ Transform visitor-generated code strings into properly structured Python packages. Handles file creation, import resolution, and package organization.
5
+
6
+ ## Key Dependencies
7
+ - **Input**: Code strings from `../visit/` visitors
8
+ - **Output**: Python files in target package structure
9
+ - **Services**: `FileManager` from `../context/file_manager.py`
10
+ - **Context**: `RenderContext` for import management
11
+
12
+ ## Essential Architecture
13
+
14
+ ### 1. Emitter Responsibilities
15
+ ```python
16
+ # Each emitter handles one aspect of the generated client
17
+ models_emitter.py → models/ directory with dataclasses/enums
18
+ endpoints_emitter.py → endpoints/ directory with operation methods
19
+ client_emitter.py → client.py main interface
20
+ core_emitter.py → core/ directory with runtime dependencies
21
+ exceptions_emitter.py → exceptions.py error hierarchy
22
+ ```
23
+
24
+ ### 2. Package Structure Creation
25
+ ```python
26
+ # Target structure for generated client
27
+ output_package/
28
+ ├── __init__.py # Package initialization
29
+ ├── client.py # Main client class
30
+ ├── models/ # Data models
31
+ │ ├── __init__.py
32
+ │ ├── user.py
33
+ │ └── order.py
34
+ ├── endpoints/ # Operation methods
35
+ │ ├── __init__.py
36
+ │ ├── users.py
37
+ │ └── orders.py
38
+ ├── core/ # Runtime dependencies
39
+ │ ├── __init__.py
40
+ │ ├── auth/
41
+ │ ├── exceptions.py
42
+ │ └── http_transport.py
43
+ └── exceptions.py # Exception hierarchy
44
+ ```
45
+
46
+ ## Critical Components
47
+
48
+ ### models_emitter.py
49
+ **Purpose**: Create models/ directory with dataclass and enum files
50
+ ```python
51
+ def emit_models(self, schemas: Dict[str, IRSchema], context: RenderContext) -> None:
52
+ # 1. Group schemas by module (one file per schema or logical grouping)
53
+ # 2. Generate code for each schema using ModelVisitor
54
+ # 3. Create __init__.py with imports
55
+ # 4. Write files to models/ directory
56
+
57
+ for schema_name, schema in schemas.items():
58
+ module_name = self.get_module_name(schema_name)
59
+ file_path = self.output_path / "models" / f"{module_name}.py"
60
+
61
+ # Generate model code
62
+ model_code = self.model_visitor.visit_schema(schema, context)
63
+
64
+ # Write file
65
+ self.file_manager.write_file(file_path, model_code)
66
+ ```
67
+
68
+ ### endpoints_emitter.py
69
+ **Purpose**: Create endpoints/ directory with operation methods grouped by tag
70
+ ```python
71
+ def emit_endpoints(self, operations: List[IROperation], context: RenderContext) -> None:
72
+ # 1. Group operations by OpenAPI tag
73
+ operations_by_tag = self.group_by_tag(operations)
74
+
75
+ # 2. Generate endpoint class for each tag
76
+ for tag, tag_operations in operations_by_tag.items():
77
+ class_name = f"{tag.capitalize()}Endpoints"
78
+ file_path = self.output_path / "endpoints" / f"{tag}.py"
79
+
80
+ # Generate endpoint class code
81
+ endpoint_code = self.endpoint_visitor.visit_tag_operations(tag_operations, context)
82
+
83
+ # Write file
84
+ self.file_manager.write_file(file_path, endpoint_code)
85
+ ```
86
+
87
+ ### client_emitter.py
88
+ **Purpose**: Create main client.py with tag-grouped properties
89
+ ```python
90
+ def emit_client(self, spec: IRSpec, context: RenderContext) -> None:
91
+ # 1. Generate main client class
92
+ # 2. Create properties for each tag endpoint
93
+ # 3. Generate context manager methods
94
+ # 4. Handle authentication setup
95
+
96
+ client_code = self.client_visitor.visit_spec(spec, context)
97
+ self.file_manager.write_file(self.output_path / "client.py", client_code)
98
+ ```
99
+
100
+ ### core_emitter.py
101
+ **Purpose**: Copy runtime dependencies to core/ directory
102
+ ```python
103
+ def emit_core(self, output_package: str, core_package: str) -> None:
104
+ # 1. Copy auth/ directory
105
+ # 2. Copy exceptions.py, http_transport.py, etc.
106
+ # 3. Update import paths for target package
107
+ # 4. Handle shared core vs embedded core
108
+
109
+ if self.use_shared_core:
110
+ # Create symlinks or references to shared core
111
+ pass
112
+ else:
113
+ # Copy all core files to client package
114
+ self.copy_core_files()
115
+ ```
116
+
117
+ ## File Management Patterns
118
+
119
+ ### 1. Import Resolution
120
+ ```python
121
+ # Always resolve imports after code generation
122
+ def write_file_with_imports(self, file_path: Path, code: str, context: RenderContext) -> None:
123
+ # 1. Collect imports from context
124
+ imports = context.get_imports()
125
+
126
+ # 2. Sort and deduplicate imports
127
+ sorted_imports = self.sort_imports(imports)
128
+
129
+ # 3. Combine imports with code
130
+ final_code = self.combine_imports_and_code(sorted_imports, code)
131
+
132
+ # 4. Write file
133
+ self.file_manager.write_file(file_path, final_code)
134
+ ```
135
+
136
+ ### 2. Package Initialization
137
+ ```python
138
+ # Always create __init__.py files
139
+ def create_package_init(self, package_path: Path, exports: List[str]) -> None:
140
+ init_content = []
141
+
142
+ # Add imports for all public exports
143
+ for export in exports:
144
+ init_content.append(f"from .{export} import {export}")
145
+
146
+ # Add __all__ for explicit exports
147
+ init_content.append(f"__all__ = {exports}")
148
+
149
+ self.file_manager.write_file(package_path / "__init__.py", "\n".join(init_content))
150
+ ```
151
+
152
+ ### 3. Relative Import Handling
153
+ ```python
154
+ # Convert absolute imports to relative for generated packages
155
+ def convert_to_relative_imports(self, code: str, current_package: str) -> str:
156
+ # Replace absolute imports with relative imports
157
+ # Example: "from my_client.models.user import User" → "from ..models.user import User"
158
+
159
+ import_pattern = re.compile(rf"from {re.escape(current_package)}\.(.+?) import")
160
+
161
+ def replace_import(match):
162
+ import_path = match.group(1)
163
+ depth = len(import_path.split("."))
164
+ relative_prefix = "." * depth
165
+ return f"from {relative_prefix}{import_path} import"
166
+
167
+ return import_pattern.sub(replace_import, code)
168
+ ```
169
+
170
+ ## Dependencies on Other Systems
171
+
172
+ ### From visit/
173
+ - Consumes generated code strings
174
+ - Coordinates with visitors for code generation
175
+
176
+ ### From context/
177
+ - `FileManager` for file operations
178
+ - `RenderContext` for import management
179
+ - Path resolution utilities
180
+
181
+ ### From core/
182
+ - Runtime components copied to generated clients
183
+ - Template files for package structure
184
+
185
+ ## Testing Requirements
186
+
187
+ ### File Creation Tests
188
+ ```python
189
+ def test_models_emitter__simple_schema__creates_correct_file():
190
+ # Arrange
191
+ schema = IRSchema(name="User", type="object", properties={"name": {"type": "string"}})
192
+ emitter = ModelsEmitter(output_path="/tmp/test")
193
+
194
+ # Act
195
+ emitter.emit_models({"User": schema}, context)
196
+
197
+ # Assert
198
+ assert Path("/tmp/test/models/user.py").exists()
199
+ content = Path("/tmp/test/models/user.py").read_text()
200
+ assert "@dataclass" in content
201
+ assert "name: str" in content
202
+ ```
203
+
204
+ ### Import Resolution Tests
205
+ ```python
206
+ def test_emitter__complex_types__resolves_imports_correctly():
207
+ # Test that imports are correctly collected and written
208
+ # Verify no duplicate imports
209
+ # Verify correct import sorting
210
+ ```
211
+
212
+ ## Extension Points
213
+
214
+ ### Adding New Emitters
215
+ ```python
216
+ # Create new emitter for new output aspects
217
+ class CustomEmitter:
218
+ def __init__(self, output_path: Path, file_manager: FileManager):
219
+ self.output_path = output_path
220
+ self.file_manager = file_manager
221
+
222
+ def emit_custom(self, data: Any, context: RenderContext) -> None:
223
+ # Custom file creation logic
224
+ pass
225
+ ```
226
+
227
+ ### Custom Package Structures
228
+ ```python
229
+ # Modify emitters to create different package layouts
230
+ class AlternativeModelsEmitter(ModelsEmitter):
231
+ def get_file_path(self, schema_name: str) -> Path:
232
+ # Custom file organization logic
233
+ # Example: Group models by domain
234
+ domain = self.get_domain(schema_name)
235
+ return self.output_path / "models" / domain / f"{schema_name.lower()}.py"
236
+ ```
237
+
238
+ ## Critical Implementation Details
239
+
240
+ ### File Path Resolution
241
+ ```python
242
+ # Always use pathlib.Path for cross-platform compatibility
243
+ def get_output_path(self, package_name: str, module_name: str) -> Path:
244
+ # Convert package.module to file path
245
+ parts = package_name.split(".")
246
+ path = Path(self.project_root)
247
+ for part in parts:
248
+ path = path / part
249
+ return path / f"{module_name}.py"
250
+ ```
251
+
252
+ ### Error Handling
253
+ ```python
254
+ def emit_safely(self, generator_func: Callable, context: RenderContext) -> None:
255
+ try:
256
+ generator_func(context)
257
+ except Exception as e:
258
+ # Add context to file emission errors
259
+ raise FileEmissionError(f"Failed to emit {self.__class__.__name__}: {e}")
260
+ ```
261
+
262
+ ### Atomic File Operations
263
+ ```python
264
+ def write_file_atomically(self, file_path: Path, content: str) -> None:
265
+ # Write to temporary file first, then move
266
+ temp_path = file_path.with_suffix(f"{file_path.suffix}.tmp")
267
+
268
+ try:
269
+ temp_path.write_text(content)
270
+ temp_path.replace(file_path) # Atomic move
271
+ except Exception:
272
+ if temp_path.exists():
273
+ temp_path.unlink()
274
+ raise
275
+ ```
276
+
277
+ ### Diff Checking
278
+ ```python
279
+ def should_write_file(self, file_path: Path, new_content: str) -> bool:
280
+ # Only write if content changed
281
+ if not file_path.exists():
282
+ return True
283
+
284
+ existing_content = file_path.read_text()
285
+ return existing_content != new_content
286
+ ```
@@ -0,0 +1,51 @@
1
+ import tempfile
2
+ import traceback
3
+ from pathlib import Path
4
+
5
+ from pyopenapi_gen import IRSpec
6
+ from pyopenapi_gen.context.render_context import RenderContext
7
+
8
+ from ..visit.client_visitor import ClientVisitor
9
+
10
+ # NOTE: ClientConfig and transports are only referenced in template strings, not at runtime
11
+ # hence we avoid importing config and http_transport modules to prevent runtime errors
12
+
13
+ # Jinja template for base async client file with tag-specific clients
14
+ # CLIENT_TEMPLATE = ''' ... removed ... '''
15
+
16
+
17
+ class ClientEmitter:
18
+ """Generates core client files (client.py) from IRSpec using visitor/context."""
19
+
20
+ def __init__(self, context: RenderContext) -> None:
21
+ self.visitor = ClientVisitor()
22
+ self.context = context
23
+
24
+ def emit(self, spec: IRSpec, output_dir_str: str) -> list[str]:
25
+ error_log = Path(tempfile.gettempdir()) / "pyopenapi_gen_error.log"
26
+ generated_files = []
27
+ try:
28
+ output_dir_abs = Path(output_dir_str)
29
+ output_dir_abs.mkdir(parents=True, exist_ok=True)
30
+
31
+ client_path = output_dir_abs / "client.py"
32
+
33
+ self.context.set_current_file(str(client_path))
34
+
35
+ client_code = self.visitor.visit(spec, self.context)
36
+ imports_code = self.context.render_imports()
37
+ file_content = imports_code + "\n\n" + client_code
38
+
39
+ self.context.file_manager.write_file(str(client_path), file_content)
40
+ generated_files.append(str(client_path))
41
+
42
+ pytyped_path = output_dir_abs / "py.typed"
43
+ if not pytyped_path.exists():
44
+ self.context.file_manager.write_file(str(pytyped_path), "")
45
+ generated_files.append(str(pytyped_path))
46
+ return generated_files
47
+ except Exception as e:
48
+ with open(error_log, "a") as f:
49
+ f.write(f"ERROR in ClientEmitter.emit: {e}\n")
50
+ f.write(traceback.format_exc())
51
+ raise
@@ -0,0 +1,181 @@
1
+ import importlib.resources
2
+ import os
3
+ from typing import List
4
+
5
+ from pyopenapi_gen.context.file_manager import FileManager
6
+
7
+ # Each tuple: (module, filename, destination)
8
+ RUNTIME_FILES = [
9
+ ("pyopenapi_gen.core", "http_transport.py", "core/http_transport.py"),
10
+ ("pyopenapi_gen.core", "exceptions.py", "core/exceptions.py"),
11
+ ("pyopenapi_gen.core", "streaming_helpers.py", "core/streaming_helpers.py"),
12
+ ("pyopenapi_gen.core", "pagination.py", "core/pagination.py"),
13
+ ("pyopenapi_gen.core", "cattrs_converter.py", "core/cattrs_converter.py"),
14
+ ("pyopenapi_gen.core", "utils.py", "core/utils.py"),
15
+ ("pyopenapi_gen.core.auth", "base.py", "core/auth/base.py"),
16
+ ("pyopenapi_gen.core.auth", "plugins.py", "core/auth/plugins.py"),
17
+ ]
18
+
19
+ # +++ Add template README location +++
20
+ CORE_README_TEMPLATE_MODULE = "pyopenapi_gen.core_package_template"
21
+ CORE_README_TEMPLATE_FILENAME = "README.md"
22
+
23
+ CONFIG_TEMPLATE = """
24
+ from dataclasses import dataclass
25
+ @dataclass
26
+ class ClientConfig:
27
+ base_url: str
28
+ timeout: float | None = 30.0
29
+ """
30
+
31
+
32
+ class CoreEmitter:
33
+ """Copies all required runtime files into the generated core module."""
34
+
35
+ def __init__(
36
+ self, core_dir: str = "core", core_package: str = "core", exception_alias_names: List[str] | None = None
37
+ ):
38
+ # core_dir is the relative path WITHIN the output package, e.g., "core" or "shared/core"
39
+ # core_package is the Python import name, e.g., "core" or "shared.core"
40
+ self.core_dir_name = os.path.basename(core_dir) # e.g., "core"
41
+ self.core_dir_relative = core_dir # e.g., "core" or "shared/core"
42
+ self.core_package = core_package
43
+ self.exception_alias_names = exception_alias_names if exception_alias_names is not None else []
44
+ self.file_manager = FileManager()
45
+
46
+ def emit(self, package_output_dir: str) -> list[str]:
47
+ """
48
+ Emits the core files into the specified core directory within the package output directory.
49
+ Args:
50
+ package_output_dir: The root directory where the generated package is being placed.
51
+ e.g., /path/to/gen/my_client
52
+ Returns:
53
+ List of generated file paths relative to the workspace root.
54
+ """
55
+ # Determine the absolute path for the core directory, e.g., /path/to/gen/my_client/core
56
+ actual_core_dir = os.path.join(package_output_dir, self.core_dir_relative)
57
+
58
+ generated_files = []
59
+ # Ensure the core directory exists (e.g., my_client/core or my_client/shared/core)
60
+ self.file_manager.ensure_dir(actual_core_dir)
61
+
62
+ for module, filename, rel_dst in RUNTIME_FILES:
63
+ # rel_dst is like "core/http_transport.py" or "core/auth/base.py"
64
+ # We want the part after "core/", e.g., "http_transport.py" or "auth/base.py"
65
+ # And join it with the actual_core_dir
66
+ destination_relative_to_core = rel_dst.replace("core/", "", 1)
67
+ dst = os.path.join(actual_core_dir, destination_relative_to_core)
68
+
69
+ self.file_manager.ensure_dir(os.path.dirname(dst))
70
+ # Use importlib.resources to read the file from the package
71
+ try:
72
+ # Read from pyopenapi_gen.core... or pyopenapi_gen.core.auth...
73
+ with importlib.resources.files(module).joinpath(filename).open("r") as f:
74
+ content = f.read()
75
+ self.file_manager.write_file(dst, content)
76
+ generated_files.append(dst)
77
+ except FileNotFoundError:
78
+ print(f"Warning: Could not find runtime file {filename} in module {module}. Skipping.")
79
+
80
+ # Always create __init__.py files for core and subfolders within the actual core dir
81
+ core_init_path = os.path.join(actual_core_dir, "__init__.py")
82
+ core_init_content = [
83
+ "# Re-export core exceptions and generated aliases",
84
+ "from .exceptions import HTTPError, ClientError, ServerError",
85
+ "from .exception_aliases import * # noqa: F403",
86
+ "",
87
+ "# Re-export other commonly used core components",
88
+ "from .http_transport import HttpTransport, HttpxTransport",
89
+ "from .config import ClientConfig",
90
+ "from .cattrs_converter import structure_from_dict, unstructure_to_dict, converter",
91
+ "from .utils import DataclassSerializer",
92
+ "from .auth.base import BaseAuth",
93
+ "from .auth.plugins import ApiKeyAuth, BearerAuth, OAuth2Auth",
94
+ "",
95
+ "__all__ = [",
96
+ " # Base exceptions",
97
+ ' "HTTPError",',
98
+ ' "ClientError",',
99
+ ' "ServerError",',
100
+ " # All ErrorXXX from exception_aliases are implicitly in __all__ due to star import",
101
+ "",
102
+ " # Transport layer",
103
+ ' "HttpTransport",',
104
+ ' "HttpxTransport",',
105
+ "",
106
+ " # Configuration",
107
+ ' "ClientConfig",',
108
+ "",
109
+ " # Serialization (cattrs)",
110
+ ' "structure_from_dict",',
111
+ ' "unstructure_to_dict",',
112
+ ' "converter",',
113
+ "",
114
+ " # Utilities",
115
+ ' "DataclassSerializer",',
116
+ "",
117
+ " # Authentication",
118
+ ' "BaseAuth",',
119
+ ' "ApiKeyAuth",',
120
+ ' "BearerAuth",',
121
+ ' "OAuth2Auth",',
122
+ ]
123
+ # Add discovered exception alias names to __all__
124
+ if self.exception_alias_names:
125
+ core_init_content.append(" # Generated exception aliases")
126
+ for alias_name in sorted(list(set(self.exception_alias_names))): # Sort and unique
127
+ core_init_content.append(f' "{alias_name}",')
128
+
129
+ core_init_content.append("]")
130
+
131
+ self.file_manager.write_file(core_init_path, "\n".join(core_init_content))
132
+ generated_files.append(core_init_path)
133
+
134
+ auth_dir = os.path.join(actual_core_dir, "auth")
135
+ if os.path.exists(auth_dir): # Only create auth/__init__.py if auth files were copied
136
+ auth_init_path = os.path.join(auth_dir, "__init__.py")
137
+ self.file_manager.ensure_dir(os.path.dirname(auth_init_path))
138
+ auth_init_content = [
139
+ "# Core Auth __init__",
140
+ "from .base import BaseAuth",
141
+ "from .plugins import ApiKeyAuth, BearerAuth, OAuth2Auth",
142
+ "",
143
+ "__all__ = [",
144
+ ' "BaseAuth",',
145
+ ' "ApiKeyAuth",',
146
+ ' "BearerAuth",',
147
+ ' "OAuth2Auth",',
148
+ "]",
149
+ ]
150
+ self.file_manager.write_file(auth_init_path, "\n".join(auth_init_content) + "\n")
151
+ generated_files.append(auth_init_path)
152
+
153
+ # Ensure py.typed marker for mypy in the actual core directory
154
+ pytyped_path = os.path.join(actual_core_dir, "py.typed")
155
+ if not os.path.exists(pytyped_path):
156
+ self.file_manager.write_file(pytyped_path, "") # Create empty py.typed
157
+ generated_files.append(pytyped_path)
158
+
159
+ # Copy the core README template into the actual core directory
160
+ readme_dst = os.path.join(actual_core_dir, "README.md")
161
+ try:
162
+ with (
163
+ importlib.resources.files(CORE_README_TEMPLATE_MODULE)
164
+ .joinpath(CORE_README_TEMPLATE_FILENAME)
165
+ .open("r") as f
166
+ ):
167
+ readme_content = f.read()
168
+ self.file_manager.write_file(readme_dst, readme_content)
169
+ generated_files.append(readme_dst)
170
+ except FileNotFoundError:
171
+ print(
172
+ f"Warning: Could not find core README template {CORE_README_TEMPLATE_FILENAME} "
173
+ f"in {CORE_README_TEMPLATE_MODULE}. Skipping."
174
+ )
175
+
176
+ # Generate config.py from template inside the actual core directory
177
+ config_path = os.path.join(actual_core_dir, "config.py")
178
+ self.file_manager.write_file(config_path, CONFIG_TEMPLATE)
179
+ generated_files.append(config_path)
180
+
181
+ return generated_files
@@ -0,0 +1,44 @@
1
+ import os
2
+
3
+ from pyopenapi_gen import IRSpec
4
+ from pyopenapi_gen.context.render_context import RenderContext
5
+
6
+ from ..visit.docs_visitor import DocsVisitor
7
+
8
+ """Simple documentation emitter using markdown with Python str.format placeholders."""
9
+ DOCS_INDEX_TEMPLATE = """# API Documentation
10
+
11
+ Generated documentation for the API.
12
+
13
+ ## Tags
14
+ {tags_list}
15
+ """
16
+
17
+ DOCS_TAG_TEMPLATE = """# {tag} Operations
18
+
19
+ {operations_list}
20
+ """
21
+
22
+ DOCS_OPERATION_TEMPLATE = """### {operation_id}
23
+
24
+ **Method:** `{method}`
25
+ **Path:** `{path}`
26
+
27
+ {description}
28
+ """
29
+
30
+
31
+ class DocsEmitter:
32
+ """Generates markdown documentation per tag from IRSpec using visitor/context."""
33
+
34
+ def __init__(self) -> None:
35
+ self.visitor = DocsVisitor()
36
+
37
+ def emit(self, spec: IRSpec, output_dir: str) -> None:
38
+ """Render docs into <output_dir> as markdown files."""
39
+ docs_dir = os.path.join(output_dir)
40
+ context = RenderContext()
41
+ context.file_manager.ensure_dir(docs_dir)
42
+ docs = self.visitor.visit(spec, context)
43
+ for filename, content in docs.items():
44
+ context.file_manager.write_file(os.path.join(docs_dir, filename), content)