proxyml 0.1.3__tar.gz → 0.1.5__tar.gz
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.
- {proxyml-0.1.3 → proxyml-0.1.5}/PKG-INFO +1 -1
- {proxyml-0.1.3 → proxyml-0.1.5}/pyproject.toml +1 -1
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml/__init__.py +10 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml/client.py +76 -6
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml.egg-info/PKG-INFO +1 -1
- {proxyml-0.1.3 → proxyml-0.1.5}/tests/test_client.py +139 -7
- {proxyml-0.1.3 → proxyml-0.1.5}/LICENSE +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/README.md +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/setup.cfg +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml/schema.py +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml.egg-info/SOURCES.txt +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml.egg-info/dependency_links.txt +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml.egg-info/requires.txt +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/src/proxyml.egg-info/top_level.txt +0 -0
- {proxyml-0.1.3 → proxyml-0.1.5}/tests/test_schema.py +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from proxyml.client import (
|
|
2
2
|
put_schema,
|
|
3
|
+
fetch_schema,
|
|
4
|
+
list_schemas,
|
|
5
|
+
delete_schema,
|
|
3
6
|
synthesize_data,
|
|
4
7
|
train_surrogate,
|
|
5
8
|
predict,
|
|
@@ -12,6 +15,8 @@ from proxyml.client import (
|
|
|
12
15
|
diff_models,
|
|
13
16
|
list_models,
|
|
14
17
|
delete_model,
|
|
18
|
+
get_usage,
|
|
19
|
+
rotate_key,
|
|
15
20
|
)
|
|
16
21
|
from proxyml.schema import (
|
|
17
22
|
get_schema,
|
|
@@ -22,6 +27,9 @@ from proxyml.schema import (
|
|
|
22
27
|
|
|
23
28
|
__all__ = [
|
|
24
29
|
"put_schema",
|
|
30
|
+
"fetch_schema",
|
|
31
|
+
"list_schemas",
|
|
32
|
+
"delete_schema",
|
|
25
33
|
"synthesize_data",
|
|
26
34
|
"train_surrogate",
|
|
27
35
|
"predict",
|
|
@@ -34,6 +42,8 @@ __all__ = [
|
|
|
34
42
|
"diff_models",
|
|
35
43
|
"list_models",
|
|
36
44
|
"delete_model",
|
|
45
|
+
"get_usage",
|
|
46
|
+
"rotate_key",
|
|
37
47
|
"get_schema",
|
|
38
48
|
"gen_continuous_schema",
|
|
39
49
|
"gen_categorical_schema",
|
|
@@ -61,10 +61,10 @@ def delete(endpoint: str) -> requests.models.Response:
|
|
|
61
61
|
return r
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def put_schema(schema: dict):
|
|
65
|
-
r = put(endpoint='/schema', payload=schema)
|
|
64
|
+
def put_schema(schema: dict, name: str = "default"):
|
|
65
|
+
r = put(endpoint=f'/schema/{name}', payload=schema)
|
|
66
66
|
if r.status_code == 200:
|
|
67
|
-
logger.info("Schema uploaded successfully")
|
|
67
|
+
logger.info("Schema '%s' uploaded successfully", name)
|
|
68
68
|
return r.json()
|
|
69
69
|
logger.error(
|
|
70
70
|
"Schema upload failed with status %s: %s",
|
|
@@ -74,6 +74,48 @@ def put_schema(schema: dict):
|
|
|
74
74
|
return None
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def fetch_schema(name: str = "default") -> dict | None:
|
|
78
|
+
"""Retrieve a stored feature schema by name."""
|
|
79
|
+
r = get(endpoint=f'/schema/{name}', params={})
|
|
80
|
+
if r.status_code == 200:
|
|
81
|
+
return r.json()
|
|
82
|
+
logger.error(
|
|
83
|
+
"Fetch schema failed with status %s: %s",
|
|
84
|
+
r.status_code,
|
|
85
|
+
r.text,
|
|
86
|
+
)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def list_schemas() -> list[dict] | None:
|
|
91
|
+
"""Return all named schemas for the authenticated user."""
|
|
92
|
+
r = get(endpoint='/schemas', params={})
|
|
93
|
+
if r.status_code == 200:
|
|
94
|
+
return r.json()['schemas']
|
|
95
|
+
logger.error(
|
|
96
|
+
"List schemas failed with status %s: %s",
|
|
97
|
+
r.status_code,
|
|
98
|
+
r.text,
|
|
99
|
+
)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def delete_schema(name: str) -> bool:
|
|
104
|
+
"""Delete a named schema. Returns True on success, False if not found."""
|
|
105
|
+
r = delete(endpoint=f'/schema/{name}')
|
|
106
|
+
if r.status_code == 204:
|
|
107
|
+
return True
|
|
108
|
+
if r.status_code == 404:
|
|
109
|
+
logger.warning("Schema '%s' not found", name)
|
|
110
|
+
return False
|
|
111
|
+
logger.error(
|
|
112
|
+
"Delete schema failed with status %s: %s",
|
|
113
|
+
r.status_code,
|
|
114
|
+
r.text,
|
|
115
|
+
)
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
77
119
|
def _cast_column(series: pd.Series, ftype: str) -> pd.Series:
|
|
78
120
|
if ftype in ("continuous",):
|
|
79
121
|
return series.astype(float)
|
|
@@ -87,11 +129,11 @@ def _cast_column(series: pd.Series, ftype: str) -> pd.Series:
|
|
|
87
129
|
return series
|
|
88
130
|
|
|
89
131
|
|
|
90
|
-
def synthesize_data(num_points: int = 100, sample: list | None = None, as_df: bool = True):
|
|
132
|
+
def synthesize_data(num_points: int = 100, sample: list | None = None, as_df: bool = True, schema_name: str = "default"):
|
|
91
133
|
if sample is None:
|
|
92
|
-
r = post(endpoint='/synthesize/neighbors', payload={'n': num_points})
|
|
134
|
+
r = post(endpoint='/synthesize/neighbors', payload={'n': num_points, 'schema_name': schema_name})
|
|
93
135
|
else:
|
|
94
|
-
r = post(endpoint='/synthesize/blended', payload={'n': num_points, 'instance': [col.item() for col in sample]})
|
|
136
|
+
r = post(endpoint='/synthesize/blended', payload={'n': num_points, 'instance': [col.item() for col in sample], 'schema_name': schema_name})
|
|
95
137
|
if r.status_code == 200:
|
|
96
138
|
payload = r.json()
|
|
97
139
|
if as_df:
|
|
@@ -114,6 +156,7 @@ def train_surrogate(
|
|
|
114
156
|
feature_names: list[str] | None,
|
|
115
157
|
task: str = 'auto',
|
|
116
158
|
test_size: float = 0.2,
|
|
159
|
+
schema_name: str = "default",
|
|
117
160
|
name: str | None = None,
|
|
118
161
|
comments: str | None = None,
|
|
119
162
|
):
|
|
@@ -123,6 +166,7 @@ def train_surrogate(
|
|
|
123
166
|
'feature_names': feature_names,
|
|
124
167
|
'task': task,
|
|
125
168
|
'test_size': test_size,
|
|
169
|
+
'schema_name': schema_name,
|
|
126
170
|
}
|
|
127
171
|
if name is not None:
|
|
128
172
|
payload['name'] = name
|
|
@@ -316,6 +360,32 @@ def get_feature_importances(version: str | None = None):
|
|
|
316
360
|
return None
|
|
317
361
|
|
|
318
362
|
|
|
363
|
+
def get_usage() -> dict | None:
|
|
364
|
+
"""Return current tier, usage counts, and quota for the authenticated user."""
|
|
365
|
+
r = get(endpoint='/account/usage', params={})
|
|
366
|
+
if r.status_code == 200:
|
|
367
|
+
return r.json()
|
|
368
|
+
logger.error(
|
|
369
|
+
"Get usage failed with status %s: %s",
|
|
370
|
+
r.status_code,
|
|
371
|
+
r.text,
|
|
372
|
+
)
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def rotate_key() -> str | None:
|
|
377
|
+
"""Rotate the API key, revoking all old keys. Returns the new key string or None on failure."""
|
|
378
|
+
r = post(endpoint='/account/keys/rotate', payload={})
|
|
379
|
+
if r.status_code == 201:
|
|
380
|
+
return r.json()['api_key']
|
|
381
|
+
logger.error(
|
|
382
|
+
"Key rotation failed with status %s: %s",
|
|
383
|
+
r.status_code,
|
|
384
|
+
r.text,
|
|
385
|
+
)
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
|
|
319
389
|
def list_models() -> list[dict] | None:
|
|
320
390
|
"""Return metadata for all trained surrogate models, newest first."""
|
|
321
391
|
r = get(endpoint='/surrogate/models', params={})
|
|
@@ -8,14 +8,19 @@ from proxyml.client import (
|
|
|
8
8
|
_cast_column,
|
|
9
9
|
_headers,
|
|
10
10
|
delete_model,
|
|
11
|
+
delete_schema,
|
|
11
12
|
diff_models,
|
|
13
|
+
fetch_schema,
|
|
12
14
|
find_counterfactuals,
|
|
13
15
|
get_model_summary,
|
|
16
|
+
get_usage,
|
|
14
17
|
interpret_counterfactual,
|
|
15
18
|
list_models,
|
|
19
|
+
list_schemas,
|
|
16
20
|
predict,
|
|
17
21
|
predict_batch,
|
|
18
22
|
put_schema,
|
|
23
|
+
rotate_key,
|
|
19
24
|
synthesize_data,
|
|
20
25
|
train_surrogate,
|
|
21
26
|
)
|
|
@@ -139,11 +144,18 @@ def _mock_response(status_code, json_body):
|
|
|
139
144
|
|
|
140
145
|
|
|
141
146
|
@patch("proxyml.client.put")
|
|
142
|
-
def
|
|
143
|
-
mock_put.return_value = _mock_response(200, {"
|
|
147
|
+
def test_put_schema_default(mock_put):
|
|
148
|
+
mock_put.return_value = _mock_response(200, {"features": []})
|
|
144
149
|
result = put_schema({"features": []})
|
|
145
|
-
assert result == {"
|
|
146
|
-
mock_put.assert_called_once_with(endpoint="/schema", payload={"features": []})
|
|
150
|
+
assert result == {"features": []}
|
|
151
|
+
mock_put.assert_called_once_with(endpoint="/schema/default", payload={"features": []})
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@patch("proxyml.client.put")
|
|
155
|
+
def test_put_schema_named(mock_put):
|
|
156
|
+
mock_put.return_value = _mock_response(200, {"features": []})
|
|
157
|
+
put_schema({"features": []}, name="credit")
|
|
158
|
+
mock_put.assert_called_once_with(endpoint="/schema/credit", payload={"features": []})
|
|
147
159
|
|
|
148
160
|
|
|
149
161
|
@patch("proxyml.client.put")
|
|
@@ -153,6 +165,60 @@ def test_put_schema_failure(mock_put):
|
|
|
153
165
|
assert result is None
|
|
154
166
|
|
|
155
167
|
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# fetch_schema / list_schemas / delete_schema
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
@patch("proxyml.client.get")
|
|
173
|
+
def test_fetch_schema_default(mock_get):
|
|
174
|
+
mock_get.return_value = _mock_response(200, {"features": [{"type": "continuous", "name": "age"}]})
|
|
175
|
+
result = fetch_schema()
|
|
176
|
+
assert result["features"][0]["name"] == "age"
|
|
177
|
+
mock_get.assert_called_once_with(endpoint="/schema/default", params={})
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@patch("proxyml.client.get")
|
|
181
|
+
def test_fetch_schema_named(mock_get):
|
|
182
|
+
mock_get.return_value = _mock_response(200, {"features": []})
|
|
183
|
+
fetch_schema(name="credit")
|
|
184
|
+
mock_get.assert_called_once_with(endpoint="/schema/credit", params={})
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@patch("proxyml.client.get")
|
|
188
|
+
def test_fetch_schema_not_found_returns_none(mock_get):
|
|
189
|
+
mock_get.return_value = _mock_response(404, {"detail": "not found"})
|
|
190
|
+
assert fetch_schema("missing") is None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@patch("proxyml.client.get")
|
|
194
|
+
def test_list_schemas_success(mock_get):
|
|
195
|
+
schemas = [{"name": "default", "updated_at": "2026-04-22T10:00:00"},
|
|
196
|
+
{"name": "credit", "updated_at": "2026-04-22T11:00:00"}]
|
|
197
|
+
mock_get.return_value = _mock_response(200, {"schemas": schemas})
|
|
198
|
+
result = list_schemas()
|
|
199
|
+
assert result == schemas
|
|
200
|
+
mock_get.assert_called_once_with(endpoint="/schemas", params={})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@patch("proxyml.client.get")
|
|
204
|
+
def test_list_schemas_failure_returns_none(mock_get):
|
|
205
|
+
mock_get.return_value = _mock_response(401, {"detail": "unauthorized"})
|
|
206
|
+
assert list_schemas() is None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@patch("proxyml.client.delete")
|
|
210
|
+
def test_delete_schema_success(mock_del):
|
|
211
|
+
mock_del.return_value = _mock_response(204, None)
|
|
212
|
+
assert delete_schema("credit") is True
|
|
213
|
+
mock_del.assert_called_once_with(endpoint="/schema/credit")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@patch("proxyml.client.delete")
|
|
217
|
+
def test_delete_schema_not_found(mock_del):
|
|
218
|
+
mock_del.return_value = _mock_response(404, {"detail": "not found"})
|
|
219
|
+
assert delete_schema("no-such-schema") is False
|
|
220
|
+
|
|
221
|
+
|
|
156
222
|
@patch("proxyml.client.post")
|
|
157
223
|
def test_predict_success(mock_post):
|
|
158
224
|
mock_post.return_value = _mock_response(200, {"prediction": 1, "probability": 0.9})
|
|
@@ -189,9 +255,19 @@ def test_synthesize_data_no_sample(mock_post):
|
|
|
189
255
|
df = synthesize_data(num_points=2, sample=None)
|
|
190
256
|
assert isinstance(df, pd.DataFrame)
|
|
191
257
|
assert list(df.columns) == ["f_cont", "f_cat"]
|
|
192
|
-
mock_post.
|
|
193
|
-
|
|
194
|
-
|
|
258
|
+
payload = mock_post.call_args.kwargs["payload"]
|
|
259
|
+
assert payload["n"] == 2
|
|
260
|
+
assert payload["schema_name"] == "default"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@patch("proxyml.client.post")
|
|
264
|
+
def test_synthesize_data_named_schema(mock_post):
|
|
265
|
+
mock_post.return_value = _mock_response(200, {
|
|
266
|
+
"samples": [[1.0]], "feature_names": ["x"], "feature_types": ["continuous"],
|
|
267
|
+
})
|
|
268
|
+
synthesize_data(num_points=1, schema_name="credit")
|
|
269
|
+
payload = mock_post.call_args.kwargs["payload"]
|
|
270
|
+
assert payload["schema_name"] == "credit"
|
|
195
271
|
|
|
196
272
|
|
|
197
273
|
@patch("proxyml.client.post")
|
|
@@ -215,10 +291,19 @@ def test_train_surrogate_with_metadata(mock_post):
|
|
|
215
291
|
payload = mock_post.call_args.kwargs["payload"]
|
|
216
292
|
assert payload["name"] == "v1"
|
|
217
293
|
assert payload["comments"] == "test run"
|
|
294
|
+
assert payload["schema_name"] == "default"
|
|
218
295
|
assert result["version"] == "abc-123"
|
|
219
296
|
assert result["trained_at"] == "2026-04-19T12:00:00"
|
|
220
297
|
|
|
221
298
|
|
|
299
|
+
@patch("proxyml.client.post")
|
|
300
|
+
def test_train_surrogate_named_schema(mock_post):
|
|
301
|
+
mock_post.return_value = _mock_response(200, {"version": "abc-123"})
|
|
302
|
+
train_surrogate(samples=[[1.0]], predictions=[1.0], feature_names=None, schema_name="credit")
|
|
303
|
+
payload = mock_post.call_args.kwargs["payload"]
|
|
304
|
+
assert payload["schema_name"] == "credit"
|
|
305
|
+
|
|
306
|
+
|
|
222
307
|
@patch("proxyml.client.post")
|
|
223
308
|
def test_train_surrogate_omits_none_metadata(mock_post):
|
|
224
309
|
mock_post.return_value = _mock_response(200, {"version": "abc-123"})
|
|
@@ -420,3 +505,50 @@ def test_diff_models_success(mock_get):
|
|
|
420
505
|
def test_diff_models_failure_returns_none(mock_get):
|
|
421
506
|
mock_get.return_value = _mock_response(422, {"detail": "different tasks"})
|
|
422
507
|
assert diff_models(version_a="aaa-111", version_b="bbb-222") is None
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# ---------------------------------------------------------------------------
|
|
511
|
+
# get_usage
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
_USAGE_RESPONSE = {
|
|
515
|
+
"tier": "hobbyist",
|
|
516
|
+
"period": "2026-04",
|
|
517
|
+
"calls_this_period": 42,
|
|
518
|
+
"calls_limit": 1000,
|
|
519
|
+
"calls_remaining": 958,
|
|
520
|
+
"surrogates_trained": 2,
|
|
521
|
+
"surrogate_limit": 3,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@patch("proxyml.client.get")
|
|
526
|
+
def test_get_usage_success(mock_get):
|
|
527
|
+
mock_get.return_value = _mock_response(200, _USAGE_RESPONSE)
|
|
528
|
+
result = get_usage()
|
|
529
|
+
assert result == _USAGE_RESPONSE
|
|
530
|
+
mock_get.assert_called_once_with(endpoint="/account/usage", params={})
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@patch("proxyml.client.get")
|
|
534
|
+
def test_get_usage_failure_returns_none(mock_get):
|
|
535
|
+
mock_get.return_value = _mock_response(401, {"detail": "unauthorized"})
|
|
536
|
+
assert get_usage() is None
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# ---------------------------------------------------------------------------
|
|
540
|
+
# rotate_key
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
@patch("proxyml.client.post")
|
|
544
|
+
def test_rotate_key_success(mock_post):
|
|
545
|
+
mock_post.return_value = _mock_response(201, {"api_key": "proxyml_new_secret_key"})
|
|
546
|
+
result = rotate_key()
|
|
547
|
+
assert result == "proxyml_new_secret_key"
|
|
548
|
+
mock_post.assert_called_once_with(endpoint="/account/keys/rotate", payload={})
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@patch("proxyml.client.post")
|
|
552
|
+
def test_rotate_key_failure_returns_none(mock_post):
|
|
553
|
+
mock_post.return_value = _mock_response(403, {"detail": "Key rotation is not available for test accounts"})
|
|
554
|
+
assert rotate_key() is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|