proxyml 0.2.0__tar.gz → 0.2.1__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.1
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.1"
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,11 +145,15 @@ 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
159
  def put_schema(schema: dict, name: str = "default") -> Any:
@@ -345,7 +375,7 @@ def find_counterfactual(sample, target, n_neighbors: int = 10000, perturbation_s
345
375
  sample (list): point around which neighbors will be found.
346
376
  target (Any): the desired prediction outcome, can be a string or number for classification or a target predicted value for regression.
347
377
  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.
378
+ perturbation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
349
379
  version (str): name of the surrogate model to run, defaults to "default."
350
380
  as_df (bool): if True, return the counterfactual as a dataframe. If False, returns JSON object.
351
381
  Returns:
@@ -467,7 +497,7 @@ def find_counterfactuals(
467
497
  samples (list): list of points around which neighbors will be found.
468
498
  target (Any): the desired prediction outcome, can be a string or number for classification or a target predicted value for regression.
469
499
  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.
500
+ perturbation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
471
501
  version (str): name of the surrogate model to run, defaults to "default."
472
502
  as_df (bool): if True, return the counterfactual as a dataframe. If False, returns JSON object.
473
503
  Returns:
@@ -592,6 +622,23 @@ def get_feature_importances(version: str | None = None) -> Any:
592
622
  return None
593
623
 
594
624
 
625
+ def health_check() -> dict | None:
626
+ """Check API connectivity and version. Does not require authentication and does not count against usage quota.
627
+
628
+ Returns:
629
+ Dict with ``status``, ``model_loaded``, and ``version``, or None on failure.
630
+ """
631
+ try:
632
+ r = requests.get(url=f'{_base_url()}/health', timeout=_TIMEOUT)
633
+ except requests.exceptions.RequestException as exc:
634
+ logger.error("Network error calling /health: %s", exc)
635
+ return None
636
+ if r.status_code == 200:
637
+ return r.json()
638
+ logger.error("Health check failed with status %s: %s", r.status_code, r.text)
639
+ return None
640
+
641
+
595
642
  def get_usage() -> dict | None:
596
643
  """Return current tier, usage counts, and quota for the authenticated user."""
597
644
  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.1
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
  # ---------------------------------------------------------------------------
@@ -312,7 +339,7 @@ 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],
File without changes
File without changes
File without changes
File without changes