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