reflectapi-runtime 0.1.0__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.
- reflectapi_runtime/__init__.py +109 -0
- reflectapi_runtime/auth.py +507 -0
- reflectapi_runtime/batch.py +165 -0
- reflectapi_runtime/client.py +924 -0
- reflectapi_runtime/exceptions.py +120 -0
- reflectapi_runtime/hypothesis_strategies.py +275 -0
- reflectapi_runtime/middleware.py +254 -0
- reflectapi_runtime/option.py +295 -0
- reflectapi_runtime/response.py +126 -0
- reflectapi_runtime/streaming.py +435 -0
- reflectapi_runtime/testing.py +380 -0
- reflectapi_runtime/types.py +32 -0
- reflectapi_runtime-0.1.0.dist-info/METADATA +36 -0
- reflectapi_runtime-0.1.0.dist-info/RECORD +15 -0
- reflectapi_runtime-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Option type handling for reflectapi code generation.
|
|
2
|
+
|
|
3
|
+
This module provides proper handling of Rust's Option<T> types in Python,
|
|
4
|
+
distinguishing between undefined, null, and actual values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pydantic import GetCoreSchemaHandler
|
|
13
|
+
|
|
14
|
+
from pydantic_core import core_schema
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _UndefinedType:
|
|
20
|
+
"""Sentinel type for undefined values in reflectapi Option types.
|
|
21
|
+
|
|
22
|
+
This represents the state where a field was not provided at all,
|
|
23
|
+
as opposed to being explicitly set to None/null.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return "Undefined"
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return "Undefined"
|
|
31
|
+
|
|
32
|
+
def __bool__(self) -> bool:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
def __eq__(self, other: Any) -> bool:
|
|
36
|
+
return isinstance(other, _UndefinedType)
|
|
37
|
+
|
|
38
|
+
def __hash__(self) -> int:
|
|
39
|
+
return hash("_UndefinedType")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Global singleton instance
|
|
43
|
+
Undefined = _UndefinedType()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ReflectapiOption(Generic[T]):
|
|
47
|
+
"""Proper representation of Rust's Option<T> type in Python.
|
|
48
|
+
|
|
49
|
+
This class encapsulates the three possible states:
|
|
50
|
+
- Undefined: Field was not provided
|
|
51
|
+
- None: Field was explicitly set to null
|
|
52
|
+
- Some(value): Field has an actual value
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
```python
|
|
56
|
+
# Field not provided
|
|
57
|
+
option = ReflectapiOption() # or ReflectapiOption(Undefined)
|
|
58
|
+
|
|
59
|
+
# Field explicitly set to null
|
|
60
|
+
option = ReflectapiOption(None)
|
|
61
|
+
|
|
62
|
+
# Field has a value
|
|
63
|
+
option = ReflectapiOption(42)
|
|
64
|
+
```
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, value: T | None | _UndefinedType = Undefined):
|
|
68
|
+
self._value = value
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def __get_pydantic_core_schema__(
|
|
72
|
+
cls, source_type: Any, handler: GetCoreSchemaHandler
|
|
73
|
+
) -> core_schema.CoreSchema:
|
|
74
|
+
"""Generate Pydantic V2 core schema for ReflectapiOption."""
|
|
75
|
+
from typing import get_args, get_origin
|
|
76
|
+
|
|
77
|
+
# Extract the generic type argument if available
|
|
78
|
+
origin = get_origin(source_type)
|
|
79
|
+
args = get_args(source_type)
|
|
80
|
+
|
|
81
|
+
def validate_option(value: Any) -> ReflectapiOption[Any]:
|
|
82
|
+
if isinstance(value, cls):
|
|
83
|
+
return value
|
|
84
|
+
return cls(value)
|
|
85
|
+
|
|
86
|
+
def serialize_option(option_value: ReflectapiOption[Any]) -> Any:
|
|
87
|
+
"""Serialize ReflectapiOption handling all three states correctly."""
|
|
88
|
+
if isinstance(option_value, cls):
|
|
89
|
+
if option_value.is_undefined:
|
|
90
|
+
# Return None for undefined to avoid pydantic undefined serialization issues
|
|
91
|
+
return None
|
|
92
|
+
# Return the actual value (including None for explicit null)
|
|
93
|
+
return option_value._value
|
|
94
|
+
# Fallback for non-ReflectapiOption values
|
|
95
|
+
return option_value
|
|
96
|
+
|
|
97
|
+
if origin is cls and args:
|
|
98
|
+
# We have ReflectapiOption[SomeType]
|
|
99
|
+
inner_type = args[0]
|
|
100
|
+
inner_schema = handler(inner_type)
|
|
101
|
+
|
|
102
|
+
return core_schema.no_info_plain_validator_function(
|
|
103
|
+
validate_option,
|
|
104
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
105
|
+
serialize_option,
|
|
106
|
+
return_schema=core_schema.union_schema([
|
|
107
|
+
inner_schema,
|
|
108
|
+
core_schema.none_schema(),
|
|
109
|
+
]),
|
|
110
|
+
when_used='json',
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
# Fallback for untyped ReflectapiOption
|
|
115
|
+
return core_schema.no_info_plain_validator_function(validate_option)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def is_undefined(self) -> bool:
|
|
120
|
+
"""Check if the option is undefined (field not provided)."""
|
|
121
|
+
return self._value is Undefined
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def is_none(self) -> bool:
|
|
125
|
+
"""Check if the option is explicitly None/null."""
|
|
126
|
+
return self._value is None
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def is_some(self) -> bool:
|
|
130
|
+
"""Check if the option has a value (not undefined and not None)."""
|
|
131
|
+
return self._value is not Undefined and self._value is not None
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def value(self) -> T | None:
|
|
135
|
+
"""Get the wrapped value.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The wrapped value, or None if undefined or null.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If trying to access value when undefined.
|
|
142
|
+
"""
|
|
143
|
+
if self.is_undefined:
|
|
144
|
+
raise ValueError("Cannot access value of undefined option")
|
|
145
|
+
return self._value
|
|
146
|
+
|
|
147
|
+
def unwrap(self) -> T:
|
|
148
|
+
"""Unwrap the option, returning the value or raising an error.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The wrapped value.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ValueError: If the option is undefined or None.
|
|
155
|
+
"""
|
|
156
|
+
if self.is_undefined:
|
|
157
|
+
raise ValueError("Cannot unwrap undefined option")
|
|
158
|
+
if self.is_none:
|
|
159
|
+
raise ValueError("Cannot unwrap None option")
|
|
160
|
+
return self._value
|
|
161
|
+
|
|
162
|
+
def unwrap_or(self, default: T) -> T:
|
|
163
|
+
"""Unwrap the option or return a default value.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
default: Default value to return if undefined or None.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The wrapped value or the default.
|
|
170
|
+
"""
|
|
171
|
+
if self.is_some:
|
|
172
|
+
return self._value
|
|
173
|
+
return default
|
|
174
|
+
|
|
175
|
+
def map(self, func: callable) -> ReflectapiOption:
|
|
176
|
+
"""Apply a function to the wrapped value if it exists.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
func: Function to apply to the value.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
New ReflectapiOption with the result, or unchanged if undefined/None.
|
|
183
|
+
"""
|
|
184
|
+
if self.is_some:
|
|
185
|
+
return ReflectapiOption(func(self._value))
|
|
186
|
+
return ReflectapiOption(self._value)
|
|
187
|
+
|
|
188
|
+
def filter(self, predicate: callable) -> ReflectapiOption:
|
|
189
|
+
"""Filter the option based on a predicate.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
predicate: Function that returns True to keep the value.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The option if predicate returns True, otherwise undefined.
|
|
196
|
+
"""
|
|
197
|
+
if self.is_some and predicate(self._value):
|
|
198
|
+
return self
|
|
199
|
+
return ReflectapiOption(Undefined)
|
|
200
|
+
|
|
201
|
+
def __eq__(self, other: Any) -> bool:
|
|
202
|
+
if isinstance(other, ReflectapiOption):
|
|
203
|
+
return self._value == other._value
|
|
204
|
+
return self._value == other
|
|
205
|
+
|
|
206
|
+
def __hash__(self) -> int:
|
|
207
|
+
return hash(self._value)
|
|
208
|
+
|
|
209
|
+
def __repr__(self) -> str:
|
|
210
|
+
if self.is_undefined:
|
|
211
|
+
return "ReflectapiOption(Undefined)"
|
|
212
|
+
elif self.is_none:
|
|
213
|
+
return "ReflectapiOption(None)"
|
|
214
|
+
else:
|
|
215
|
+
return f"ReflectapiOption({self._value!r})"
|
|
216
|
+
|
|
217
|
+
def __str__(self) -> str:
|
|
218
|
+
if self.is_undefined:
|
|
219
|
+
return "Undefined"
|
|
220
|
+
elif self.is_none:
|
|
221
|
+
return "None"
|
|
222
|
+
else:
|
|
223
|
+
return str(self._value)
|
|
224
|
+
|
|
225
|
+
def __bool__(self) -> bool:
|
|
226
|
+
"""Return True if the option has a truthy value."""
|
|
227
|
+
return self.is_some and bool(self._value)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Type alias for more concise usage
|
|
231
|
+
Option = ReflectapiOption
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def some(value: T) -> ReflectapiOption[T]:
|
|
235
|
+
"""Create an Option with a value."""
|
|
236
|
+
return ReflectapiOption(value)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def none() -> ReflectapiOption[None]:
|
|
240
|
+
"""Create an Option with None."""
|
|
241
|
+
return ReflectapiOption(None)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def undefined() -> ReflectapiOption:
|
|
245
|
+
"""Create an undefined Option."""
|
|
246
|
+
return ReflectapiOption(Undefined)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Utility functions for serialization
|
|
250
|
+
def serialize_option_dict(data: dict) -> dict:
|
|
251
|
+
"""Serialize a dictionary, excluding undefined option fields.
|
|
252
|
+
|
|
253
|
+
This is used in client serialization to properly handle Option types
|
|
254
|
+
by excluding undefined fields from the JSON payload.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
data: Dictionary that may contain ReflectapiOption values.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Dictionary with undefined options excluded and others unwrapped.
|
|
261
|
+
"""
|
|
262
|
+
result = {}
|
|
263
|
+
|
|
264
|
+
for key, value in data.items():
|
|
265
|
+
if isinstance(value, ReflectapiOption):
|
|
266
|
+
if not value.is_undefined:
|
|
267
|
+
# Include None values but not undefined ones
|
|
268
|
+
result[key] = value._value
|
|
269
|
+
elif isinstance(value, dict):
|
|
270
|
+
# Recursively handle nested dictionaries
|
|
271
|
+
result[key] = serialize_option_dict(value)
|
|
272
|
+
elif isinstance(value, list):
|
|
273
|
+
# Handle lists that might contain options or nested structures
|
|
274
|
+
processed_items = []
|
|
275
|
+
for item in value:
|
|
276
|
+
if isinstance(item, ReflectapiOption):
|
|
277
|
+
if not item.is_undefined:
|
|
278
|
+
processed_items.append(item._value)
|
|
279
|
+
elif isinstance(item, dict):
|
|
280
|
+
# Recursively handle dictionaries within lists
|
|
281
|
+
processed_items.append(serialize_option_dict(item))
|
|
282
|
+
else:
|
|
283
|
+
processed_items.append(item)
|
|
284
|
+
result[key] = processed_items
|
|
285
|
+
else:
|
|
286
|
+
result[key] = value
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def is_undefined(value: Any) -> bool:
|
|
292
|
+
"""Check if a value is undefined."""
|
|
293
|
+
if isinstance(value, ReflectapiOption):
|
|
294
|
+
return value.is_undefined
|
|
295
|
+
return value is Undefined
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Response and metadata classes for ReflectAPI Python clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
import httpx # noqa: TC002
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class TransportMetadata:
|
|
16
|
+
"""Immutable metadata about an HTTP response.
|
|
17
|
+
|
|
18
|
+
Contains timing information, HTTP status, headers, and the raw response object.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
status_code: int
|
|
22
|
+
headers: httpx.Headers
|
|
23
|
+
timing: float # Request duration in seconds
|
|
24
|
+
raw_response: httpx.Response
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_response(
|
|
28
|
+
cls, response: httpx.Response, start_time: float
|
|
29
|
+
) -> TransportMetadata:
|
|
30
|
+
"""Create TransportMetadata from an httpx Response."""
|
|
31
|
+
return cls(
|
|
32
|
+
status_code=response.status_code,
|
|
33
|
+
headers=response.headers,
|
|
34
|
+
timing=time.time() - start_time,
|
|
35
|
+
raw_response=response,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ApiResponse(Generic[T]):
|
|
40
|
+
"""Wrapper for successful API responses.
|
|
41
|
+
|
|
42
|
+
Provides ergonomic access to both the deserialized value and transport metadata.
|
|
43
|
+
The deserialized value can be accessed directly via attribute access.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, value: T, metadata: TransportMetadata) -> None:
|
|
47
|
+
self._value = value
|
|
48
|
+
self._metadata = metadata
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def value(self) -> T:
|
|
52
|
+
"""The deserialized response value."""
|
|
53
|
+
return self._value
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def metadata(self) -> TransportMetadata:
|
|
57
|
+
"""Transport metadata including timing, headers, and status."""
|
|
58
|
+
return self._metadata
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def data(self) -> T:
|
|
62
|
+
"""Alias for value for ergonomic access (useful when the payload is a dict)."""
|
|
63
|
+
return self._value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __dir__(self) -> list[str]:
|
|
67
|
+
"""Provide comprehensive attribute listing for better introspection.
|
|
68
|
+
|
|
69
|
+
This enhances IDE auto-completion and debugging by merging attributes
|
|
70
|
+
from both the ApiResponse wrapper and the wrapped value.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of available attributes from both wrapper and value.
|
|
74
|
+
"""
|
|
75
|
+
# Get ApiResponse's own attributes
|
|
76
|
+
wrapper_attrs = ['value', 'metadata', 'data']
|
|
77
|
+
|
|
78
|
+
# Get attributes from the wrapped value
|
|
79
|
+
value_attrs = []
|
|
80
|
+
if hasattr(self._value, '__dict__'):
|
|
81
|
+
value_attrs.extend(self._value.__dict__.keys())
|
|
82
|
+
|
|
83
|
+
# For Pydantic models, also include field names
|
|
84
|
+
if hasattr(self._value, 'model_fields'):
|
|
85
|
+
# Access from class to avoid deprecation warning
|
|
86
|
+
value_attrs.extend(self._value.__class__.model_fields.keys())
|
|
87
|
+
|
|
88
|
+
# For dict-like objects
|
|
89
|
+
if isinstance(self._value, dict):
|
|
90
|
+
value_attrs.extend(self._value.keys())
|
|
91
|
+
|
|
92
|
+
# Get methods and properties from the wrapped value's class
|
|
93
|
+
if hasattr(self._value, '__class__'):
|
|
94
|
+
value_attrs.extend([
|
|
95
|
+
attr for attr in dir(self._value.__class__)
|
|
96
|
+
if not attr.startswith('__') or attr in ['__len__', '__getitem__', '__contains__']
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
# Combine and deduplicate
|
|
100
|
+
all_attrs = list(set(wrapper_attrs + value_attrs))
|
|
101
|
+
|
|
102
|
+
# Sort for consistent ordering
|
|
103
|
+
return sorted(all_attrs)
|
|
104
|
+
|
|
105
|
+
def __contains__(self, item: Any) -> bool:
|
|
106
|
+
"""Delegate containment checks to the wrapped value."""
|
|
107
|
+
if hasattr(self._value, "__contains__"):
|
|
108
|
+
return item in self._value
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def __len__(self) -> int:
|
|
112
|
+
"""Delegate length checks to the wrapped value."""
|
|
113
|
+
if hasattr(self._value, "__len__"):
|
|
114
|
+
return len(self._value)
|
|
115
|
+
raise TypeError(f"object of type '{type(self._value).__name__}' has no len()")
|
|
116
|
+
|
|
117
|
+
def __getitem__(self, key: Any) -> Any:
|
|
118
|
+
"""Delegate item access to the wrapped value."""
|
|
119
|
+
if hasattr(self._value, "__getitem__"):
|
|
120
|
+
return self._value[key]
|
|
121
|
+
raise TypeError(f"'{type(self._value).__name__}' object is not subscriptable")
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return (
|
|
125
|
+
f"ApiResponse(value={self._value!r}, status={self._metadata.status_code})"
|
|
126
|
+
)
|