pyopenapi-gen 0.10.1__py3-none-any.whl → 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyopenapi_gen/__init__.py CHANGED
@@ -43,7 +43,7 @@ __all__ = [
43
43
  ]
44
44
 
45
45
  # Semantic version of the generator core – automatically managed by semantic-release.
46
- __version__: str = "0.9.0"
46
+ __version__: str = "0.10.2"
47
47
 
48
48
 
49
49
  # ---------------------------------------------------------------------------
@@ -1,12 +1,7 @@
1
- from typing import Optional
2
-
3
- import httpx
4
-
5
-
6
1
  class HTTPError(Exception):
7
2
  """Base HTTP error with status code and message."""
8
3
 
9
- def __init__(self, status_code: int, message: str, response: Optional[httpx.Response] = None) -> None:
4
+ def __init__(self, status_code: int, message: str, response: object | None = None) -> None:
10
5
  super().__init__(f"{status_code}: {message}")
11
6
  self.status_code = status_code
12
7
  self.message = message
@@ -87,8 +87,22 @@ class SpecLoader:
87
87
  validate_spec(cast(Mapping[Hashable, Any], self.spec))
88
88
  except Exception as e:
89
89
  warning_msg = f"OpenAPI spec validation error: {e}"
90
+ # Always collect the message
90
91
  warnings_list.append(warning_msg)
91
- warnings.warn(warning_msg, UserWarning)
92
+
93
+ # Heuristic: if this error originates from jsonschema or
94
+ # openapi_spec_validator, prefer logging over global warnings
95
+ # to avoid noisy test output while still surfacing the issue.
96
+ origin_module = getattr(e.__class__, "__module__", "")
97
+ if (
98
+ isinstance(e, RecursionError)
99
+ or origin_module.startswith("jsonschema")
100
+ or origin_module.startswith("openapi_spec_validator")
101
+ ):
102
+ logger.warning(warning_msg)
103
+ else:
104
+ # Preserve explicit warning behavior for unexpected failures
105
+ warnings.warn(warning_msg, UserWarning)
92
106
 
93
107
  return warnings_list
94
108
 
@@ -79,6 +79,29 @@ def parse_parameter(
79
79
  base_param_promo_name = f"{operation_id_for_promo}Param" if operation_id_for_promo else ""
80
80
  name_for_inline_param_schema = f"{base_param_promo_name}{NameSanitizer.sanitize_class_name(param_name)}"
81
81
 
82
+ # General rule: if a parameter is defined inline but a components parameter exists with the
83
+ # same name and location, prefer the components schema (often richer: arrays/enums/refs).
84
+ try:
85
+ if isinstance(context, ParsingContext):
86
+ components_params = context.raw_spec_components.get("parameters", {})
87
+ if isinstance(components_params, Mapping):
88
+ for comp_key, comp_param in components_params.items():
89
+ if not isinstance(comp_param, Mapping):
90
+ continue
91
+ if comp_param.get("name") == param_name and comp_param.get("in") == node.get("in"):
92
+ comp_schema = comp_param.get("schema")
93
+ if isinstance(comp_schema, Mapping):
94
+ # Prefer component schema if inline is missing or clearly less specific
95
+ inline_is_specific = isinstance(sch, Mapping) and (
96
+ sch.get("type") in {"array", "object"} or "$ref" in sch or "enum" in sch
97
+ )
98
+ if not inline_is_specific:
99
+ sch = comp_schema
100
+ break
101
+ except Exception:
102
+ # Be conservative on any unexpected structure
103
+ pass
104
+
82
105
  # For parameters, we want to avoid creating complex schemas for simple enum arrays
83
106
  # Check if this is a simple enum array and handle it specially
84
107
  if (
@@ -128,6 +128,8 @@ def extract_inline_array_items(schemas: Dict[str, IRSchema]) -> Dict[str, IRSche
128
128
  def extract_inline_enums(schemas: Dict[str, IRSchema]) -> Dict[str, IRSchema]:
129
129
  """Extract inline property enums as unique schemas and update property references.
130
130
 
131
+ Also ensures top-level enum schemas are properly marked for generation.
132
+
131
133
  Contracts:
132
134
  Preconditions:
133
135
  - schemas is a dict of IRSchema objects
@@ -136,6 +138,7 @@ def extract_inline_enums(schemas: Dict[str, IRSchema]) -> Dict[str, IRSchema]:
136
138
  - All property schemas with enums have proper names
137
139
  - All array item schemas have proper names
138
140
  - No duplicate schema names are created
141
+ - Top-level enum schemas have generation_name set
139
142
  """
140
143
  assert isinstance(schemas, dict), "schemas must be a dict"
141
144
  assert all(isinstance(s, IRSchema) for s in schemas.values()), "all values must be IRSchema objects"
@@ -149,6 +152,19 @@ def extract_inline_enums(schemas: Dict[str, IRSchema]) -> Dict[str, IRSchema]:
149
152
 
150
153
  new_enums = {}
151
154
  for schema_name, schema in list(schemas.items()):
155
+ # Handle top-level enum schemas (those defined directly in components/schemas)
156
+ # These are already enums but need generation_name set
157
+ if schema.enum and schema.type in ["string", "integer", "number"]:
158
+ # This is a top-level enum schema
159
+ # Ensure it has generation_name set (will be properly set by emitter later,
160
+ # but we can set it here to avoid the warning)
161
+ if not hasattr(schema, "generation_name") or not schema.generation_name:
162
+ schema.generation_name = schema.name
163
+ # Mark this as a properly processed enum by ensuring generation_name is set
164
+ # This serves as the marker that this enum was properly processed
165
+ logger.debug(f"Marked top-level enum schema: {schema_name}")
166
+
167
+ # Extract inline enums from properties
152
168
  for prop_name, prop_schema in list(schema.properties.items()):
153
169
  if prop_schema.enum and not prop_schema.name:
154
170
  enum_name = (
@@ -167,6 +183,7 @@ def extract_inline_enums(schemas: Dict[str, IRSchema]) -> Dict[str, IRSchema]:
167
183
  enum=copy.deepcopy(prop_schema.enum),
168
184
  description=prop_schema.description or f"Enum for {schema_name}.{prop_name}",
169
185
  )
186
+ enum_schema.generation_name = enum_name # Set generation_name for extracted enums
170
187
  new_enums[enum_name] = enum_schema
171
188
 
172
189
  # Update the original property to reference the extracted enum
@@ -77,9 +77,10 @@ class ClientGenerator:
77
77
  else:
78
78
  log_msg = f"{timestamp} ({elapsed:.2f}s) {message}"
79
79
 
80
- # logger.info(log_msg) # Keep commented out to ensure test_gen_nonexistent_spec_path passes
81
- # Also print to stdout for CLI users
82
- # print(log_msg) # Keep commented out
80
+ logger.info(log_msg)
81
+ # Also print to stdout for CLI users when verbose mode is enabled
82
+ if self.verbose:
83
+ print(log_msg)
83
84
 
84
85
  def generate(
85
86
  self,
@@ -523,9 +524,23 @@ class ClientGenerator:
523
524
  """
524
525
  spec_path_obj = Path(path_or_url)
525
526
  if spec_path_obj.exists() and spec_path_obj.is_file(): # Added is_file() check
526
- import yaml
527
+ text = spec_path_obj.read_text()
528
+ # Prefer JSON for .json files to avoid optional PyYAML dependency in tests
529
+ if spec_path_obj.suffix.lower() == ".json":
530
+ import json
531
+
532
+ data = json.loads(text)
533
+ else:
534
+ try:
535
+ import yaml
536
+
537
+ data = yaml.safe_load(text)
538
+ except ModuleNotFoundError:
539
+ # Fallback: attempt JSON parsing if YAML is unavailable
540
+ import json
541
+
542
+ data = json.loads(text)
527
543
 
528
- data = yaml.safe_load(spec_path_obj.read_text())
529
544
  if not isinstance(data, dict):
530
545
  raise GenerationError("Loaded spec is not a dictionary.")
531
546
  return data
@@ -172,10 +172,18 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
172
172
 
173
173
  def _resolve_string(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
174
174
  """Resolve string type, handling enums and formats."""
175
+ # Check if this is a properly processed enum (has generation_name)
175
176
  if hasattr(schema, "enum") and schema.enum:
176
- # This is an enum - should be promoted to named type
177
- logger.warning("Found inline enum in string schema - should be promoted")
178
- return ResolvedType(python_type="str", is_optional=not required)
177
+ # Check if this enum was properly processed (has generation_name)
178
+ if hasattr(schema, "generation_name") and schema.generation_name:
179
+ # This is a properly processed enum, it should have been handled earlier
180
+ # by _resolve_named_schema. If we're here, it might be during initial processing.
181
+ # Return the enum type name
182
+ return ResolvedType(python_type=schema.generation_name, is_optional=not required)
183
+ else:
184
+ # This is an unprocessed inline enum - log warning but continue
185
+ logger.warning(f"Found inline enum in string schema that wasn't promoted: {schema.name or 'unnamed'}")
186
+ return ResolvedType(python_type="str", is_optional=not required)
179
187
 
180
188
  # Handle string formats
181
189
  format_type = getattr(schema, "format", None)
@@ -68,17 +68,9 @@ class EndpointMethodSignatureGenerator:
68
68
  p = p_orig.copy() # Work with a copy
69
69
  arg_str = f"{NameSanitizer.sanitize_method_name(p['name'])}: {p['type']}" # Ensure param name is sanitized
70
70
  if not p.get("required", False):
71
- # Default value handling: if default is None, it should be ' = None'
72
- # If default is a string, it should be ' = "default_value"'
73
- # Otherwise, ' = default_value'
74
- default_val = p.get("default")
75
- if default_val is None and not p.get("required", False): # Explicitly check for None for Optional types
76
- arg_str += f" = None"
77
- elif default_val is not None: # Only add if default is not None
78
- if isinstance(default_val, str):
79
- arg_str += f' = "{default_val}"'
80
- else:
81
- arg_str += f" = {default_val}"
71
+ # For optional parameters, always default to None to avoid type mismatches
72
+ # (e.g., enum-typed params with string defaults)
73
+ arg_str += " = None"
82
74
  args.append(arg_str)
83
75
 
84
76
  actual_return_type = return_type
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyopenapi-gen
3
- Version: 0.10.1
3
+ Version: 0.11.0
4
4
  Summary: Modern, async-first Python client generator for OpenAPI specifications with advanced cycle detection and unified type resolution
5
5
  Project-URL: Homepage, https://github.com/your-org/pyopenapi-gen
6
6
  Project-URL: Documentation, https://github.com/your-org/pyopenapi-gen/blob/main/README.md
@@ -1,4 +1,4 @@
1
- pyopenapi_gen/__init__.py,sha256=9DJOZ8R4qQ3tHN0XxuSOK0PWXBtbpjaNEJAgzfUVTeE,3016
1
+ pyopenapi_gen/__init__.py,sha256=jU5GYbWa4MBDeDzOzwUHl_8eLGtotxRGLj42d2yskUo,3017
2
2
  pyopenapi_gen/__main__.py,sha256=4-SCaCNhBd7rtyRK58uoDbdl93J0KhUeajP_b0CPpLE,110
3
3
  pyopenapi_gen/cli.py,sha256=_ewksNDaA5st3TJJMZJWgCZdBGOQp__tkMVqr_6U3vs,2339
4
4
  pyopenapi_gen/http_types.py,sha256=EMMYZBt8PNVZKPFu77TQija-JI-nOKyXvpiQP9-VSWE,467
@@ -10,7 +10,7 @@ pyopenapi_gen/context/import_collector.py,sha256=rnOgR5-GsHs_oS1iUVbOF3tagcH5nam
10
10
  pyopenapi_gen/context/render_context.py,sha256=AS08ha9WVjgRUsM1LFPjMCgrsHbczHH7c60Z5PbojhY,30320
11
11
  pyopenapi_gen/core/CLAUDE.md,sha256=bz48K-PSrhxCq5ScmiLiU9kfpVVzSWRKOA9RdKk_pbg,6482
12
12
  pyopenapi_gen/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- pyopenapi_gen/core/exceptions.py,sha256=KemyLDXl8pBgzxxV6ZhsGi7WjwKGooKU6Idy63eC2ko,553
13
+ pyopenapi_gen/core/exceptions.py,sha256=HYFiYdmzsZUl46vB8M3B6Vpp6m8iqjUcKDWdL4yEKHo,498
14
14
  pyopenapi_gen/core/http_transport.py,sha256=77ZOTyl0_CLuDtSCOVDQoxHDQBnclJgz6f3Hs6cy7hY,9675
15
15
  pyopenapi_gen/core/pagination.py,sha256=aeDOKo-Lu8mcSDqv0TlPXV9Ul-Nca76ZuKhQHKlsMUs,2301
16
16
  pyopenapi_gen/core/postprocess_manager.py,sha256=ky27ijbq6zEo43aqe-odz9CR3vFD_3XHhQR35XgMZo0,6879
@@ -22,17 +22,17 @@ pyopenapi_gen/core/warning_collector.py,sha256=DYl9D7eZYs04mDU84KeonS-5-d0aM7hNq
22
22
  pyopenapi_gen/core/auth/base.py,sha256=E2KUerA_mYv9D7xulUm-lenIxqZHqanjA4oRKpof2ZE,792
23
23
  pyopenapi_gen/core/auth/plugins.py,sha256=bDWx4MTRFsCKp1i__BsQtZEvQPGU-NKI137-zoxmrgs,3465
24
24
  pyopenapi_gen/core/loader/__init__.py,sha256=bt-MQ35fbq-f1YnCcopPg53TuXCI9_7wcMzQZoWVpjU,391
25
- pyopenapi_gen/core/loader/loader.py,sha256=bogAgDr2XvJWmMAFE006b3K_5AmC8eg2uj_TpYtLAfg,5591
25
+ pyopenapi_gen/core/loader/loader.py,sha256=aehmWhOwWzv2af4Q0XxuOwP9AoAozG36o4-yMVMO0hE,6318
26
26
  pyopenapi_gen/core/loader/operations/__init__.py,sha256=7se21D-BOy7Qw6C9auJ9v6D3NCuRiDpRlhqxGq11fJs,366
27
27
  pyopenapi_gen/core/loader/operations/parser.py,sha256=ai5iZZA2nicouC77iEvo8jKGXbHKbX_NcTy44lkwOVQ,6896
28
28
  pyopenapi_gen/core/loader/operations/post_processor.py,sha256=3FZ5o59J2bSpZP-tNIec0A2hw095cC27GKqkhGrgHZA,2437
29
29
  pyopenapi_gen/core/loader/operations/request_body.py,sha256=qz-wh014ejb1SGTuVRNODSXc95_iOLAjw05aDRhbXoo,3099
30
30
  pyopenapi_gen/core/loader/parameters/__init__.py,sha256=p13oSibCRC5RCfsP6w7yD9MYs5TXcdI4WwPv7oGUYKk,284
31
- pyopenapi_gen/core/loader/parameters/parser.py,sha256=4-p9s8VRm2hukXvvMF8zkd2YqSZsB18NYccVS3rwqIg,4773
31
+ pyopenapi_gen/core/loader/parameters/parser.py,sha256=ohyi0Ay7vterM8ho_kscWzzNXuwKISDGjgpArByeFSw,6089
32
32
  pyopenapi_gen/core/loader/responses/__init__.py,sha256=6APWoH3IdNkgVmI0KsgZoZ6knDaG-S-pnUCa6gkzT8E,216
33
33
  pyopenapi_gen/core/loader/responses/parser.py,sha256=T0QXH_3c-Y6_S6DvveKHPfV_tID7qhBoX0nFtdLCa0A,3896
34
34
  pyopenapi_gen/core/loader/schemas/__init__.py,sha256=rlhujYfw_IzWgzhVhYMJ3eIhE6C5Vi1Ylba-BHEVqOg,296
35
- pyopenapi_gen/core/loader/schemas/extractor.py,sha256=7-lpDhs9W9wVhG1OCASdyft_V1kUH7NdP8D4x-raGjk,8364
35
+ pyopenapi_gen/core/loader/schemas/extractor.py,sha256=TlLflugYrkQg-Fe7p-C4ppT8rRC9OKvK4nubEUR58tY,9445
36
36
  pyopenapi_gen/core/parsing/__init__.py,sha256=RJsIR6cHaNoI4tBcpMlAa0JsY64vsHb9sPxPg6rd8FQ,486
37
37
  pyopenapi_gen/core/parsing/context.py,sha256=crn5oTkzEvnSzYdPuHBA_s72kmq0RKzXpyaqNh67k68,7947
38
38
  pyopenapi_gen/core/parsing/cycle_helpers.py,sha256=nG5ysNavL_6lpnHWFUZR9qraBxqOzuNfI6NgSEa8a5M,5939
@@ -74,7 +74,7 @@ pyopenapi_gen/emitters/endpoints_emitter.py,sha256=2VuJJF0hpzyN-TqY9XVLQtTJ-Qged
74
74
  pyopenapi_gen/emitters/exceptions_emitter.py,sha256=qPTIPXDyqSUtpmBIp-V4ap1uMHUPmYziCSN62t7qcAE,1918
75
75
  pyopenapi_gen/emitters/models_emitter.py,sha256=I3IKwtKVocD3UVrI7cINXI8NjLwUZqHuGgvS3hjWUJ8,22192
76
76
  pyopenapi_gen/generator/CLAUDE.md,sha256=BS9KkmLvk2WD-Io-_apoWjGNeMU4q4LKy4UOxYF9WxM,10870
77
- pyopenapi_gen/generator/client_generator.py,sha256=avYz5GCp7t6vRAFJCvXw0ipLQGX2QfgWtfOwr3QlOqY,28880
77
+ pyopenapi_gen/generator/client_generator.py,sha256=MULKJY9SdRuYjt_R4XCXh3vJSW-92rsxOu-MVpIklho,29333
78
78
  pyopenapi_gen/helpers/CLAUDE.md,sha256=GyIJ0grp4SkD3plAUzyycW4nTUZf9ewtvvsdAGkmIZw,10609
79
79
  pyopenapi_gen/helpers/__init__.py,sha256=m4jSQ1sDH6CesIcqIl_kox4LcDFabGxBpSIWVwbHK0M,39
80
80
  pyopenapi_gen/helpers/endpoint_utils.py,sha256=bkRu6YddIPQQD3rZLbB8L5WYzG-2Bd_JgMbxMUYY2wY,22198
@@ -97,7 +97,7 @@ pyopenapi_gen/types/contracts/types.py,sha256=-Qvbx3N_14AaN-1BeyocrvsjiwXPn_eWQh
97
97
  pyopenapi_gen/types/resolvers/__init__.py,sha256=_5kA49RvyOTyXgt0GbbOfHJcdQw2zHxvU9af8GGyNWc,295
98
98
  pyopenapi_gen/types/resolvers/reference_resolver.py,sha256=qnaZeLmtyh4_NBMcKib58s6o5ycUJaattYt8F38_qIo,2053
99
99
  pyopenapi_gen/types/resolvers/response_resolver.py,sha256=Kb1a2803lyoukoZy06ztPBlUw-A1lHiZ6NlJmsixxA8,6500
100
- pyopenapi_gen/types/resolvers/schema_resolver.py,sha256=R0N03MDLVzaFBQcrFOOTre1zqIg6APiWdHAT96ldgQ0,16898
100
+ pyopenapi_gen/types/resolvers/schema_resolver.py,sha256=6cgJPgRsEyMNrEnaR_ONExvMNWjWWBuWa69D7gKTsSY,17528
101
101
  pyopenapi_gen/types/services/__init__.py,sha256=inSUKmY_Vnuym6tC-AhvjCTj16GbkfxCGLESRr_uQPE,123
102
102
  pyopenapi_gen/types/services/type_service.py,sha256=-LQj7oSx1mxb10Zi6DpawS8uyoUrUbnYhmUA0GuKZTc,4402
103
103
  pyopenapi_gen/types/strategies/__init__.py,sha256=bju8_KEPNIow1-woMO-zJCgK_E0M6JnFq0NFsK1R4Ss,171
@@ -114,7 +114,7 @@ pyopenapi_gen/visit/endpoint/generators/docstring_generator.py,sha256=U02qvuYtFE
114
114
  pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py,sha256=wUJ4_gaA1gRrFCHYFCObBIankxGQu0MNqiOSoZOZmoA,4352
115
115
  pyopenapi_gen/visit/endpoint/generators/request_generator.py,sha256=OnkrkRk39_BrK9ZDvyWqJYLz1mocD2zY7j70yIpS0J4,5374
116
116
  pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py,sha256=VuyYpUUQ3hIOwM0X_hrMk9qmQTC0P6xRQdC2HTTIyQw,22828
117
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py,sha256=VC9Q_exZMpUh46hn1JXaDRlgVm3Gb-D2v36h9SDF10k,4390
117
+ pyopenapi_gen/visit/endpoint/generators/signature_generator.py,sha256=CYtfsPMlTZN95g2WxrdnTloGx2RmqeNQRiyP9fOkUEQ,3892
118
118
  pyopenapi_gen/visit/endpoint/generators/url_args_generator.py,sha256=EsmNuVSkGfUqrmV7-1GiLPzdN86V5UqLfs1SVY0jsf0,9590
119
119
  pyopenapi_gen/visit/endpoint/processors/__init__.py,sha256=_6RqpOdDuDheArqDBi3ykhsaetACny88WUuuAJvr_ME,29
120
120
  pyopenapi_gen/visit/endpoint/processors/import_analyzer.py,sha256=tNmhgWwt-CjLE774TC8sPVH1-yaTKKm6JmfgBT2-iRk,3386
@@ -124,8 +124,8 @@ pyopenapi_gen/visit/model/alias_generator.py,sha256=3iPFDjCXU0Vm59Hfp64jTDfHoUL8
124
124
  pyopenapi_gen/visit/model/dataclass_generator.py,sha256=WtcQNx6l2sxVyTlH1MdQ-UFYWVMsxQk5nyJr1Mk02iM,9999
125
125
  pyopenapi_gen/visit/model/enum_generator.py,sha256=QWsD-IAxGOxKQuC6LLNUvbT8Ot3NWrLFsaYT0DI16DU,9670
126
126
  pyopenapi_gen/visit/model/model_visitor.py,sha256=PZeQd7-dlxZf5gY10BW-DhswmAGF903NccV6L56mjoE,9439
127
- pyopenapi_gen-0.10.1.dist-info/METADATA,sha256=dmrKeU6KlcuRSdXhM2caLE5mbVqlcdt6y2SYbJcNpBI,14040
128
- pyopenapi_gen-0.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
- pyopenapi_gen-0.10.1.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
130
- pyopenapi_gen-0.10.1.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
131
- pyopenapi_gen-0.10.1.dist-info/RECORD,,
127
+ pyopenapi_gen-0.11.0.dist-info/METADATA,sha256=gv2QjmQpa6Oj87vq7sE14_6MqXO3YEmQsZS6-4NQsNQ,14040
128
+ pyopenapi_gen-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
+ pyopenapi_gen-0.11.0.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
130
+ pyopenapi_gen-0.11.0.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
131
+ pyopenapi_gen-0.11.0.dist-info/RECORD,,