featrixsphere 0.2.5566__py3-none-any.whl → 0.2.6127__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.
- featrixsphere/__init__.py +37 -18
- featrixsphere/api/__init__.py +50 -0
- featrixsphere/api/api_endpoint.py +280 -0
- featrixsphere/api/client.py +396 -0
- featrixsphere/api/foundational_model.py +658 -0
- featrixsphere/api/http_client.py +209 -0
- featrixsphere/api/notebook_helper.py +584 -0
- featrixsphere/api/prediction_result.py +231 -0
- featrixsphere/api/predictor.py +537 -0
- featrixsphere/api/reference_record.py +227 -0
- featrixsphere/api/vector_database.py +269 -0
- featrixsphere/client.py +218 -8
- {featrixsphere-0.2.5566.dist-info → featrixsphere-0.2.6127.dist-info}/METADATA +1 -1
- featrixsphere-0.2.6127.dist-info/RECORD +17 -0
- featrixsphere-0.2.5566.dist-info/RECORD +0 -7
- {featrixsphere-0.2.5566.dist-info → featrixsphere-0.2.6127.dist-info}/WHEEL +0 -0
- {featrixsphere-0.2.5566.dist-info → featrixsphere-0.2.6127.dist-info}/entry_points.txt +0 -0
- {featrixsphere-0.2.5566.dist-info → featrixsphere-0.2.6127.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP Client functionality for FeatrixSphere API.
|
|
3
|
+
|
|
4
|
+
This module provides the base HTTP client mixin that handles all
|
|
5
|
+
communication with the FeatrixSphere server.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import logging
|
|
10
|
+
import requests
|
|
11
|
+
from typing import Dict, Any, Optional, BinaryIO
|
|
12
|
+
from requests.exceptions import HTTPError, ConnectionError, Timeout
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HTTPClientMixin:
|
|
18
|
+
"""
|
|
19
|
+
Mixin providing HTTP client functionality.
|
|
20
|
+
|
|
21
|
+
Classes using this mixin must have:
|
|
22
|
+
- self._session: requests.Session
|
|
23
|
+
- self._base_url: str
|
|
24
|
+
- self._default_max_retries: int
|
|
25
|
+
- self._retry_base_delay: float
|
|
26
|
+
- self._retry_max_delay: float
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def _make_request(
|
|
30
|
+
self,
|
|
31
|
+
method: str,
|
|
32
|
+
endpoint: str,
|
|
33
|
+
max_retries: Optional[int] = None,
|
|
34
|
+
max_retry_time: Optional[float] = None,
|
|
35
|
+
**kwargs
|
|
36
|
+
) -> requests.Response:
|
|
37
|
+
"""
|
|
38
|
+
Make an HTTP request with retry logic.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
method: HTTP method (GET, POST, DELETE, etc.)
|
|
42
|
+
endpoint: API endpoint (e.g., "/session/123/predict")
|
|
43
|
+
max_retries: Maximum retry attempts
|
|
44
|
+
max_retry_time: Maximum total retry time in seconds
|
|
45
|
+
**kwargs: Additional arguments passed to requests
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Response object
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
HTTPError: If request fails after retries
|
|
52
|
+
"""
|
|
53
|
+
if max_retries is None:
|
|
54
|
+
max_retries = self._default_max_retries
|
|
55
|
+
|
|
56
|
+
# Auto-add /compute prefix for session endpoints
|
|
57
|
+
if endpoint.startswith('/session/') and not endpoint.startswith('/compute/session/'):
|
|
58
|
+
endpoint = f"/compute{endpoint}"
|
|
59
|
+
|
|
60
|
+
# Special handling for upload endpoints
|
|
61
|
+
is_upload = '/upload_with_new_session' in endpoint
|
|
62
|
+
if is_upload:
|
|
63
|
+
if 'timeout' not in kwargs:
|
|
64
|
+
kwargs['timeout'] = 600 # 10 minutes for uploads
|
|
65
|
+
if max_retry_time is None:
|
|
66
|
+
max_retry_time = 600.0
|
|
67
|
+
elif max_retry_time is None:
|
|
68
|
+
max_retry_time = 120.0
|
|
69
|
+
|
|
70
|
+
url = f"{self._base_url}{endpoint}"
|
|
71
|
+
start_time = time.time()
|
|
72
|
+
attempt = 0
|
|
73
|
+
last_error = None
|
|
74
|
+
|
|
75
|
+
while True:
|
|
76
|
+
attempt += 1
|
|
77
|
+
elapsed = time.time() - start_time
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
response = self._session.request(method, url, **kwargs)
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
return response
|
|
83
|
+
|
|
84
|
+
except HTTPError as e:
|
|
85
|
+
last_error = e
|
|
86
|
+
status_code = e.response.status_code if e.response is not None else None
|
|
87
|
+
|
|
88
|
+
# Retry on certain status codes
|
|
89
|
+
if status_code in (500, 502, 503, 504):
|
|
90
|
+
if attempt < max_retries and elapsed < max_retry_time:
|
|
91
|
+
delay = min(
|
|
92
|
+
self._retry_base_delay * (2 ** (attempt - 1)),
|
|
93
|
+
self._retry_max_delay
|
|
94
|
+
)
|
|
95
|
+
logger.warning(
|
|
96
|
+
f"HTTP {status_code} on {method} {endpoint}, "
|
|
97
|
+
f"retrying in {delay:.1f}s (attempt {attempt}/{max_retries})"
|
|
98
|
+
)
|
|
99
|
+
time.sleep(delay)
|
|
100
|
+
continue
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
except (ConnectionError, Timeout) as e:
|
|
104
|
+
last_error = e
|
|
105
|
+
if attempt < max_retries and elapsed < max_retry_time:
|
|
106
|
+
delay = min(
|
|
107
|
+
self._retry_base_delay * (2 ** (attempt - 1)),
|
|
108
|
+
self._retry_max_delay
|
|
109
|
+
)
|
|
110
|
+
logger.warning(
|
|
111
|
+
f"Connection error on {method} {endpoint}, "
|
|
112
|
+
f"retrying in {delay:.1f}s (attempt {attempt}/{max_retries})"
|
|
113
|
+
)
|
|
114
|
+
time.sleep(delay)
|
|
115
|
+
continue
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
# Should not reach here, but just in case
|
|
119
|
+
if last_error:
|
|
120
|
+
raise last_error
|
|
121
|
+
|
|
122
|
+
def _unwrap_response(self, response_json: Dict[str, Any]) -> Dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
Unwrap server response, handling the 'response' wrapper if present.
|
|
125
|
+
|
|
126
|
+
The server sometimes wraps responses in {"response": {...}}.
|
|
127
|
+
"""
|
|
128
|
+
if isinstance(response_json, dict) and 'response' in response_json and len(response_json) == 1:
|
|
129
|
+
return response_json['response']
|
|
130
|
+
return response_json
|
|
131
|
+
|
|
132
|
+
def _get_json(
|
|
133
|
+
self,
|
|
134
|
+
endpoint: str,
|
|
135
|
+
max_retries: Optional[int] = None,
|
|
136
|
+
**kwargs
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""Make a GET request and return JSON response."""
|
|
139
|
+
response = self._make_request("GET", endpoint, max_retries=max_retries, **kwargs)
|
|
140
|
+
return self._unwrap_response(response.json())
|
|
141
|
+
|
|
142
|
+
def _post_json(
|
|
143
|
+
self,
|
|
144
|
+
endpoint: str,
|
|
145
|
+
data: Optional[Dict[str, Any]] = None,
|
|
146
|
+
max_retries: Optional[int] = None,
|
|
147
|
+
**kwargs
|
|
148
|
+
) -> Dict[str, Any]:
|
|
149
|
+
"""Make a POST request with JSON data and return JSON response."""
|
|
150
|
+
if data is not None:
|
|
151
|
+
kwargs['json'] = data
|
|
152
|
+
response = self._make_request("POST", endpoint, max_retries=max_retries, **kwargs)
|
|
153
|
+
return self._unwrap_response(response.json())
|
|
154
|
+
|
|
155
|
+
def _delete_json(
|
|
156
|
+
self,
|
|
157
|
+
endpoint: str,
|
|
158
|
+
max_retries: Optional[int] = None,
|
|
159
|
+
**kwargs
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""Make a DELETE request and return JSON response."""
|
|
162
|
+
response = self._make_request("DELETE", endpoint, max_retries=max_retries, **kwargs)
|
|
163
|
+
return self._unwrap_response(response.json())
|
|
164
|
+
|
|
165
|
+
def _post_multipart(
|
|
166
|
+
self,
|
|
167
|
+
endpoint: str,
|
|
168
|
+
data: Optional[Dict[str, Any]] = None,
|
|
169
|
+
files: Optional[Dict[str, Any]] = None,
|
|
170
|
+
max_retries: Optional[int] = None,
|
|
171
|
+
**kwargs
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Make a POST request with multipart/form-data and return JSON response."""
|
|
174
|
+
response = self._make_request(
|
|
175
|
+
"POST", endpoint,
|
|
176
|
+
data=data, files=files,
|
|
177
|
+
max_retries=max_retries,
|
|
178
|
+
**kwargs
|
|
179
|
+
)
|
|
180
|
+
return self._unwrap_response(response.json())
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ClientContext:
|
|
184
|
+
"""
|
|
185
|
+
Context object passed to resource classes to access HTTP client.
|
|
186
|
+
|
|
187
|
+
This allows resource classes (FoundationalModel, Predictor, etc.) to
|
|
188
|
+
make API calls without directly inheriting from the main client.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, client: 'HTTPClientMixin'):
|
|
192
|
+
self._client = client
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def base_url(self) -> str:
|
|
196
|
+
return self._client._base_url
|
|
197
|
+
|
|
198
|
+
def get_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
199
|
+
return self._client._get_json(endpoint, **kwargs)
|
|
200
|
+
|
|
201
|
+
def post_json(self, endpoint: str, data: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]:
|
|
202
|
+
return self._client._post_json(endpoint, data, **kwargs)
|
|
203
|
+
|
|
204
|
+
def delete_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
205
|
+
return self._client._delete_json(endpoint, **kwargs)
|
|
206
|
+
|
|
207
|
+
def post_multipart(self, endpoint: str, data: Dict[str, Any] = None,
|
|
208
|
+
files: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]:
|
|
209
|
+
return self._client._post_multipart(endpoint, data, files, **kwargs)
|