pyopenapi-gen 0.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. pyopenapi_gen/__init__.py +114 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +86 -0
  4. pyopenapi_gen/context/file_manager.py +52 -0
  5. pyopenapi_gen/context/import_collector.py +382 -0
  6. pyopenapi_gen/context/render_context.py +630 -0
  7. pyopenapi_gen/core/__init__.py +0 -0
  8. pyopenapi_gen/core/auth/base.py +22 -0
  9. pyopenapi_gen/core/auth/plugins.py +89 -0
  10. pyopenapi_gen/core/exceptions.py +25 -0
  11. pyopenapi_gen/core/http_transport.py +219 -0
  12. pyopenapi_gen/core/loader/__init__.py +12 -0
  13. pyopenapi_gen/core/loader/loader.py +158 -0
  14. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  15. pyopenapi_gen/core/loader/operations/parser.py +155 -0
  16. pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
  17. pyopenapi_gen/core/loader/operations/request_body.py +85 -0
  18. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  19. pyopenapi_gen/core/loader/parameters/parser.py +121 -0
  20. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  21. pyopenapi_gen/core/loader/responses/parser.py +104 -0
  22. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  23. pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
  24. pyopenapi_gen/core/pagination.py +64 -0
  25. pyopenapi_gen/core/parsing/__init__.py +13 -0
  26. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  27. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  28. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  29. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  30. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  37. pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
  38. pyopenapi_gen/core/parsing/context.py +184 -0
  39. pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
  40. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  41. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
  42. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
  43. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
  44. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
  45. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  46. pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
  47. pyopenapi_gen/core/parsing/schema_parser.py +610 -0
  48. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  49. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  50. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
  51. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  52. pyopenapi_gen/core/postprocess_manager.py +161 -0
  53. pyopenapi_gen/core/schemas.py +40 -0
  54. pyopenapi_gen/core/streaming_helpers.py +86 -0
  55. pyopenapi_gen/core/telemetry.py +67 -0
  56. pyopenapi_gen/core/utils.py +409 -0
  57. pyopenapi_gen/core/warning_collector.py +83 -0
  58. pyopenapi_gen/core/writers/code_writer.py +135 -0
  59. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  60. pyopenapi_gen/core/writers/line_writer.py +217 -0
  61. pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
  62. pyopenapi_gen/core_package_template/README.md +21 -0
  63. pyopenapi_gen/emit/models_emitter.py +143 -0
  64. pyopenapi_gen/emitters/client_emitter.py +51 -0
  65. pyopenapi_gen/emitters/core_emitter.py +181 -0
  66. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  67. pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
  68. pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
  69. pyopenapi_gen/emitters/models_emitter.py +428 -0
  70. pyopenapi_gen/generator/client_generator.py +562 -0
  71. pyopenapi_gen/helpers/__init__.py +1 -0
  72. pyopenapi_gen/helpers/endpoint_utils.py +552 -0
  73. pyopenapi_gen/helpers/type_cleaner.py +341 -0
  74. pyopenapi_gen/helpers/type_helper.py +112 -0
  75. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  76. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  77. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  78. pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
  79. pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
  80. pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
  81. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
  82. pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
  83. pyopenapi_gen/helpers/url_utils.py +14 -0
  84. pyopenapi_gen/http_types.py +20 -0
  85. pyopenapi_gen/ir.py +167 -0
  86. pyopenapi_gen/py.typed +1 -0
  87. pyopenapi_gen/types/__init__.py +11 -0
  88. pyopenapi_gen/types/contracts/__init__.py +13 -0
  89. pyopenapi_gen/types/contracts/protocols.py +106 -0
  90. pyopenapi_gen/types/contracts/types.py +30 -0
  91. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  92. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  93. pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
  94. pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
  95. pyopenapi_gen/types/services/__init__.py +5 -0
  96. pyopenapi_gen/types/services/type_service.py +133 -0
  97. pyopenapi_gen/visit/client_visitor.py +228 -0
  98. pyopenapi_gen/visit/docs_visitor.py +38 -0
  99. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  100. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
  101. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  102. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
  103. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
  104. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  105. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
  106. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
  107. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
  108. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  109. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
  110. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  111. pyopenapi_gen/visit/exception_visitor.py +52 -0
  112. pyopenapi_gen/visit/model/__init__.py +0 -0
  113. pyopenapi_gen/visit/model/alias_generator.py +89 -0
  114. pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
  115. pyopenapi_gen/visit/model/enum_generator.py +200 -0
  116. pyopenapi_gen/visit/model/model_visitor.py +197 -0
  117. pyopenapi_gen/visit/visitor.py +97 -0
  118. pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
  119. pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
  120. pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
  121. pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
  122. pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,228 @@
1
+ import logging # Added for logging
2
+ import re
3
+ import textwrap
4
+ from typing import TYPE_CHECKING, cast
5
+
6
+ from pyopenapi_gen import IRSpec
7
+
8
+ from ..context.render_context import RenderContext
9
+ from ..core.utils import NameSanitizer
10
+ from ..core.writers.code_writer import CodeWriter
11
+ from ..core.writers.documentation_writer import DocumentationBlock, DocumentationWriter
12
+
13
+ if TYPE_CHECKING:
14
+ # To prevent circular imports if any type from core itself is needed for hints
15
+ pass
16
+
17
+ logger = logging.getLogger(__name__) # Added for logging
18
+
19
+
20
+ class ClientVisitor:
21
+ """Visitor for rendering the Python API client from IRSpec."""
22
+
23
+ def __init__(self) -> None:
24
+ pass
25
+
26
+ def visit(self, spec: IRSpec, context: RenderContext) -> str:
27
+ tag_candidates: dict[str, list[str]] = {}
28
+ for op in spec.operations:
29
+ # Use DEFAULT_TAG consistent with EndpointsEmitter
30
+ tags = op.tags or ["default"] # Use literal "default" here
31
+ # Loop through the determined tags (original or default)
32
+ for tag in tags:
33
+ key = NameSanitizer.normalize_tag_key(tag)
34
+ if key not in tag_candidates:
35
+ tag_candidates[key] = []
36
+ tag_candidates[key].append(tag)
37
+ # Ensure the old logic is removed (idempotent if already gone)
38
+ # if op.tags:
39
+ # ...
40
+ # else:
41
+ # ...
42
+
43
+ def tag_score(t: str) -> tuple[bool, int, int, str]:
44
+ is_pascal = bool(re.search(r"[a-z][A-Z]", t)) or bool(re.search(r"[A-Z]{2,}", t))
45
+ words = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+", t)
46
+ words += re.split(r"[_-]+", t)
47
+ word_count = len([w for w in words if w])
48
+ upper = sum(1 for c in t if c.isupper())
49
+ return (is_pascal, word_count, upper, t)
50
+
51
+ tag_map = {}
52
+ for key, candidates in tag_candidates.items():
53
+ best = max(candidates, key=tag_score)
54
+ tag_map[key] = best
55
+ tag_tuples = [
56
+ (
57
+ tag_map[key],
58
+ NameSanitizer.sanitize_class_name(tag_map[key]) + "Client",
59
+ NameSanitizer.sanitize_module_name(tag_map[key]),
60
+ )
61
+ for key in sorted(tag_map)
62
+ ]
63
+ writer = CodeWriter()
64
+ # Register all endpoint client imports using relative imports (endpoints are within the same package)
65
+ for _, class_name, module_name in tag_tuples:
66
+ # Use relative imports for endpoints since they're part of the same generated package
67
+ context.import_collector.add_relative_import(f".endpoints.{module_name}", class_name)
68
+
69
+ # Register core/config/typing imports for class signature
70
+ # Use LOGICAL import path for core components
71
+
72
+ # Use the core_package name from the context to form the base of the import path
73
+ # RenderContext.add_import will handle making it relative correctly based on the current file.
74
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
75
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpxTransport")
76
+ context.add_import(f"{context.core_package_name}.config", "ClientConfig")
77
+ # If security schemes are present and an auth plugin like ApiKeyAuth is used by the client itself,
78
+ # it would also be registered here using context.core_package.
79
+ # For now, the client_py_content check in the test looks for this for ApiKeyAuth specifically:
80
+ context.add_import(f"{context.core_package_name}.auth.plugins", "ApiKeyAuth")
81
+
82
+ context.add_typing_imports_for_type("Optional[HttpTransport]")
83
+ context.add_typing_imports_for_type("Any")
84
+ context.add_typing_imports_for_type("Dict")
85
+ # Class definition
86
+ writer.write_line("class APIClient:")
87
+ writer.indent()
88
+ # Build docstring for APIClient
89
+ docstring_lines = []
90
+ # Add API title and version
91
+ docstring_lines.append(f"{spec.title} (version {spec.version})")
92
+ # Add API description if present
93
+ if getattr(spec, "description", None):
94
+ desc = spec.description
95
+ if desc is not None:
96
+ # Remove triple quotes, escape backslashes, and dedent
97
+ desc_clean = desc.replace('"""', "'").replace("'''", "'").replace("\\", "\\\\").strip()
98
+ desc_clean = textwrap.dedent(desc_clean)
99
+ docstring_lines.append("")
100
+ docstring_lines.append(desc_clean)
101
+ # Add a blank line before the generated summary/args
102
+ docstring_lines.append("")
103
+ summary = "Async API client with pluggable transport, tag-specific clients, and client-level headers."
104
+ args: list[tuple[str, str, str]] = [
105
+ ("config", "ClientConfig", "Client configuration object."),
106
+ ("transport", "Optional[HttpTransport]", "Custom HTTP transport (optional)."),
107
+ ]
108
+ for tag, class_name, module_name in tag_tuples:
109
+ args.append((module_name, class_name, f"Client for '{tag}' endpoints."))
110
+ doc_block = DocumentationBlock(
111
+ summary=summary,
112
+ args=cast(list[tuple[str, str, str] | tuple[str, str]], args),
113
+ )
114
+ docstring = DocumentationWriter(width=88).render_docstring(doc_block, indent=0)
115
+ docstring_lines.extend([line for line in docstring.splitlines()])
116
+ # Write only one docstring, no extra triple quotes after
117
+ writer.write_line('"""') # At class indent (1)
118
+ writer.dedent() # Go to indent 0 for docstring content
119
+ for line in docstring_lines:
120
+ writer.write_line(line.rstrip('"'))
121
+ writer.indent() # Back to class indent (1)
122
+ writer.write_line('"""')
123
+ # __init__
124
+ writer.write_line(
125
+ "def __init__(self, config: ClientConfig, transport: Optional[HttpTransport] = None) -> None:"
126
+ )
127
+ writer.indent()
128
+ writer.write_line("self.config = config")
129
+ writer.write_line(
130
+ "self.transport = transport if transport is not None else "
131
+ "HttpxTransport(str(config.base_url), config.timeout)"
132
+ )
133
+ writer.write_line("self._base_url: str = str(self.config.base_url)")
134
+ # Initialize private fields for each tag client
135
+ for tag, class_name, module_name in tag_tuples:
136
+ context.add_typing_imports_for_type(f"Optional[{class_name}]")
137
+ writer.write_line(f"self._{module_name}: Optional[{class_name}] = None")
138
+ writer.dedent()
139
+ writer.write_line("")
140
+ # @property for each tag client
141
+ for tag, class_name, module_name in tag_tuples:
142
+ writer.write_line(f"@property")
143
+ # Use context.add_import here too
144
+ current_gen_pkg_name_prop = context.get_current_package_name_for_generated_code()
145
+ if not current_gen_pkg_name_prop:
146
+ logger.error(
147
+ f"[ClientVisitor Property] Could not determine generated package name from context. "
148
+ f"Cannot form fully qualified import for endpoints.{module_name}.{class_name}"
149
+ )
150
+ # Fallback or raise error
151
+ context.add_import(f"endpoints.{module_name}", class_name)
152
+ else:
153
+ logical_module_for_add_import_prop = f"{current_gen_pkg_name_prop}.endpoints.{module_name}"
154
+ context.add_import(logical_module_for_add_import_prop, class_name)
155
+
156
+ writer.write_line(f"def {module_name}(self) -> {class_name}:")
157
+ writer.indent()
158
+ writer.write_line(f'"""Client for \'{tag}\' endpoints."""')
159
+ writer.write_line(f"if self._{module_name} is None:")
160
+ writer.indent()
161
+ writer.write_line(f"self._{module_name} = {class_name}(self.transport, self._base_url)")
162
+ writer.dedent()
163
+ writer.write_line(f"return self._{module_name}")
164
+ writer.dedent()
165
+ writer.write_line("")
166
+ # request method
167
+ context.add_typing_imports_for_type("Any")
168
+ writer.write_line("async def request(self, method: str, url: str, **kwargs: Any) -> Any:")
169
+ writer.indent()
170
+ writer.write_line('"""Send an HTTP request via the transport."""')
171
+ writer.write_line("return await self.transport.request(method, url, **kwargs)")
172
+ writer.dedent()
173
+ writer.write_line("")
174
+ # close method
175
+ context.add_typing_imports_for_type("None")
176
+ writer.write_line("async def close(self) -> None:")
177
+ writer.indent()
178
+ writer.write_line('"""Close the underlying transport if supported."""')
179
+ writer.write_line("if hasattr(self.transport, 'close'):")
180
+ writer.indent()
181
+ writer.write_line("await self.transport.close()")
182
+ writer.dedent()
183
+ writer.write_line("else:")
184
+ writer.indent()
185
+ writer.write_line("pass # Or log a warning if close is expected but not found")
186
+ writer.dedent()
187
+ writer.dedent()
188
+ writer.write_line("")
189
+ # __aenter__ for async context management (dedented)
190
+ writer.write_line("async def __aenter__(self) -> 'APIClient':")
191
+ writer.indent()
192
+ writer.write_line('"""Enter the async context manager. Returns self."""')
193
+ writer.write_line("if hasattr(self.transport, '__aenter__'):")
194
+ writer.indent()
195
+ writer.write_line("await self.transport.__aenter__()")
196
+ writer.dedent()
197
+ writer.write_line("return self")
198
+ writer.dedent()
199
+ writer.write_line("")
200
+ # __aexit__ for async context management (dedented)
201
+ context.add_typing_imports_for_type("type[BaseException] | None")
202
+ context.add_typing_imports_for_type("BaseException | None")
203
+ context.add_typing_imports_for_type("object | None")
204
+ writer.write_line(
205
+ "async def __aexit__(self, exc_type: type[BaseException] | None, "
206
+ "exc_val: BaseException | None, exc_tb: object | None) -> None:"
207
+ )
208
+ writer.indent()
209
+ writer.write_line('"""Exit the async context manager, ensuring transport is closed."""')
210
+ # Close internal transport if it supports __aexit__
211
+ writer.write_line("if hasattr(self.transport, '__aexit__'):")
212
+ writer.indent()
213
+ writer.write_line("await self.transport.__aexit__(exc_type, exc_val, exc_tb)")
214
+ writer.dedent()
215
+ writer.write_line("else:") # Fallback if transport doesn't have __aexit__ but has close()
216
+ writer.indent()
217
+ writer.write_line("await self.close()")
218
+ writer.dedent()
219
+ writer.dedent()
220
+ writer.write_line("")
221
+
222
+ # Ensure crucial core imports are present before final rendering
223
+ # This is a fallback / re-emphasis due to potential issues with ImportCollector
224
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
225
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpxTransport")
226
+ context.add_import(f"{context.core_package_name}.config", "ClientConfig")
227
+
228
+ return writer.get_code()
@@ -0,0 +1,38 @@
1
+ from pyopenapi_gen import IRSpec
2
+
3
+ from ..context.render_context import RenderContext
4
+ from ..core.utils import NameSanitizer
5
+ from ..core.writers.code_writer import CodeWriter
6
+
7
+
8
+ class DocsVisitor:
9
+ """Visitor for rendering markdown documentation from IRSpec."""
10
+
11
+ def visit(self, spec: IRSpec, context: RenderContext) -> dict[str, str]:
12
+ # List tags
13
+ tags = sorted({t for op in spec.operations for t in op.tags})
14
+ # Generate index.md with sanitized links
15
+ writer = CodeWriter()
16
+ writer.write_line("# API Documentation\n")
17
+ writer.write_line("Generated documentation for the API.\n")
18
+ writer.write_line("## Tags")
19
+ for tag in tags:
20
+ writer.write_line(f"- [{tag}]({NameSanitizer.sanitize_module_name(tag)}.md)")
21
+ index_content = writer.get_code()
22
+ result = {"index.md": index_content}
23
+ # Generate docs per tag
24
+ for tag in tags:
25
+ ops = [op for op in spec.operations if tag in op.tags]
26
+ tag_writer = CodeWriter()
27
+ tag_writer.write_line(f"# {tag.capitalize()} Operations\n")
28
+ for op in ops:
29
+ tag_writer.write_line(f"### {op.operation_id}\n")
30
+ tag_writer.write_line(f"**Method:** `{op.method.value}` ")
31
+ tag_writer.write_line(f"**Path:** `{op.path}` \n")
32
+ desc = op.description or ""
33
+ if desc:
34
+ tag_writer.write_line(desc)
35
+ tag_writer.write_line("")
36
+ filename = NameSanitizer.sanitize_module_name(tag) + ".md"
37
+ result[filename] = tag_writer.get_code()
38
+ return result
@@ -0,0 +1 @@
1
+ # This directory will contain helper classes for EndpointMethodGenerator
@@ -0,0 +1,103 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from pyopenapi_gen import IROperation
5
+ from pyopenapi_gen.helpers.endpoint_utils import (
6
+ get_return_type_unified,
7
+ )
8
+
9
+ from ...context.render_context import RenderContext
10
+ from ...core.utils import NameSanitizer
11
+ from ...core.writers.code_writer import CodeWriter
12
+ from ..visitor import Visitor
13
+ from .generators.endpoint_method_generator import EndpointMethodGenerator
14
+
15
+ # Get logger instance
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class EndpointVisitor(Visitor[IROperation, str]):
20
+ """
21
+ Visitor for rendering a Python endpoint client method/class from an IROperation.
22
+ The method generation part is delegated to EndpointMethodGenerator.
23
+ This class remains responsible for assembling methods into a class (emit_endpoint_client_class).
24
+ Returns the rendered code as a string (does not write files).
25
+ """
26
+
27
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
28
+ self.schemas = schemas or {}
29
+ # Formatter is likely not needed here anymore if all formatting happens in EndpointMethodGenerator
30
+ # self.formatter = Formatter()
31
+
32
+ def visit_IROperation(self, op: IROperation, context: RenderContext) -> str:
33
+ """
34
+ Generate a fully functional async endpoint method for the given operation
35
+ by delegating to EndpointMethodGenerator.
36
+ Returns the method code as a string.
37
+ """
38
+ # Instantiate the new generator
39
+ method_generator = EndpointMethodGenerator(schemas=self.schemas)
40
+ return method_generator.generate(op, context)
41
+
42
+ def emit_endpoint_client_class(
43
+ self,
44
+ tag: str,
45
+ method_codes: list[str],
46
+ context: RenderContext,
47
+ ) -> str:
48
+ """
49
+ Emit the endpoint client class for a tag, aggregating all endpoint methods.
50
+ The generated class is fully type-annotated and uses HttpTransport for HTTP communication.
51
+ Args:
52
+ tag: The tag name for the endpoint group.
53
+ method_codes: List of method code blocks as strings.
54
+ context: The RenderContext for import tracking.
55
+ """
56
+ context.add_import("typing", "cast")
57
+ # Import core transport and streaming helpers
58
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
59
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
60
+ context.add_import("typing", "Callable")
61
+ context.add_import("typing", "Optional")
62
+ writer = CodeWriter()
63
+ class_name = NameSanitizer.sanitize_class_name(tag) + "Client"
64
+ writer.write_line(f"class {class_name}:")
65
+ writer.indent()
66
+ writer.write_line(f'"""Client for {tag} endpoints. Uses HttpTransport for all HTTP and header management."""')
67
+ writer.write_line("")
68
+
69
+ writer.write_line("def __init__(self, transport: HttpTransport, base_url: str) -> None:")
70
+ writer.indent()
71
+ writer.write_line("self._transport = transport")
72
+ writer.write_line("self.base_url: str = base_url")
73
+ writer.dedent()
74
+ writer.write_line("")
75
+
76
+ # Write methods
77
+ for i, method_code in enumerate(method_codes):
78
+ # Revert to write_block, as it handles indentation correctly
79
+ writer.write_block(method_code)
80
+
81
+ if i < len(method_codes) - 1:
82
+ writer.write_line("") # First blank line
83
+ writer.write_line("") # Second blank line (for testing separation)
84
+
85
+ writer.dedent() # Dedent to close the class block
86
+ return writer.get_code()
87
+
88
+ def _get_response_return_type_details(self, context: RenderContext, op: IROperation) -> tuple[str, bool, bool, str]:
89
+ """Gets type details for the endpoint response."""
90
+ # Check if this is a streaming response (either at op level or in schema)
91
+ is_streaming = any(getattr(resp, "stream", False) for resp in op.responses if resp.status_code.startswith("2"))
92
+
93
+ # Get the primary Python type for the operation's success response using unified service
94
+ return_type = get_return_type_unified(op, context, self.schemas)
95
+ should_unwrap = False # Unified service handles unwrapping internally
96
+
97
+ # Determine the summary description (for docstring)
98
+ success_resp = next((r for r in op.responses if r.status_code.startswith("2")), None)
99
+ return_description = (
100
+ success_resp.description if success_resp and success_resp.description else "Successful operation"
101
+ )
102
+
103
+ return return_type, should_unwrap, is_streaming, return_description
@@ -0,0 +1 @@
1
+ # पैसाले सुख दिदैन।
@@ -0,0 +1,121 @@
1
+ """
2
+ Helper class for generating the docstring for an endpoint method.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import textwrap # For _wrap_docstring logic
9
+ from typing import TYPE_CHECKING, Any, Dict, Optional
10
+
11
+ from pyopenapi_gen.core.writers.code_writer import CodeWriter
12
+ from pyopenapi_gen.core.writers.documentation_writer import DocumentationBlock, DocumentationWriter
13
+ from pyopenapi_gen.helpers.endpoint_utils import get_param_type, get_request_body_type, get_return_type_unified
14
+
15
+ if TYPE_CHECKING:
16
+ from pyopenapi_gen import IROperation
17
+ from pyopenapi_gen.context.render_context import RenderContext
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class EndpointDocstringGenerator:
23
+ """Generates the Python docstring for an endpoint operation."""
24
+
25
+ def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
26
+ self.schemas: Dict[str, Any] = schemas or {}
27
+ self.doc_writer = DocumentationWriter(width=88)
28
+
29
+ def _wrap_docstring(self, prefix: str, text: str, width: int = 88) -> str:
30
+ """Internal helper to wrap text for docstrings."""
31
+ # This was a staticmethod in EndpointMethodGenerator, can be a helper here.
32
+ if not text:
33
+ return prefix.rstrip()
34
+ initial_indent = prefix
35
+ subsequent_indent = " " * len(prefix)
36
+ wrapped = textwrap.wrap(text, width=width, initial_indent=initial_indent, subsequent_indent=subsequent_indent)
37
+ # The original had "\n ".join(wrapped), which might be too specific if prefix changes.
38
+ # Let's ensure it joins with newline and respects the subsequent_indent for multi-lines.
39
+ if not wrapped:
40
+ return prefix.rstrip()
41
+ # For single line, no complex join needed, just the wrapped line.
42
+ if len(wrapped) == 1:
43
+ return wrapped[0]
44
+ # For multi-line, ensure proper joining. textwrap handles indent per line.
45
+ return "\n".join(wrapped)
46
+
47
+ def generate_docstring(
48
+ self,
49
+ writer: CodeWriter,
50
+ op: IROperation,
51
+ context: RenderContext,
52
+ primary_content_type: Optional[str],
53
+ ) -> None:
54
+ """Writes the method docstring to the provided CodeWriter."""
55
+ summary = op.summary or None
56
+ description = op.description or None
57
+ args: list[tuple[str, str, str] | tuple[str, str]] = []
58
+
59
+ for param in op.parameters:
60
+ param_type = get_param_type(param, context, self.schemas)
61
+ desc = param.description or ""
62
+ args.append((param.name, param_type, desc))
63
+
64
+ if op.request_body and primary_content_type:
65
+ body_desc = op.request_body.description or "Request body."
66
+ # Standardized body parameter names based on content type
67
+ if primary_content_type == "multipart/form-data":
68
+ args.append(("files", "Dict[str, IO[Any]]", body_desc + " (multipart/form-data)"))
69
+ elif primary_content_type == "application/x-www-form-urlencoded":
70
+ # The type here could be more specific if schema is available, but Dict[str, Any] is a safe default.
71
+ args.append(("form_data", "Dict[str, Any]", body_desc + " (x-www-form-urlencoded)"))
72
+ elif primary_content_type == "application/json":
73
+ body_type = get_request_body_type(op.request_body, context, self.schemas)
74
+ args.append(("body", body_type, body_desc + " (json)"))
75
+ else: # Fallback for other types like application/octet-stream
76
+ args.append(("bytes_content", "bytes", body_desc + f" ({primary_content_type})"))
77
+
78
+ return_type = get_return_type_unified(op, context, self.schemas)
79
+ response_desc = None
80
+ # Prioritize 2xx success codes for the main response description
81
+ for code in ("200", "201", "202", "default"): # Include default as it might be the success response
82
+ resp = next((r for r in op.responses if r.status_code == code), None)
83
+ if resp and resp.description:
84
+ response_desc = resp.description.strip()
85
+ break
86
+ if not response_desc: # Fallback to any response description if no 2xx/default found
87
+ for resp in op.responses:
88
+ if resp.description:
89
+ response_desc = resp.description.strip()
90
+ break
91
+
92
+ returns = (return_type, response_desc or "Response object.") if return_type and return_type != "None" else None
93
+
94
+ error_codes = [r for r in op.responses if r.status_code.isdigit() and int(r.status_code) >= 400]
95
+ raises = []
96
+ if error_codes:
97
+ for resp in error_codes:
98
+ # Using a generic HTTPError, specific error classes could be mapped later
99
+ code_to_raise = "HTTPError"
100
+ desc = f"{resp.status_code}: {resp.description.strip() if resp.description else 'HTTP error.'}"
101
+ raises.append((code_to_raise, desc))
102
+ else:
103
+ raises.append(("HTTPError", "If the server returns a non-2xx HTTP response."))
104
+
105
+ doc_block = DocumentationBlock(
106
+ summary=summary,
107
+ description=description,
108
+ args=args,
109
+ returns=returns,
110
+ raises=raises,
111
+ )
112
+
113
+ # The DocumentationWriter handles the actual formatting and wrapping.
114
+ # The _wrap_docstring helper is not directly used here if DocumentationWriter handles it all.
115
+ # However, DocumentationWriter.render_docstring itself might need indentation control.
116
+ # Original called writer.write_line(line) for each line of docstring.
117
+ docstring_str = self.doc_writer.render_docstring(
118
+ doc_block, indent=0
119
+ ) # indent=0 as CodeWriter handles method indent
120
+ for line in docstring_str.splitlines():
121
+ writer.write_line(line)
@@ -0,0 +1,87 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from pyopenapi_gen import IROperation
5
+
6
+ from ....context.render_context import RenderContext
7
+ from ....core.utils import Formatter
8
+ from ....core.writers.code_writer import CodeWriter
9
+ from ..processors.import_analyzer import EndpointImportAnalyzer
10
+ from ..processors.parameter_processor import EndpointParameterProcessor
11
+ from .docstring_generator import EndpointDocstringGenerator
12
+ from .request_generator import EndpointRequestGenerator
13
+ from .response_handler_generator import EndpointResponseHandlerGenerator
14
+ from .signature_generator import EndpointMethodSignatureGenerator
15
+ from .url_args_generator import EndpointUrlArgsGenerator
16
+
17
+ # Get logger instance
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class EndpointMethodGenerator:
22
+ """
23
+ Generates the Python code for a single endpoint method.
24
+ """
25
+
26
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
27
+ self.schemas = schemas or {}
28
+ self.formatter = Formatter()
29
+ self.parameter_processor = EndpointParameterProcessor(self.schemas)
30
+ self.import_analyzer = EndpointImportAnalyzer(self.schemas)
31
+ self.signature_generator = EndpointMethodSignatureGenerator(self.schemas)
32
+ self.docstring_generator = EndpointDocstringGenerator(self.schemas)
33
+ self.url_args_generator = EndpointUrlArgsGenerator(self.schemas)
34
+ self.request_generator = EndpointRequestGenerator(self.schemas)
35
+ self.response_handler_generator = EndpointResponseHandlerGenerator(self.schemas)
36
+
37
+ def generate(self, op: IROperation, context: RenderContext) -> str:
38
+ """
39
+ Generate a fully functional async endpoint method for the given operation.
40
+ Returns the method code as a string.
41
+ """
42
+ writer = CodeWriter()
43
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
44
+ context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
45
+
46
+ # Special case for updateAgentDataSource was removed.
47
+
48
+ self.import_analyzer.analyze_and_register_imports(op, context)
49
+
50
+ ordered_params, primary_content_type, resolved_body_type = self.parameter_processor.process_parameters(
51
+ op, context
52
+ )
53
+ self.signature_generator.generate_signature(writer, op, context, ordered_params)
54
+
55
+ self.docstring_generator.generate_docstring(writer, op, context, primary_content_type)
56
+
57
+ # Snapshot of code *before* main body parts are written
58
+ # This includes signature and docstring.
59
+ code_snapshot_before_body_parts = writer.get_code()
60
+
61
+ has_header_params = self.url_args_generator.generate_url_and_args(
62
+ writer, op, context, ordered_params, primary_content_type, resolved_body_type
63
+ )
64
+ self.request_generator.generate_request_call(writer, op, context, has_header_params, primary_content_type)
65
+
66
+ # Call the new response handler generator
67
+ self.response_handler_generator.generate_response_handling(writer, op, context)
68
+
69
+ # Check if any actual statements were added for the body
70
+ current_full_code = writer.get_code()
71
+ # The part of the code added by the body-writing methods
72
+ body_part_actually_written = current_full_code[len(code_snapshot_before_body_parts) :]
73
+
74
+ body_is_effectively_empty = True
75
+ # Check if the written body part contains any non-comment, non-whitespace lines
76
+ if body_part_actually_written.strip(): # Check if non-whitespace exists at all
77
+ if any(
78
+ line.strip() and not line.strip().startswith("#") for line in body_part_actually_written.splitlines()
79
+ ):
80
+ body_is_effectively_empty = False
81
+
82
+ if body_is_effectively_empty:
83
+ writer.write_line("pass")
84
+
85
+ writer.dedent() # This matches the indent() from _write_method_signature
86
+
87
+ return writer.get_code().strip()