tabapi 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.
- tabapi/__init__.py +7 -0
- tabapi/_base.py +59 -0
- tabapi/classifier.py +133 -0
- tabapi/client.py +306 -0
- tabapi/py.typed +0 -0
- tabapi-0.1.0.dist-info/METADATA +92 -0
- tabapi-0.1.0.dist-info/RECORD +9 -0
- tabapi-0.1.0.dist-info/WHEEL +4 -0
- tabapi-0.1.0.dist-info/licenses/LICENSE +28 -0
tabapi/__init__.py
ADDED
tabapi/_base.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Base class for the scikit-learn estimators backed by the TabICL API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from sklearn.base import BaseEstimator
|
|
8
|
+
from sklearn.utils.validation import check_is_fitted
|
|
9
|
+
|
|
10
|
+
from tabapi.client import TabAPIClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _TabAPIEstimator(BaseEstimator):
|
|
14
|
+
"""Fit and predict on a remote TabICL server, whatever the task.
|
|
15
|
+
|
|
16
|
+
Subclasses set ``_task`` and list every parameter in their ``__init__``
|
|
17
|
+
(scikit-learn reads them from its signature). The connection parameters
|
|
18
|
+
are ``base_url``, ``timeout``, ``token``, ``max_retries`` and
|
|
19
|
+
``backoff_factor``. The model parameters shared by all tasks are
|
|
20
|
+
``n_estimators``, ``random_state`` and ``target_name``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_task: str
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
"""Like the sklearn repr, but never show the token."""
|
|
27
|
+
text = super().__repr__()
|
|
28
|
+
if self.token:
|
|
29
|
+
text = text.replace(repr(self.token), "'***'")
|
|
30
|
+
return text
|
|
31
|
+
|
|
32
|
+
def __sklearn_is_fitted__(self) -> bool:
|
|
33
|
+
return hasattr(self, "_model_id")
|
|
34
|
+
|
|
35
|
+
def _fit_remote(self, X, y, **config) -> None:
|
|
36
|
+
"""Fit a model on the server, with ``config`` as the task-specific parameters."""
|
|
37
|
+
self._client = TabAPIClient(
|
|
38
|
+
base_url=self.base_url,
|
|
39
|
+
timeout=self.timeout,
|
|
40
|
+
token=self.token,
|
|
41
|
+
max_retries=self.max_retries,
|
|
42
|
+
backoff_factor=self.backoff_factor,
|
|
43
|
+
)
|
|
44
|
+
self._model_id = self._client.fit(
|
|
45
|
+
pd.DataFrame(X),
|
|
46
|
+
y if isinstance(y, pd.Series) else pd.Series(y),
|
|
47
|
+
task=self._task,
|
|
48
|
+
target_name=self.target_name,
|
|
49
|
+
n_estimators=self.n_estimators,
|
|
50
|
+
random_state=self.random_state,
|
|
51
|
+
**config,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _predict_remote(self, X, output_type: str) -> np.ndarray:
|
|
55
|
+
check_is_fitted(self)
|
|
56
|
+
prediction = self._client.predict(
|
|
57
|
+
self._model_id, pd.DataFrame(X), task=self._task, output_type=output_type
|
|
58
|
+
)
|
|
59
|
+
return np.asarray(prediction)
|
tabapi/classifier.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""TabAPIClassifier: scikit-learn-style classifier that calls the TabICL API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from sklearn.base import ClassifierMixin
|
|
8
|
+
|
|
9
|
+
from tabapi._base import _TabAPIEstimator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TabAPIClassifier(ClassifierMixin, _TabAPIEstimator):
|
|
13
|
+
"""
|
|
14
|
+
Scikit-learn-style classifier that fits and predicts via the TabICL API.
|
|
15
|
+
|
|
16
|
+
This estimator does not run TabICL locally; it sends data to a remote
|
|
17
|
+
server (e.g. deployed via the tabapi Docker image) and uses the returned
|
|
18
|
+
model_id for subsequent predictions.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
base_url : str
|
|
23
|
+
Base URL of the TabICL API (e.g. "http://localhost:8000").
|
|
24
|
+
timeout : float
|
|
25
|
+
Request timeout in seconds for fit and predict.
|
|
26
|
+
token : str or None
|
|
27
|
+
Bearer token sent as "Authorization: Bearer <token>" when the server
|
|
28
|
+
requires auth. If None, the TABAPI_API_TOKEN environment variable is
|
|
29
|
+
read at request time. Prefer the environment variable: an explicit
|
|
30
|
+
token is an estimator parameter, so it is kept when the estimator is
|
|
31
|
+
pickled (e.g. saved to a skore project).
|
|
32
|
+
max_retries : int
|
|
33
|
+
How many times a request the server could not take (busy, or no
|
|
34
|
+
healthy backend behind the proxy) is retried. 0 disables retries.
|
|
35
|
+
backoff_factor : float
|
|
36
|
+
Base wait between retries, in seconds; doubled after each retry
|
|
37
|
+
unless the server sends Retry-After.
|
|
38
|
+
n_estimators : int or None
|
|
39
|
+
Passed to the server at fit time (TabICL ensemble size).
|
|
40
|
+
random_state : int or None
|
|
41
|
+
Passed to the server at fit time.
|
|
42
|
+
softmax_temperature : float or None
|
|
43
|
+
Passed to the server at fit time.
|
|
44
|
+
target_name : str
|
|
45
|
+
Name the target column is given in the payload sent to the server. Only
|
|
46
|
+
matters for the server-side schema; predictions are made by model_id.
|
|
47
|
+
Change it if X already has a feature column named "target", otherwise
|
|
48
|
+
the client refuses to overwrite that column.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_task = "classification"
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
base_url: str = "http://localhost:8000",
|
|
57
|
+
timeout: float = 300.0,
|
|
58
|
+
token: str | None = None,
|
|
59
|
+
max_retries: int = 3,
|
|
60
|
+
backoff_factor: float = 1.0,
|
|
61
|
+
n_estimators: int | None = None,
|
|
62
|
+
random_state: int | None = None,
|
|
63
|
+
softmax_temperature: float | None = None,
|
|
64
|
+
target_name: str = "target",
|
|
65
|
+
):
|
|
66
|
+
self.base_url = base_url
|
|
67
|
+
self.timeout = timeout
|
|
68
|
+
self.token = token
|
|
69
|
+
self.max_retries = max_retries
|
|
70
|
+
self.backoff_factor = backoff_factor
|
|
71
|
+
self.n_estimators = n_estimators
|
|
72
|
+
self.random_state = random_state
|
|
73
|
+
self.softmax_temperature = softmax_temperature
|
|
74
|
+
self.target_name = target_name
|
|
75
|
+
|
|
76
|
+
def fit(self, X: pd.DataFrame | np.ndarray, y: pd.Series | np.ndarray) -> TabAPIClassifier:
|
|
77
|
+
"""
|
|
78
|
+
Fit a model on the server using (X, y).
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
X : DataFrame or ndarray
|
|
83
|
+
Feature matrix. Converted to DataFrame if ndarray.
|
|
84
|
+
y : Series or ndarray
|
|
85
|
+
Target labels.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
self
|
|
90
|
+
"""
|
|
91
|
+
self._fit_remote(X, y, softmax_temperature=self.softmax_temperature)
|
|
92
|
+
self.classes_ = np.unique(y)
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def predict(self, X: pd.DataFrame | np.ndarray) -> np.ndarray:
|
|
96
|
+
"""
|
|
97
|
+
Predict class labels for X using the model fitted on the server.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
X : DataFrame or ndarray
|
|
102
|
+
Feature matrix. Must have same columns as training data.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
ndarray of shape (n_samples,) with class labels.
|
|
107
|
+
|
|
108
|
+
Raises
|
|
109
|
+
------
|
|
110
|
+
sklearn.exceptions.NotFittedError
|
|
111
|
+
If called before ``fit``.
|
|
112
|
+
"""
|
|
113
|
+
return self._predict_remote(X, output_type="preds")
|
|
114
|
+
|
|
115
|
+
def predict_proba(self, X: pd.DataFrame | np.ndarray) -> np.ndarray:
|
|
116
|
+
"""
|
|
117
|
+
Predict class probabilities for X using the model fitted on the server.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
X : DataFrame or ndarray
|
|
122
|
+
Feature matrix.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
ndarray of shape (n_samples, n_classes) with class probabilities.
|
|
127
|
+
|
|
128
|
+
Raises
|
|
129
|
+
------
|
|
130
|
+
sklearn.exceptions.NotFittedError
|
|
131
|
+
If called before ``fit``.
|
|
132
|
+
"""
|
|
133
|
+
return self._predict_remote(X, output_type="probas")
|
tabapi/client.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Low-level HTTP client for the TabICL API (fit and predict)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
TOKEN_ENV_VAR = "TABAPI_API_TOKEN"
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# 503 means the request was not processed: either the server had no free
|
|
22
|
+
# compute slot (it says so with "retryable": true), or the reverse proxy had
|
|
23
|
+
# no healthy backend to forward it to. Safe to retry for every endpoint.
|
|
24
|
+
_RETRY_ALWAYS = frozenset({503})
|
|
25
|
+
# 502/504 may come back after the server started (or even finished) the work.
|
|
26
|
+
# Retrying is only safe for idempotent calls: predict, not fit (a retried fit
|
|
27
|
+
# would train, and keep in server memory, a second model).
|
|
28
|
+
_RETRY_IF_IDEMPOTENT = frozenset({502, 503, 504})
|
|
29
|
+
# Upper bound on one wait, whatever the server's Retry-After says.
|
|
30
|
+
_MAX_RETRY_DELAY = 60.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_token(token: str | None) -> str | None:
|
|
34
|
+
"""Token to send: explicit value, else TABAPI_API_TOKEN.
|
|
35
|
+
|
|
36
|
+
Whitespace is stripped, and for a comma-separated list (the server-side
|
|
37
|
+
format of TABAPI_API_TOKEN) the first non-empty entry is used.
|
|
38
|
+
"""
|
|
39
|
+
raw = token if token is not None else os.environ.get(TOKEN_ENV_VAR)
|
|
40
|
+
if not raw:
|
|
41
|
+
return None
|
|
42
|
+
for part in raw.split(","):
|
|
43
|
+
if part.strip():
|
|
44
|
+
return part.strip()
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TabAPIClient:
|
|
49
|
+
"""Client for the TabICL API (POST /v1/fit and POST /v1/predict).
|
|
50
|
+
|
|
51
|
+
If the server requires bearer auth, pass ``token`` (or set the
|
|
52
|
+
TABAPI_API_TOKEN environment variable); it is sent as
|
|
53
|
+
"Authorization: Bearer <token>". The environment variable is read at
|
|
54
|
+
request time and never stored on the client, so it is not persisted
|
|
55
|
+
when a fitted estimator is pickled.
|
|
56
|
+
|
|
57
|
+
Requests the server could not take (busy server, or proxy with no healthy
|
|
58
|
+
backend) are retried up to ``max_retries`` times with exponential backoff
|
|
59
|
+
(``backoff_factor`` * 1, 2, 4, ... seconds, or the server's Retry-After).
|
|
60
|
+
fit is retried only on 503 and on errors marked ``"retryable": true``;
|
|
61
|
+
predict, which is idempotent, also on 502 and 504. Set ``max_retries=0``
|
|
62
|
+
to disable retries.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
base_url: str = "http://localhost:8000",
|
|
68
|
+
timeout: float = 300.0,
|
|
69
|
+
token: str | None = None,
|
|
70
|
+
max_retries: int = 3,
|
|
71
|
+
backoff_factor: float = 1.0,
|
|
72
|
+
):
|
|
73
|
+
if max_retries < 0:
|
|
74
|
+
raise ValueError(f"max_retries must be >= 0, got {max_retries}")
|
|
75
|
+
if backoff_factor < 0:
|
|
76
|
+
raise ValueError(f"backoff_factor must be >= 0, got {backoff_factor}")
|
|
77
|
+
self.base_url = base_url.rstrip("/")
|
|
78
|
+
self.timeout = timeout
|
|
79
|
+
self.token = token
|
|
80
|
+
self.max_retries = max_retries
|
|
81
|
+
self.backoff_factor = backoff_factor
|
|
82
|
+
|
|
83
|
+
def _headers(self) -> dict[str, str]:
|
|
84
|
+
token = _resolve_token(self.token)
|
|
85
|
+
if token:
|
|
86
|
+
return {"Authorization": f"Bearer {token}"}
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _check_response(r: requests.Response) -> None:
|
|
91
|
+
"""Raise requests.HTTPError, including the server's error detail if present."""
|
|
92
|
+
if r.ok:
|
|
93
|
+
return
|
|
94
|
+
try:
|
|
95
|
+
body = r.json()
|
|
96
|
+
except ValueError:
|
|
97
|
+
body = None
|
|
98
|
+
if isinstance(body, dict):
|
|
99
|
+
detail = body.get("detail", r.text)
|
|
100
|
+
else:
|
|
101
|
+
# Non-dict JSON (bare string, list, ...) or non-JSON body: fall back to text.
|
|
102
|
+
detail = r.text if body is None else str(body)
|
|
103
|
+
if isinstance(detail, dict):
|
|
104
|
+
detail = detail.get("detail") or detail.get("code") or str(detail)
|
|
105
|
+
raise requests.HTTPError(
|
|
106
|
+
f"{r.status_code} {r.reason} for {r.url}: {detail}",
|
|
107
|
+
response=r,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _marked_retryable(r: requests.Response) -> bool:
|
|
112
|
+
"""True if the server's structured error says ``"retryable": true``."""
|
|
113
|
+
try:
|
|
114
|
+
body = r.json()
|
|
115
|
+
except ValueError:
|
|
116
|
+
return False
|
|
117
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
118
|
+
return isinstance(detail, dict) and detail.get("retryable") is True
|
|
119
|
+
|
|
120
|
+
def _should_retry(self, r: requests.Response, idempotent: bool) -> bool:
|
|
121
|
+
if r.ok:
|
|
122
|
+
return False
|
|
123
|
+
if r.status_code in _RETRY_ALWAYS:
|
|
124
|
+
return True
|
|
125
|
+
if idempotent and r.status_code in _RETRY_IF_IDEMPOTENT:
|
|
126
|
+
return True
|
|
127
|
+
return self._marked_retryable(r)
|
|
128
|
+
|
|
129
|
+
def _retry_delay(self, r: requests.Response, attempt: int) -> float:
|
|
130
|
+
"""Server's Retry-After (in seconds) if given, else exponential backoff.
|
|
131
|
+
|
|
132
|
+
Stretched by a random 0-50% so clients turned away together do not all
|
|
133
|
+
come back at the same instant and collide again.
|
|
134
|
+
"""
|
|
135
|
+
retry_after = r.headers.get("Retry-After")
|
|
136
|
+
try:
|
|
137
|
+
delay = float(retry_after) if retry_after is not None else None
|
|
138
|
+
except ValueError:
|
|
139
|
+
delay = None # HTTP-date form: fall back to backoff
|
|
140
|
+
if delay is None or delay < 0:
|
|
141
|
+
delay = self.backoff_factor * (2**attempt)
|
|
142
|
+
delay *= random.uniform(1.0, 1.5)
|
|
143
|
+
return min(delay, _MAX_RETRY_DELAY)
|
|
144
|
+
|
|
145
|
+
def _post(
|
|
146
|
+
self,
|
|
147
|
+
path: str,
|
|
148
|
+
make_files: Callable[[], dict[str, Any]],
|
|
149
|
+
*,
|
|
150
|
+
idempotent: bool,
|
|
151
|
+
) -> requests.Response:
|
|
152
|
+
"""POST with retries; ``make_files`` builds fresh upload buffers per attempt."""
|
|
153
|
+
url = f"{self.base_url}{path}"
|
|
154
|
+
attempt = 0
|
|
155
|
+
while True:
|
|
156
|
+
r = requests.post(
|
|
157
|
+
url,
|
|
158
|
+
files=make_files(),
|
|
159
|
+
headers=self._headers(),
|
|
160
|
+
timeout=self.timeout,
|
|
161
|
+
)
|
|
162
|
+
if attempt >= self.max_retries or not self._should_retry(r, idempotent):
|
|
163
|
+
self._check_response(r)
|
|
164
|
+
return r
|
|
165
|
+
delay = self._retry_delay(r, attempt)
|
|
166
|
+
attempt += 1
|
|
167
|
+
logger.warning(
|
|
168
|
+
"%s returned %s; retrying in %.1fs (attempt %d/%d)",
|
|
169
|
+
path,
|
|
170
|
+
r.status_code,
|
|
171
|
+
delay,
|
|
172
|
+
attempt,
|
|
173
|
+
self.max_retries,
|
|
174
|
+
)
|
|
175
|
+
time.sleep(delay)
|
|
176
|
+
|
|
177
|
+
def fit(
|
|
178
|
+
self,
|
|
179
|
+
X: pd.DataFrame,
|
|
180
|
+
y: pd.Series,
|
|
181
|
+
*,
|
|
182
|
+
task: str = "classification",
|
|
183
|
+
target_name: str = "target",
|
|
184
|
+
n_estimators: int | None = None,
|
|
185
|
+
random_state: int | None = None,
|
|
186
|
+
softmax_temperature: float | None = None,
|
|
187
|
+
) -> str:
|
|
188
|
+
"""
|
|
189
|
+
Fit a model on the server. Returns the model_id for use in predict.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
X : DataFrame
|
|
194
|
+
Feature matrix.
|
|
195
|
+
y : Series
|
|
196
|
+
Target labels.
|
|
197
|
+
task : str
|
|
198
|
+
Task the server fits the model for.
|
|
199
|
+
target_name : str
|
|
200
|
+
Name of the target column (used in the API schema).
|
|
201
|
+
n_estimators, random_state, softmax_temperature
|
|
202
|
+
Optional server-side TabICL config.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
model_id : str
|
|
207
|
+
UUID of the fitted model on the server.
|
|
208
|
+
|
|
209
|
+
Raises
|
|
210
|
+
------
|
|
211
|
+
ValueError
|
|
212
|
+
If ``target_name`` is already a column of ``X``.
|
|
213
|
+
"""
|
|
214
|
+
if target_name in X.columns:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"target_name {target_name!r} is already a column in X; "
|
|
217
|
+
"rename that feature or pass a different target_name"
|
|
218
|
+
)
|
|
219
|
+
df = X.copy()
|
|
220
|
+
df[target_name] = y
|
|
221
|
+
buf = io.BytesIO()
|
|
222
|
+
df.to_csv(buf, index=False)
|
|
223
|
+
csv_bytes = buf.getvalue()
|
|
224
|
+
|
|
225
|
+
payload: dict[str, Any] = {
|
|
226
|
+
"task": task,
|
|
227
|
+
"schema": {"target": target_name},
|
|
228
|
+
}
|
|
229
|
+
config: dict[str, Any] = {}
|
|
230
|
+
if n_estimators is not None:
|
|
231
|
+
config["n_estimators"] = n_estimators
|
|
232
|
+
if random_state is not None:
|
|
233
|
+
config["random_state"] = random_state
|
|
234
|
+
if softmax_temperature is not None:
|
|
235
|
+
config["softmax_temperature"] = softmax_temperature
|
|
236
|
+
if config:
|
|
237
|
+
payload["config"] = config
|
|
238
|
+
data_field = json.dumps(payload)
|
|
239
|
+
|
|
240
|
+
r = self._post(
|
|
241
|
+
"/v1/fit",
|
|
242
|
+
lambda: {
|
|
243
|
+
"data": (None, data_field, "application/json"),
|
|
244
|
+
"dataset_file": ("train.csv", io.BytesIO(csv_bytes), "text/csv"),
|
|
245
|
+
},
|
|
246
|
+
idempotent=False,
|
|
247
|
+
)
|
|
248
|
+
data = r.json()
|
|
249
|
+
return data["model_id"]
|
|
250
|
+
|
|
251
|
+
def predict(
|
|
252
|
+
self,
|
|
253
|
+
model_id: str,
|
|
254
|
+
X: pd.DataFrame,
|
|
255
|
+
*,
|
|
256
|
+
task: str = "classification",
|
|
257
|
+
output_type: str = "preds",
|
|
258
|
+
) -> Any:
|
|
259
|
+
"""
|
|
260
|
+
Run predictions on the server.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
model_id : str
|
|
265
|
+
Model ID returned by fit().
|
|
266
|
+
X : DataFrame
|
|
267
|
+
Feature matrix (same columns as training).
|
|
268
|
+
task : str
|
|
269
|
+
Task the model was fitted for.
|
|
270
|
+
output_type : str
|
|
271
|
+
"preds" for class labels, "probas" for probability arrays.
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
If output_type == "preds": list of class labels.
|
|
276
|
+
If output_type == "probas": list of lists (probability per class).
|
|
277
|
+
"""
|
|
278
|
+
buf = io.BytesIO()
|
|
279
|
+
X.to_csv(buf, index=False)
|
|
280
|
+
csv_bytes = buf.getvalue()
|
|
281
|
+
|
|
282
|
+
payload = {
|
|
283
|
+
"model_id": model_id,
|
|
284
|
+
"task": task,
|
|
285
|
+
"output_type": output_type,
|
|
286
|
+
}
|
|
287
|
+
data_field = json.dumps(payload)
|
|
288
|
+
|
|
289
|
+
r = self._post(
|
|
290
|
+
"/v1/predict",
|
|
291
|
+
lambda: {
|
|
292
|
+
"data": (None, data_field, "application/json"),
|
|
293
|
+
"file": ("predict.csv", io.BytesIO(csv_bytes), "text/csv"),
|
|
294
|
+
},
|
|
295
|
+
idempotent=True,
|
|
296
|
+
)
|
|
297
|
+
data = r.json()
|
|
298
|
+
return data["prediction"]
|
|
299
|
+
|
|
300
|
+
def health(self) -> bool:
|
|
301
|
+
"""Return True if the server is reachable."""
|
|
302
|
+
try:
|
|
303
|
+
r = requests.get(f"{self.base_url}/health", timeout=5.0)
|
|
304
|
+
return r.status_code == 200
|
|
305
|
+
except Exception:
|
|
306
|
+
return False
|
tabapi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tabapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: scikit-learn-style Python client for tabular foundation models (TabICL) served over HTTP
|
|
5
|
+
Project-URL: Homepage, https://probabl.ai
|
|
6
|
+
Author: Probabl
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: api-client,foundation-model,in-context-learning,scikit-learn,tabicl,tabular
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: numpy>=1.24.0
|
|
24
|
+
Requires-Dist: pandas>=2.0.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# tabapi
|
|
32
|
+
|
|
33
|
+
A scikit-learn-style Python client for tabular foundation models (TFMs) such as
|
|
34
|
+
[TabICL](https://github.com/soda-inria/tabicl), served over HTTP.
|
|
35
|
+
|
|
36
|
+
TFMs predict by in-context learning: there is no training loop on your machine.
|
|
37
|
+
`tabapi` sends your labelled rows to a TabAPI server, which runs the model, and
|
|
38
|
+
gives you back predictions through the familiar `fit` / `predict` /
|
|
39
|
+
`predict_proba` interface. You don't need torch or a GPU locally.
|
|
40
|
+
|
|
41
|
+
> **Status: alpha.** The API may change between minor versions. You need access
|
|
42
|
+
> to a running TabAPI server.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install tabapi
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requires Python 3.10+.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from sklearn.datasets import load_breast_cancer
|
|
56
|
+
from sklearn.model_selection import train_test_split
|
|
57
|
+
from tabapi import TabAPIClassifier
|
|
58
|
+
|
|
59
|
+
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
|
|
60
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
|
|
61
|
+
|
|
62
|
+
clf = TabAPIClassifier(base_url="https://your-tabapi-server.example.com")
|
|
63
|
+
clf.fit(X_train, y_train) # uploads the training context, gets a model id
|
|
64
|
+
y_pred = clf.predict(X_test)
|
|
65
|
+
proba = clf.predict_proba(X_test)
|
|
66
|
+
print(clf.score(X_test, y_test))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`TabAPIClassifier` is a regular scikit-learn estimator, so it works with
|
|
70
|
+
`cross_val_score`, `Pipeline`, `clone`, and so on.
|
|
71
|
+
|
|
72
|
+
## Authentication
|
|
73
|
+
|
|
74
|
+
If the server requires a bearer token, pass `token=...` or set the
|
|
75
|
+
`TABAPI_API_TOKEN` environment variable. The environment variable is read at
|
|
76
|
+
request time and is never stored on the estimator, so it does not end up in a
|
|
77
|
+
pickled model.
|
|
78
|
+
|
|
79
|
+
## Retries
|
|
80
|
+
|
|
81
|
+
Requests the server could not take (busy server, proxy with no healthy backend)
|
|
82
|
+
are retried with exponential backoff, honouring `Retry-After`. Tune this with
|
|
83
|
+
`max_retries` and `backoff_factor`, or turn it off with `max_retries=0`.
|
|
84
|
+
|
|
85
|
+
## Low-level client
|
|
86
|
+
|
|
87
|
+
`TabAPIClient` exposes the raw HTTP calls (`fit`, `predict`, `health`) if you
|
|
88
|
+
don't want the scikit-learn wrapper.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
tabapi/__init__.py,sha256=1CXIF99E1z7TUnz0piwGJS5mVXqJBjZYEm_fXggdDxQ,245
|
|
2
|
+
tabapi/_base.py,sha256=u_TB3g8ScBgrymHWL5V4szgBOQeqxU4kLSg0Pep6zJw,2061
|
|
3
|
+
tabapi/classifier.py,sha256=mLhyPX7Gy6WeDiBka6mMFEiszRr8_m9w6UNViXPdeBw,4482
|
|
4
|
+
tabapi/client.py,sha256=a4AnoWsHlE0lyich9iZgo-JnHnDaG050q7UB711MW-k,10155
|
|
5
|
+
tabapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
tabapi-0.1.0.dist-info/METADATA,sha256=-OwG9-vSKj_dpOGaY0MiduEaMElUF9BhRKvkXheMx2A,3168
|
|
7
|
+
tabapi-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
tabapi-0.1.0.dist-info/licenses/LICENSE,sha256=p5FMhe5GUbgQ_RZASK7inMy3EB_i58z0O6YuI2wVLtA,1494
|
|
9
|
+
tabapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Probabl
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|