proxyml 0.7.0__tar.gz → 0.9.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.7.0
3
+ Version: 0.9.0
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -211,7 +211,7 @@ Requires-Dist: pandas>=1.5
211
211
  Requires-Dist: orjson>=3.9
212
212
  Requires-Dist: proxyml-core>=0.1
213
213
  Provides-Extra: local
214
- Requires-Dist: proxyml-core[modeling]>=0.2.1; extra == "local"
214
+ Requires-Dist: proxyml-core[modeling]>=0.2.2; extra == "local"
215
215
  Provides-Extra: dev
216
216
  Requires-Dist: build; extra == "dev"
217
217
  Requires-Dist: twine; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proxyml"
7
- version = "0.7.0"
7
+ version = "0.9.0"
8
8
  description = "Python SDK for calling the ProxyML API"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [project.optional-dependencies]
37
37
  local = [
38
- "proxyml-core[modeling]>=0.2.1",
38
+ "proxyml-core[modeling]>=0.2.2",
39
39
  ]
40
40
  dev = [
41
41
  "build",
@@ -10,6 +10,7 @@ from proxyml.local.challenger import (
10
10
  Complexity,
11
11
  Rung,
12
12
  TrainedChallenger,
13
+ fingerprint_labels,
13
14
  score_champion,
14
15
  to_challenger_upload,
15
16
  train_auto_challenger,
@@ -21,6 +22,7 @@ __all__ = [
21
22
  "train_auto_challenger",
22
23
  "score_champion",
23
24
  "to_challenger_upload",
25
+ "fingerprint_labels",
24
26
  "Complexity",
25
27
  "Rung",
26
28
  "TrainedChallenger",
@@ -27,6 +27,7 @@ from sklearn.pipeline import Pipeline
27
27
 
28
28
  from proxyml.schema_builder import get_schema
29
29
  from proxyml_core.export import SurrogateExport
30
+ from proxyml_core.fingerprint import FINGERPRINT_MISMATCH_MESSAGE, check_fingerprints
30
31
  from proxyml_core.modeling.estimators import (
31
32
  binarize_if_probabilities,
32
33
  extract_hyperparameters,
@@ -127,7 +128,7 @@ class TrainedChallenger:
127
128
  champion_metrics: dict[str, float] | None = None
128
129
 
129
130
 
130
- def _fingerprint_values(values: np.ndarray | list) -> str:
131
+ def fingerprint_labels(values: np.ndarray | list) -> str:
131
132
  """Hash an array of labels, deterministically, without the data ever leaving this process.
132
133
 
133
134
  ``.tolist()`` converts numpy scalars to native Python types before
@@ -135,6 +136,13 @@ def _fingerprint_values(values: np.ndarray | list) -> str:
135
136
  different scalar repr behavior. Order is preserved (not sorted) since
136
137
  it encodes row alignment — that's exactly what a "same data?" check
137
138
  needs to be sensitive to.
139
+
140
+ Public so callers that score a champion decoupled from
141
+ ``train_challenger()`` — e.g. two separate tool calls in an MCP
142
+ server, with no shared Python state between them — can compute
143
+ ``champion_data_fingerprint`` themselves and carry it into a payload
144
+ assembled by hand, the same way ``to_challenger_upload(champion_labels=...)``
145
+ does internally.
138
146
  """
139
147
  canonical = json.dumps(np.asarray(values).tolist())
140
148
  return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
@@ -202,7 +210,7 @@ def train_challenger(
202
210
  and the result is attached as ``TrainedChallenger.champion_metrics``.
203
211
  """
204
212
  target_arr = np.asarray(target)
205
- target_fingerprint = _fingerprint_values(target_arr)
213
+ target_fingerprint = fingerprint_labels(target_arr)
206
214
  if champion_predictions is not None and len(champion_predictions) != len(target_arr):
207
215
  raise ValueError(
208
216
  f"champion_predictions must have one entry per row of target "
@@ -343,6 +351,13 @@ def to_challenger_upload(
343
351
  itself still requires ``champion_metrics`` at upload time; this function
344
352
  just doesn't force you to have it up front.
345
353
 
354
+ If both fingerprints end up present and don't match, raises
355
+ ``ValueError`` immediately — via the same
356
+ ``proxyml_core.check_fingerprints``/``FINGERPRINT_MISMATCH_MESSAGE`` the
357
+ upload endpoint itself uses — rather than waiting for a round-trip to
358
+ the server to find out the challenger and champion were scored on
359
+ different data.
360
+
346
361
  Args:
347
362
  result: output of ``train_challenger()``/``train_auto_challenger()``.
348
363
  n_samples: size of the evaluation set both ``result.metrics`` and
@@ -390,9 +405,13 @@ def to_challenger_upload(
390
405
  payload["champion_metrics"] = champion_metrics
391
406
  payload["challenger_data_fingerprint"] = result.target_fingerprint
392
407
  if champion_labels is not None:
393
- payload["champion_data_fingerprint"] = _fingerprint_values(champion_labels)
408
+ payload["champion_data_fingerprint"] = fingerprint_labels(champion_labels)
394
409
  elif used_internal_champion_metrics:
395
410
  payload["champion_data_fingerprint"] = result.target_fingerprint
411
+ if check_fingerprints(
412
+ payload["challenger_data_fingerprint"], payload.get("champion_data_fingerprint")
413
+ ) is False:
414
+ raise ValueError(FINGERPRINT_MISMATCH_MESSAGE)
396
415
  return payload
397
416
 
398
417
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -211,7 +211,7 @@ Requires-Dist: pandas>=1.5
211
211
  Requires-Dist: orjson>=3.9
212
212
  Requires-Dist: proxyml-core>=0.1
213
213
  Provides-Extra: local
214
- Requires-Dist: proxyml-core[modeling]>=0.2.1; extra == "local"
214
+ Requires-Dist: proxyml-core[modeling]>=0.2.2; extra == "local"
215
215
  Provides-Extra: dev
216
216
  Requires-Dist: build; extra == "dev"
217
217
  Requires-Dist: twine; extra == "dev"
@@ -12,4 +12,4 @@ black
12
12
  ruff
13
13
 
14
14
  [local]
15
- proxyml-core[modeling]>=0.2.1
15
+ proxyml-core[modeling]>=0.2.2
@@ -8,6 +8,7 @@ from proxyml.local import (
8
8
  Complexity,
9
9
  LADDERS,
10
10
  TrainedChallenger,
11
+ fingerprint_labels,
11
12
  score_champion,
12
13
  to_challenger_upload,
13
14
  train_auto_challenger,
@@ -446,18 +447,15 @@ def test_to_challenger_upload_champion_labels_fingerprint_for_decoupled_path():
446
447
  assert payload["champion_data_fingerprint"] == result.target_fingerprint
447
448
 
448
449
 
449
- def test_to_challenger_upload_champion_labels_fingerprint_differs_for_different_data():
450
+ def test_to_challenger_upload_raises_on_fingerprint_mismatch():
450
451
  df = _labeled_df(seed=33)
451
452
  target = df["approved"]
452
453
  result = train_challenger(df, target, _schema(), task="classification")
453
454
  champion_metrics = score_champion(target, target, task="classification")
454
455
  other_labels = ~target
455
456
 
456
- payload = to_challenger_upload(
457
- result, champion_metrics=champion_metrics, champion_labels=other_labels
458
- )
459
-
460
- assert payload["challenger_data_fingerprint"] != payload["champion_data_fingerprint"]
457
+ with pytest.raises(ValueError, match="don't match"):
458
+ to_challenger_upload(result, champion_metrics=champion_metrics, champion_labels=other_labels)
461
459
 
462
460
 
463
461
  def test_to_challenger_upload_omits_champion_fingerprint_without_labels_or_internal_path():
@@ -480,3 +478,14 @@ def test_to_challenger_upload_without_champion_metrics_omits_fingerprints():
480
478
 
481
479
  assert "challenger_data_fingerprint" not in payload
482
480
  assert "champion_data_fingerprint" not in payload
481
+
482
+
483
+ def test_fingerprint_labels_is_public_and_matches_target_fingerprint():
484
+ """Exercises the exact use case this was made public for: a caller with
485
+ no TrainedChallenger in hand (e.g. a decoupled score_champion() call)
486
+ computing the same fingerprint train_challenger() would have."""
487
+ df = _labeled_df(seed=36)
488
+ target = df["approved"]
489
+ result = train_challenger(df, target, _schema(), task="classification")
490
+
491
+ assert fingerprint_labels(target) == result.target_fingerprint
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes