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.
@@ -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
+ )