jentic-openapi-datamodels 1.0.0a18__py3-none-any.whl → 1.0.0a19__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 (73) hide show
  1. jentic/apitools/openapi/datamodels/low/extractors.py +3 -3
  2. jentic/apitools/openapi/datamodels/low/v30/__init__.py +76 -0
  3. jentic/apitools/openapi/datamodels/low/v30/builders/__init__.py +312 -0
  4. jentic/apitools/openapi/datamodels/low/v30/callback.py +131 -0
  5. jentic/apitools/openapi/datamodels/low/v30/components.py +236 -0
  6. jentic/apitools/openapi/datamodels/low/v30/contact.py +4 -10
  7. jentic/apitools/openapi/datamodels/low/v30/discriminator.py +4 -9
  8. jentic/apitools/openapi/datamodels/low/v30/encoding.py +81 -0
  9. jentic/apitools/openapi/datamodels/low/v30/example.py +91 -0
  10. jentic/apitools/openapi/datamodels/low/v30/external_documentation.py +4 -10
  11. jentic/apitools/openapi/datamodels/low/v30/header.py +120 -0
  12. jentic/apitools/openapi/datamodels/low/v30/info.py +14 -23
  13. jentic/apitools/openapi/datamodels/low/v30/license.py +4 -10
  14. jentic/apitools/openapi/datamodels/low/v30/link.py +141 -0
  15. jentic/apitools/openapi/datamodels/low/v30/media_type.py +110 -0
  16. jentic/apitools/openapi/datamodels/low/v30/oauth_flow.py +4 -10
  17. jentic/apitools/openapi/datamodels/low/v30/oauth_flows.py +7 -15
  18. jentic/apitools/openapi/datamodels/low/v30/openapi.py +149 -0
  19. jentic/apitools/openapi/datamodels/low/v30/operation.py +134 -0
  20. jentic/apitools/openapi/datamodels/low/v30/parameter.py +123 -0
  21. jentic/apitools/openapi/datamodels/low/v30/path_item.py +125 -0
  22. jentic/apitools/openapi/datamodels/low/v30/paths.py +108 -0
  23. jentic/apitools/openapi/datamodels/low/v30/reference.py +5 -9
  24. jentic/apitools/openapi/datamodels/low/v30/request_body.py +108 -0
  25. jentic/apitools/openapi/datamodels/low/v30/response.py +104 -0
  26. jentic/apitools/openapi/datamodels/low/v30/responses.py +109 -0
  27. jentic/apitools/openapi/datamodels/low/v30/schema.py +81 -97
  28. jentic/apitools/openapi/datamodels/low/v30/security_requirement.py +14 -9
  29. jentic/apitools/openapi/datamodels/low/v30/security_scheme.py +42 -22
  30. jentic/apitools/openapi/datamodels/low/v30/server.py +111 -0
  31. jentic/apitools/openapi/datamodels/low/v30/server_variable.py +4 -10
  32. jentic/apitools/openapi/datamodels/low/v30/tag.py +8 -46
  33. jentic/apitools/openapi/datamodels/low/v30/xml.py +4 -10
  34. jentic/apitools/openapi/datamodels/low/v31/__init__.py +77 -0
  35. jentic/apitools/openapi/datamodels/low/v31/builders/__init__.py +347 -0
  36. jentic/apitools/openapi/datamodels/low/v31/callback.py +131 -0
  37. jentic/apitools/openapi/datamodels/low/v31/components.py +240 -0
  38. jentic/apitools/openapi/datamodels/low/v31/contact.py +61 -0
  39. jentic/apitools/openapi/datamodels/low/v31/discriminator.py +62 -0
  40. jentic/apitools/openapi/datamodels/low/v31/encoding.py +81 -0
  41. jentic/apitools/openapi/datamodels/low/v31/example.py +91 -0
  42. jentic/apitools/openapi/datamodels/low/v31/external_documentation.py +59 -0
  43. jentic/apitools/openapi/datamodels/low/v31/header.py +120 -0
  44. jentic/apitools/openapi/datamodels/low/v31/info.py +116 -0
  45. jentic/apitools/openapi/datamodels/low/v31/license.py +61 -0
  46. jentic/apitools/openapi/datamodels/low/v31/link.py +141 -0
  47. jentic/apitools/openapi/datamodels/low/v31/media_type.py +110 -0
  48. jentic/apitools/openapi/datamodels/low/v31/oauth_flow.py +65 -0
  49. jentic/apitools/openapi/datamodels/low/v31/oauth_flows.py +108 -0
  50. jentic/apitools/openapi/datamodels/low/v31/openapi.py +168 -0
  51. jentic/apitools/openapi/datamodels/low/v31/operation.py +133 -0
  52. jentic/apitools/openapi/datamodels/low/v31/parameter.py +123 -0
  53. jentic/apitools/openapi/datamodels/low/v31/path_item.py +125 -0
  54. jentic/apitools/openapi/datamodels/low/v31/paths.py +108 -0
  55. jentic/apitools/openapi/datamodels/low/v31/reference.py +65 -0
  56. jentic/apitools/openapi/datamodels/low/v31/request_body.py +108 -0
  57. jentic/apitools/openapi/datamodels/low/v31/response.py +104 -0
  58. jentic/apitools/openapi/datamodels/low/v31/responses.py +109 -0
  59. jentic/apitools/openapi/datamodels/low/v31/schema.py +498 -0
  60. jentic/apitools/openapi/datamodels/low/v31/security_requirement.py +106 -0
  61. jentic/apitools/openapi/datamodels/low/v31/security_scheme.py +129 -0
  62. jentic/apitools/openapi/datamodels/low/v31/server.py +111 -0
  63. jentic/apitools/openapi/datamodels/low/v31/server_variable.py +70 -0
  64. jentic/apitools/openapi/datamodels/low/v31/tag.py +63 -0
  65. jentic/apitools/openapi/datamodels/low/v31/xml.py +54 -0
  66. jentic_openapi_datamodels-1.0.0a19.dist-info/METADATA +372 -0
  67. jentic_openapi_datamodels-1.0.0a19.dist-info/RECORD +75 -0
  68. jentic/apitools/openapi/datamodels/low/model_builder.py +0 -129
  69. jentic_openapi_datamodels-1.0.0a18.dist-info/METADATA +0 -211
  70. jentic_openapi_datamodels-1.0.0a18.dist-info/RECORD +0 -27
  71. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a19.dist-info}/WHEEL +0 -0
  72. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a19.dist-info}/licenses/LICENSE +0 -0
  73. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a19.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,141 @@
1
+ from dataclasses import dataclass, field, replace
2
+
3
+ from ruamel import yaml
4
+
5
+ from ..context import Context
6
+ from ..fields import fixed_field
7
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
8
+ from .builders import build_field_source, build_model
9
+ from .reference import Reference
10
+ from .reference import build as build_reference
11
+ from .server import Server
12
+ from .server import build as build_server
13
+
14
+
15
+ __all__ = ["Link", "build", "build_link_or_reference"]
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class Link:
20
+ """
21
+ Link Object representation for OpenAPI 3.0.
22
+
23
+ The Link Object represents a possible design-time link for a response.
24
+
25
+ Attributes:
26
+ root_node: The top-level node representing the entire Link object in the original source file
27
+ operation_ref: A relative or absolute URI reference to an OAS operation.
28
+ operation_id: The name of an existing, resolvable OAS operation.
29
+ parameters: A map representing parameters to pass to an operation as specified with operationId
30
+ or identified via operationRef.
31
+ request_body: A literal value or {expression} to use as a request body when calling the target operation.
32
+ description: A description of the link. CommonMark syntax MAY be used for rich text representation.
33
+ server: A server object to be used by the target operation.
34
+ extensions: Specification extensions (x-* fields)
35
+ """
36
+
37
+ root_node: yaml.Node
38
+ operation_ref: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "operationRef"})
39
+ operation_id: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "operationId"})
40
+ parameters: FieldSource[dict[KeySource[str], ValueSource[YAMLValue]]] | None = fixed_field()
41
+ request_body: FieldSource[YAMLValue] | None = fixed_field(metadata={"yaml_name": "requestBody"})
42
+ description: FieldSource[str] | None = fixed_field()
43
+ server: FieldSource[Server] | None = fixed_field()
44
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
45
+
46
+
47
+ def build(root: yaml.Node, context: Context | None = None) -> Link | ValueSource[YAMLInvalidValue]:
48
+ """
49
+ Build a Link object from a YAML node.
50
+
51
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
52
+ model that provides complete source fidelity for inspection and validation.
53
+
54
+ Args:
55
+ root: The YAML node to parse (should be a MappingNode)
56
+ context: Optional parsing context. If None, a default context will be created.
57
+
58
+ Returns:
59
+ A Link object if the node is valid, or a ValueSource containing
60
+ the invalid data if the root is not a MappingNode (preserving the invalid data
61
+ and its source location for validation).
62
+
63
+ Example:
64
+ from ruamel.yaml import YAML
65
+ yaml = YAML()
66
+ root = yaml.compose("operationId: getUserById\\nparameters:\\n userId: $response.body#/id")
67
+ link = build(root)
68
+ assert link.operation_id.value == 'getUserById'
69
+ """
70
+ context = context or Context()
71
+
72
+ # Use build_model for initial construction
73
+ link = build_model(root, Link, context=context)
74
+
75
+ # If build_model returned ValueSource (invalid node), return it immediately
76
+ if not isinstance(link, Link):
77
+ return link
78
+
79
+ # Manually handle nested server object and parameters dict
80
+ replacements = {}
81
+ for key_node, value_node in root.value:
82
+ key = context.yaml_constructor.construct_yaml_str(key_node)
83
+
84
+ if key == "server":
85
+ # Handle nested Server object - child builder handles invalid nodes
86
+ replacements["server"] = FieldSource(
87
+ value=build_server(value_node, context=context),
88
+ key_node=key_node,
89
+ value_node=value_node,
90
+ )
91
+ elif key == "parameters":
92
+ # Handle parameters map with KeySource/ValueSource wrapping
93
+ if isinstance(value_node, yaml.MappingNode):
94
+ params_dict: dict[KeySource[str], ValueSource[YAMLValue]] = {}
95
+ for param_key_node, param_value_node in value_node.value:
96
+ param_key = context.yaml_constructor.construct_yaml_str(param_key_node)
97
+ param_value = context.yaml_constructor.construct_object(
98
+ param_value_node, deep=True
99
+ )
100
+ params_dict[KeySource(value=param_key, key_node=param_key_node)] = ValueSource(
101
+ value=param_value, value_node=param_value_node
102
+ )
103
+ replacements["parameters"] = FieldSource(
104
+ value=params_dict, key_node=key_node, value_node=value_node
105
+ )
106
+ else:
107
+ # Not a mapping - preserve as-is for validation
108
+ replacements["parameters"] = build_field_source(key_node, value_node, context)
109
+
110
+ # Apply all replacements at once
111
+ if replacements:
112
+ link = replace(link, **replacements)
113
+
114
+ return link
115
+
116
+
117
+ def build_link_or_reference(
118
+ node: yaml.Node, context: Context
119
+ ) -> Link | Reference | ValueSource[YAMLInvalidValue]:
120
+ """
121
+ Build either a Link or Reference from a YAML node.
122
+
123
+ This helper handles the polymorphic nature of OpenAPI where many fields
124
+ can contain either a Link object or a Reference object ($ref).
125
+
126
+ Args:
127
+ node: The YAML node to parse
128
+ context: Parsing context
129
+
130
+ Returns:
131
+ A Link, Reference, or ValueSource if the node is invalid
132
+ """
133
+ # Check if it's a reference (has $ref key)
134
+ if isinstance(node, yaml.MappingNode):
135
+ for key_node, _ in node.value:
136
+ key = context.yaml_constructor.construct_yaml_str(key_node)
137
+ if key == "$ref":
138
+ return build_reference(node, context)
139
+
140
+ # Otherwise, try to build as Link
141
+ return build(node, context)
@@ -0,0 +1,110 @@
1
+ from dataclasses import dataclass, field, replace
2
+
3
+ from ruamel import yaml
4
+
5
+ from ..context import Context
6
+ from ..fields import fixed_field
7
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
8
+ from .builders import build_field_source, build_model
9
+ from .encoding import Encoding
10
+ from .encoding import build as build_encoding
11
+ from .example import Example
12
+ from .reference import Reference
13
+ from .schema import Schema
14
+
15
+
16
+ __all__ = ["MediaType", "build"]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class MediaType:
21
+ """
22
+ Media Type Object representation for OpenAPI 3.0.
23
+
24
+ Each Media Type Object provides schema and examples for the media type identified by its key.
25
+
26
+ Attributes:
27
+ root_node: The top-level node representing the entire Media Type object in the original source file
28
+ schema: The schema defining the content of the request, response, or parameter.
29
+ example: Example of the media type. The example SHOULD match the specified schema and encoding properties if present.
30
+ examples: Examples of the media type. Each example SHOULD contain a value in the correct format as specified in the parameter encoding.
31
+ encoding: A map between a property name and its encoding information. The key, being the property name,
32
+ MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects
33
+ when the media type is multipart or application/x-www-form-urlencoded.
34
+ extensions: Specification extensions (x-* fields)
35
+ """
36
+
37
+ root_node: yaml.Node
38
+ schema: FieldSource["Schema | Reference"] | None = fixed_field()
39
+ example: FieldSource[YAMLValue] | None = fixed_field()
40
+ examples: FieldSource[dict[KeySource[str], "Example | Reference"]] | None = fixed_field()
41
+ encoding: FieldSource[dict[KeySource[str], Encoding]] | None = fixed_field()
42
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
43
+
44
+
45
+ def build(
46
+ root: yaml.Node, context: Context | None = None
47
+ ) -> MediaType | ValueSource[YAMLInvalidValue]:
48
+ """
49
+ Build a MediaType object from a YAML node.
50
+
51
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
52
+ model that provides complete source fidelity for inspection and validation.
53
+
54
+ Args:
55
+ root: The YAML node to parse (should be a MappingNode)
56
+ context: Optional parsing context. If None, a default context will be created.
57
+
58
+ Returns:
59
+ A MediaType object if the node is valid, or a ValueSource containing
60
+ the invalid data if the root is not a MappingNode (preserving the invalid data
61
+ and its source location for validation).
62
+
63
+ Example:
64
+ from ruamel.yaml import YAML
65
+ yaml = YAML()
66
+ root = yaml.compose('''
67
+ schema:
68
+ type: string
69
+ examples:
70
+ user:
71
+ value: John Doe
72
+ ''')
73
+ media_type = build(root)
74
+ assert media_type.schema.value.type.value == 'string'
75
+ """
76
+ context = context or Context()
77
+
78
+ # Use build_model for initial construction
79
+ media_type = build_model(root, MediaType, context=context)
80
+
81
+ # If build_model returned ValueSource (invalid node), return it immediately
82
+ if not isinstance(media_type, MediaType):
83
+ return media_type
84
+
85
+ # Manually handle nested complex fields
86
+ for key_node, value_node in root.value:
87
+ key = context.yaml_constructor.construct_yaml_str(key_node)
88
+
89
+ if key == "encoding":
90
+ # Handle encoding field - map of Encoding objects
91
+ if isinstance(value_node, yaml.MappingNode):
92
+ encoding_dict: dict[KeySource[str], Encoding | ValueSource[YAMLInvalidValue]] = {}
93
+ for encoding_key_node, encoding_value_node in value_node.value:
94
+ encoding_key = context.yaml_constructor.construct_yaml_str(encoding_key_node)
95
+ # Build Encoding - child builder handles invalid nodes
96
+ encoding_obj = build_encoding(encoding_value_node, context)
97
+ encoding_dict[KeySource(value=encoding_key, key_node=encoding_key_node)] = (
98
+ encoding_obj
99
+ )
100
+ encoding = FieldSource(
101
+ value=encoding_dict, key_node=key_node, value_node=value_node
102
+ )
103
+ media_type = replace(media_type, encoding=encoding)
104
+ else:
105
+ # Not a mapping - preserve as-is for validation
106
+ encoding = build_field_source(key_node, value_node, context)
107
+ media_type = replace(media_type, encoding=encoding)
108
+ break
109
+
110
+ return media_type
@@ -2,16 +2,10 @@ from dataclasses import dataclass, field
2
2
 
3
3
  from ruamel import yaml
4
4
 
5
- from jentic.apitools.openapi.datamodels.low.context import Context
6
- from jentic.apitools.openapi.datamodels.low.fields import fixed_field
7
- from jentic.apitools.openapi.datamodels.low.model_builder import build_model
8
- from jentic.apitools.openapi.datamodels.low.sources import (
9
- FieldSource,
10
- KeySource,
11
- ValueSource,
12
- YAMLInvalidValue,
13
- YAMLValue,
14
- )
5
+ from ..context import Context
6
+ from ..fields import fixed_field
7
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
8
+ from .builders import build_model
15
9
 
16
10
 
17
11
  __all__ = ["OAuthFlow", "build"]
@@ -3,18 +3,12 @@ from typing import Any
3
3
 
4
4
  from ruamel import yaml
5
5
 
6
- from jentic.apitools.openapi.datamodels.low.context import Context
7
- from jentic.apitools.openapi.datamodels.low.extractors import extract_extension_fields
8
- from jentic.apitools.openapi.datamodels.low.fields import fixed_field, fixed_fields
9
- from jentic.apitools.openapi.datamodels.low.sources import (
10
- FieldSource,
11
- KeySource,
12
- ValueSource,
13
- YAMLInvalidValue,
14
- YAMLValue,
15
- )
16
- from jentic.apitools.openapi.datamodels.low.v30.oauth_flow import OAuthFlow
17
- from jentic.apitools.openapi.datamodels.low.v30.oauth_flow import build as build_oauth_flow
6
+ from ..context import Context
7
+ from ..extractors import extract_extension_fields
8
+ from ..fields import fixed_field, fixed_fields
9
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
10
+ from .oauth_flow import OAuthFlow
11
+ from .oauth_flow import build as build_oauth_flow
18
12
 
19
13
 
20
14
  __all__ = ["OAuthFlows", "build"]
@@ -74,9 +68,7 @@ def build(
74
68
  flows = build(root)
75
69
  assert flows.implicit.value.authorization_url.value == 'https://example.com/auth'
76
70
  """
77
- # Initialize context once at the beginning
78
- if context is None:
79
- context = Context()
71
+ context = context or Context()
80
72
 
81
73
  if not isinstance(root, yaml.MappingNode):
82
74
  # Preserve invalid root data instead of returning None
@@ -0,0 +1,149 @@
1
+ from dataclasses import dataclass, field, replace
2
+
3
+ from ruamel import yaml
4
+
5
+ from ..context import Context
6
+ from ..fields import fixed_field
7
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
8
+ from .builders import build_field_source, build_model
9
+ from .components import Components
10
+ from .components import build as build_components
11
+ from .external_documentation import ExternalDocumentation
12
+ from .info import Info
13
+ from .info import build as build_info
14
+ from .paths import Paths
15
+ from .paths import build as build_paths
16
+ from .security_requirement import SecurityRequirement
17
+ from .server import Server
18
+ from .tag import Tag
19
+ from .tag import build as build_tag
20
+
21
+
22
+ __all__ = ["OpenAPI30", "build"]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class OpenAPI30:
27
+ """
28
+ OpenAPI Object representation for OpenAPI 3.0.
29
+
30
+ This is the root document object of the OpenAPI document.
31
+
32
+ Attributes:
33
+ root_node: The top-level node representing the entire OpenAPI object in the original source file
34
+ openapi: REQUIRED. The version number of the OpenAPI Specification that the document uses.
35
+ Must match the specification version (e.g., "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4").
36
+ info: REQUIRED. Provides metadata about the API (title, version, description, etc.)
37
+ paths: REQUIRED. The available paths and operations for the API
38
+ servers: An array of Server Objects providing connectivity information to target servers
39
+ components: An element to hold reusable objects for different aspects of the OAS
40
+ security: A declaration of which security mechanisms can be used across the API
41
+ tags: A list of tags used by the specification with additional metadata
42
+ external_docs: Additional external documentation
43
+ extensions: Specification extensions (x-* fields)
44
+ """
45
+
46
+ root_node: yaml.Node
47
+ openapi: FieldSource[str] | None = fixed_field()
48
+ info: FieldSource[Info] | None = fixed_field()
49
+ servers: FieldSource[list["Server"]] | None = fixed_field()
50
+ paths: FieldSource[Paths] | None = fixed_field()
51
+ components: FieldSource[Components] | None = fixed_field()
52
+ security: FieldSource[list["SecurityRequirement"]] | None = fixed_field()
53
+ tags: FieldSource[list[Tag]] | None = fixed_field()
54
+ external_docs: FieldSource["ExternalDocumentation"] | None = fixed_field(
55
+ metadata={"yaml_name": "externalDocs"}
56
+ )
57
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
58
+
59
+
60
+ def build(
61
+ root: yaml.Node, context: Context | None = None
62
+ ) -> OpenAPI30 | ValueSource[YAMLInvalidValue]:
63
+ """
64
+ Build an OpenAPI object from a YAML node.
65
+
66
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
67
+ model that provides complete source fidelity for inspection and validation.
68
+
69
+ Args:
70
+ root: The YAML node to parse (should be a MappingNode)
71
+ context: Optional parsing context. If None, a default context will be created.
72
+
73
+ Returns:
74
+ An OpenAPI object if the node is valid, or a ValueSource containing
75
+ the invalid data if the root is not a MappingNode (preserving the invalid data
76
+ and its source location for validation).
77
+
78
+ Example:
79
+ from ruamel.yaml import YAML
80
+ yaml = YAML()
81
+ root = yaml.compose('''
82
+ openapi: 3.0.4
83
+ info:
84
+ title: Sample API
85
+ version: 1.0.0
86
+ paths:
87
+ /users:
88
+ get:
89
+ responses:
90
+ '200':
91
+ description: Success
92
+ ''')
93
+ openapi_doc = build(root)
94
+ assert openapi_doc.openapi.value == '3.0.4'
95
+ assert openapi_doc.info.value.title.value == 'Sample API'
96
+ """
97
+ context = context or Context()
98
+
99
+ # Use build_model for initial construction
100
+ openapi_obj = build_model(root, OpenAPI30, context=context)
101
+
102
+ # If build_model returned ValueSource (invalid node), return it immediately
103
+ if not isinstance(openapi_obj, OpenAPI30):
104
+ return openapi_obj
105
+
106
+ # Manually handle nested complex fields and list fields with custom builders
107
+ replacements = {}
108
+ for key_node, value_node in root.value:
109
+ key = context.yaml_constructor.construct_yaml_str(key_node)
110
+
111
+ if key == "info":
112
+ # Handle info field - Info object
113
+ replacements["info"] = FieldSource(
114
+ value=build_info(value_node, context), key_node=key_node, value_node=value_node
115
+ )
116
+
117
+ elif key == "paths":
118
+ # Handle paths field - Paths object
119
+ replacements["paths"] = FieldSource(
120
+ value=build_paths(value_node, context), key_node=key_node, value_node=value_node
121
+ )
122
+
123
+ elif key == "components":
124
+ # Handle components field - Components object
125
+ replacements["components"] = FieldSource(
126
+ value=build_components(value_node, context),
127
+ key_node=key_node,
128
+ value_node=value_node,
129
+ )
130
+
131
+ elif key == "tags":
132
+ # Handle tags field - array of Tag objects
133
+ if isinstance(value_node, yaml.SequenceNode):
134
+ tags_list = []
135
+ for tag_node in value_node.value:
136
+ tag_obj = build_tag(tag_node, context)
137
+ tags_list.append(tag_obj)
138
+ replacements["tags"] = FieldSource(
139
+ value=tags_list, key_node=key_node, value_node=value_node
140
+ )
141
+ else:
142
+ # Not a sequence - preserve as-is for validation
143
+ replacements["tags"] = build_field_source(key_node, value_node, context)
144
+
145
+ # Apply all replacements at once
146
+ if replacements:
147
+ openapi_obj = replace(openapi_obj, **replacements)
148
+
149
+ return openapi_obj
@@ -0,0 +1,134 @@
1
+ from dataclasses import dataclass, field, replace
2
+ from typing import TYPE_CHECKING
3
+
4
+ from ruamel import yaml
5
+
6
+ from ..context import Context
7
+ from ..fields import fixed_field
8
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
9
+ from .builders import build_model
10
+ from .external_documentation import ExternalDocumentation
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from .callback import Callback
15
+
16
+ from .parameter import Parameter
17
+ from .reference import Reference
18
+ from .request_body import RequestBody, build_request_body_or_reference
19
+ from .responses import Responses
20
+ from .responses import build as build_responses
21
+ from .security_requirement import SecurityRequirement
22
+ from .server import Server
23
+
24
+
25
+ __all__ = ["Operation", "build"]
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class Operation:
30
+ """
31
+ Operation Object representation for OpenAPI 3.0.
32
+
33
+ Describes a single API operation on a path.
34
+
35
+ Attributes:
36
+ root_node: The top-level node representing the entire Operation object in the original source file
37
+ tags: List of tags for API documentation control
38
+ summary: Short summary of what the operation does
39
+ description: Verbose explanation of the operation behavior (may contain CommonMark syntax)
40
+ external_docs: Additional external documentation for this operation
41
+ operation_id: Unique string used to identify the operation
42
+ parameters: List of parameters that are applicable for this operation
43
+ request_body: Request body applicable for this operation
44
+ responses: REQUIRED. The list of possible responses as they are returned from executing this operation
45
+ callbacks: Map of possible out-of-band callbacks related to the parent operation
46
+ deprecated: Declares this operation to be deprecated
47
+ security: Declaration of which security mechanisms can be used for this operation
48
+ servers: Alternative server array to service this operation
49
+ extensions: Specification extensions (x-* fields)
50
+ """
51
+
52
+ root_node: yaml.Node
53
+ tags: FieldSource[list[ValueSource[str]]] | None = fixed_field()
54
+ summary: FieldSource[str] | None = fixed_field()
55
+ description: FieldSource[str] | None = fixed_field()
56
+ external_docs: FieldSource["ExternalDocumentation"] | None = fixed_field(
57
+ metadata={"yaml_name": "externalDocs"}
58
+ )
59
+ operation_id: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "operationId"})
60
+ parameters: FieldSource[list["Parameter | Reference"]] | None = fixed_field()
61
+ request_body: FieldSource[RequestBody | Reference] | None = fixed_field(
62
+ metadata={"yaml_name": "requestBody"}
63
+ )
64
+ responses: FieldSource[Responses] | None = fixed_field()
65
+ callbacks: FieldSource[dict[KeySource[str], "Callback | Reference"]] | None = fixed_field()
66
+ deprecated: FieldSource[bool] | None = fixed_field()
67
+ security: FieldSource[list["SecurityRequirement"]] | None = fixed_field()
68
+ servers: FieldSource[list["Server"]] | None = fixed_field()
69
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
70
+
71
+
72
+ def build(
73
+ root: yaml.Node, context: Context | None = None
74
+ ) -> Operation | ValueSource[YAMLInvalidValue]:
75
+ """
76
+ Build an Operation object from a YAML node.
77
+
78
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
79
+ model that provides complete source fidelity for inspection and validation.
80
+
81
+ Args:
82
+ root: The YAML node to parse (should be a MappingNode)
83
+ context: Optional parsing context. If None, a default context will be created.
84
+
85
+ Returns:
86
+ An Operation object if the node is valid, or a ValueSource containing
87
+ the invalid data if the root is not a MappingNode (preserving the invalid data
88
+ and its source location for validation).
89
+
90
+ Example:
91
+ from ruamel.yaml import YAML
92
+ yaml = YAML()
93
+ root = yaml.compose('''
94
+ summary: List users
95
+ operationId: listUsers
96
+ responses:
97
+ '200':
98
+ description: successful operation
99
+ ''')
100
+ operation = build(root)
101
+ assert operation.summary.value == 'List users'
102
+ """
103
+ context = context or Context()
104
+
105
+ # Use build_model for initial construction
106
+ operation = build_model(root, Operation, context=context)
107
+
108
+ # If build_model returned ValueSource (invalid node), return it immediately
109
+ if not isinstance(operation, Operation):
110
+ return operation
111
+
112
+ # Manually handle nested complex fields
113
+ replacements = {}
114
+ for key_node, value_node in root.value:
115
+ key = context.yaml_constructor.construct_yaml_str(key_node)
116
+
117
+ if key == "requestBody":
118
+ # Handle requestBody field - RequestBody or Reference
119
+ request_body_or_reference = build_request_body_or_reference(value_node, context)
120
+ replacements["request_body"] = FieldSource(
121
+ value=request_body_or_reference, key_node=key_node, value_node=value_node
122
+ )
123
+ elif key == "responses":
124
+ # Handle responses field - Responses object
125
+ responses_obj = build_responses(value_node, context)
126
+ replacements["responses"] = FieldSource(
127
+ value=responses_obj, key_node=key_node, value_node=value_node
128
+ )
129
+
130
+ # Apply all replacements at once
131
+ if replacements:
132
+ operation = replace(operation, **replacements)
133
+
134
+ return operation