proxyml 0.2.0__tar.gz → 0.2.2__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.2.0
3
+ Version: 0.2.2
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.2.0"
7
+ version = "0.2.2"
8
8
  description = "Python SDK for calling the ProxyML API"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -1,4 +1,5 @@
1
1
  from proxyml.client import (
2
+ health_check,
2
3
  put_schema,
3
4
  fetch_schema,
4
5
  list_schemas,
@@ -31,6 +32,7 @@ from proxyml.schema import (
31
32
  )
32
33
 
33
34
  __all__ = [
35
+ "health_check",
34
36
  "put_schema",
35
37
  "fetch_schema",
36
38
  "list_schemas",
@@ -1,16 +1,26 @@
1
1
  import logging
2
- logger = logging.getLogger(__name__)
2
+ import os
3
3
  from typing import Any
4
4
 
5
5
  import orjson
6
- import requests
7
- import os
8
6
  import numpy as np
9
7
  import pandas as pd
8
+ import requests
10
9
  from pandas.api.types import is_float_dtype, is_integer_dtype
11
10
 
11
+ logger = logging.getLogger(__name__)
12
12
 
13
13
  _BOOL_STRINGS = {"true", "false"}
14
+ _TIMEOUT = 120
15
+
16
+
17
+ class _ErrorResponse:
18
+ """Returned by HTTP helpers when a network-level error occurs."""
19
+ status_code = 0
20
+ text = ""
21
+
22
+ def json(self) -> dict:
23
+ return {}
14
24
 
15
25
 
16
26
  def _base_url() -> str:
@@ -33,7 +43,7 @@ def _headers() -> dict:
33
43
  }
34
44
 
35
45
 
36
- def post(endpoint: str, payload: dict) -> requests.models.Response:
46
+ def post(endpoint: str, payload: dict) -> requests.models.Response | _ErrorResponse:
37
47
  """
38
48
  POSTs a request to a ProxyML API endpoint.
39
49
 
@@ -44,53 +54,65 @@ def post(endpoint: str, payload: dict) -> requests.models.Response:
44
54
  Returns:
45
55
  requests Response object.
46
56
  """
47
- r = requests.post(
48
- url=f'{_base_url()}{endpoint}',
49
- data=orjson.dumps(payload, option=orjson.OPT_SERIALIZE_NUMPY),
50
- headers=_headers()
51
- )
52
- return r
57
+ try:
58
+ return requests.post(
59
+ url=f'{_base_url()}{endpoint}',
60
+ data=orjson.dumps(payload, option=orjson.OPT_SERIALIZE_NUMPY),
61
+ headers=_headers(),
62
+ timeout=_TIMEOUT,
63
+ )
64
+ except requests.exceptions.RequestException as exc:
65
+ logger.error("Network error POSTing to %s: %s", endpoint, exc)
66
+ return _ErrorResponse()
53
67
 
54
68
 
55
- def put(endpoint: str, payload: dict) -> requests.models.Response:
69
+ def put(endpoint: str, payload: dict) -> requests.models.Response | _ErrorResponse:
56
70
  """
57
71
  PUTs a request to a ProxyML API endpoint.
58
72
 
59
73
  Args:
60
74
  endpoint (str): ProxyML API endpoint. PROXYML_BASE_URL is prepended e.g. use endpoint='/schemas' not 'https://api.proxyml.ai/api/v1/schemas'.
61
- payload (dict): JSON payload to POST.
75
+ payload (dict): JSON payload to PUT.
62
76
 
63
77
  Returns:
64
78
  requests Response object.
65
- """
66
- r = requests.put(
67
- url=f'{_base_url()}{endpoint}',
68
- data=orjson.dumps(payload, option=orjson.OPT_SERIALIZE_NUMPY),
69
- headers=_headers()
70
- )
71
- return r
79
+ """
80
+ try:
81
+ return requests.put(
82
+ url=f'{_base_url()}{endpoint}',
83
+ data=orjson.dumps(payload, option=orjson.OPT_SERIALIZE_NUMPY),
84
+ headers=_headers(),
85
+ timeout=_TIMEOUT,
86
+ )
87
+ except requests.exceptions.RequestException as exc:
88
+ logger.error("Network error PUTting to %s: %s", endpoint, exc)
89
+ return _ErrorResponse()
72
90
 
73
91
 
74
- def get(endpoint: str, params: dict) -> requests.models.Response:
92
+ def get(endpoint: str, params: dict | None = None) -> requests.models.Response | _ErrorResponse:
75
93
  """
76
94
  GETs a request to a ProxyML API endpoint.
77
95
 
78
96
  Args:
79
97
  endpoint (str): ProxyML API endpoint. PROXYML_BASE_URL is prepended e.g. use endpoint='/schemas' not 'https://api.proxyml.ai/api/v1/schemas'.
80
- params (dict): JSON payload to POST.
98
+ params (dict): query parameters.
81
99
 
82
100
  Returns:
83
101
  requests Response object.
84
102
  """
85
- r = requests.get(
86
- url=f'{_base_url()}{endpoint}',
87
- headers=_headers(),
88
- params=params
89
- )
90
- return r
103
+ try:
104
+ return requests.get(
105
+ url=f'{_base_url()}{endpoint}',
106
+ headers=_headers(),
107
+ params=params,
108
+ timeout=_TIMEOUT,
109
+ )
110
+ except requests.exceptions.RequestException as exc:
111
+ logger.error("Network error GETting %s: %s", endpoint, exc)
112
+ return _ErrorResponse()
91
113
 
92
114
 
93
- def patch(endpoint: str, payload: dict) -> requests.models.Response:
115
+ def patch(endpoint: str, payload: dict) -> requests.models.Response | _ErrorResponse:
94
116
  """
95
117
  PATCHes a request to a ProxyML API endpoint.
96
118
 
@@ -101,15 +123,19 @@ def patch(endpoint: str, payload: dict) -> requests.models.Response:
101
123
  Returns:
102
124
  requests Response object.
103
125
  """
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
126
+ try:
127
+ return requests.patch(
128
+ url=f'{_base_url()}{endpoint}',
129
+ data=orjson.dumps(payload, option=orjson.OPT_SERIALIZE_NUMPY),
130
+ headers=_headers(),
131
+ timeout=_TIMEOUT,
132
+ )
133
+ except requests.exceptions.RequestException as exc:
134
+ logger.error("Network error PATCHing %s: %s", endpoint, exc)
135
+ return _ErrorResponse()
110
136
 
111
137
 
112
- def delete(endpoint: str) -> requests.models.Response:
138
+ def delete(endpoint: str) -> requests.models.Response | _ErrorResponse:
113
139
  """
114
140
  DELETE request to a ProxyML API endpoint.
115
141
 
@@ -119,20 +145,24 @@ def delete(endpoint: str) -> requests.models.Response:
119
145
  Returns:
120
146
  requests Response object.
121
147
  """
122
- r = requests.delete(
123
- url=f'{_base_url()}{endpoint}',
124
- headers=_headers()
125
- )
126
- return r
148
+ try:
149
+ return requests.delete(
150
+ url=f'{_base_url()}{endpoint}',
151
+ headers=_headers(),
152
+ timeout=_TIMEOUT,
153
+ )
154
+ except requests.exceptions.RequestException as exc:
155
+ logger.error("Network error DELETEing %s: %s", endpoint, exc)
156
+ return _ErrorResponse()
127
157
 
128
158
 
129
- def put_schema(schema: dict, name: str = "default") -> Any:
159
+ def put_schema(schema: dict, name: str) -> Any:
130
160
  """
131
161
  Uploads a data schema.
132
162
 
133
163
  Args:
134
164
  schema (dict): data schema object
135
- name (str): optional name for the schema, defaults to "default".
165
+ name (str): name for the schema.
136
166
  Returns:
137
167
  API response JSON object, or None if the return code was not 200.
138
168
  """
@@ -148,7 +178,7 @@ def put_schema(schema: dict, name: str = "default") -> Any:
148
178
  return None
149
179
 
150
180
 
151
- def fetch_schema(name: str = "default") -> dict | None:
181
+ def fetch_schema(name: str) -> dict | None:
152
182
  """Retrieve a stored feature schema by name."""
153
183
  r = get(endpoint=f'/schema/{name}', params={})
154
184
  if r.status_code == 200:
@@ -212,7 +242,7 @@ def _cast_column(series: pd.Series, ftype: str) -> pd.Series:
212
242
  return series
213
243
 
214
244
 
215
- def synthesize_data(num_points: int = 100, sample: list | None = None, as_df: bool = True, schema_name: str = "default") -> Any:
245
+ def synthesize_data(num_points: int = 100, sample: list | None = None, as_df: bool = True, *, schema_name: str) -> Any:
216
246
  """
217
247
  Synthesizes data based on a data schema.
218
248
 
@@ -220,7 +250,7 @@ def synthesize_data(num_points: int = 100, sample: list | None = None, as_df: bo
220
250
  num_points (int): number of samples to synthesize, defaults to 100.
221
251
  sample (list): if specified, synthesized data will be a blend of samples generated from the schema and a number of perturbations around this sample. If not specified (default), all synthesized data is generated by sampling from the data schema.
222
252
  as_df (bool): if True (default), data are returned as a pandas DataFrame.
223
- schema_name (str): name of the data schema to use to generate the data, defaults to "default."
253
+ schema_name (str): name of the data schema to use to generate the data.
224
254
  Returns:
225
255
  JSON object representing the synthesized data if as_df=False, a pandas DataFrame if as_df=True, or None if an error occurred.
226
256
  """
@@ -250,7 +280,8 @@ def train_surrogate(
250
280
  feature_names: list[str] | None,
251
281
  task: str = 'auto',
252
282
  test_size: float = 0.2,
253
- schema_name: str = "default",
283
+ *,
284
+ schema_name: str,
254
285
  name: str | None = None,
255
286
  comments: str | None = None,
256
287
  ) -> Any:
@@ -263,7 +294,7 @@ def train_surrogate(
263
294
  feature_names (list): names of the features (columns) in the data.
264
295
  task (str): specifies the modeling task, one of "classification," "regression," or "auto" in which case ProxyML will attempt to automatically determine the modeling task.
265
296
  test_size (float): fraction of the data to set aside for test data, defaults to 0.2.
266
- schema_name (str): name of the data schema to use, defaults to "default."
297
+ schema_name (str): name of the data schema to use.
267
298
  name (str): an optional name for the surrogate.
268
299
  comments (str): an optional comment string for the surrogate.
269
300
  Returns:
@@ -345,7 +376,7 @@ def find_counterfactual(sample, target, n_neighbors: int = 10000, perturbation_s
345
376
  sample (list): point around which neighbors will be found.
346
377
  target (Any): the desired prediction outcome, can be a string or number for classification or a target predicted value for regression.
347
378
  n_neighbors (int): the number of neighbors to search, defaults to 10000.
348
- pertubation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
379
+ perturbation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
349
380
  version (str): name of the surrogate model to run, defaults to "default."
350
381
  as_df (bool): if True, return the counterfactual as a dataframe. If False, returns JSON object.
351
382
  Returns:
@@ -467,7 +498,7 @@ def find_counterfactuals(
467
498
  samples (list): list of points around which neighbors will be found.
468
499
  target (Any): the desired prediction outcome, can be a string or number for classification or a target predicted value for regression.
469
500
  n_neighbors (int): the number of neighbors to search, defaults to 10000.
470
- pertubation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
501
+ perturbation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
471
502
  version (str): name of the surrogate model to run, defaults to "default."
472
503
  as_df (bool): if True, return the counterfactual as a dataframe. If False, returns JSON object.
473
504
  Returns:
@@ -592,6 +623,23 @@ def get_feature_importances(version: str | None = None) -> Any:
592
623
  return None
593
624
 
594
625
 
626
+ def health_check() -> dict | None:
627
+ """Check API connectivity and version. Does not require authentication and does not count against usage quota.
628
+
629
+ Returns:
630
+ Dict with ``status``, ``model_loaded``, and ``version``, or None on failure.
631
+ """
632
+ try:
633
+ r = requests.get(url=f'{_base_url()}/health', timeout=_TIMEOUT)
634
+ except requests.exceptions.RequestException as exc:
635
+ logger.error("Network error calling /health: %s", exc)
636
+ return None
637
+ if r.status_code == 200:
638
+ return r.json()
639
+ logger.error("Health check failed with status %s: %s", r.status_code, r.text)
640
+ return None
641
+
642
+
595
643
  def get_usage() -> dict | None:
596
644
  """Return current tier, usage counts, and quota for the authenticated user."""
597
645
  r = get(endpoint='/account/usage', params={})
@@ -1,11 +1,11 @@
1
1
  import logging
2
- logger = logging.getLogger(__name__)
3
2
 
4
- import os
5
3
  import numpy as np
6
4
  import pandas as pd
7
5
  from pandas.api.types import is_float_dtype, is_integer_dtype
8
6
 
7
+ logger = logging.getLogger(__name__)
8
+
9
9
 
10
10
  def gen_continuous_schema(s: pd.Series, name: str | None = None) -> dict:
11
11
  """
@@ -73,6 +73,11 @@ def get_schema(df: pd.DataFrame, immutable_cols: list[str] | None) -> dict:
73
73
  Returns:
74
74
  data schema as dict. Review and adjust as needed.
75
75
  """
76
+ if immutable_cols:
77
+ unknown = set(immutable_cols) - set(df.columns)
78
+ if unknown:
79
+ logger.warning("immutable_cols not found in DataFrame and will be ignored: %s", sorted(unknown))
80
+
76
81
  schema = {
77
82
  'features': list(),
78
83
  '_note': (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -3,11 +3,13 @@ from unittest.mock import MagicMock, patch
3
3
 
4
4
  import pandas as pd
5
5
  import pytest
6
+ import requests
6
7
 
7
8
  from proxyml.client import (
8
9
  _cast_column,
9
10
  _base_url,
10
11
  _headers,
12
+ health_check,
11
13
  delete_model,
12
14
  delete_schema,
13
15
  diff_models,
@@ -34,6 +36,31 @@ from proxyml.client import (
34
36
  )
35
37
 
36
38
 
39
+ # ---------------------------------------------------------------------------
40
+ # health_check
41
+ # ---------------------------------------------------------------------------
42
+
43
+ @patch("proxyml.client.requests.get")
44
+ def test_health_check_success(mock_get):
45
+ mock_get.return_value = _mock_response(200, {"status": "ok", "model_loaded": True, "version": "0.1.0"})
46
+ result = health_check()
47
+ assert result == {"status": "ok", "model_loaded": True, "version": "0.1.0"}
48
+ mock_get.assert_called_once()
49
+ assert mock_get.call_args.kwargs["url"].endswith("/health")
50
+ assert "headers" not in mock_get.call_args.kwargs # no auth header
51
+
52
+
53
+ @patch("proxyml.client.requests.get")
54
+ def test_health_check_failure_returns_none(mock_get):
55
+ mock_get.return_value = _mock_response(503, {"detail": "unavailable"})
56
+ assert health_check() is None
57
+
58
+
59
+ @patch("proxyml.client.requests.get", side_effect=requests.exceptions.ConnectionError("connection refused"))
60
+ def test_health_check_network_error_returns_none(mock_get):
61
+ assert health_check() is None
62
+
63
+
37
64
  # ---------------------------------------------------------------------------
38
65
  # _headers
39
66
  # ---------------------------------------------------------------------------
@@ -174,11 +201,11 @@ def _mock_response(status_code, json_body):
174
201
 
175
202
 
176
203
  @patch("proxyml.client.put")
177
- def test_put_schema_default(mock_put):
204
+ def test_put_schema_success(mock_put):
178
205
  mock_put.return_value = _mock_response(200, {"features": []})
179
- result = put_schema({"features": []})
206
+ result = put_schema({"features": []}, name="myschema")
180
207
  assert result == {"features": []}
181
- mock_put.assert_called_once_with(endpoint="/schema/default", payload={"features": []})
208
+ mock_put.assert_called_once_with(endpoint="/schema/myschema", payload={"features": []})
182
209
 
183
210
 
184
211
  @patch("proxyml.client.put")
@@ -191,7 +218,7 @@ def test_put_schema_named(mock_put):
191
218
  @patch("proxyml.client.put")
192
219
  def test_put_schema_failure(mock_put):
193
220
  mock_put.return_value = _mock_response(422, {"detail": "invalid"})
194
- result = put_schema({"features": []})
221
+ result = put_schema({"features": []}, name="myschema")
195
222
  assert result is None
196
223
 
197
224
 
@@ -200,11 +227,11 @@ def test_put_schema_failure(mock_put):
200
227
  # ---------------------------------------------------------------------------
201
228
 
202
229
  @patch("proxyml.client.get")
203
- def test_fetch_schema_default(mock_get):
230
+ def test_fetch_schema_success(mock_get):
204
231
  mock_get.return_value = _mock_response(200, {"features": [{"type": "continuous", "name": "age"}]})
205
- result = fetch_schema()
232
+ result = fetch_schema(name="myschema")
206
233
  assert result["features"][0]["name"] == "age"
207
- mock_get.assert_called_once_with(endpoint="/schema/default", params={})
234
+ mock_get.assert_called_once_with(endpoint="/schema/myschema", params={})
208
235
 
209
236
 
210
237
  @patch("proxyml.client.get")
@@ -282,12 +309,12 @@ def test_synthesize_data_no_sample(mock_post):
282
309
  "feature_names": ["f_cont", "f_cat"],
283
310
  "feature_types": ["continuous", "categorical"],
284
311
  })
285
- df = synthesize_data(num_points=2, sample=None)
312
+ df = synthesize_data(num_points=2, sample=None, schema_name="myschema")
286
313
  assert isinstance(df, pd.DataFrame)
287
314
  assert list(df.columns) == ["f_cont", "f_cat"]
288
315
  payload = mock_post.call_args.kwargs["payload"]
289
316
  assert payload["n"] == 2
290
- assert payload["schema_name"] == "default"
317
+ assert payload["schema_name"] == "myschema"
291
318
 
292
319
 
293
320
  @patch("proxyml.client.post")
@@ -303,7 +330,7 @@ def test_synthesize_data_named_schema(mock_post):
303
330
  @patch("proxyml.client.post")
304
331
  def test_synthesize_data_failure_returns_none(mock_post):
305
332
  mock_post.return_value = _mock_response(500, {})
306
- result = synthesize_data(num_points=10, sample=None)
333
+ result = synthesize_data(num_points=10, sample=None, schema_name="myschema")
307
334
  assert result is None
308
335
 
309
336
 
@@ -312,16 +339,16 @@ def test_train_surrogate_with_metadata(mock_post):
312
339
  mock_post.return_value = _mock_response(200, {
313
340
  "version": "abc-123", "trained_at": "2026-04-19T12:00:00",
314
341
  "task": "regression", "name": "v1", "comments": "test run",
315
- "feature_names": None, "metrics": {"r2": 0.95}, "warning": None,
342
+ "feature_names": None, "metrics": {"r2": 0.95}, "warnings": [],
316
343
  })
317
344
  result = train_surrogate(
318
345
  samples=[[1.0, 2.0]], predictions=[3.0],
319
- feature_names=None, name="v1", comments="test run",
346
+ feature_names=None, schema_name="myschema", name="v1", comments="test run",
320
347
  )
321
348
  payload = mock_post.call_args.kwargs["payload"]
322
349
  assert payload["name"] == "v1"
323
350
  assert payload["comments"] == "test run"
324
- assert payload["schema_name"] == "default"
351
+ assert payload["schema_name"] == "myschema"
325
352
  assert result["version"] == "abc-123"
326
353
  assert result["trained_at"] == "2026-04-19T12:00:00"
327
354
 
@@ -337,7 +364,7 @@ def test_train_surrogate_named_schema(mock_post):
337
364
  @patch("proxyml.client.post")
338
365
  def test_train_surrogate_omits_none_metadata(mock_post):
339
366
  mock_post.return_value = _mock_response(200, {"version": "abc-123"})
340
- train_surrogate(samples=[[1.0]], predictions=[1.0], feature_names=None)
367
+ train_surrogate(samples=[[1.0]], predictions=[1.0], feature_names=None, schema_name="myschema")
341
368
  payload = mock_post.call_args.kwargs["payload"]
342
369
  assert "name" not in payload
343
370
  assert "comments" not in payload
File without changes
File without changes
File without changes
File without changes