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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proxyml"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "Python SDK for calling the ProxyML API"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -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={})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -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 test_put_schema_success(mock_put):
143
- mock_put.return_value = _mock_response(200, {"status": "ok"})
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 == {"status": "ok"}
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.assert_called_once_with(
193
- endpoint="/synthesize/neighbors", payload={"n": 2}
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