otterapi 0.0.5__py3-none-any.whl → 0.0.6__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 (52) hide show
  1. README.md +581 -8
  2. otterapi/__init__.py +73 -0
  3. otterapi/cli.py +327 -29
  4. otterapi/codegen/__init__.py +115 -0
  5. otterapi/codegen/ast_utils.py +134 -5
  6. otterapi/codegen/client.py +1271 -0
  7. otterapi/codegen/codegen.py +1736 -0
  8. otterapi/codegen/dataframes.py +392 -0
  9. otterapi/codegen/emitter.py +473 -0
  10. otterapi/codegen/endpoints.py +2597 -343
  11. otterapi/codegen/pagination.py +1026 -0
  12. otterapi/codegen/schema.py +593 -0
  13. otterapi/codegen/splitting.py +1397 -0
  14. otterapi/codegen/types.py +1345 -0
  15. otterapi/codegen/utils.py +180 -1
  16. otterapi/config.py +1017 -24
  17. otterapi/exceptions.py +231 -0
  18. otterapi/openapi/__init__.py +46 -0
  19. otterapi/openapi/v2/__init__.py +86 -0
  20. otterapi/openapi/v2/spec.json +1607 -0
  21. otterapi/openapi/v2/v2.py +1776 -0
  22. otterapi/openapi/v3/__init__.py +131 -0
  23. otterapi/openapi/v3/spec.json +1651 -0
  24. otterapi/openapi/v3/v3.py +1557 -0
  25. otterapi/openapi/v3_1/__init__.py +133 -0
  26. otterapi/openapi/v3_1/spec.json +1411 -0
  27. otterapi/openapi/v3_1/v3_1.py +798 -0
  28. otterapi/openapi/v3_2/__init__.py +133 -0
  29. otterapi/openapi/v3_2/spec.json +1666 -0
  30. otterapi/openapi/v3_2/v3_2.py +777 -0
  31. otterapi/tests/__init__.py +3 -0
  32. otterapi/tests/fixtures/__init__.py +455 -0
  33. otterapi/tests/test_ast_utils.py +680 -0
  34. otterapi/tests/test_codegen.py +610 -0
  35. otterapi/tests/test_dataframe.py +1038 -0
  36. otterapi/tests/test_exceptions.py +493 -0
  37. otterapi/tests/test_openapi_support.py +616 -0
  38. otterapi/tests/test_openapi_upgrade.py +215 -0
  39. otterapi/tests/test_pagination.py +1101 -0
  40. otterapi/tests/test_splitting_config.py +319 -0
  41. otterapi/tests/test_splitting_integration.py +427 -0
  42. otterapi/tests/test_splitting_resolver.py +512 -0
  43. otterapi/tests/test_splitting_tree.py +525 -0
  44. otterapi-0.0.6.dist-info/METADATA +627 -0
  45. otterapi-0.0.6.dist-info/RECORD +48 -0
  46. {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
  47. otterapi/codegen/generator.py +0 -358
  48. otterapi/codegen/openapi_processor.py +0 -27
  49. otterapi/codegen/type_generator.py +0 -559
  50. otterapi-0.0.5.dist-info/METADATA +0 -54
  51. otterapi-0.0.5.dist-info/RECORD +0 -16
  52. {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/entry_points.txt +0 -0
otterapi/exceptions.py ADDED
@@ -0,0 +1,231 @@
1
+ """Custom exceptions for OtterAPI.
2
+
3
+ This module defines a hierarchy of exceptions used throughout the OtterAPI library
4
+ to provide clear, actionable error messages for different failure scenarios.
5
+ """
6
+
7
+
8
+ class OtterAPIError(Exception):
9
+ """Base exception for all OtterAPI errors.
10
+
11
+ All exceptions raised by OtterAPI inherit from this class, making it easy
12
+ to catch all OtterAPI-related errors with a single except clause.
13
+
14
+ Example:
15
+ try:
16
+ codegen.generate()
17
+ except OtterAPIError as e:
18
+ print(f"OtterAPI error: {e}")
19
+ """
20
+
21
+ def __init__(self, message: str, *args, **kwargs):
22
+ self.message = message
23
+ super().__init__(message, *args, **kwargs)
24
+
25
+
26
+ class SchemaError(OtterAPIError):
27
+ """Base exception for schema-related errors."""
28
+
29
+ pass
30
+
31
+
32
+ class SchemaLoadError(SchemaError):
33
+ """Failed to load an OpenAPI schema from a source.
34
+
35
+ This exception is raised when the schema cannot be loaded from
36
+ the specified URL or file path.
37
+
38
+ Attributes:
39
+ source: The source path or URL that failed to load.
40
+ cause: The underlying exception that caused the failure.
41
+ """
42
+
43
+ def __init__(self, source: str, cause: Exception | None = None):
44
+ self.source = source
45
+ self.cause = cause
46
+ message = f"Failed to load schema from '{source}'"
47
+ if cause:
48
+ message += f': {cause}'
49
+ super().__init__(message)
50
+
51
+
52
+ class SchemaValidationError(SchemaError):
53
+ """Schema failed OpenAPI specification validation.
54
+
55
+ This exception is raised when the loaded schema does not conform
56
+ to the OpenAPI specification.
57
+
58
+ Attributes:
59
+ source: The source path or URL of the invalid schema.
60
+ errors: List of validation error messages.
61
+ """
62
+
63
+ def __init__(self, source: str, errors: list[str] | None = None):
64
+ self.source = source
65
+ self.errors = errors or []
66
+ message = f"Schema validation failed for '{source}'"
67
+ if errors:
68
+ message += f': {"; ".join(errors)}'
69
+ super().__init__(message)
70
+
71
+
72
+ class SchemaReferenceError(SchemaError):
73
+ """Failed to resolve a $ref reference in the schema.
74
+
75
+ This exception is raised when a JSON Reference ($ref) cannot be resolved,
76
+ either because the referenced component doesn't exist or the reference
77
+ format is not supported.
78
+
79
+ Attributes:
80
+ reference: The $ref string that could not be resolved.
81
+ reason: Explanation of why the reference couldn't be resolved.
82
+ """
83
+
84
+ def __init__(self, reference: str, reason: str | None = None):
85
+ self.reference = reference
86
+ self.reason = reason
87
+ message = f"Failed to resolve reference '{reference}'"
88
+ if reason:
89
+ message += f': {reason}'
90
+ super().__init__(message)
91
+
92
+
93
+ class CodeGenerationError(OtterAPIError):
94
+ """Error during code generation.
95
+
96
+ This exception is raised when the code generation process fails
97
+ after successfully loading and validating the schema.
98
+
99
+ Attributes:
100
+ context: Additional context about what was being generated.
101
+ cause: The underlying exception that caused the failure.
102
+ """
103
+
104
+ def __init__(
105
+ self, message: str, context: str | None = None, cause: Exception | None = None
106
+ ):
107
+ self.context = context
108
+ self.cause = cause
109
+ full_message = message
110
+ if context:
111
+ full_message = f'{message} (while generating {context})'
112
+ if cause:
113
+ full_message += f': {cause}'
114
+ super().__init__(full_message)
115
+
116
+
117
+ class TypeGenerationError(CodeGenerationError):
118
+ """Error generating a type from a schema.
119
+
120
+ This exception is raised when a specific type cannot be generated
121
+ from an OpenAPI schema definition.
122
+
123
+ Attributes:
124
+ type_name: The name of the type being generated.
125
+ schema_path: The path to the schema in the OpenAPI document.
126
+ """
127
+
128
+ def __init__(
129
+ self,
130
+ type_name: str,
131
+ schema_path: str | None = None,
132
+ cause: Exception | None = None,
133
+ ):
134
+ self.type_name = type_name
135
+ self.schema_path = schema_path
136
+ message = f"Failed to generate type '{type_name}'"
137
+ if schema_path:
138
+ message += f" at '{schema_path}'"
139
+ super().__init__(message, context=type_name, cause=cause)
140
+
141
+
142
+ class EndpointGenerationError(CodeGenerationError):
143
+ """Error generating an endpoint function.
144
+
145
+ This exception is raised when an endpoint function cannot be generated
146
+ from an OpenAPI operation definition.
147
+
148
+ Attributes:
149
+ operation_id: The operationId of the endpoint.
150
+ method: The HTTP method of the endpoint.
151
+ path: The URL path of the endpoint.
152
+ """
153
+
154
+ def __init__(
155
+ self,
156
+ operation_id: str,
157
+ method: str | None = None,
158
+ path: str | None = None,
159
+ cause: Exception | None = None,
160
+ ):
161
+ self.operation_id = operation_id
162
+ self.method = method
163
+ self.path = path
164
+ message = f"Failed to generate endpoint '{operation_id}'"
165
+ if method and path:
166
+ message += f' ({method.upper()} {path})'
167
+ super().__init__(message, context=operation_id, cause=cause)
168
+
169
+
170
+ class ConfigurationError(OtterAPIError):
171
+ """Error in configuration.
172
+
173
+ This exception is raised when the configuration is invalid or
174
+ cannot be loaded.
175
+
176
+ Attributes:
177
+ config_path: The path to the configuration file, if applicable.
178
+ field: The specific configuration field that is invalid.
179
+ """
180
+
181
+ def __init__(
182
+ self, message: str, config_path: str | None = None, field: str | None = None
183
+ ):
184
+ self.config_path = config_path
185
+ self.field = field
186
+ full_message = message
187
+ if config_path:
188
+ full_message = f"{message} in '{config_path}'"
189
+ if field:
190
+ full_message += f' (field: {field})'
191
+ super().__init__(full_message)
192
+
193
+
194
+ class OutputError(OtterAPIError):
195
+ """Error writing generated output.
196
+
197
+ This exception is raised when the generated code cannot be written
198
+ to the output location.
199
+
200
+ Attributes:
201
+ output_path: The path where output was being written.
202
+ cause: The underlying exception that caused the failure.
203
+ """
204
+
205
+ def __init__(self, output_path: str, cause: Exception | None = None):
206
+ self.output_path = output_path
207
+ self.cause = cause
208
+ message = f"Failed to write output to '{output_path}'"
209
+ if cause:
210
+ message += f': {cause}'
211
+ super().__init__(message)
212
+
213
+
214
+ class UnsupportedFeatureError(OtterAPIError):
215
+ """Attempted to use an unsupported feature.
216
+
217
+ This exception is raised when the schema uses a feature that
218
+ is not yet supported by OtterAPI.
219
+
220
+ Attributes:
221
+ feature: Description of the unsupported feature.
222
+ suggestion: Optional suggestion for a workaround.
223
+ """
224
+
225
+ def __init__(self, feature: str, suggestion: str | None = None):
226
+ self.feature = feature
227
+ self.suggestion = suggestion
228
+ message = f'Unsupported feature: {feature}'
229
+ if suggestion:
230
+ message += f'. {suggestion}'
231
+ super().__init__(message)
@@ -0,0 +1,46 @@
1
+ from typing import Annotated, Any, Literal
2
+
3
+ from pydantic import Discriminator, RootModel, Tag
4
+
5
+ from otterapi.openapi.v2 import Swagger
6
+ from otterapi.openapi.v3 import OpenAPI as OpenAPIv3_0
7
+ from otterapi.openapi.v3_1 import OpenAPI as OpenAPIv3_1
8
+ from otterapi.openapi.v3_2 import OpenAPI as OpenAPIv3_2
9
+
10
+ __all__ = [
11
+ 'UniversalOpenAPI',
12
+ ]
13
+
14
+
15
+ def _get_openapi_version(data: Any) -> Literal['2.0', '3.0', '3.1', '3.2']:
16
+ """Discriminator function to determine the OpenAPI version from raw data."""
17
+ if isinstance(data, dict):
18
+ # Check for Swagger 2.0 (uses 'swagger' field)
19
+ if 'swagger' in data:
20
+ return '2.0'
21
+ # Check for OpenAPI 3.x (uses 'openapi' field)
22
+ openapi_version = data.get('openapi', '')
23
+ if openapi_version.startswith('3.0'):
24
+ return '3.0'
25
+ elif openapi_version.startswith('3.1'):
26
+ return '3.1'
27
+ elif openapi_version.startswith('3.2'):
28
+ return '3.2'
29
+ # Default fallback - will let Pydantic try to match
30
+ return '3.0'
31
+
32
+
33
+ class UniversalOpenAPI(
34
+ RootModel[
35
+ Annotated[
36
+ Annotated[Swagger, Tag('2.0')]
37
+ | Annotated[OpenAPIv3_0, Tag('3.0')]
38
+ | Annotated[OpenAPIv3_1, Tag('3.1')]
39
+ | Annotated[OpenAPIv3_2, Tag('3.2')],
40
+ Discriminator(_get_openapi_version),
41
+ ]
42
+ ]
43
+ ):
44
+ """Universal OpenAPI model that can parse Swagger 2.0, OpenAPI 3.0, 3.1, or 3.2 documents."""
45
+
46
+ pass
@@ -0,0 +1,86 @@
1
+ """OpenAPI/Swagger 2.0 specification models."""
2
+
3
+ from otterapi.openapi.v2.v2 import (
4
+ XML,
5
+ ApiKeySecurity,
6
+ BasicAuthenticationSecurity,
7
+ BodyParameter,
8
+ CollectionFormat,
9
+ CollectionFormatWithMulti,
10
+ Contact,
11
+ ExternalDocs,
12
+ FileSchema,
13
+ Header,
14
+ Info,
15
+ JsonReference,
16
+ License,
17
+ NonBodyParameter,
18
+ OAuth2AccessCodeSecurity,
19
+ OAuth2ApplicationSecurity,
20
+ OAuth2Flow,
21
+ OAuth2ImplicitSecurity,
22
+ OAuth2PasswordSecurity,
23
+ Operation,
24
+ Parameter,
25
+ ParameterLocation,
26
+ PathItem,
27
+ Paths,
28
+ PrimitivesItems,
29
+ PrimitiveType,
30
+ Response,
31
+ Responses,
32
+ ResponseValue,
33
+ Schema,
34
+ SchemeType,
35
+ SecurityScheme,
36
+ SecuritySchemeType,
37
+ Swagger,
38
+ Tag,
39
+ )
40
+
41
+ __all__ = [
42
+ # Main model
43
+ 'Swagger',
44
+ # Info models
45
+ 'Info',
46
+ 'Contact',
47
+ 'License',
48
+ 'Tag',
49
+ 'ExternalDocs',
50
+ # Schema models
51
+ 'Schema',
52
+ 'FileSchema',
53
+ 'XML',
54
+ # Parameter models
55
+ 'Parameter',
56
+ 'BodyParameter',
57
+ 'NonBodyParameter',
58
+ 'PrimitivesItems',
59
+ # Response models
60
+ 'Response',
61
+ 'Responses',
62
+ 'ResponseValue',
63
+ 'Header',
64
+ # Operation models
65
+ 'Operation',
66
+ 'PathItem',
67
+ 'Paths',
68
+ # Security models
69
+ 'SecurityScheme',
70
+ 'BasicAuthenticationSecurity',
71
+ 'ApiKeySecurity',
72
+ 'OAuth2ImplicitSecurity',
73
+ 'OAuth2PasswordSecurity',
74
+ 'OAuth2ApplicationSecurity',
75
+ 'OAuth2AccessCodeSecurity',
76
+ # Enums
77
+ 'SchemeType',
78
+ 'ParameterLocation',
79
+ 'PrimitiveType',
80
+ 'CollectionFormat',
81
+ 'CollectionFormatWithMulti',
82
+ 'SecuritySchemeType',
83
+ 'OAuth2Flow',
84
+ # Utilities
85
+ 'JsonReference',
86
+ ]