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,285 @@
1
+ """
2
+ Handles the extraction of inline enums from schema definitions.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import Any, Mapping, Optional
9
+
10
+ from .... import IRSchema
11
+ from ...utils import NameSanitizer
12
+ from ..context import ParsingContext
13
+
14
+
15
+ def _extract_enum_from_property_node(
16
+ parent_schema_name: Optional[str],
17
+ property_key: str,
18
+ property_node_data: Mapping[str, Any],
19
+ context: ParsingContext,
20
+ logger: logging.Logger,
21
+ ) -> Optional[IRSchema]:
22
+ """
23
+ Checks a property's schema node for an inline enum definition.
24
+
25
+ If an inline enum is found (i.e., the node is a dict, has "enum", and is not a $ref),
26
+ this function:
27
+ 1. Creates a new global IRSchema for the enum.
28
+ 2. Adds this new enum schema to context.parsed_schemas.
29
+ 3. Returns a new IRSchema representing the property, which refers (by type)
30
+ to the globally registered enum.
31
+
32
+ Args:
33
+ parent_schema_name: The name of the parent schema containing the property.
34
+ Used for generating a descriptive enum name.
35
+ property_key: The key (name) of the property being processed.
36
+ property_node_data: The raw schema definition for the property.
37
+ context: The global parsing context.
38
+ logger: Logger instance for logging messages.
39
+
40
+ Returns:
41
+ An IRSchema for the property referring to the new global enum if an inline
42
+ enum was extracted. Otherwise, returns None.
43
+ """
44
+ if not (isinstance(property_node_data, dict) and "enum" in property_node_data and "$ref" not in property_node_data):
45
+ return None
46
+
47
+ enum_values = property_node_data["enum"]
48
+ # Default to "string" if type is not specified, as per OpenAPI for enums
49
+ enum_type_from_node = property_node_data.get("type", "string")
50
+ enum_description = property_node_data.get("description")
51
+
52
+ # Improved naming for inline enums - create more descriptive and semantic names
53
+ # First, process the parent schema name
54
+ if parent_schema_name:
55
+ parent_class_name_for_enum = NameSanitizer.sanitize_class_name(parent_schema_name)
56
+ else:
57
+ # Instead of generic "AnonymousSchema", try to give a more descriptive context name
58
+ # based on common patterns like "status" or "type" fields
59
+ if property_key.lower() in ["status", "state", "type", "category", "role", "permission", "level"]:
60
+ # These are common fields that often have enums - use a descriptive prefix based on the property
61
+ parent_class_name_for_enum = {
62
+ "status": "Status",
63
+ "state": "State",
64
+ "type": "Type",
65
+ "category": "Category",
66
+ "role": "Role",
67
+ "permission": "Permission",
68
+ "level": "Level",
69
+ }.get(property_key.lower(), "Resource")
70
+ else:
71
+ # Use a more semantic name than "AnonymousSchema"
72
+ parent_class_name_for_enum = "AnonymousSchema"
73
+
74
+ # Process the property name for the enum
75
+ prop_class_name_for_enum = NameSanitizer.sanitize_class_name(property_key)
76
+
77
+ # Special handling for common property names - make the enum name more specific
78
+ if prop_class_name_for_enum.lower() in ["status", "state", "type", "category"]:
79
+ # Example: Convert "Status" property in "Order" class to "OrderStatusEnum"
80
+ # Instead of just "OrderStatusEnum" which doesn't indicate what type of status it is
81
+ base_generated_enum_name = f"{parent_class_name_for_enum}{prop_class_name_for_enum}Enum"
82
+ else:
83
+ # For other properties, keep the standard naming pattern but ensure "Enum" suffix
84
+ if not prop_class_name_for_enum.endswith("Enum"):
85
+ base_generated_enum_name = f"{parent_class_name_for_enum}{prop_class_name_for_enum}Enum"
86
+ else:
87
+ base_generated_enum_name = f"{parent_class_name_for_enum}{prop_class_name_for_enum}"
88
+
89
+ # Ensure uniqueness with counter if needed
90
+ generated_enum_name = base_generated_enum_name
91
+ counter = 1
92
+ while generated_enum_name in context.parsed_schemas:
93
+ # Ensure the schema in context isn't the exact same definition
94
+ # For now, simple name collision avoidance is sufficient
95
+ generated_enum_name = f"{base_generated_enum_name}{counter}"
96
+ counter += 1
97
+
98
+ # Construct the name for logging/description before sanitization for clarity
99
+ prop_schema_context_name = f"{parent_schema_name}.{property_key}" if parent_schema_name else property_key
100
+
101
+ new_enum_ir = IRSchema(
102
+ name=generated_enum_name,
103
+ type=enum_type_from_node,
104
+ enum=enum_values,
105
+ description=property_node_data.get("description") or f"An enumeration for {prop_schema_context_name}",
106
+ properties={}, # Enums do not have properties in this context
107
+ required=[], # Enums do not have required fields
108
+ )
109
+ context.parsed_schemas[generated_enum_name] = new_enum_ir
110
+ logger.debug(
111
+ f"INLINE_ENUM_EXTRACT: Extracted inline enum for '{prop_schema_context_name}' "
112
+ f"to global schema '{generated_enum_name}'. In context: {generated_enum_name in context.parsed_schemas}"
113
+ )
114
+
115
+ # Return an IRSchema for the property that refers to this new global enum
116
+ property_ir_referencing_enum = IRSchema(
117
+ name=property_key, # The property keeps its original name
118
+ type=generated_enum_name, # Type is the name of the new global enum
119
+ description=enum_description, # Property's original description
120
+ is_nullable=property_node_data.get("nullable", False)
121
+ or ("null" in enum_type_from_node if isinstance(enum_type_from_node, list) else False),
122
+ # Other fields like format, etc., are not typically on the property ref if it's just a type ref to an enum.
123
+ # The enum definition itself holds those.
124
+ )
125
+ return property_ir_referencing_enum
126
+
127
+
128
+ def _process_standalone_inline_enum(
129
+ schema_name: Optional[str], # The original intended name for this schema
130
+ node_data: Mapping[str, Any], # The raw node data for this schema
131
+ schema_obj: IRSchema, # The IRSchema object already partially parsed for this node
132
+ context: ParsingContext,
133
+ logger: logging.Logger,
134
+ ) -> IRSchema:
135
+ """
136
+ Processes a schema node that might itself be an inline enum, not nested within a property.
137
+
138
+ If the provided `node_data` defines an enum (has "enum" key, is not a $ref),
139
+ and `schema_obj` doesn't yet have its enum values populated or a proper name,
140
+ this function will:
141
+ 1. Ensure `schema_obj.name` is correctly sanitized and globally unique if it's an enum.
142
+ 2. Populate `schema_obj.enum` and `schema_obj.type` from `node_data`.
143
+ 3. Ensure the potentially renamed/finalized `schema_obj` is in `context.parsed_schemas`.
144
+
145
+ This is for cases like:
146
+ components:
147
+ schemas:
148
+ MyTopLevelEnum:
149
+ type: string
150
+ enum: ["A", "B"]
151
+ AnotherObject:
152
+ properties:
153
+ some_field:
154
+ type: string # Not an enum
155
+ another_enum_prop: # Handled by _extract_enum_from_property_node
156
+ type: string
157
+ enum: ["X", "Y"]
158
+ requestBodies:
159
+ EnumBody:
160
+ content:
161
+ application/json:
162
+ schema:
163
+ type: string # This schema itself is an inline enum
164
+ enum: ["C", "D"]
165
+
166
+ Args:
167
+ schema_name: The original name hint for this schema (e.g. from components.schemas key or a synthesized name).
168
+ node_data: The raw OpenAPI node dictionary for the schema.
169
+ schema_obj: The IRSchema instance created from initial parsing of `node_data`.
170
+ context: The global parsing context.
171
+ logger: Logger instance.
172
+
173
+ Returns:
174
+ The (potentially updated) IRSchema object.
175
+ """
176
+ # If it's not an enum defined directly in this node, or if schema_obj already has enum values, do nothing more here.
177
+ if not (isinstance(node_data, dict) and "enum" in node_data and "$ref" not in node_data) or schema_obj.enum:
178
+ return schema_obj
179
+
180
+ logger.debug(
181
+ f"STANDALONE_ENUM_CHECK: Processing node for "
182
+ f"'{schema_name or schema_obj.name or 'anonymous_schema'}' for direct enum properties."
183
+ )
184
+
185
+ # Ensure basic enum properties are on schema_obj if not already there from initial _parse_schema pass
186
+ if not schema_obj.enum:
187
+ schema_obj.enum = node_data.get("enum")
188
+ if not schema_obj.type and node_data.get("type"):
189
+ # TODO: Handle type extraction more robustly here, possibly using extract_primary_type_and_nullability
190
+ # For now, simple type assignment for enums.
191
+ raw_type = node_data.get("type", "string")
192
+ if isinstance(raw_type, list):
193
+ # For enums like type: [string, null], the primary type is string for the enum values themselves.
194
+ # Nullability should be handled by schema_obj.is_nullable elsewhere.
195
+ primary_enum_type = next((t for t in raw_type if t != "null"), "string")
196
+ schema_obj.type = primary_enum_type
197
+ else:
198
+ schema_obj.type = raw_type
199
+ elif not schema_obj.type: # Default type for enum if not specified
200
+ schema_obj.type = "string"
201
+
202
+ # Ensure a proper, unique name if this is indeed an enum and needs one.
203
+ # schema_obj.name might be None if it was parsed from, e.g., a requestBody schema directly.
204
+ if not schema_obj.name and schema_name:
205
+ schema_obj.name = NameSanitizer.sanitize_class_name(schema_name)
206
+ elif not schema_obj.name:
207
+ # Create a more descriptive name based on enum values if possible
208
+ enum_values = schema_obj.enum or []
209
+
210
+ # Try to determine a semantic name from the enum values
211
+ if enum_values and all(isinstance(v, str) for v in enum_values):
212
+ # For string enums, look for common patterns in the values
213
+ enum_values_str = [str(v).lower() for v in enum_values]
214
+
215
+ # Check for common patterns in enum values
216
+ if any(v in ["pending", "active", "completed", "cancelled", "failed"] for v in enum_values_str):
217
+ base_name = "StatusEnum"
218
+ elif any(v in ["admin", "user", "guest", "moderator"] for v in enum_values_str):
219
+ base_name = "RoleEnum"
220
+ elif any(v in ["read", "write", "admin", "owner"] for v in enum_values_str):
221
+ base_name = "PermissionEnum"
222
+ elif any(v in ["asc", "desc", "ascending", "descending"] for v in enum_values_str):
223
+ base_name = "SortOrderEnum"
224
+ elif any(v in ["true", "false", "yes", "no"] for v in enum_values_str):
225
+ base_name = "BooleanEnum"
226
+ else:
227
+ # Default to more descriptive name than just "UnnamedEnum"
228
+ base_name = "ResourceTypeEnum"
229
+ else:
230
+ # If we can't determine from values, use a generic but still meaningful name
231
+ base_name = "ResourceTypeEnum"
232
+
233
+ # Ensure uniqueness
234
+ counter = 1
235
+ candidate_name = base_name
236
+ while candidate_name in context.parsed_schemas:
237
+ candidate_name = f"{base_name}{counter}"
238
+ counter += 1
239
+
240
+ schema_obj.name = candidate_name
241
+
242
+ # If the name (now sanitized and potentially unique) exists in context.parsed_schemas
243
+ # but refers to a different object, we need to ensure this schema_obj gets a unique name.
244
+ if (
245
+ schema_obj.name
246
+ and schema_obj.name in context.parsed_schemas
247
+ and context.parsed_schemas[schema_obj.name] is not schema_obj
248
+ ):
249
+ original_name_attempt = schema_obj.name
250
+ counter = 1
251
+ new_name_base = original_name_attempt
252
+ # Avoid potential infinite loop if somehow new_name_base ends up empty or just a number.
253
+ if not new_name_base or new_name_base.isnumeric():
254
+ new_name_base = "Enum" # Fallback to a generic base name
255
+
256
+ while schema_obj.name in context.parsed_schemas and context.parsed_schemas[schema_obj.name] is not schema_obj:
257
+ schema_obj.name = f"{new_name_base}{counter}"
258
+ counter += 1
259
+ logger.warning(
260
+ f"STANDALONE_ENUM_NAME_COLLISION: Renamed schema from '{original_name_attempt}' to '{schema_obj.name}' "
261
+ f"due to name collision with a different existing schema."
262
+ )
263
+
264
+ # Ensure the final named schema_obj is in the context
265
+ if schema_obj.name and schema_obj.name not in context.parsed_schemas:
266
+ context.parsed_schemas[schema_obj.name] = schema_obj
267
+ logger.debug(
268
+ f"STANDALONE_ENUM_FINALIZED: Added/updated schema '{schema_obj.name}' "
269
+ f"in context after processing as standalone enum."
270
+ )
271
+ elif schema_obj.name and context.parsed_schemas[schema_obj.name] is not schema_obj:
272
+ # This case should ideally be caught by the renaming logic above.
273
+ # If we are here, it means a schema with this name exists, but it's not our schema_obj.
274
+ # This indicates a problem if schema_obj was supposed to be *the* definition for that name.
275
+ logger.error(
276
+ f"STANDALONE_ENUM_ERROR: Schema '{schema_obj.name}' exists in context "
277
+ f"but is not the current schema_obj. This is unexpected."
278
+ )
279
+ elif not schema_obj.name:
280
+ logger.warning(
281
+ "STANDALONE_ENUM_UNNAMED: Processed a standalone enum but it ended up "
282
+ "without a name. This might be an issue."
283
+ )
284
+
285
+ return schema_obj
@@ -0,0 +1,117 @@
1
+ """
2
+ Handles the promotion of inline object schemas to global schemas.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import Optional
9
+
10
+ from .... import IRSchema
11
+ from ...utils import NameSanitizer
12
+ from ..context import ParsingContext
13
+
14
+
15
+ def _attempt_promote_inline_object(
16
+ parent_schema_name: Optional[str], # Name of the schema containing the property
17
+ property_key: str, # The key (name) of the property being processed
18
+ property_schema_obj: IRSchema, # The IRSchema of the property itself (already parsed)
19
+ context: ParsingContext,
20
+ logger: logging.Logger,
21
+ ) -> Optional[IRSchema]:
22
+ logger.debug(
23
+ f"PROMO_ATTEMPT: parent='{parent_schema_name}', prop_key='{property_key}', "
24
+ f"prop_schema_name='{property_schema_obj.name}', prop_schema_type='{property_schema_obj.type}', "
25
+ f"prop_is_enum='{property_schema_obj.enum is not None}', "
26
+ f"prop_is_ref='{property_schema_obj._from_unresolved_ref}'"
27
+ )
28
+ """
29
+ Checks if a given property's schema (`property_schema_obj`) represents an inline object
30
+ that should be "promoted" to a global schema definition.
31
+
32
+ Conditions for promotion:
33
+ - `property_schema_obj.type` is "object".
34
+ - `property_schema_obj.enum` is None (i.e., it's not an enum).
35
+ - `property_schema_obj._from_unresolved_ref` is False (it wasn't from a $ref).
36
+
37
+ If promoted:
38
+ 1. A unique global name is generated for `property_schema_obj`.
39
+ 2. `property_schema_obj.name` is updated to this new global name.
40
+ 3. `property_schema_obj` is added/updated in `context.parsed_schemas` under its new global name.
41
+ 4. A new IRSchema is returned for the original property, which now *refers* (by type)
42
+ to the globally registered `property_schema_obj`.
43
+ This new IRSchema preserves the original property's description, nullability etc.
44
+ """
45
+ if property_schema_obj.type != "object":
46
+ logger.debug(
47
+ f"PROMO_SKIP: parent='{parent_schema_name}', prop_key='{property_key}' "
48
+ f"not promoted, type is '{property_schema_obj.type}', not 'object'."
49
+ )
50
+ return None
51
+
52
+ if property_schema_obj.enum is not None:
53
+ logger.debug(
54
+ f"PROMO_SKIP: parent='{parent_schema_name}', prop_key='{property_key}' not promoted, it is an enum."
55
+ )
56
+ return None
57
+
58
+ if property_schema_obj._from_unresolved_ref:
59
+ logger.debug(
60
+ f"PROMO_SKIP: parent='{parent_schema_name}', prop_key='{property_key}' not promoted, it was from a $ref."
61
+ )
62
+ return None
63
+
64
+ # Improved naming strategy for more intuitive and descriptive schema naming
65
+ sanitized_prop_key_class_name = NameSanitizer.sanitize_class_name(property_key)
66
+
67
+ # Primary candidate name: ParentName + PropertyName (sanitized)
68
+ # Example: parent="User", prop_key="address" (sanitized_prop_key_class_name="Address") -> UserAddress
69
+ # Example: parent=None, prop_key="user_profile" -> UserProfile
70
+ if parent_schema_name:
71
+ # First sanitize the parent name to ensure it's in PascalCase
72
+ sanitized_parent_name = NameSanitizer.sanitize_class_name(parent_schema_name)
73
+ # Then combine with the sanitized property key
74
+ base_name_candidate = f"{sanitized_parent_name}{sanitized_prop_key_class_name}"
75
+ else:
76
+ base_name_candidate = sanitized_prop_key_class_name
77
+
78
+ chosen_global_name: Optional[str] = None
79
+
80
+ # Check if the primary candidate name is available or already points to this object
81
+ if base_name_candidate in context.parsed_schemas:
82
+ existing_schema = context.parsed_schemas[base_name_candidate]
83
+ # If the existing schema is identical to our property schema, reuse it
84
+ if existing_schema.properties == property_schema_obj.properties:
85
+ chosen_global_name = base_name_candidate
86
+ else:
87
+ # If different schema, try with counter
88
+ counter = 1
89
+ temp_name = f"{base_name_candidate}{counter}"
90
+ while temp_name in context.parsed_schemas:
91
+ existing_schema = context.parsed_schemas[temp_name]
92
+ if existing_schema.properties == property_schema_obj.properties:
93
+ chosen_global_name = temp_name
94
+ break
95
+ counter += 1
96
+ temp_name = f"{base_name_candidate}{counter}"
97
+ if chosen_global_name is None:
98
+ chosen_global_name = temp_name
99
+ else:
100
+ chosen_global_name = base_name_candidate
101
+
102
+ original_name_of_promoted_obj = property_schema_obj.name
103
+ property_schema_obj.name = chosen_global_name
104
+ context.parsed_schemas[chosen_global_name] = property_schema_obj
105
+
106
+ # Corrected logger call for clarity and f-string safety
107
+ parent_display_name = parent_schema_name or "<None>"
108
+
109
+ property_ref_ir = IRSchema(
110
+ name=property_key,
111
+ type=chosen_global_name,
112
+ description=property_schema_obj.description,
113
+ is_nullable=property_schema_obj.is_nullable,
114
+ )
115
+ property_ref_ir._refers_to_schema = property_schema_obj
116
+
117
+ return property_ref_ir