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,84 @@
1
+ """
2
+ Parser for 'anyOf' keyword in OpenAPI schemas.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any, Callable, List, Mapping
8
+
9
+ from pyopenapi_gen import IRSchema # Main IR model
10
+
11
+ from ..context import ParsingContext # Context object - MOVED
12
+
13
+ if TYPE_CHECKING:
14
+ # from ..context import ParsingContext # No longer here
15
+ # No direct import of _parse_schema from schema_parser to avoid circularity
16
+ pass
17
+
18
+
19
+ def _parse_any_of_schemas(
20
+ any_of_nodes: List[Mapping[str, Any]],
21
+ context: ParsingContext,
22
+ max_depth: int,
23
+ parse_fn: Callable[ # Accepts the main schema parsing function
24
+ [str | None, Mapping[str, Any] | None, ParsingContext, int], IRSchema
25
+ ],
26
+ ) -> tuple[List[IRSchema] | None, bool, str | None]:
27
+ """Parses 'anyOf' sub-schemas using a provided parsing function.
28
+
29
+ Contracts:
30
+ Pre-conditions:
31
+ - any_of_nodes is a list of schema node mappings.
32
+ - context is a valid ParsingContext instance.
33
+ - max_depth >= 0.
34
+ - parse_fn is a callable that can parse a schema node.
35
+ Post-conditions:
36
+ - Returns a tuple: (parsed_schemas, is_nullable, effective_schema_type)
37
+ - parsed_schemas: List of IRSchema for non-null sub-schemas, or None.
38
+ - is_nullable: True if a null type was present.
39
+ - effective_schema_type: Potential schema_type if list becomes empty/None (currently always None).
40
+ """
41
+ if not isinstance(any_of_nodes, list):
42
+ raise TypeError("any_of_nodes must be a list")
43
+ if not all(isinstance(n, Mapping) for n in any_of_nodes):
44
+ raise TypeError("all items in any_of_nodes must be Mappings")
45
+ if not isinstance(context, ParsingContext):
46
+ raise TypeError("context must be a ParsingContext instance")
47
+ if not max_depth >= 0:
48
+ raise ValueError("max_depth must be non-negative")
49
+ if not callable(parse_fn):
50
+ raise TypeError("parse_fn must be a callable")
51
+
52
+ parsed_schemas_list: List[IRSchema] = [] # Renamed to avoid confusion with module name
53
+ is_nullable_from_any_of = False
54
+ effective_schema_type: str | None = None
55
+
56
+ for sub_node in any_of_nodes:
57
+ if isinstance(sub_node, dict) and sub_node.get("type") == "null":
58
+ is_nullable_from_any_of = True
59
+ continue
60
+
61
+ parsed_schemas_list.append(parse_fn(None, sub_node, context, max_depth))
62
+
63
+ filtered_schemas = [
64
+ s
65
+ for s in parsed_schemas_list
66
+ if not (
67
+ s.type is None
68
+ and not s.properties
69
+ and not s.items
70
+ and not s.enum
71
+ and not s.any_of
72
+ and not s.one_of
73
+ and not s.all_of
74
+ )
75
+ ]
76
+
77
+ if not filtered_schemas:
78
+ effective_schema_type = None
79
+ return None, is_nullable_from_any_of, effective_schema_type
80
+
81
+ return filtered_schemas, is_nullable_from_any_of, effective_schema_type
82
+
83
+
84
+ # ... existing code ...
@@ -0,0 +1,72 @@
1
+ """
2
+ Dedicated parser for handling 'items' within an array schema.
3
+ Renamed from array_parser.py for clarity.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING, Any, Callable, Mapping
9
+
10
+ from pyopenapi_gen import IRSchema
11
+
12
+ from ..context import ParsingContext
13
+
14
+ if TYPE_CHECKING:
15
+ # from pyopenapi_gen import IRSchema # Already above or handled by context
16
+ # from ..context import ParsingContext # No longer here
17
+ pass
18
+
19
+
20
+ def _parse_array_items_schema(
21
+ parent_schema_name: str | None,
22
+ items_node_data: Mapping[str, Any],
23
+ context: ParsingContext,
24
+ parse_fn: Callable[ # Accepts the main schema parsing function
25
+ [str | None, Mapping[str, Any] | None, ParsingContext, int], IRSchema
26
+ ],
27
+ max_depth: int,
28
+ ) -> IRSchema | None:
29
+ """Parses the 'items' sub-schema of an array.
30
+
31
+ Args:
32
+ parent_schema_name: The name of the parent array schema (if any).
33
+ items_node_data: The raw dictionary of the 'items' schema.
34
+ context: The parsing context.
35
+ parse_fn: The main schema parsing function to call recursively (_parse_schema from schema_parser.py).
36
+ max_depth: Maximum recursion depth.
37
+
38
+ Returns:
39
+ The parsed IRSchema for the items, or None if items_node_data is not suitable.
40
+
41
+ Contracts:
42
+ Pre-conditions:
43
+ - items_node_data is a mapping representing the items schema.
44
+ - context is a valid ParsingContext instance.
45
+ - parse_fn is a callable function.
46
+ - max_depth is a non-negative integer.
47
+ Post-conditions:
48
+ - Returns an IRSchema if items_node_data is a valid schema mapping.
49
+ - Returns None if items_node_data is not a mapping.
50
+ - Calls parse_fn with an appropriate name for the item schema.
51
+ """
52
+ # Pre-conditions
53
+ # items_node_data is checked later, as it can be non-Mapping to return None
54
+ if not isinstance(context, ParsingContext):
55
+ raise TypeError("context must be a ParsingContext instance")
56
+ if not callable(parse_fn):
57
+ raise TypeError("parse_fn must be callable")
58
+ if not (isinstance(max_depth, int) and max_depth >= 0):
59
+ raise ValueError("max_depth must be a non-negative integer")
60
+
61
+ item_name_for_parse = f"{parent_schema_name}Item" if parent_schema_name else None
62
+ if (
63
+ isinstance(items_node_data, dict)
64
+ and "$ref" in items_node_data
65
+ and items_node_data["$ref"].startswith("#/components/schemas/")
66
+ ):
67
+ item_name_for_parse = items_node_data["$ref"].split("/")[-1]
68
+
69
+ if not isinstance(items_node_data, Mapping):
70
+ return None
71
+
72
+ return parse_fn(item_name_for_parse, items_node_data, context, max_depth)
@@ -0,0 +1,77 @@
1
+ """
2
+ Parser for 'oneOf' keyword in OpenAPI schemas.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any, Callable, List, Mapping
8
+
9
+ from pyopenapi_gen import IRSchema
10
+
11
+ from ..context import ParsingContext
12
+
13
+ if TYPE_CHECKING:
14
+ # from ..context import ParsingContext # No longer here
15
+ pass
16
+
17
+
18
+ def _parse_one_of_schemas(
19
+ one_of_nodes: List[Mapping[str, Any]],
20
+ context: ParsingContext,
21
+ max_depth: int,
22
+ parse_fn: Callable[[str | None, Mapping[str, Any] | None, ParsingContext, int], IRSchema],
23
+ ) -> tuple[List[IRSchema] | None, bool, str | None]:
24
+ """Parses 'oneOf' sub-schemas using a provided parsing function.
25
+
26
+ Contracts:
27
+ Pre-conditions:
28
+ - one_of_nodes is a list of schema node mappings.
29
+ - context is a valid ParsingContext instance.
30
+ - max_depth >= 0.
31
+ - parse_fn is a callable that can parse a schema node.
32
+ Post-conditions:
33
+ - Returns a tuple: (parsed_schemas, is_nullable, effective_schema_type)
34
+ - parsed_schemas: List of IRSchema for non-null sub-schemas, or None.
35
+ - is_nullable: True if a null type was present.
36
+ - effective_schema_type: Potential schema_type if list becomes empty/None (currently always None).
37
+ """
38
+ if not isinstance(one_of_nodes, list):
39
+ raise TypeError("one_of_nodes must be a list")
40
+ if not all(isinstance(n, Mapping) for n in one_of_nodes):
41
+ raise TypeError("all items in one_of_nodes must be Mappings")
42
+ if not isinstance(context, ParsingContext):
43
+ raise TypeError("context must be a ParsingContext instance")
44
+ if not max_depth >= 0:
45
+ raise ValueError("max_depth must be non-negative")
46
+ if not callable(parse_fn):
47
+ raise TypeError("parse_fn must be a callable")
48
+
49
+ parsed_schemas_list: List[IRSchema] = []
50
+ is_nullable_from_one_of = False
51
+ effective_schema_type: str | None = None
52
+
53
+ for sub_node in one_of_nodes:
54
+ if isinstance(sub_node, dict) and sub_node.get("type") == "null":
55
+ is_nullable_from_one_of = True
56
+ continue
57
+ parsed_schemas_list.append(parse_fn(None, sub_node, context, max_depth))
58
+
59
+ filtered_schemas = [
60
+ s
61
+ for s in parsed_schemas_list
62
+ if not (
63
+ s.type is None
64
+ and not s.properties
65
+ and not s.items
66
+ and not s.enum
67
+ and not s.any_of
68
+ and not s.one_of
69
+ and not s.all_of
70
+ )
71
+ ]
72
+
73
+ if not filtered_schemas:
74
+ effective_schema_type = None
75
+ return None, is_nullable_from_one_of, effective_schema_type
76
+
77
+ return filtered_schemas, is_nullable_from_one_of, effective_schema_type
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any, Callable, Mapping
5
+
6
+ # Import NameSanitizer for use in name generation
7
+ from pyopenapi_gen.core.utils import NameSanitizer
8
+
9
+ # REVERTED: TEMPORARY DEBUG LOGGER REMOVED
10
+ # properties_logger = logging.getLogger("pyopenapi_gen.core.parsing.keywords.properties_parser_DEBUG")
11
+ # properties_logger.setLevel(logging.DEBUG)
12
+ # # Ensure there's a handler if running in some environments
13
+ # if not properties_logger.handlers:
14
+ # properties_logger.addHandler(logging.StreamHandler())
15
+ from ..context import ParsingContext
16
+ from ..transformers.inline_object_promoter import _attempt_promote_inline_object
17
+
18
+ # Specific helpers needed by _parse_properties
19
+
20
+ if TYPE_CHECKING:
21
+ from pyopenapi_gen import IRSchema # Main IR model
22
+
23
+ # from ..context import ParsingContext # No longer here
24
+ # No direct import of _parse_schema from schema_parser to avoid circularity
25
+ pass
26
+
27
+
28
+ def _parse_properties(
29
+ properties_node: Mapping[str, Any],
30
+ parent_schema_name: str | None,
31
+ context: ParsingContext,
32
+ max_depth: int,
33
+ parse_fn: Callable[[str | None, Mapping[str, Any] | None, ParsingContext, int | None], IRSchema],
34
+ logger: logging.Logger,
35
+ ) -> dict[str, IRSchema]:
36
+ """Parse properties from a schema node.
37
+
38
+ Contracts:
39
+ Pre-conditions:
40
+ - properties_node is a valid Mapping
41
+ - context is a valid ParsingContext instance
42
+ - max_depth >= 0
43
+ - parse_fn is a callable for parsing schemas
44
+ Post-conditions:
45
+ - Returns a dictionary mapping property names to IRSchema instances
46
+ - Property references are properly maintained
47
+ """
48
+ properties_map: dict[str, IRSchema] = {}
49
+
50
+ for prop_key, prop_schema_node in properties_node.items():
51
+ # Skip invalid property names
52
+ if not prop_key or not isinstance(prop_key, str):
53
+ logger.warning(f"Skipping property with invalid name: {prop_key}")
54
+ continue
55
+
56
+ # Align with _parse_schema property naming logic:
57
+ # ParentNamePropName or just PropName if parent is anonymous.
58
+ sanitized_prop_key_for_name = NameSanitizer.sanitize_class_name(prop_key)
59
+ if parent_schema_name:
60
+ # Ensure parent_schema_name is also sanitized if it's part of the name
61
+ # This is implicitly handled if parent_schema_name comes from an IRSchema.name already.
62
+ # For direct use, ensure it's PascalCase for consistency,
63
+ # though _parse_schema currently doesn't re-sanitize it.
64
+ # Assuming parent_schema_name is already a valid schema name (e.g. "ParentSchema")
65
+ prop_schema_context_name = f"{parent_schema_name}{sanitized_prop_key_for_name}"
66
+ else:
67
+ prop_schema_context_name = sanitized_prop_key_for_name
68
+
69
+ # Parse the property schema
70
+ prop_schema_ir = parse_fn(prop_schema_context_name, prop_schema_node, context, max_depth)
71
+
72
+ # Handle $ref in property schema
73
+ if isinstance(prop_schema_node, Mapping) and "$ref" in prop_schema_node:
74
+ ref_value = prop_schema_node["$ref"]
75
+ ref_name = ref_value.split("/")[-1]
76
+ if ref_name in context.parsed_schemas:
77
+ prop_schema_ir._refers_to_schema = context.parsed_schemas[ref_name]
78
+
79
+ # Attempt to promote inline object
80
+ promoted_ir = _attempt_promote_inline_object(parent_schema_name, prop_key, prop_schema_ir, context, logger)
81
+ if promoted_ir is not None:
82
+ properties_map[prop_key] = promoted_ir
83
+ logger.debug(
84
+ f"Added promoted '{prop_key}' (name: {getattr(promoted_ir, 'name', 'N/A')}) "
85
+ f"to properties_map for '{parent_schema_name}'"
86
+ )
87
+ else:
88
+ properties_map[prop_key] = prop_schema_ir
89
+ logger.debug(
90
+ f"Added original '{prop_key}' (name: {getattr(prop_schema_ir, 'name', 'N/A')}, "
91
+ f"type: {getattr(prop_schema_ir, 'type', 'N/A')}, "
92
+ f"circular: {getattr(prop_schema_ir, '_is_circular_ref', 'N/A')}) "
93
+ f"to properties_map for '{parent_schema_name}'"
94
+ )
95
+
96
+ logger.debug(f"_parse_properties FINALLY returning for parent '{parent_schema_name}': {properties_map}")
97
+ # import pdb; pdb.set_trace() # For local debugging if needed
98
+ return properties_map
@@ -0,0 +1,169 @@
1
+ """
2
+ Finalization an IRSchema object during OpenAPI parsing.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any, Callable, List, Mapping, Set, Union
8
+
9
+ from pyopenapi_gen import IRSchema
10
+ from pyopenapi_gen.core.utils import NameSanitizer
11
+
12
+ from .context import ParsingContext
13
+
14
+ # Specific helpers needed
15
+ from .transformers.inline_enum_extractor import _process_standalone_inline_enum
16
+
17
+ # Note: _parse_schema will be passed as parse_fn, not imported directly
18
+
19
+ if TYPE_CHECKING:
20
+ pass
21
+
22
+
23
+ def _finalize_schema_object(
24
+ name: str | None,
25
+ node: Mapping[str, Any],
26
+ context: ParsingContext,
27
+ schema_type: str | None,
28
+ is_nullable: bool,
29
+ any_of_schemas: List[IRSchema] | None,
30
+ one_of_schemas: List[IRSchema] | None,
31
+ parsed_all_of_components: List[IRSchema] | None,
32
+ final_properties_map: dict[str, IRSchema],
33
+ merged_required_set: Set[str],
34
+ final_items_schema: IRSchema | None,
35
+ additional_properties_node: Union[bool, Mapping[str, Any]] | None,
36
+ enum_node: List[Any] | None,
37
+ format_node: str | None,
38
+ description_node: str | None,
39
+ from_unresolved_ref_node: bool,
40
+ max_depth: int,
41
+ parse_fn: Callable[[str | None, Mapping[str, Any] | None, ParsingContext, int], IRSchema],
42
+ logger: Any, # Changed to Any to support both real and mock loggers
43
+ ) -> IRSchema:
44
+ """Constructs the IRSchema object, performs final adjustments, and updates context.
45
+
46
+ Contracts:
47
+ Pre-conditions:
48
+ - All input arguments are appropriately populated.
49
+ - context is valid.
50
+ - parse_fn is a callable for parsing (e.g. additionalProperties).
51
+ - logger is a logging.Logger instance or a mock logger for testing.
52
+ Post-conditions:
53
+ - Returns a finalized IRSchema instance.
54
+ - If schema has a name and isn't a placeholder, it's in context.parsed_schemas.
55
+ """
56
+ if not isinstance(node, Mapping):
57
+ raise TypeError("node must be a Mapping")
58
+ if not isinstance(context, ParsingContext):
59
+ raise TypeError("context must be a ParsingContext instance")
60
+ if not callable(parse_fn):
61
+ raise TypeError("parse_fn must be callable")
62
+ # Remove logger type check to support mock loggers in tests
63
+
64
+ # If a placeholder for this schema name already exists due to a cycle detected deeper,
65
+ # return that placeholder immediately to preserve the cycle information.
66
+ if name and name in context.parsed_schemas:
67
+ existing_schema = context.parsed_schemas[name]
68
+ if getattr(existing_schema, "_is_circular_ref", False) or getattr(
69
+ existing_schema, "_from_unresolved_ref", False
70
+ ):
71
+ # Early return of cycle placeholder
72
+ return existing_schema
73
+
74
+ final_enum_values: List[Any] | None = enum_node if isinstance(enum_node, list) else None
75
+ final_required_fields_list: List[str] = sorted(list(merged_required_set))
76
+
77
+ final_additional_properties: Union[bool, IRSchema] | None = None
78
+ if isinstance(additional_properties_node, bool):
79
+ final_additional_properties = additional_properties_node
80
+ elif isinstance(additional_properties_node, dict):
81
+ final_additional_properties = parse_fn(None, additional_properties_node, context, max_depth)
82
+
83
+ final_schema_name_for_obj = NameSanitizer.sanitize_class_name(name) if name else None
84
+ current_schema_type = schema_type
85
+ if current_schema_type is None and final_properties_map:
86
+ current_schema_type = "object"
87
+
88
+ is_data_wrapper_flag = (
89
+ current_schema_type == "object"
90
+ and "data" in final_properties_map
91
+ and "data" in final_required_fields_list
92
+ and len(final_properties_map) == 1
93
+ )
94
+
95
+ schema_obj = IRSchema(
96
+ name=final_schema_name_for_obj,
97
+ type=current_schema_type,
98
+ format=format_node,
99
+ description=description_node,
100
+ required=final_required_fields_list,
101
+ properties=final_properties_map,
102
+ items=final_items_schema,
103
+ enum=final_enum_values,
104
+ additional_properties=final_additional_properties,
105
+ is_nullable=is_nullable,
106
+ any_of=any_of_schemas,
107
+ one_of=one_of_schemas,
108
+ all_of=parsed_all_of_components,
109
+ is_data_wrapper=is_data_wrapper_flag,
110
+ _from_unresolved_ref=from_unresolved_ref_node,
111
+ )
112
+
113
+ if (
114
+ schema_obj.name
115
+ and schema_obj.name in context.parsed_schemas
116
+ and context.parsed_schemas[schema_obj.name].type is None
117
+ and schema_obj.type is not None
118
+ ):
119
+ # Named schema in context has no type, adopting type
120
+ # schema_obj will be placed in context later, overwriting if necessary.
121
+ pass
122
+
123
+ if schema_obj.type is None and (
124
+ schema_obj.properties
125
+ or (
126
+ isinstance(node.get("_def"), dict)
127
+ and node["_def"].get("typeName") == "ZodObject"
128
+ and node["_def"].get("shape")
129
+ )
130
+ ):
131
+ # Schema has properties but no type; setting to 'object'
132
+ schema_obj.type = "object"
133
+
134
+ if name and "." in name:
135
+ is_explicitly_simple = node.get("type") in ["string", "integer", "number", "boolean", "array"]
136
+ is_explicitly_enum = "enum" in node
137
+ if schema_obj.type is None and not is_explicitly_simple and not is_explicitly_enum:
138
+ # Inline property lacks complex type, defaulting to 'object'
139
+ schema_obj.type = "object"
140
+
141
+ # Update the context with the finalized schema object
142
+ # This should only happen if we didn't return an existing placeholder above.
143
+ if name:
144
+ # Never overwrite a cycle placeholder!
145
+ if name in context.parsed_schemas and (
146
+ getattr(context.parsed_schemas[name], "_is_circular_ref", False)
147
+ or getattr(context.parsed_schemas[name], "_from_unresolved_ref", False)
148
+ ):
149
+ # Not overwriting cycle placeholder
150
+ pass # Do not overwrite
151
+ elif name not in context.parsed_schemas:
152
+ context.parsed_schemas[name] = schema_obj
153
+ # If name is in context and is already a full schema, and schema_obj is also full,
154
+ # current one is considered fresher.
155
+ elif not (
156
+ getattr(context.parsed_schemas[name], "_is_circular_ref", False)
157
+ or getattr(context.parsed_schemas[name], "_from_unresolved_ref", False)
158
+ ):
159
+ context.parsed_schemas[name] = schema_obj # Overwrite existing full with new full
160
+
161
+ if schema_obj:
162
+ schema_obj = _process_standalone_inline_enum(name, node, schema_obj, context, logger)
163
+
164
+ if schema_obj and schema_obj.name and context.parsed_schemas.get(schema_obj.name) is not schema_obj:
165
+ context.parsed_schemas[schema_obj.name] = schema_obj
166
+ # Ensured schema is in context post-standalone-enum processing
167
+
168
+ # Returning finalized schema object
169
+ return schema_obj