proxyml 0.1.9__tar.gz → 0.2.0__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.9
3
+ Version: 0.2.0
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -221,9 +221,6 @@ Dynamic: license-file
221
221
 
222
222
  Python SDK for the [ProxyML API](https://proxyml.ai).
223
223
 
224
- > **Status:** Early access — server endpoints coming soon.
225
- > [Request early access](mailto:contact@proxyml.ai) or star this repo to follow progress.
226
-
227
224
  <img width="577" height="115" alt="creditg_car_counterfactual" src="https://github.com/user-attachments/assets/8f913c95-1190-4b3c-ba7d-6690e9ebb3e0" />
228
225
 
229
226
  ## Why ProxyML?
@@ -316,7 +313,7 @@ See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough and [`docs
316
313
  | `put_schema(schema)` | Upload a schema to the API |
317
314
  | `synthesize_data(num_points, sample, as_df)` | Generate synthetic data points |
318
315
  | `train_surrogate(samples, predictions, feature_names, task, test_size)` | Train a surrogate model |
319
- | `predict(samples, version)` | Score samples with the surrogate model |
316
+ | `predict(sample, version)` | Score a single sample with the surrogate model |
320
317
  | `find_counterfactual(sample, target, ...)` | Find a counterfactual for a given sample |
321
318
  | `interpret_counterfactual(sample, counterfactual, ...)` | Generate a human-readable explanation |
322
319
 
@@ -2,9 +2,6 @@
2
2
 
3
3
  Python SDK for the [ProxyML API](https://proxyml.ai).
4
4
 
5
- > **Status:** Early access — server endpoints coming soon.
6
- > [Request early access](mailto:contact@proxyml.ai) or star this repo to follow progress.
7
-
8
5
  <img width="577" height="115" alt="creditg_car_counterfactual" src="https://github.com/user-attachments/assets/8f913c95-1190-4b3c-ba7d-6690e9ebb3e0" />
9
6
 
10
7
  ## Why ProxyML?
@@ -97,7 +94,7 @@ See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough and [`docs
97
94
  | `put_schema(schema)` | Upload a schema to the API |
98
95
  | `synthesize_data(num_points, sample, as_df)` | Generate synthetic data points |
99
96
  | `train_surrogate(samples, predictions, feature_names, task, test_size)` | Train a surrogate model |
100
- | `predict(samples, version)` | Score samples with the surrogate model |
97
+ | `predict(sample, version)` | Score a single sample with the surrogate model |
101
98
  | `find_counterfactual(sample, target, ...)` | Find a counterfactual for a given sample |
102
99
  | `interpret_counterfactual(sample, counterfactual, ...)` | Generate a human-readable explanation |
103
100
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proxyml"
7
- version = "0.1.9"
7
+ version = "0.2.0"
8
8
  description = "Python SDK for calling the ProxyML API"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -20,6 +20,8 @@ from proxyml.client import (
20
20
  get_usage,
21
21
  rotate_key,
22
22
  explain_local,
23
+ explain_local_batch,
24
+ update_model,
23
25
  )
24
26
  from proxyml.schema import (
25
27
  get_schema,
@@ -50,6 +52,8 @@ __all__ = [
50
52
  "get_usage",
51
53
  "rotate_key",
52
54
  "explain_local",
55
+ "explain_local_batch",
56
+ "update_model",
53
57
  "get_schema",
54
58
  "gen_continuous_schema",
55
59
  "gen_categorical_schema",
@@ -90,6 +90,25 @@ def get(endpoint: str, params: dict) -> requests.models.Response:
90
90
  return r
91
91
 
92
92
 
93
+ def patch(endpoint: str, payload: dict) -> requests.models.Response:
94
+ """
95
+ PATCHes a request to a ProxyML API endpoint.
96
+
97
+ Args:
98
+ endpoint (str): ProxyML API endpoint.
99
+ payload (dict): JSON payload to PATCH.
100
+
101
+ Returns:
102
+ requests Response object.
103
+ """
104
+ r = requests.patch(
105
+ url=f'{_base_url()}{endpoint}',
106
+ data=orjson.dumps(payload, option=orjson.OPT_SERIALIZE_NUMPY),
107
+ headers=_headers()
108
+ )
109
+ return r
110
+
111
+
93
112
  def delete(endpoint: str) -> requests.models.Response:
94
113
  """
95
114
  DELETE request to a ProxyML API endpoint.
@@ -599,11 +618,19 @@ def rotate_key() -> str | None:
599
618
  return None
600
619
 
601
620
 
602
- def list_models() -> list[dict] | None:
603
- """Return metadata for all trained surrogate models, newest first."""
604
- r = get(endpoint='/surrogate/models', params={})
621
+ def list_models(limit: int = 50, offset: int = 0) -> dict | None:
622
+ """Return a page of trained surrogate models, newest first.
623
+
624
+ Args:
625
+ limit (int): maximum number of models to return (1–200, default 50).
626
+ offset (int): number of models to skip for pagination (default 0).
627
+ Returns:
628
+ Dict with ``models`` (list of metadata dicts) and ``total`` (int,
629
+ total number of surrogates regardless of pagination), or None on failure.
630
+ """
631
+ r = get(endpoint='/surrogate/models', params={'limit': limit, 'offset': offset})
605
632
  if r.status_code == 200:
606
- return r.json()['models']
633
+ return r.json()
607
634
  logger.error(
608
635
  "List models failed with status %s: %s",
609
636
  r.status_code,
@@ -612,6 +639,30 @@ def list_models() -> list[dict] | None:
612
639
  return None
613
640
 
614
641
 
642
+ def explain_local_batch(instances: list[list], version: str | None = None) -> dict | None:
643
+ """Per-feature contribution breakdown for multiple instances in one call.
644
+
645
+ Args:
646
+ instances (list[list]): list of feature vectors, each in schema order.
647
+ version (str): surrogate version UUID. None uses the latest version.
648
+ Returns:
649
+ Dict with ``results`` (one contribution entry per instance), ``model_version``,
650
+ ``task``, and ``schema_warning``, or None on failure.
651
+ """
652
+ payload: dict = {"instances": instances}
653
+ if version is not None:
654
+ payload["version"] = version
655
+ r = post(endpoint="/explain/local/batch", payload=payload)
656
+ if r.status_code == 200:
657
+ return r.json()
658
+ logger.error(
659
+ "Batch local explanation failed with status %s: %s",
660
+ r.status_code,
661
+ r.text,
662
+ )
663
+ return None
664
+
665
+
615
666
  def explain_local(instance: list, version: str | None = None) -> dict | None:
616
667
  """Per-feature contribution breakdown for a single instance.
617
668
 
@@ -633,6 +684,37 @@ def explain_local(instance: list, version: str | None = None) -> dict | None:
633
684
  return None
634
685
 
635
686
 
687
+ def update_model(version: str, name: str | None = ..., comments: str | None = ...) -> dict | None:
688
+ """Update the name and/or comments of a surrogate without retraining.
689
+
690
+ Pass a string to set the field, or ``None`` to clear it. Omit a parameter
691
+ entirely to leave that field unchanged.
692
+
693
+ Args:
694
+ version (str): UUID of the surrogate to update.
695
+ name (str | None): new name, None to clear, or omit to leave unchanged.
696
+ comments (str | None): new comments, None to clear, or omit to leave unchanged.
697
+ Returns:
698
+ Updated model metadata dict, or None on failure.
699
+ """
700
+ payload: dict = {}
701
+ if name is not ...:
702
+ payload["name"] = name
703
+ if comments is not ...:
704
+ payload["comments"] = comments
705
+ if not payload:
706
+ raise ValueError("Provide at least one of: name, comments")
707
+ r = patch(endpoint=f'/surrogate/models/{version}', payload=payload)
708
+ if r.status_code == 200:
709
+ return r.json()
710
+ logger.error(
711
+ "Update model failed with status %s: %s",
712
+ r.status_code,
713
+ r.text,
714
+ )
715
+ return None
716
+
717
+
636
718
  def delete_model(model_id: str) -> bool:
637
719
  """Delete a surrogate model by its UUID. Returns True on success, False if not found."""
638
720
  r = delete(endpoint=f'/surrogate/models/{model_id}')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -221,9 +221,6 @@ Dynamic: license-file
221
221
 
222
222
  Python SDK for the [ProxyML API](https://proxyml.ai).
223
223
 
224
- > **Status:** Early access — server endpoints coming soon.
225
- > [Request early access](mailto:contact@proxyml.ai) or star this repo to follow progress.
226
-
227
224
  <img width="577" height="115" alt="creditg_car_counterfactual" src="https://github.com/user-attachments/assets/8f913c95-1190-4b3c-ba7d-6690e9ebb3e0" />
228
225
 
229
226
  ## Why ProxyML?
@@ -316,7 +313,7 @@ See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough and [`docs
316
313
  | `put_schema(schema)` | Upload a schema to the API |
317
314
  | `synthesize_data(num_points, sample, as_df)` | Generate synthetic data points |
318
315
  | `train_surrogate(samples, predictions, feature_names, task, test_size)` | Train a surrogate model |
319
- | `predict(samples, version)` | Score samples with the surrogate model |
316
+ | `predict(sample, version)` | Score a single sample with the surrogate model |
320
317
  | `find_counterfactual(sample, target, ...)` | Find a counterfactual for a given sample |
321
318
  | `interpret_counterfactual(sample, counterfactual, ...)` | Generate a human-readable explanation |
322
319
 
@@ -12,6 +12,7 @@ from proxyml.client import (
12
12
  delete_schema,
13
13
  diff_models,
14
14
  explain_local,
15
+ explain_local_batch,
15
16
  export_surrogate,
16
17
  fetch_schema,
17
18
  find_counterfactual,
@@ -29,6 +30,7 @@ from proxyml.client import (
29
30
  rotate_key,
30
31
  synthesize_data,
31
32
  train_surrogate,
33
+ update_model,
32
34
  )
33
35
 
34
36
 
@@ -345,11 +347,24 @@ def test_train_surrogate_omits_none_metadata(mock_post):
345
347
  def test_list_models_success(mock_get):
346
348
  models = [{"version": "abc-123", "task": "regression", "name": "v1",
347
349
  "comments": None, "feature_names": None, "metrics": {"r2": 0.9},
348
- "trained_at": "2026-04-19T12:00:00"}]
349
- mock_get.return_value = _mock_response(200, {"models": models})
350
+ "trained_at": "2026-04-19T12:00:00", "mlflow_run_id": None}]
351
+ mock_get.return_value = _mock_response(200, {"models": models, "total": 1})
350
352
  result = list_models()
351
- assert result == models
352
- mock_get.assert_called_once_with(endpoint="/surrogate/models", params={})
353
+ assert result == {"models": models, "total": 1}
354
+ mock_get.assert_called_once_with(endpoint="/surrogate/models", params={"limit": 50, "offset": 0})
355
+
356
+
357
+ @patch("proxyml.client.get")
358
+ def test_list_models_pagination(mock_get):
359
+ models = [{"version": f"v{i}", "task": "regression", "name": None,
360
+ "comments": None, "feature_names": None, "metrics": None,
361
+ "trained_at": "2026-05-01T00:00:00", "mlflow_run_id": None}
362
+ for i in range(10)]
363
+ mock_get.return_value = _mock_response(200, {"models": models, "total": 42})
364
+ result = list_models(limit=10, offset=20)
365
+ assert result["total"] == 42
366
+ assert len(result["models"]) == 10
367
+ mock_get.assert_called_once_with(endpoint="/surrogate/models", params={"limit": 10, "offset": 20})
353
368
 
354
369
 
355
370
  @patch("proxyml.client.get")
@@ -798,3 +813,92 @@ def test_explain_local_with_version(mock_post):
798
813
  def test_explain_local_failure_returns_none(mock_post):
799
814
  mock_post.return_value = _mock_response(422, {"detail": "bad input"})
800
815
  assert explain_local(instance=[1.0, 2.0]) is None
816
+
817
+
818
+ # ---------------------------------------------------------------------------
819
+ # explain_local_batch
820
+ # ---------------------------------------------------------------------------
821
+
822
+ _EXPLAIN_LOCAL_BATCH_RESPONSE = {
823
+ "results": [_EXPLAIN_LOCAL_RESPONSE, _EXPLAIN_LOCAL_RESPONSE],
824
+ "model_version": "surrogate-abc-regression",
825
+ "task": "regression",
826
+ "schema_warning": None,
827
+ }
828
+
829
+
830
+ @patch("proxyml.client.post")
831
+ def test_explain_local_batch_success(mock_post):
832
+ mock_post.return_value = _mock_response(200, _EXPLAIN_LOCAL_BATCH_RESPONSE)
833
+ instances = [[1.0, 2.0], [3.0, 4.0]]
834
+ result = explain_local_batch(instances=instances)
835
+ assert result == _EXPLAIN_LOCAL_BATCH_RESPONSE
836
+ payload = mock_post.call_args.kwargs["payload"]
837
+ assert payload["instances"] == instances
838
+ assert "version" not in payload
839
+
840
+
841
+ @patch("proxyml.client.post")
842
+ def test_explain_local_batch_with_version(mock_post):
843
+ mock_post.return_value = _mock_response(200, _EXPLAIN_LOCAL_BATCH_RESPONSE)
844
+ uid = "550e8400-e29b-41d4-a716-446655440000"
845
+ explain_local_batch(instances=[[1.0, 2.0]], version=uid)
846
+ payload = mock_post.call_args.kwargs["payload"]
847
+ assert payload["version"] == uid
848
+
849
+
850
+ @patch("proxyml.client.post")
851
+ def test_explain_local_batch_failure_returns_none(mock_post):
852
+ mock_post.return_value = _mock_response(422, {"detail": "bad input"})
853
+ assert explain_local_batch(instances=[[1.0, 2.0]]) is None
854
+
855
+
856
+ # ---------------------------------------------------------------------------
857
+ # update_model
858
+ # ---------------------------------------------------------------------------
859
+
860
+ @patch("proxyml.client.patch")
861
+ def test_update_model_name(mock_patch):
862
+ meta = {"version": "abc-123", "task": "regression", "name": "new name",
863
+ "comments": None, "feature_names": None, "metrics": None,
864
+ "trained_at": "2026-05-07T00:00:00", "mlflow_run_id": None}
865
+ mock_patch.return_value = _mock_response(200, meta)
866
+ result = update_model("abc-123", name="new name")
867
+ assert result == meta
868
+ payload = mock_patch.call_args.kwargs["payload"]
869
+ assert payload == {"name": "new name"}
870
+
871
+
872
+ @patch("proxyml.client.patch")
873
+ def test_update_model_comments(mock_patch):
874
+ mock_patch.return_value = _mock_response(200, {})
875
+ update_model("abc-123", comments="some notes")
876
+ payload = mock_patch.call_args.kwargs["payload"]
877
+ assert payload == {"comments": "some notes"}
878
+
879
+
880
+ @patch("proxyml.client.patch")
881
+ def test_update_model_both_fields(mock_patch):
882
+ mock_patch.return_value = _mock_response(200, {})
883
+ update_model("abc-123", name="prod", comments="v2 data")
884
+ payload = mock_patch.call_args.kwargs["payload"]
885
+ assert payload == {"name": "prod", "comments": "v2 data"}
886
+
887
+
888
+ @patch("proxyml.client.patch")
889
+ def test_update_model_clear_field(mock_patch):
890
+ mock_patch.return_value = _mock_response(200, {})
891
+ update_model("abc-123", comments=None)
892
+ payload = mock_patch.call_args.kwargs["payload"]
893
+ assert payload == {"comments": None}
894
+
895
+
896
+ def test_update_model_no_fields_raises():
897
+ with pytest.raises(ValueError):
898
+ update_model("abc-123")
899
+
900
+
901
+ @patch("proxyml.client.patch")
902
+ def test_update_model_failure_returns_none(mock_patch):
903
+ mock_patch.return_value = _mock_response(404, {"detail": "not found"})
904
+ assert update_model("abc-123", name="x") is None
File without changes
File without changes
File without changes
File without changes