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,9 @@
1
+ """
2
+ Module for handling schema reference resolution.
3
+ """
4
+
5
+ from .resolve_schema_ref import resolve_schema_ref
6
+
7
+ __all__ = [
8
+ "resolve_schema_ref",
9
+ ]
@@ -0,0 +1,66 @@
1
+ """
2
+ Helper module for handling cyclic property references in schemas.
3
+ """
4
+
5
+ import logging
6
+ from typing import Set
7
+
8
+ from pyopenapi_gen.ir import IRSchema
9
+
10
+ from ....context import ParsingContext
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def mark_cyclic_property_references(schema_obj: IRSchema, ref_name: str, context: ParsingContext) -> None:
16
+ """
17
+ Marks properties in a schema that form cycles as unresolved.
18
+
19
+ Args:
20
+ schema_obj: The schema object to check for cycles
21
+ ref_name: The name of the schema being referenced
22
+ context: The parsing context
23
+
24
+ Pre-conditions:
25
+ - schema_obj is a valid IRSchema instance
26
+ - ref_name is a non-empty string
27
+ - context is a valid ParsingContext instance
28
+
29
+ Post-conditions:
30
+ - Properties that form cycles are marked as unresolved
31
+ - Non-cyclic properties remain unchanged
32
+ """
33
+ if not schema_obj.properties:
34
+ return
35
+
36
+ visited: Set[str] = set()
37
+
38
+ def _check_cycle(prop_name: str) -> bool:
39
+ if prop_name in visited:
40
+ return True
41
+ visited.add(prop_name)
42
+
43
+ prop_schema = schema_obj.properties.get(prop_name)
44
+ if not prop_schema or not prop_schema._refers_to_schema:
45
+ return False
46
+
47
+ if prop_schema._refers_to_schema.name == ref_name:
48
+ return True
49
+
50
+ if prop_schema._refers_to_schema.properties:
51
+ for nested_prop_name in prop_schema._refers_to_schema.properties:
52
+ nested_prop = prop_schema._refers_to_schema.properties[nested_prop_name]
53
+ if nested_prop._refers_to_schema and nested_prop._refers_to_schema.name == ref_name:
54
+ return True
55
+ if _check_cycle(nested_prop_name):
56
+ return True
57
+
58
+ return False
59
+
60
+ # Check each property for cycles
61
+ for prop_name, prop_schema in schema_obj.properties.items():
62
+ if _check_cycle(prop_name):
63
+ prop_schema._from_unresolved_ref = True
64
+ prop_schema._is_circular_ref = True
65
+ context.cycle_detected = True
66
+ logger.debug(f"Cyclic property reference detected: {ref_name}.{prop_name}")
@@ -0,0 +1,33 @@
1
+ """
2
+ Module for handling direct cycle detection.
3
+ """
4
+
5
+ import logging
6
+
7
+ from pyopenapi_gen.ir import IRSchema
8
+
9
+ from ....context import ParsingContext
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def handle_direct_cycle(ref_name: str, context: ParsingContext) -> IRSchema:
15
+ """
16
+ Handles a direct cycle in schema references.
17
+
18
+ Contracts:
19
+ Pre-conditions:
20
+ - ref_name must be a valid schema name
21
+ - context must be a valid ParsingContext instance
22
+ - ref_name must exist in context.parsed_schemas
23
+ Post-conditions:
24
+ - Returns the existing schema from context.parsed_schemas
25
+ - The schema's _from_unresolved_ref flag is set to True
26
+ - The schema's _is_circular_ref flag is set to True (for harmonized cycle detection)
27
+ """
28
+ existing_schema = context.parsed_schemas[ref_name]
29
+ existing_schema._from_unresolved_ref = True
30
+ existing_schema._is_circular_ref = True # Harmonize with cycle detection contract
31
+ context.cycle_detected = True # Mark cycle in context
32
+ logger.debug(f"Direct cycle detected for schema '{ref_name}'")
33
+ return existing_schema
@@ -0,0 +1,22 @@
1
+ """
2
+ Module for handling existing schema references.
3
+ """
4
+
5
+ from pyopenapi_gen.ir import IRSchema
6
+
7
+ from ....context import ParsingContext
8
+
9
+
10
+ def handle_existing_schema(ref_name: str, context: ParsingContext) -> IRSchema:
11
+ """
12
+ Handles an existing schema reference.
13
+
14
+ Contracts:
15
+ Pre-conditions:
16
+ - ref_name must be a valid schema name
17
+ - context must be a valid ParsingContext instance
18
+ - ref_name must exist in context.parsed_schemas
19
+ Post-conditions:
20
+ - Returns the existing schema from context.parsed_schemas
21
+ """
22
+ return context.parsed_schemas[ref_name]
@@ -0,0 +1,54 @@
1
+ """
2
+ Module for handling ListResponse fallback strategy.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Callable, Mapping, Optional
7
+
8
+ from pyopenapi_gen.ir import IRSchema
9
+
10
+ from ....context import ParsingContext
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def try_list_response_fallback(
16
+ ref_name: str,
17
+ ref_value: str,
18
+ context: ParsingContext,
19
+ max_depth: int,
20
+ parse_fn: Callable[[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema],
21
+ ) -> Optional[IRSchema]:
22
+ """
23
+ Attempts to resolve a reference by treating it as a list of a base type.
24
+
25
+ Contracts:
26
+ Pre-conditions:
27
+ - ref_name must end with "ListResponse"
28
+ - parse_fn must be a callable that parses schemas
29
+ - context must be a valid ParsingContext instance
30
+ Post-conditions:
31
+ - If successful, returns an array IRSchema with items of the base type
32
+ - If unsuccessful, returns None
33
+ - Successful resolutions are added to context.parsed_schemas
34
+ """
35
+ list_response_suffix = "ListResponse"
36
+ if not ref_name.endswith(list_response_suffix):
37
+ return None
38
+
39
+ base_name = ref_name[: -len(list_response_suffix)]
40
+ referenced_node_data_fallback = context.raw_spec_schemas.get(base_name)
41
+
42
+ if not referenced_node_data_fallback:
43
+ return None
44
+
45
+ item_schema = parse_fn(base_name, referenced_node_data_fallback, context, max_depth)
46
+ if item_schema._from_unresolved_ref:
47
+ return None
48
+
49
+ warning_msg = f"Resolved $ref: {ref_value} by falling back to LIST of base name '{base_name}'."
50
+ context.collected_warnings.append(warning_msg)
51
+
52
+ resolved_schema = IRSchema(name=ref_name, type="array", items=item_schema)
53
+ context.parsed_schemas[ref_name] = resolved_schema
54
+ return resolved_schema
@@ -0,0 +1,52 @@
1
+ """
2
+ Module for handling missing schema references.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Callable, Mapping, Optional
7
+
8
+ from pyopenapi_gen.ir import IRSchema
9
+
10
+ from ....context import ParsingContext
11
+ from .list_response import try_list_response_fallback
12
+ from .stripped_suffix import try_stripped_suffix_fallback
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def handle_missing_ref(
18
+ ref_value: str,
19
+ ref_name: str,
20
+ context: ParsingContext,
21
+ max_depth: int,
22
+ parse_fn: Callable[[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema],
23
+ ) -> IRSchema:
24
+ """
25
+ Handles a missing schema reference by attempting fallback strategies.
26
+
27
+ Contracts:
28
+ Pre-conditions:
29
+ - ref_value must be a valid reference string
30
+ - ref_name must be a valid schema name
31
+ - context must be a valid ParsingContext instance
32
+ - max_depth must be a non-negative integer
33
+ - parse_fn must be a callable that parses schemas
34
+ Post-conditions:
35
+ - Returns a valid IRSchema instance
36
+ - The schema is registered in context.parsed_schemas
37
+ - If no fallback succeeds, returns an unresolved schema
38
+ """
39
+ # Try ListResponse fallback
40
+ list_response_schema = try_list_response_fallback(ref_name, ref_value, context, max_depth, parse_fn)
41
+ if list_response_schema is not None:
42
+ return list_response_schema
43
+
44
+ # Try stripped suffix fallback
45
+ stripped_schema = try_stripped_suffix_fallback(ref_name, ref_value, context, max_depth, parse_fn)
46
+ if stripped_schema is not None:
47
+ return stripped_schema
48
+
49
+ # If all fallbacks fail, create an unresolved schema
50
+ unresolved_schema = IRSchema(name=ref_name, _from_unresolved_ref=True)
51
+ context.parsed_schemas[ref_name] = unresolved_schema
52
+ return unresolved_schema
@@ -0,0 +1,50 @@
1
+ """
2
+ Module for handling new schema references.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Callable, Dict, Mapping, Optional
7
+
8
+ from pyopenapi_gen.ir import IRSchema
9
+
10
+ from ....context import ParsingContext
11
+ from .cyclic_properties import mark_cyclic_property_references
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_new_schema(
17
+ ref_name: str,
18
+ node_data: Dict[str, Any],
19
+ context: ParsingContext,
20
+ max_depth: int,
21
+ parse_fn: Callable[[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema],
22
+ ) -> IRSchema:
23
+ """
24
+ Parses a new schema from raw data.
25
+
26
+ Contracts:
27
+ Pre-conditions:
28
+ - ref_name must be a valid schema name not already fully parsed
29
+ - node_data must contain raw schema definition
30
+ - parse_fn must be a callable that parses schemas
31
+ - context must be a valid ParsingContext instance
32
+ Post-conditions:
33
+ - Returns a valid parsed IRSchema instance
34
+ - The schema is registered in context.parsed_schemas
35
+ - Cyclic property references are marked correctly
36
+ """
37
+ # Create stub to prevent infinite recursion during parsing
38
+ stub_schema = IRSchema(name=ref_name)
39
+ context.parsed_schemas[ref_name] = stub_schema
40
+
41
+ # Parse the actual schema
42
+ schema_obj = parse_fn(ref_name, node_data, context, max_depth)
43
+
44
+ # Update the entry in parsed_schemas with the fully parsed schema
45
+ context.parsed_schemas[ref_name] = schema_obj
46
+
47
+ # Mark any property references involved in cycles
48
+ mark_cyclic_property_references(schema_obj, ref_name, context)
49
+
50
+ return schema_obj
@@ -0,0 +1,51 @@
1
+ """
2
+ Module for handling stripped suffix fallback strategy.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Callable, Mapping, Optional
7
+
8
+ from pyopenapi_gen.ir import IRSchema
9
+
10
+ from ....context import ParsingContext
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def try_stripped_suffix_fallback(
16
+ ref_name: str,
17
+ ref_value: str,
18
+ context: ParsingContext,
19
+ max_depth: int,
20
+ parse_fn: Callable[[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema],
21
+ ) -> Optional[IRSchema]:
22
+ """
23
+ Attempts to resolve a reference by stripping common suffixes.
24
+
25
+ Contracts:
26
+ Pre-conditions:
27
+ - ref_name must be a valid schema name
28
+ - parse_fn must be a callable that parses schemas
29
+ - context must be a valid ParsingContext instance
30
+ Post-conditions:
31
+ - If successful, returns the resolved IRSchema
32
+ - If unsuccessful, returns None
33
+ - Successful resolutions are added to context.parsed_schemas
34
+ """
35
+ suffixes = ["Response", "Request", "Input", "Output"]
36
+
37
+ for suffix in suffixes:
38
+ if ref_name.endswith(suffix):
39
+ base_name = ref_name[: -len(suffix)]
40
+ referenced_node_data_fallback = context.raw_spec_schemas.get(base_name)
41
+
42
+ if referenced_node_data_fallback:
43
+ resolved_schema = parse_fn(base_name, referenced_node_data_fallback, context, max_depth)
44
+ if not resolved_schema._from_unresolved_ref:
45
+ warning_msg = f"Resolved $ref: {ref_value} by falling back to base name '{base_name}'."
46
+ context.collected_warnings.append(warning_msg)
47
+
48
+ context.parsed_schemas[ref_name] = resolved_schema
49
+ return resolved_schema
50
+
51
+ return None
@@ -0,0 +1,86 @@
1
+ """
2
+ Main module for schema reference resolution.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Callable, Mapping, Optional
7
+
8
+ from pyopenapi_gen.ir import IRSchema
9
+
10
+ from ...context import ParsingContext
11
+ from .helpers.direct_cycle import handle_direct_cycle
12
+ from .helpers.existing_schema import handle_existing_schema
13
+ from .helpers.missing_ref import handle_missing_ref
14
+ from .helpers.new_schema import parse_new_schema
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def resolve_schema_ref(
20
+ ref_value: str,
21
+ ref_name: str,
22
+ context: ParsingContext,
23
+ max_depth: int,
24
+ _parse_schema: Callable[[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema],
25
+ ) -> IRSchema:
26
+ """
27
+ Resolves a schema reference in an OpenAPI specification.
28
+
29
+ Contracts:
30
+ Pre-conditions:
31
+ - ref_value must be a valid reference string (e.g., "#/components/schemas/MySchema")
32
+ - ref_name must be a valid schema name
33
+ - context must be a valid ParsingContext instance
34
+ - max_depth must be a non-negative integer
35
+ - _parse_schema must be a callable that parses schemas
36
+ Post-conditions:
37
+ - Returns a valid IRSchema instance
38
+ - The schema is registered in context.parsed_schemas
39
+ - Cyclic references are handled appropriately
40
+ """
41
+ # Extract the actual schema name from the reference
42
+ actual_schema_name = ref_value.split("/")[-1]
43
+
44
+ # Check for direct cycles or circular placeholders
45
+ if actual_schema_name in context.parsed_schemas:
46
+ existing_schema = context.parsed_schemas[actual_schema_name]
47
+ if getattr(existing_schema, "_is_circular_ref", False):
48
+ # logger.debug(f"Returning existing circular reference for '{actual_schema_name}'")
49
+ return existing_schema
50
+ # logger.debug(f"Direct cycle detected for '{actual_schema_name}', handling...")
51
+ return handle_direct_cycle(actual_schema_name, context)
52
+
53
+ # Check for existing fully parsed schema
54
+ if actual_schema_name in context.parsed_schemas:
55
+ # logger.debug(f"Returning existing fully parsed schema for '{actual_schema_name}'")
56
+ return handle_existing_schema(actual_schema_name, context)
57
+
58
+ # Get referenced node data
59
+ referenced_node_data = context.raw_spec_schemas.get(actual_schema_name)
60
+
61
+ # Handle missing references with stripped suffix fallback
62
+ if referenced_node_data is None:
63
+ # Try stripping common suffixes
64
+ base_name = actual_schema_name
65
+ for suffix in ["Response", "Request", "Input", "Output"]:
66
+ if base_name.endswith(suffix):
67
+ base_name = base_name[: -len(suffix)]
68
+ if base_name in context.raw_spec_schemas:
69
+ # logger.debug(f"Found schema '{base_name}' after stripping suffix '{suffix}'")
70
+ referenced_node_data = context.raw_spec_schemas[base_name]
71
+ break
72
+
73
+ if referenced_node_data is None:
74
+ logger.warning(f"Missing reference '{ref_value}' for schema '{ref_name}'")
75
+ return handle_missing_ref(ref_value, ref_name, context, max_depth, _parse_schema)
76
+
77
+ # Standard parsing path for a new schema
78
+ # logger.debug(f"Parsing new schema '{actual_schema_name}'")
79
+ schema = parse_new_schema(actual_schema_name, dict(referenced_node_data), context, max_depth, _parse_schema)
80
+
81
+ # Store the schema under the requested reference name if different
82
+ # Don't mutate the original schema name to avoid affecting other references
83
+ if schema.name != ref_name:
84
+ context.parsed_schemas[ref_name] = schema
85
+
86
+ return schema
@@ -0,0 +1,74 @@
1
+ """
2
+ Dedicated parser for determining primary type and nullability from a schema's 'type' field.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import (
8
+ Any,
9
+ List,
10
+ Optional,
11
+ Tuple,
12
+ )
13
+
14
+ # Note: IRSchema is not needed here as this function doesn't construct it.
15
+
16
+
17
+ def extract_primary_type_and_nullability(
18
+ type_node: Any, schema_name: Optional[str] = None
19
+ ) -> Tuple[Optional[str], bool, List[str]]:
20
+ """Extract the primary type and nullability from a schema's 'type' field.
21
+
22
+ Contracts:
23
+ Pre-conditions:
24
+ - type_node is the value of the 'type' field from a schema
25
+ Post-conditions:
26
+ - Returns a tuple of (primary_type, is_nullable, warnings)
27
+ - primary_type is None if type_node is None or invalid
28
+ - is_nullable is True if type_node is 'null' or contains 'null'
29
+ - warnings contains any warnings about type handling
30
+ """
31
+ warnings: List[str] = []
32
+ is_nullable = False
33
+
34
+ if type_node is None:
35
+ return None, False, warnings
36
+
37
+ # Handle array of types
38
+ if isinstance(type_node, list):
39
+ if not type_node:
40
+ warnings.append(f"Empty type array in schema '{schema_name}'")
41
+ return None, False, warnings
42
+
43
+ # Check for nullability
44
+ if "null" in type_node:
45
+ is_nullable = True
46
+ type_node = [t for t in type_node if t != "null"]
47
+
48
+ if not type_node:
49
+ warnings.append(f"Only 'null' type in array for schema '{schema_name}'")
50
+ return None, True, warnings
51
+
52
+ # Use the first non-null type
53
+ primary_type = type_node[0]
54
+ if len(type_node) > 1:
55
+ warnings.append(f"Multiple types in array for schema '{schema_name}'. Using first type: {primary_type}")
56
+ else:
57
+ primary_type = type_node
58
+
59
+ # Validate the type
60
+ if not isinstance(primary_type, str):
61
+ warnings.append(f"Invalid type value '{primary_type}' in schema '{schema_name}'")
62
+ return None, is_nullable, warnings
63
+
64
+ # Normalize the type
65
+ primary_type = primary_type.lower()
66
+ if primary_type not in {"string", "number", "integer", "boolean", "object", "array", "null"}:
67
+ warnings.append(f"Unknown type '{primary_type}' in schema '{schema_name}'")
68
+ return None, is_nullable, warnings
69
+
70
+ # If the determined primary_type is "null", it means the actual type is None, but it IS nullable.
71
+ if primary_type == "null":
72
+ return None, True, warnings # Ensure is_nullable is True
73
+
74
+ return primary_type, is_nullable, warnings