runapi-core 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.
- runapi/core/__init__.py +71 -0
- runapi/core/auth.py +36 -0
- runapi/core/config.py +23 -0
- runapi/core/constants.py +17 -0
- runapi/core/contract_gen.py +1462 -0
- runapi/core/errors.py +254 -0
- runapi/core/files.py +101 -0
- runapi/core/http_client.py +130 -0
- runapi/core/models.py +248 -0
- runapi/core/multipart.py +23 -0
- runapi/core/options.py +54 -0
- runapi/core/polling.py +55 -0
- runapi/core/py.typed +0 -0
- runapi/core/resource.py +65 -0
- runapi/core/version.py +1 -0
- runapi_core-0.1.0.dist-info/METADATA +46 -0
- runapi_core-0.1.0.dist-info/RECORD +18 -0
- runapi_core-0.1.0.dist-info/WHEEL +4 -0
runapi/core/models.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Lightweight response models with typed field declarations and recursive coercion.
|
|
2
|
+
|
|
3
|
+
A faithful port of Ruby's ``Core::BaseModel``. Subclasses declare fields with
|
|
4
|
+
:func:`required` / :func:`optional` as class attributes; the field name is the
|
|
5
|
+
attribute name::
|
|
6
|
+
|
|
7
|
+
class Image(BaseModel):
|
|
8
|
+
url = optional(str)
|
|
9
|
+
|
|
10
|
+
class TextToImageResponse(TaskResponse):
|
|
11
|
+
id = required(str)
|
|
12
|
+
images = optional([lambda: Image])
|
|
13
|
+
|
|
14
|
+
Declared fields are coerced and enum-validated on construction. Undeclared
|
|
15
|
+
fields are preserved and exposed via attribute and item access.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
|
21
|
+
|
|
22
|
+
from .errors import ValidationError
|
|
23
|
+
|
|
24
|
+
# A field's declared type. ``None`` means "untyped" (coerced dynamically); a
|
|
25
|
+
# class is used directly; a callable is invoked lazily to resolve a forward
|
|
26
|
+
# reference (e.g. ``lambda: Image``); a one-element list marks a list of items.
|
|
27
|
+
FieldType = Union[None, type, Callable[[], Any], List[Any]]
|
|
28
|
+
EnumType = Union[None, List[Any], Callable[[], List[Any]]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Field:
|
|
32
|
+
"""A declared model field. Doubles as a descriptor that reads from the instance."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, required: bool, type: FieldType = None, enum: EnumType = None) -> None:
|
|
35
|
+
self.required = required
|
|
36
|
+
self.enum = enum
|
|
37
|
+
self.name: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
if isinstance(type, list):
|
|
40
|
+
if len(type) != 1:
|
|
41
|
+
raise ValueError("Array field type must contain exactly one item type")
|
|
42
|
+
self.type: FieldType = list
|
|
43
|
+
self.item_type: FieldType = type[0]
|
|
44
|
+
else:
|
|
45
|
+
self.type = type
|
|
46
|
+
self.item_type = None
|
|
47
|
+
|
|
48
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
49
|
+
self.name = name
|
|
50
|
+
|
|
51
|
+
def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
|
|
52
|
+
if obj is None:
|
|
53
|
+
return self
|
|
54
|
+
return obj._attributes.get(self.name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def required(type: FieldType = None, *, enum: EnumType = None) -> Field:
|
|
58
|
+
return Field(required=True, type=type, enum=enum)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def optional(type: FieldType = None, *, enum: EnumType = None) -> Field:
|
|
62
|
+
return Field(required=False, type=type, enum=enum)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_type(type_: FieldType) -> Any:
|
|
66
|
+
if type_ is None:
|
|
67
|
+
return None
|
|
68
|
+
if isinstance(type_, type):
|
|
69
|
+
return type_
|
|
70
|
+
if callable(type_):
|
|
71
|
+
return type_()
|
|
72
|
+
return type_
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BaseModel:
|
|
76
|
+
_fields: Dict[str, Field] = {}
|
|
77
|
+
|
|
78
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
79
|
+
super().__init_subclass__(**kwargs)
|
|
80
|
+
merged: Dict[str, Field] = dict(getattr(cls, "_fields", {}))
|
|
81
|
+
for value in vars(cls).values():
|
|
82
|
+
if isinstance(value, Field) and value.name is not None:
|
|
83
|
+
merged[value.name] = value
|
|
84
|
+
cls._fields = merged
|
|
85
|
+
|
|
86
|
+
def __init__(self, attributes: Optional[Dict[str, Any]] = None) -> None:
|
|
87
|
+
source = self._normalize_input(attributes)
|
|
88
|
+
self._attributes: Dict[str, Any] = {}
|
|
89
|
+
|
|
90
|
+
for field in self._fields.values():
|
|
91
|
+
if field.name in source:
|
|
92
|
+
value = self._coerce_declared_value(source.pop(field.name), field)
|
|
93
|
+
self._attributes[field.name] = value
|
|
94
|
+
elif field.required:
|
|
95
|
+
raise ValidationError(f"{field.name} is required")
|
|
96
|
+
|
|
97
|
+
for key, value in source.items():
|
|
98
|
+
self._attributes[key] = self._coerce_dynamic_value(value)
|
|
99
|
+
|
|
100
|
+
# --- construction helpers -------------------------------------------------
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_dict(cls, payload: Any) -> "BaseModel":
|
|
104
|
+
if isinstance(payload, cls):
|
|
105
|
+
return payload
|
|
106
|
+
if not isinstance(payload, dict):
|
|
107
|
+
raise TypeError(f"Expected dict for {cls.__name__}, got {type(payload).__name__}")
|
|
108
|
+
return cls(payload)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def coerce(cls, value: Any, as_: FieldType = None) -> Any:
|
|
112
|
+
target = _resolve_type(as_) or DynamicModel
|
|
113
|
+
|
|
114
|
+
if value is None:
|
|
115
|
+
return None
|
|
116
|
+
if isinstance(value, BaseModel):
|
|
117
|
+
if isinstance(target, type) and issubclass(target, BaseModel) and not isinstance(value, target):
|
|
118
|
+
return target.from_dict(value.to_dict())
|
|
119
|
+
return value
|
|
120
|
+
if isinstance(value, dict):
|
|
121
|
+
model = target if (isinstance(target, type) and issubclass(target, BaseModel)) else DynamicModel
|
|
122
|
+
return model.from_dict(value)
|
|
123
|
+
if isinstance(value, list):
|
|
124
|
+
return [cls.coerce(item, as_=target) for item in value]
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
# --- access ---------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def __getattr__(self, name: str) -> Any:
|
|
130
|
+
attributes = self.__dict__.get("_attributes", {})
|
|
131
|
+
if name in attributes:
|
|
132
|
+
return attributes[name]
|
|
133
|
+
raise AttributeError(name)
|
|
134
|
+
|
|
135
|
+
def __getitem__(self, key: Any) -> Any:
|
|
136
|
+
return self._attributes.get(str(key))
|
|
137
|
+
|
|
138
|
+
def dig(self, *keys: Any) -> Any:
|
|
139
|
+
current: Any = self
|
|
140
|
+
for key in keys:
|
|
141
|
+
if isinstance(current, BaseModel):
|
|
142
|
+
current = current[key]
|
|
143
|
+
elif isinstance(current, dict):
|
|
144
|
+
current = current.get(key)
|
|
145
|
+
elif isinstance(current, list):
|
|
146
|
+
current = current[key] if isinstance(key, int) else None
|
|
147
|
+
else:
|
|
148
|
+
return None
|
|
149
|
+
if current is None:
|
|
150
|
+
return None
|
|
151
|
+
return current
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
154
|
+
return {key: self._serialize(value) for key, value in self._attributes.items()}
|
|
155
|
+
|
|
156
|
+
def __eq__(self, other: Any) -> bool:
|
|
157
|
+
if isinstance(other, BaseModel):
|
|
158
|
+
return self.to_dict() == other.to_dict()
|
|
159
|
+
if isinstance(other, dict):
|
|
160
|
+
return self.to_dict() == _stringify_keys(other)
|
|
161
|
+
return NotImplemented
|
|
162
|
+
|
|
163
|
+
def __repr__(self) -> str:
|
|
164
|
+
return f"{type(self).__name__}({self._attributes!r})"
|
|
165
|
+
|
|
166
|
+
# --- internals ------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def _coerce_declared_value(self, value: Any, field: Field) -> Any:
|
|
169
|
+
if field.type is list and isinstance(value, list):
|
|
170
|
+
coerced: Any = [self._coerce_with_type(item, field.item_type) for item in value]
|
|
171
|
+
else:
|
|
172
|
+
coerced = self._coerce_with_type(value, field.type)
|
|
173
|
+
self._validate_enum(field, coerced)
|
|
174
|
+
return coerced
|
|
175
|
+
|
|
176
|
+
def _coerce_with_type(self, value: Any, type_: FieldType) -> Any:
|
|
177
|
+
if type_ is None:
|
|
178
|
+
return self._coerce_dynamic_value(value)
|
|
179
|
+
|
|
180
|
+
resolved = _resolve_type(type_)
|
|
181
|
+
if resolved is None:
|
|
182
|
+
return self._coerce_dynamic_value(value)
|
|
183
|
+
if isinstance(resolved, type) and issubclass(resolved, BaseModel) and isinstance(value, dict):
|
|
184
|
+
return resolved.from_dict(value)
|
|
185
|
+
return value
|
|
186
|
+
|
|
187
|
+
def _coerce_dynamic_value(self, value: Any) -> Any:
|
|
188
|
+
if isinstance(value, dict):
|
|
189
|
+
return DynamicModel.from_dict(value)
|
|
190
|
+
if isinstance(value, list):
|
|
191
|
+
return [self._coerce_dynamic_value(item) for item in value]
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
def _validate_enum(self, field: Field, value: Any) -> None:
|
|
195
|
+
if value is None or field.enum is None:
|
|
196
|
+
return
|
|
197
|
+
allowed = field.enum() if callable(field.enum) else field.enum
|
|
198
|
+
if not allowed:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
candidates = value if isinstance(value, list) else [value]
|
|
202
|
+
for item in candidates:
|
|
203
|
+
if not any(option == item or str(option) == str(item) for option in allowed):
|
|
204
|
+
joined = ", ".join(str(option) for option in allowed)
|
|
205
|
+
raise ValidationError(f"Invalid {field.name}: {item}. Must be one of: {joined}")
|
|
206
|
+
|
|
207
|
+
def _serialize(self, value: Any) -> Any:
|
|
208
|
+
if isinstance(value, BaseModel):
|
|
209
|
+
return value.to_dict()
|
|
210
|
+
if isinstance(value, list):
|
|
211
|
+
return [self._serialize(item) for item in value]
|
|
212
|
+
return value
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _normalize_input(attributes: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
216
|
+
if attributes is None:
|
|
217
|
+
return {}
|
|
218
|
+
if not isinstance(attributes, dict):
|
|
219
|
+
raise TypeError(f"Expected dict, got {type(attributes).__name__}")
|
|
220
|
+
return {str(key): value for key, value in attributes.items()}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class DynamicModel(BaseModel):
|
|
224
|
+
"""Generic response model used when no typed model is provided."""
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _stringify_keys(value: Any) -> Any:
|
|
228
|
+
if isinstance(value, dict):
|
|
229
|
+
return {str(key): _stringify_keys(item) for key, item in value.items()}
|
|
230
|
+
if isinstance(value, list):
|
|
231
|
+
return [_stringify_keys(item) for item in value]
|
|
232
|
+
return value
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class TaskResponse(BaseModel):
|
|
236
|
+
"""Typed response for async task operations. Extra fields are preserved."""
|
|
237
|
+
|
|
238
|
+
class Status:
|
|
239
|
+
PENDING = "pending"
|
|
240
|
+
PROCESSING = "processing"
|
|
241
|
+
COMPLETED = "completed"
|
|
242
|
+
FAILED = "failed"
|
|
243
|
+
|
|
244
|
+
ALL = [PENDING, PROCESSING, COMPLETED, FAILED]
|
|
245
|
+
|
|
246
|
+
id = optional(str)
|
|
247
|
+
status = optional(str)
|
|
248
|
+
error = optional(str)
|
runapi/core/multipart.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Multipart upload payloads."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class MultipartFile:
|
|
9
|
+
path: str
|
|
10
|
+
filename: Optional[str] = None
|
|
11
|
+
content_type: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MultipartBody:
|
|
15
|
+
"""A multipart/form-data request body of plain fields plus file parts."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
fields: Optional[Dict[str, Any]] = None,
|
|
20
|
+
files: Optional[Dict[str, MultipartFile]] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.fields: Dict[str, Any] = {str(key): value for key, value in (fields or {}).items()}
|
|
23
|
+
self.files: Dict[str, MultipartFile] = {str(key): value for key, value in (files or {}).items()}
|
runapi/core/options.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Client, request, and polling option dataclasses."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from . import config, constants
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ClientOptions:
|
|
11
|
+
"""Configuration for an API client.
|
|
12
|
+
|
|
13
|
+
``http_client`` lets callers inject a custom transport (for tests or
|
|
14
|
+
advanced use). When set, the SDK delegates all HTTP calls to it instead of
|
|
15
|
+
building its own :class:`HttpClient`. It must expose
|
|
16
|
+
``request(method, path, body=None, options=None)``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
api_key: Optional[str] = None
|
|
20
|
+
base_url: Optional[str] = None
|
|
21
|
+
timeout: Optional[int] = None
|
|
22
|
+
max_retries: Optional[int] = None
|
|
23
|
+
retry_base_delay: Optional[float] = None
|
|
24
|
+
retry_max_delay: Optional[float] = None
|
|
25
|
+
http_client: Optional[Any] = None
|
|
26
|
+
|
|
27
|
+
def __post_init__(self) -> None:
|
|
28
|
+
if self.base_url is None:
|
|
29
|
+
self.base_url = config.base_url
|
|
30
|
+
if self.timeout is None:
|
|
31
|
+
self.timeout = constants.HTTP_REQUEST_TIMEOUT
|
|
32
|
+
if self.max_retries is None:
|
|
33
|
+
self.max_retries = constants.MAX_RETRIES
|
|
34
|
+
if self.retry_base_delay is None:
|
|
35
|
+
self.retry_base_delay = constants.RETRY_BASE_DELAY
|
|
36
|
+
if self.retry_max_delay is None:
|
|
37
|
+
self.retry_max_delay = constants.RETRY_MAX_DELAY
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class RequestOptions:
|
|
42
|
+
"""Per-request overrides of client-level defaults."""
|
|
43
|
+
|
|
44
|
+
headers: Optional[Dict[str, str]] = None
|
|
45
|
+
timeout: Optional[int] = None
|
|
46
|
+
max_retries: Optional[int] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class PollingOptions:
|
|
51
|
+
"""Options for polling async task completion."""
|
|
52
|
+
|
|
53
|
+
poll_interval: int = field(default=constants.POLLING_INTERVAL)
|
|
54
|
+
max_wait: int = field(default=constants.POLLING_MAX_WAIT)
|
runapi/core/polling.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Polling loop for async task completion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from .errors import TaskFailedError, TaskTimeoutError
|
|
9
|
+
from .models import BaseModel
|
|
10
|
+
from .options import PollingOptions
|
|
11
|
+
|
|
12
|
+
ACTIVE_STATUSES = ("pending", "processing")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def poll_until_complete(fetch: Callable[[], Any], options: Optional[PollingOptions] = None) -> Any:
|
|
16
|
+
"""Call ``fetch`` repeatedly until the task completes.
|
|
17
|
+
|
|
18
|
+
Returns the completed response. Raises :class:`TaskFailedError` on a failed
|
|
19
|
+
or unknown status, and :class:`TaskTimeoutError` once ``max_wait`` elapses.
|
|
20
|
+
"""
|
|
21
|
+
if options is None:
|
|
22
|
+
options = PollingOptions()
|
|
23
|
+
|
|
24
|
+
deadline = time.monotonic() + options.max_wait
|
|
25
|
+
|
|
26
|
+
while True:
|
|
27
|
+
response = fetch()
|
|
28
|
+
status = str(_value_for(response, "status") or "").lower()
|
|
29
|
+
|
|
30
|
+
if status == "completed":
|
|
31
|
+
return response
|
|
32
|
+
|
|
33
|
+
if status == "failed":
|
|
34
|
+
message = _value_for(response, "error") or "Task failed"
|
|
35
|
+
raise TaskFailedError(message, details=_details_for(response))
|
|
36
|
+
|
|
37
|
+
if time.monotonic() >= deadline:
|
|
38
|
+
raise TaskTimeoutError(f"Task polling timed out after {options.max_wait}s")
|
|
39
|
+
|
|
40
|
+
if status not in ACTIVE_STATUSES:
|
|
41
|
+
raise TaskFailedError(f"Unknown task status: {status}", details=_details_for(response))
|
|
42
|
+
|
|
43
|
+
time.sleep(options.poll_interval)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _value_for(response: Any, key: str) -> Any:
|
|
47
|
+
if isinstance(response, BaseModel):
|
|
48
|
+
return response[key]
|
|
49
|
+
if isinstance(response, dict):
|
|
50
|
+
return response.get(key)
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _details_for(response: Any) -> Any:
|
|
55
|
+
return response.to_dict() if isinstance(response, BaseModel) else response
|
runapi/core/py.typed
ADDED
|
File without changes
|
runapi/core/resource.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Base class for API resources: request coercion, param helpers, polling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
from . import polling
|
|
8
|
+
from .errors import ValidationError
|
|
9
|
+
from .models import BaseModel, TaskResponse
|
|
10
|
+
from .options import PollingOptions, RequestOptions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Resource:
|
|
14
|
+
"""Shared behavior for resource classes.
|
|
15
|
+
|
|
16
|
+
Subclasses set ``RESPONSE_CLASS`` (the typed model for responses) and
|
|
17
|
+
optionally ``COMPLETED_RESPONSE_CLASS`` (a narrowed model that ``run()``
|
|
18
|
+
re-coerces to once the task completes).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS: type = TaskResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS: Optional[type] = None
|
|
23
|
+
|
|
24
|
+
def __init__(self, http: Any) -> None:
|
|
25
|
+
self._http = http
|
|
26
|
+
|
|
27
|
+
def _request(
|
|
28
|
+
self,
|
|
29
|
+
method: str,
|
|
30
|
+
path: str,
|
|
31
|
+
body: Any = None,
|
|
32
|
+
options: Optional[RequestOptions] = None,
|
|
33
|
+
response_class: Optional[type] = None,
|
|
34
|
+
) -> Any:
|
|
35
|
+
response = self._http.request(method, path, body=body, options=options)
|
|
36
|
+
return BaseModel.coerce(response, as_=response_class or type(self).RESPONSE_CLASS)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _compact_params(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
40
|
+
return {
|
|
41
|
+
key: value
|
|
42
|
+
for key, value in params.items()
|
|
43
|
+
if not (value is None or (isinstance(value, str) and value.strip() == ""))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _validate_optional(params: Dict[str, Any], key: str, allowed: Sequence[Any]) -> None:
|
|
48
|
+
value = params.get(key)
|
|
49
|
+
if value is None:
|
|
50
|
+
return
|
|
51
|
+
if value not in allowed:
|
|
52
|
+
joined = ", ".join(str(option) for option in allowed)
|
|
53
|
+
raise ValidationError(f"Invalid {key}: {value}. Must be one of: {joined}")
|
|
54
|
+
|
|
55
|
+
def _poll_until_complete(
|
|
56
|
+
self, fetch: Callable[[], Any], polling_opts: Optional[PollingOptions] = None
|
|
57
|
+
) -> Any:
|
|
58
|
+
response = polling.poll_until_complete(fetch, polling_opts or PollingOptions())
|
|
59
|
+
|
|
60
|
+
completed_class = type(self).COMPLETED_RESPONSE_CLASS
|
|
61
|
+
if completed_class is None or isinstance(response, completed_class):
|
|
62
|
+
return response
|
|
63
|
+
|
|
64
|
+
payload = response.to_dict() if isinstance(response, BaseModel) else response
|
|
65
|
+
return completed_class.from_dict(payload)
|
runapi/core/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core module for the RunAPI Python SDK
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,api,runapi,sdk
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# RunAPI Core Python SDK
|
|
15
|
+
|
|
16
|
+
The RunAPI Core Python SDK provides shared authentication, HTTP, retry, error, and polling primitives for RunAPI model packages. Install `runapi-core` only when you are building SDK infrastructure or shared Python tooling; application code should normally install a concrete model package such as `runapi-flux-2`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install runapi-core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
|
|
26
|
+
Use the core package for common client options, error classes, request helpers, file uploads, and task polling behavior that model SDKs share. Configure it globally or per client:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import runapi.core as runapi
|
|
30
|
+
|
|
31
|
+
runapi.configure(api_key="sk-...") # or set RUNAPI_API_KEY in the environment
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from runapi.core import FilesClient
|
|
36
|
+
|
|
37
|
+
files = FilesClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
38
|
+
uploaded = files.create(file="./input.png")
|
|
39
|
+
print(uploaded.url)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Public SDK docs live at https://runapi.ai/docs#runapi-sdks and the model catalog lives at https://runapi.ai/models.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
runapi/core/__init__.py,sha256=hJ_3FVD39iVgkQ9QpFMImEdi9q7Ge_xj1fkfBdDZUJg,1694
|
|
2
|
+
runapi/core/auth.py,sha256=woKMW_oidp9xEmPHb2-7QVvNc8aIitycuIaTdv2Pacw,1053
|
|
3
|
+
runapi/core/config.py,sha256=9HoDPKzkV1Zhwi8qdZPuB_MQhEv59KTW-rly-Xre0YY,728
|
|
4
|
+
runapi/core/constants.py,sha256=peWEesxVWjyGMVbBzXBr4C7ZgWjzXKgh4T4q7EI-IwQ,400
|
|
5
|
+
runapi/core/contract_gen.py,sha256=9gfXFv5n05AHCpQobjcaB-HEou7Zga4vywxFdEONe9Q,46457
|
|
6
|
+
runapi/core/errors.py,sha256=g4hqy_FNeTMi_pvMecmu_jkwlBNPpnKDinTjqJjAuBE,7802
|
|
7
|
+
runapi/core/files.py,sha256=hUNsXBUwYRysWlOVpO18cHuaowj6CwXeAE6i-6psrgg,3507
|
|
8
|
+
runapi/core/http_client.py,sha256=Yh1ZlGHiBX0tZfXuwMXOl0ccD8WAvsEoy7n_aLmhOZI,4566
|
|
9
|
+
runapi/core/models.py,sha256=useACuEJ3Ob5QALyazBN7AVyFSw8GEPp94efRa1KUEs,9007
|
|
10
|
+
runapi/core/multipart.py,sha256=Hv6T6FCnTkB4KEC_gIicK9zppCA6_jseb4RKRrLdtHw,687
|
|
11
|
+
runapi/core/options.py,sha256=MlXzPWEdyuL1-JcUWfszfWZ-5MuaEHUelEatZzHj7f0,1719
|
|
12
|
+
runapi/core/polling.py,sha256=v2_tjrDGlRjn6LWcshQgIdLsXoJX4AjQQ4YSy0dUcHE,1735
|
|
13
|
+
runapi/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
runapi/core/resource.py,sha256=ZIxRA2izZeX6cnV_6_417kR4F1qW2IE7rfaGUrQRULo,2308
|
|
15
|
+
runapi/core/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
16
|
+
runapi_core-0.1.0.dist-info/METADATA,sha256=WvqR5b2-S9VB9kunCfH2KzmMwL24aVamJjwGbQIOyjo,1459
|
|
17
|
+
runapi_core-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
18
|
+
runapi_core-0.1.0.dist-info/RECORD,,
|