snowflake-ml-python 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl

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.
Files changed (128) hide show
  1. snowflake/ml/_internal/env_utils.py +13 -53
  2. snowflake/ml/experiment/_client/experiment_tracking_sql_client.py +43 -10
  3. snowflake/ml/experiment/experiment_tracking.py +73 -26
  4. snowflake/ml/feature_store/feature_store.py +123 -0
  5. snowflake/ml/feature_store/realtime_registration.py +26 -1
  6. snowflake/ml/jobs/_utils/constants.py +2 -0
  7. snowflake/ml/jobs/job.py +60 -53
  8. snowflake/ml/jobs/job_definition.py +2 -0
  9. snowflake/ml/model/_packager/model_handlers/_utils.py +10 -12
  10. snowflake/ml/model/_packager/model_handlers/catboost.py +4 -4
  11. snowflake/ml/model/_packager/model_handlers/huggingface/_handler.py +10 -10
  12. snowflake/ml/model/_packager/model_handlers/huggingface/_openai_chat_wrapper.py +19 -17
  13. snowflake/ml/model/_packager/model_handlers/huggingface/conversational.py +3 -3
  14. snowflake/ml/model/_packager/model_handlers/lightgbm.py +4 -4
  15. snowflake/ml/model/_packager/model_handlers/mlflow.py +47 -1
  16. snowflake/ml/model/_packager/model_handlers/sentence_transformers.py +29 -29
  17. snowflake/ml/model/_packager/model_handlers/sklearn.py +31 -7
  18. snowflake/ml/model/_packager/model_handlers/xgboost.py +4 -4
  19. snowflake/ml/model/_signatures/numpy_handler.py +1 -1
  20. snowflake/ml/modeling/_internal/snowpark_implementations/distributed_hpo_trainer.py +3 -3
  21. snowflake/ml/modeling/_internal/snowpark_implementations/xgboost_external_memory_trainer.py +1 -1
  22. snowflake/ml/modeling/calibration/calibrated_classifier_cv.py +9 -10
  23. snowflake/ml/modeling/cluster/agglomerative_clustering.py +6 -1
  24. snowflake/ml/modeling/cluster/birch.py +2 -2
  25. snowflake/ml/modeling/cluster/bisecting_k_means.py +10 -10
  26. snowflake/ml/modeling/cluster/k_means.py +5 -2
  27. snowflake/ml/modeling/cluster/mini_batch_k_means.py +3 -0
  28. snowflake/ml/modeling/cluster/optics.py +5 -6
  29. snowflake/ml/modeling/compose/column_transformer.py +23 -15
  30. snowflake/ml/modeling/compose/transformed_target_regressor.py +1 -1
  31. snowflake/ml/modeling/discriminant_analysis/linear_discriminant_analysis.py +1 -1
  32. snowflake/ml/modeling/discriminant_analysis/quadratic_discriminant_analysis.py +6 -6
  33. snowflake/ml/modeling/ensemble/ada_boost_classifier.py +5 -9
  34. snowflake/ml/modeling/ensemble/ada_boost_regressor.py +1 -1
  35. snowflake/ml/modeling/ensemble/bagging_classifier.py +1 -1
  36. snowflake/ml/modeling/ensemble/bagging_regressor.py +1 -1
  37. snowflake/ml/modeling/ensemble/extra_trees_classifier.py +7 -2
  38. snowflake/ml/modeling/ensemble/extra_trees_regressor.py +7 -2
  39. snowflake/ml/modeling/ensemble/gradient_boosting_classifier.py +8 -2
  40. snowflake/ml/modeling/ensemble/gradient_boosting_regressor.py +8 -2
  41. snowflake/ml/modeling/ensemble/hist_gradient_boosting_classifier.py +22 -13
  42. snowflake/ml/modeling/ensemble/hist_gradient_boosting_regressor.py +25 -14
  43. snowflake/ml/modeling/ensemble/isolation_forest.py +12 -12
  44. snowflake/ml/modeling/ensemble/random_forest_classifier.py +7 -2
  45. snowflake/ml/modeling/ensemble/random_forest_regressor.py +7 -2
  46. snowflake/ml/modeling/ensemble/stacking_regressor.py +5 -4
  47. snowflake/ml/modeling/ensemble/voting_classifier.py +1 -1
  48. snowflake/ml/modeling/ensemble/voting_regressor.py +1 -1
  49. snowflake/ml/modeling/feature_selection/sequential_feature_selector.py +9 -7
  50. snowflake/ml/modeling/gaussian_process/gaussian_process_classifier.py +1 -1
  51. snowflake/ml/modeling/gaussian_process/gaussian_process_regressor.py +4 -1
  52. snowflake/ml/modeling/impute/knn_imputer.py +3 -3
  53. snowflake/ml/modeling/kernel_ridge/kernel_ridge.py +1 -1
  54. snowflake/ml/modeling/lightgbm/lgbm_classifier.py +1 -1
  55. snowflake/ml/modeling/lightgbm/lgbm_regressor.py +1 -1
  56. snowflake/ml/modeling/linear_model/ard_regression.py +1 -1
  57. snowflake/ml/modeling/linear_model/bayesian_ridge.py +1 -1
  58. snowflake/ml/modeling/linear_model/elastic_net.py +1 -1
  59. snowflake/ml/modeling/linear_model/elastic_net_cv.py +10 -9
  60. snowflake/ml/modeling/linear_model/huber_regressor.py +1 -1
  61. snowflake/ml/modeling/linear_model/lars.py +1 -1
  62. snowflake/ml/modeling/linear_model/lars_cv.py +1 -1
  63. snowflake/ml/modeling/linear_model/lasso.py +1 -1
  64. snowflake/ml/modeling/linear_model/lasso_cv.py +10 -9
  65. snowflake/ml/modeling/linear_model/lasso_lars.py +1 -1
  66. snowflake/ml/modeling/linear_model/lasso_lars_cv.py +1 -1
  67. snowflake/ml/modeling/linear_model/lasso_lars_ic.py +1 -1
  68. snowflake/ml/modeling/linear_model/linear_regression.py +14 -1
  69. snowflake/ml/modeling/linear_model/logistic_regression.py +18 -22
  70. snowflake/ml/modeling/linear_model/logistic_regression_cv.py +23 -26
  71. snowflake/ml/modeling/linear_model/multi_task_elastic_net.py +1 -1
  72. snowflake/ml/modeling/linear_model/multi_task_elastic_net_cv.py +9 -8
  73. snowflake/ml/modeling/linear_model/multi_task_lasso.py +1 -1
  74. snowflake/ml/modeling/linear_model/multi_task_lasso_cv.py +9 -8
  75. snowflake/ml/modeling/linear_model/orthogonal_matching_pursuit.py +1 -1
  76. snowflake/ml/modeling/linear_model/passive_aggressive_classifier.py +1 -1
  77. snowflake/ml/modeling/linear_model/passive_aggressive_regressor.py +1 -1
  78. snowflake/ml/modeling/linear_model/perceptron.py +1 -1
  79. snowflake/ml/modeling/linear_model/ransac_regressor.py +7 -7
  80. snowflake/ml/modeling/linear_model/ridge.py +1 -1
  81. snowflake/ml/modeling/linear_model/ridge_classifier.py +1 -1
  82. snowflake/ml/modeling/linear_model/ridge_classifier_cv.py +11 -13
  83. snowflake/ml/modeling/linear_model/ridge_cv.py +13 -17
  84. snowflake/ml/modeling/linear_model/sgd_classifier.py +13 -4
  85. snowflake/ml/modeling/linear_model/sgd_regressor.py +9 -2
  86. snowflake/ml/modeling/linear_model/theil_sen_regressor.py +3 -3
  87. snowflake/ml/modeling/manifold/mds.py +10 -10
  88. snowflake/ml/modeling/manifold/tsne.py +3 -9
  89. snowflake/ml/modeling/mixture/bayesian_gaussian_mixture.py +10 -11
  90. snowflake/ml/modeling/mixture/gaussian_mixture.py +3 -0
  91. snowflake/ml/modeling/multiclass/one_vs_one_classifier.py +1 -1
  92. snowflake/ml/modeling/multiclass/one_vs_rest_classifier.py +1 -1
  93. snowflake/ml/modeling/multiclass/output_code_classifier.py +1 -1
  94. snowflake/ml/modeling/naive_bayes/bernoulli_nb.py +1 -1
  95. snowflake/ml/modeling/naive_bayes/categorical_nb.py +1 -1
  96. snowflake/ml/modeling/naive_bayes/complement_nb.py +1 -1
  97. snowflake/ml/modeling/naive_bayes/gaussian_nb.py +1 -1
  98. snowflake/ml/modeling/naive_bayes/multinomial_nb.py +1 -1
  99. snowflake/ml/modeling/neighbors/k_neighbors_regressor.py +5 -1
  100. snowflake/ml/modeling/neighbors/nearest_centroid.py +17 -5
  101. snowflake/ml/modeling/neighbors/neighborhood_components_analysis.py +2 -2
  102. snowflake/ml/modeling/neighbors/radius_neighbors_regressor.py +1 -1
  103. snowflake/ml/modeling/neural_network/mlp_classifier.py +1 -1
  104. snowflake/ml/modeling/neural_network/mlp_regressor.py +12 -2
  105. snowflake/ml/modeling/preprocessing/one_hot_encoder.py +1 -1
  106. snowflake/ml/modeling/preprocessing/polynomial_features.py +2 -2
  107. snowflake/ml/modeling/semi_supervised/label_propagation.py +2 -2
  108. snowflake/ml/modeling/semi_supervised/label_spreading.py +1 -1
  109. snowflake/ml/modeling/svm/linear_svc.py +1 -1
  110. snowflake/ml/modeling/svm/linear_svr.py +1 -1
  111. snowflake/ml/modeling/svm/nu_svc.py +3 -1
  112. snowflake/ml/modeling/svm/nu_svr.py +3 -1
  113. snowflake/ml/modeling/svm/svc.py +4 -2
  114. snowflake/ml/modeling/svm/svr.py +3 -1
  115. snowflake/ml/modeling/tree/decision_tree_classifier.py +15 -13
  116. snowflake/ml/modeling/tree/decision_tree_regressor.py +8 -3
  117. snowflake/ml/modeling/tree/extra_tree_classifier.py +4 -2
  118. snowflake/ml/modeling/tree/extra_tree_regressor.py +4 -2
  119. snowflake/ml/modeling/xgboost/xgb_classifier.py +17 -6
  120. snowflake/ml/modeling/xgboost/xgb_regressor.py +17 -6
  121. snowflake/ml/modeling/xgboost/xgbrf_classifier.py +17 -6
  122. snowflake/ml/modeling/xgboost/xgbrf_regressor.py +17 -6
  123. snowflake/ml/version.py +1 -1
  124. {snowflake_ml_python-2.0.0.dist-info → snowflake_ml_python-2.1.0.dist-info}/METADATA +36 -9
  125. {snowflake_ml_python-2.0.0.dist-info → snowflake_ml_python-2.1.0.dist-info}/RECORD +128 -128
  126. {snowflake_ml_python-2.0.0.dist-info → snowflake_ml_python-2.1.0.dist-info}/WHEEL +0 -0
  127. {snowflake_ml_python-2.0.0.dist-info → snowflake_ml_python-2.1.0.dist-info}/licenses/LICENSE.txt +0 -0
  128. {snowflake_ml_python-2.0.0.dist-info → snowflake_ml_python-2.1.0.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ import textwrap
8
8
  import warnings
9
9
  from enum import Enum
10
10
  from importlib import metadata as importlib_metadata
11
- from typing import Any, DefaultDict, Optional
11
+ from typing import Any, DefaultDict
12
12
 
13
13
  import yaml
14
14
  from packaging import requirements, specifiers, version
@@ -376,53 +376,13 @@ def get_package_spec_with_supported_ops_only(req: requirements.Requirement) -> r
376
376
  A requirements.Requirement object with supported ops only
377
377
  """
378
378
 
379
- if req.name == "numpy":
380
- import numpy as np
381
-
382
- package_specifiers = get_numpy_specifiers(req, version.Version(np.__version__).major)
383
- else:
384
- package_specifiers = [spec for spec in req.specifier if spec.operator in _SUPPORTED_PACKAGE_SPEC_OPS]
379
+ package_specifiers = [spec for spec in req.specifier if spec.operator in _SUPPORTED_PACKAGE_SPEC_OPS]
385
380
 
386
381
  new_req = copy.deepcopy(req)
387
382
  new_req.specifier = specifiers.SpecifierSet(specifiers=",".join([str(spec) for spec in package_specifiers]))
388
383
  return new_req
389
384
 
390
385
 
391
- def get_numpy_specifiers(
392
- req: requirements.Requirement,
393
- client_numpy_major_version: int,
394
- ) -> list[specifiers.Specifier]:
395
- """Get the package spec with supported ops only including ==, >=, <=, > and < based on the client numpy
396
- major version.
397
-
398
- Args:
399
- req: A requirements.Requirement object showing the requirement.
400
- client_numpy_major_version: The major version of numpy to be used.
401
-
402
- Returns:
403
- A list of specifiers with supported ops only
404
- """
405
- req_specifiers = []
406
- for org_spec in req.specifier:
407
- # check specifier that provides upper bound
408
- if org_spec.operator in ["<", "<="]:
409
- client_version = version.Version(str(client_numpy_major_version))
410
- org_spec_version = version.Version(org_spec.version)
411
- # check if the client's numpy major version is less than the specifier's upper bound
412
- # if so, pin to max possible client major version
413
- if client_version.major < org_spec_version.major:
414
- modified_spec = specifiers.Specifier(f"<{client_version.major + 1}")
415
- req_specifiers.append(modified_spec)
416
- else:
417
- # use the original specifier
418
- req_specifiers.append(org_spec)
419
- else:
420
- # use the original specifier
421
- req_specifiers.append(org_spec)
422
-
423
- return req_specifiers
424
-
425
-
426
386
  def _relax_specifier_set(
427
387
  specifier_set: specifiers.SpecifierSet, strategy: relax_version_strategy.RelaxVersionStrategy
428
388
  ) -> specifiers.SpecifierSet:
@@ -493,7 +453,7 @@ def get_matched_package_versions_in_information_schema(
493
453
  session: session.Session,
494
454
  reqs: list[requirements.Requirement],
495
455
  python_version: str,
496
- statement_params: Optional[dict[str, Any]] = None,
456
+ statement_params: dict[str, Any] | None = None,
497
457
  ) -> dict[str, list[version.Version]]:
498
458
  """Look up the information_schema table to check if a package with the specified specifier exists in the Snowflake
499
459
  Conda channel. Note that this is not the source of truth due to the potential delay caused by a package that might
@@ -567,7 +527,7 @@ def save_conda_env_file(
567
527
  path: pathlib.Path,
568
528
  conda_chan_deps: DefaultDict[str, list[requirements.Requirement]],
569
529
  python_version: str,
570
- cuda_version: Optional[str] = None,
530
+ cuda_version: str | None = None,
571
531
  default_channel_override: str = SNOWFLAKE_CONDA_CHANNEL_URL,
572
532
  ) -> None:
573
533
  """Generate conda.yml file given a dict of dependencies after validation.
@@ -688,7 +648,7 @@ def _has_torch_dependency(pip_deps: list[requirements.Requirement]) -> bool:
688
648
  def save_requirements_file(
689
649
  path: pathlib.Path,
690
650
  pip_deps: list[requirements.Requirement],
691
- cuda_version: Optional[str] = None,
651
+ cuda_version: str | None = None,
692
652
  ) -> list[str]:
693
653
  """Generate Python requirements.txt file in the given directory path.
694
654
 
@@ -727,9 +687,9 @@ def load_conda_env_file(
727
687
  path: pathlib.Path,
728
688
  ) -> tuple[
729
689
  DefaultDict[str, list[requirements.Requirement]],
730
- Optional[list[requirements.Requirement]],
731
- Optional[str],
732
- Optional[str],
690
+ list[requirements.Requirement] | None,
691
+ str | None,
692
+ str | None,
733
693
  ]:
734
694
  """Read conda.yml file to get a dict of dependencies after validation.
735
695
  The channels part of conda.yml file will be processed with following rules:
@@ -830,7 +790,7 @@ def load_requirements_file(path: pathlib.Path) -> list[requirements.Requirement]
830
790
  PYTHON_VERSION_PATTERN = re.compile(r"python(?:(?P<op>=|==|>|<|>=|<=|~=|===)(?P<ver>\d(?:\.\d+)+))?(\.*)?")
831
791
 
832
792
 
833
- def parse_python_version_string(dep: str) -> Optional[str]:
793
+ def parse_python_version_string(dep: str) -> str | None:
834
794
  if dep.startswith("python"):
835
795
  m = PYTHON_VERSION_PATTERN.match(dep)
836
796
  if m is None:
@@ -849,7 +809,7 @@ def parse_python_version_string(dep: str) -> Optional[str]:
849
809
 
850
810
  def _find_conda_dep_spec(
851
811
  conda_chan_deps: DefaultDict[str, list[requirements.Requirement]], pkg_name: str
852
- ) -> Optional[tuple[str, requirements.Requirement]]:
812
+ ) -> tuple[str, requirements.Requirement] | None:
853
813
  for channel in conda_chan_deps:
854
814
  spec = next(filter(lambda req: req.name == pkg_name, conda_chan_deps[channel]), None)
855
815
  if spec:
@@ -857,7 +817,7 @@ def _find_conda_dep_spec(
857
817
  return None
858
818
 
859
819
 
860
- def _find_pip_req_spec(pip_reqs: list[requirements.Requirement], pkg_name: str) -> Optional[requirements.Requirement]:
820
+ def _find_pip_req_spec(pip_reqs: list[requirements.Requirement], pkg_name: str) -> requirements.Requirement | None:
861
821
  spec = next(filter(lambda req: req.name == pkg_name, pip_reqs), None)
862
822
  return spec
863
823
 
@@ -866,9 +826,9 @@ def find_dep_spec(
866
826
  conda_chan_deps: DefaultDict[str, list[requirements.Requirement]],
867
827
  pip_reqs: list[requirements.Requirement],
868
828
  conda_pkg_name: str,
869
- pip_pkg_name: Optional[str] = None,
829
+ pip_pkg_name: str | None = None,
870
830
  remove_spec: bool = False,
871
- ) -> Optional[requirements.Requirement]:
831
+ ) -> requirements.Requirement | None:
872
832
  if pip_pkg_name is None:
873
833
  pip_pkg_name = conda_pkg_name
874
834
  spec_conda = _find_conda_dep_spec(conda_chan_deps, conda_pkg_name)
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
 
3
3
  from snowflake.ml._internal import telemetry
4
4
  from snowflake.ml._internal.utils import query_result_checker, sql_identifier
@@ -6,7 +6,7 @@ from snowflake.ml.experiment._client import artifact
6
6
  from snowflake.ml.experiment._entities import run_metadata
7
7
  from snowflake.ml.model._client.sql import _base
8
8
  from snowflake.ml.utils import sql_client
9
- from snowflake.snowpark import dataframe, file_operation, row, session, types
9
+ from snowflake.snowpark import dataframe, file_operation, functions, row, session, types
10
10
 
11
11
  RUN_NAME_COL_NAME = "name"
12
12
  RUN_METADATA_COL_NAME = "metadata"
@@ -16,7 +16,7 @@ def pivot_run_attributes(
16
16
  sp_session: session.Session,
17
17
  rows: list[row.Row],
18
18
  run_names: list[str],
19
- cast_value: Optional[type] = None,
19
+ cast_value: type | None = None,
20
20
  ) -> dataframe.DataFrame:
21
21
  """Pivot SHOW RUN METRICS/PARAMETERS rows into one row per run.
22
22
 
@@ -123,7 +123,7 @@ class ExperimentTrackingSQLClient(_base._BaseSQLClient):
123
123
  *,
124
124
  experiment_name: sql_identifier.SqlIdentifier,
125
125
  run_name: sql_identifier.SqlIdentifier,
126
- source_info: Optional[str] = None,
126
+ source_info: str | None = None,
127
127
  ) -> None:
128
128
  experiment_fqn = self.fully_qualified_object_name(self._database_name, self._schema_name, experiment_name)
129
129
  query = f"ALTER EXPERIMENT {experiment_fqn} ADD RUN {run_name}"
@@ -139,7 +139,7 @@ class ExperimentTrackingSQLClient(_base._BaseSQLClient):
139
139
  *,
140
140
  experiment_name: sql_identifier.SqlIdentifier,
141
141
  run_name: sql_identifier.SqlIdentifier,
142
- status: Optional[run_metadata.RunStatus] = None,
142
+ status: run_metadata.RunStatus | None = None,
143
143
  ) -> None:
144
144
  experiment_fqn = self.fully_qualified_object_name(self._database_name, self._schema_name, experiment_name)
145
145
  query = f"ALTER EXPERIMENT {experiment_fqn} COMMIT RUN {run_name}"
@@ -255,7 +255,7 @@ class ExperimentTrackingSQLClient(_base._BaseSQLClient):
255
255
 
256
256
  @telemetry.send_api_usage_telemetry(project=telemetry.TelemetryProject.EXPERIMENT_TRACKING.value)
257
257
  def show_runs_in_experiment(
258
- self, *, experiment_name: sql_identifier.SqlIdentifier, like: Optional[str] = None
258
+ self, *, experiment_name: sql_identifier.SqlIdentifier, like: str | None = None
259
259
  ) -> list[row.Row]:
260
260
  experiment_fqn = self.fully_qualified_object_name(self._database_name, self._schema_name, experiment_name)
261
261
  like_clause = f"LIKE '{like}'" if like else ""
@@ -268,8 +268,8 @@ class ExperimentTrackingSQLClient(_base._BaseSQLClient):
268
268
  self,
269
269
  *,
270
270
  experiment_name: sql_identifier.SqlIdentifier,
271
- run_name: Optional[sql_identifier.SqlIdentifier] = None,
272
- like: Optional[str] = None,
271
+ run_name: sql_identifier.SqlIdentifier | None = None,
272
+ like: str | None = None,
273
273
  ) -> list[row.Row]:
274
274
  experiment_fqn = self.fully_qualified_object_name(self._database_name, self._schema_name, experiment_name)
275
275
  run_name_clause = f"RUN {run_name}" if run_name else ""
@@ -278,13 +278,46 @@ class ExperimentTrackingSQLClient(_base._BaseSQLClient):
278
278
  self._session, f"SHOW RUN METRICS {like_clause} IN EXPERIMENT {experiment_fqn} {run_name_clause}"
279
279
  ).validate()
280
280
 
281
+ @telemetry.send_api_usage_telemetry(project=telemetry.TelemetryProject.EXPERIMENT_TRACKING.value)
282
+ def get_run_metrics_history(
283
+ self,
284
+ *,
285
+ experiment_name: sql_identifier.SqlIdentifier,
286
+ run_name: sql_identifier.SqlIdentifier,
287
+ ) -> dataframe.DataFrame:
288
+ """Return every logged metric point of a run, ordered by name then step.
289
+
290
+ Args:
291
+ experiment_name: Name of the experiment containing the run.
292
+ run_name: Name of the run to read metric history from.
293
+
294
+ Returns:
295
+ A Snowpark DataFrame with ``name``, ``step``, ``value``, and ``timestamp`` columns.
296
+ """
297
+ experiment_fqn = self.fully_qualified_object_name(self._database_name, self._schema_name, experiment_name)
298
+ history: dataframe.DataFrame = (
299
+ self._session.table_function(
300
+ "SYSTEM$GET_EXPERIMENT_RUN_METRICS",
301
+ functions.lit(experiment_fqn),
302
+ functions.lit(str(run_name)),
303
+ )
304
+ .select(
305
+ functions.col("METRIC_NAME").alias('"name"'),
306
+ functions.col("STEP").cast(types.IntegerType()).alias('"step"'),
307
+ functions.col("VALUE").cast(types.DoubleType()).alias('"value"'),
308
+ functions.col("TIMESTAMP").alias('"timestamp"'),
309
+ )
310
+ .sort(functions.col('"name"').asc(), functions.col('"step"').asc())
311
+ )
312
+ return history
313
+
281
314
  @telemetry.send_api_usage_telemetry(project=telemetry.TelemetryProject.EXPERIMENT_TRACKING.value)
282
315
  def show_run_parameters_in_experiment(
283
316
  self,
284
317
  *,
285
318
  experiment_name: sql_identifier.SqlIdentifier,
286
- run_name: Optional[sql_identifier.SqlIdentifier] = None,
287
- like: Optional[str] = None,
319
+ run_name: sql_identifier.SqlIdentifier | None = None,
320
+ like: str | None = None,
288
321
  ) -> list[row.Row]:
289
322
  experiment_fqn = self.fully_qualified_object_name(self._database_name, self._schema_name, experiment_name)
290
323
  run_name_clause = f"RUN {run_name}" if run_name else ""
@@ -6,7 +6,7 @@ import traceback
6
6
  import warnings
7
7
  from collections.abc import Callable
8
8
  from types import TracebackType
9
- from typing import Any, Optional, Union
9
+ from typing import Any, Optional
10
10
  from urllib.parse import quote
11
11
 
12
12
  from snowflake import snowpark
@@ -28,6 +28,7 @@ from snowflake.ml.experiment._entities import run_metadata
28
28
  from snowflake.ml.lineage import lineage_node
29
29
  from snowflake.ml.model import type_hints
30
30
  from snowflake.ml.utils import sql_client as sql_client_utils
31
+ from snowflake.snowpark import functions
31
32
 
32
33
  DEFAULT_EXPERIMENT_NAME = sql_identifier.SqlIdentifier("DEFAULT")
33
34
 
@@ -48,8 +49,8 @@ class ExperimentTracking:
48
49
  self,
49
50
  session: snowpark.Session,
50
51
  *,
51
- database_name: Optional[str] = None,
52
- schema_name: Optional[str] = None,
52
+ database_name: str | None = None,
53
+ schema_name: str | None = None,
53
54
  capture_source_info: bool = True,
54
55
  ) -> None:
55
56
  """
@@ -116,11 +117,11 @@ class ExperimentTracking:
116
117
  self._session = session
117
118
 
118
119
  # The experiment in context
119
- self._experiment: Optional[entities.Experiment] = None
120
+ self._experiment: entities.Experiment | None = None
120
121
  # The run in context
121
- self._run: Optional[entities.Run] = None
122
+ self._run: entities.Run | None = None
122
123
  # The logging context used for patching stdout and stderr.
123
- self._logging_context: Optional[experiment_logging.ExperimentLoggingContext] = None
124
+ self._logging_context: experiment_logging.ExperimentLoggingContext | None = None
124
125
  self._live_logging_enabled = False
125
126
 
126
127
  self._initialized = True
@@ -182,8 +183,8 @@ class ExperimentTracking:
182
183
  def set_experiment(
183
184
  self,
184
185
  experiment_name: str,
185
- database_name: Optional[str] = None,
186
- schema_name: Optional[str] = None,
186
+ database_name: str | None = None,
187
+ schema_name: str | None = None,
187
188
  ) -> entities.Experiment:
188
189
  """
189
190
  Set the experiment in context. Creates a new experiment if it doesn't exist.
@@ -228,8 +229,8 @@ class ExperimentTracking:
228
229
  def delete_experiment(
229
230
  self,
230
231
  experiment_name: str,
231
- database_name: Optional[str] = None,
232
- schema_name: Optional[str] = None,
232
+ database_name: str | None = None,
233
+ schema_name: str | None = None,
233
234
  ) -> None:
234
235
  """
235
236
  Delete an experiment.
@@ -270,7 +271,7 @@ class ExperimentTracking:
270
271
  @functools.wraps(registry.Registry.log_model)
271
272
  def log_model(
272
273
  self,
273
- model: Union[type_hints.SupportedModelType, ml_model.ModelVersion],
274
+ model: type_hints.SupportedModelType | ml_model.ModelVersion,
274
275
  *,
275
276
  model_name: str,
276
277
  **kwargs: Any,
@@ -279,7 +280,7 @@ class ExperimentTracking:
279
280
  with experiment_info.ExperimentInfoPatcher(experiment_info=run._get_experiment_info()):
280
281
  return self._registry.log_model(model, model_name=model_name, **kwargs)
281
282
 
282
- def list_model_versions(self, run_name: Optional[str] = None) -> list[ml_model.ModelVersion]:
283
+ def list_model_versions(self, run_name: str | None = None) -> list[ml_model.ModelVersion]:
283
284
  """
284
285
  List the model versions that were logged under a run.
285
286
 
@@ -319,7 +320,7 @@ class ExperimentTracking:
319
320
 
320
321
  def start_run(
321
322
  self,
322
- run_name: Optional[str] = None,
323
+ run_name: str | None = None,
323
324
  ) -> entities.Run:
324
325
  """
325
326
  Start a new run. If a run name of an existing run is provided, resumes the run if it is running.
@@ -354,7 +355,7 @@ class ExperimentTracking:
354
355
  return self._run
355
356
 
356
357
  run_name = sql_identifier.SqlIdentifier(run_name)
357
- source_info_json: Optional[str] = None
358
+ source_info_json: str | None = None
358
359
  if self._capture_source_info:
359
360
  captured = source_info.SourceInfo.collect()
360
361
  if not captured.is_empty():
@@ -368,7 +369,7 @@ class ExperimentTracking:
368
369
  assert self._run is not None # for mypy
369
370
  return self._run
370
371
 
371
- def end_run(self, run_name: Optional[str] = None, status: Optional[str] = None) -> None:
372
+ def end_run(self, run_name: str | None = None, status: str | None = None) -> None:
372
373
  """
373
374
  End the current run if no run name is provided. Otherwise, the specified run is ended.
374
375
 
@@ -526,7 +527,7 @@ class ExperimentTracking:
526
527
  def log_artifact(
527
528
  self,
528
529
  local_path: str,
529
- artifact_path: Optional[str] = None,
530
+ artifact_path: str | None = None,
530
531
  ) -> None:
531
532
  """
532
533
  Log an artifact or a directory of artifacts under the current run. If no run is active, this method will create
@@ -546,11 +547,12 @@ class ExperimentTracking:
546
547
  file_path=file_path,
547
548
  )
548
549
 
549
- def list_metrics(self, run_name: Optional[str] = None) -> snowpark.DataFrame:
550
+ def list_metrics(self, run_name: str | None = None) -> snowpark.DataFrame:
550
551
  """
551
552
  List metrics for runs within the current experiment.
552
553
 
553
554
  When a metric is logged at multiple steps, the returned value corresponds to the highest step.
555
+ Use ``get_metric_history`` to retrieve every logged step.
554
556
 
555
557
  The returned DataFrame has a ``run_name`` column (str) and one float column per distinct metric name.
556
558
  Runs that did not log a given metric have ``NULL`` for that column.
@@ -588,7 +590,52 @@ class ExperimentTracking:
588
590
 
589
591
  return sql_client.pivot_run_attributes(self._session, rows, run_names=run_names, cast_value=float)
590
592
 
591
- def list_params(self, run_name: Optional[str] = None) -> snowpark.DataFrame:
593
+ def get_metric_history(
594
+ self,
595
+ run_name: str | None = None,
596
+ metric_name: str | None = None,
597
+ ) -> snowpark.DataFrame:
598
+ """Return the full history of metrics logged under a run as a Snowpark DataFrame.
599
+
600
+ Unlike ``list_metrics``, which collapses each metric to its highest step, this returns every
601
+ recorded point. The result has columns ``name``, ``step``, ``value``, and ``timestamp``,
602
+ ordered by name then step.
603
+
604
+ Filtering and aggregation are pushed down to Snowflake::
605
+
606
+ history = experiment.get_metric_history(run_name="RUN_A", metric_name="accuracy")
607
+ best = history.agg(functions.max_(functions.col('"value"'))).collect()[0][0]
608
+
609
+ Args:
610
+ run_name: Run to query. If None, uses the currently active run.
611
+ metric_name: Metric to filter on. If None, returns all metrics of the run.
612
+
613
+ Returns:
614
+ A Snowpark DataFrame. Use ``.to_pandas()`` for a pandas DataFrame or ``.collect()``
615
+ for a list of Row objects.
616
+
617
+ Raises:
618
+ RuntimeError: If no experiment is set, or if no run is active and ``run_name`` is None.
619
+ """
620
+ if not self._experiment:
621
+ raise RuntimeError("No experiment set. Please set an experiment before getting metric history.")
622
+
623
+ if run_name:
624
+ resolved_run_name = sql_identifier.SqlIdentifier(run_name)
625
+ elif self._run:
626
+ resolved_run_name = self._run.name
627
+ else:
628
+ raise RuntimeError("No run is active. Please provide a run_name or start a run.")
629
+
630
+ history = self._sql_client.get_run_metrics_history(
631
+ experiment_name=self._experiment.name,
632
+ run_name=resolved_run_name,
633
+ )
634
+ if metric_name is not None:
635
+ history = history.filter(functions.col('"name"') == metric_name)
636
+ return history
637
+
638
+ def list_params(self, run_name: str | None = None) -> snowpark.DataFrame:
592
639
  """
593
640
  List parameters for runs within the current experiment.
594
641
 
@@ -632,7 +679,7 @@ class ExperimentTracking:
632
679
  def list_artifacts(
633
680
  self,
634
681
  run_name: str,
635
- artifact_path: Optional[str] = None,
682
+ artifact_path: str | None = None,
636
683
  ) -> list[artifact.ArtifactInfo]:
637
684
  """
638
685
  List artifacts for a given run within the current experiment.
@@ -660,8 +707,8 @@ class ExperimentTracking:
660
707
  def download_artifacts(
661
708
  self,
662
709
  run_name: str,
663
- artifact_path: Optional[str] = None,
664
- target_path: Optional[str] = None,
710
+ artifact_path: str | None = None,
711
+ target_path: str | None = None,
665
712
  ) -> None:
666
713
  """
667
714
  Download artifacts from a run to a local directory.
@@ -770,7 +817,7 @@ class ExperimentTracking:
770
817
 
771
818
  def _try_patch_ipython_showtraceback(
772
819
  self, stderr_logger: experiment_logging.ExperimentLogger
773
- ) -> tuple[Any, Optional[Callable[..., None]]]:
820
+ ) -> tuple[Any, Callable[..., None] | None]:
774
821
  try:
775
822
  from IPython import get_ipython
776
823
 
@@ -780,7 +827,7 @@ class ExperimentTracking:
780
827
 
781
828
  def _patched_showtraceback(
782
829
  etype: type[BaseException],
783
- evalue: Optional[BaseException],
830
+ evalue: BaseException | None,
784
831
  stb: list[str],
785
832
  ) -> None:
786
833
  try:
@@ -804,8 +851,8 @@ class ExperimentTracking:
804
851
  def _try_log_exception(
805
852
  self,
806
853
  exc_type: type[BaseException],
807
- exc_value: Optional[BaseException],
808
- exc_tb: Optional[TracebackType],
854
+ exc_value: BaseException | None,
855
+ exc_tb: TracebackType | None,
809
856
  ) -> None:
810
857
  """Append a traceback to the stderr logger before unpatching.
811
858
 
@@ -844,7 +891,7 @@ class ExperimentTracking:
844
891
  assert self._experiment is not None and self._run is not None # for mypy
845
892
  stdout_logger, stderr_logger, stdout_ctx, stderr_ctx = None, None, None, None # for exception handling
846
893
  ip: Any = None
847
- original_showtraceback: Optional[Callable[..., None]] = None
894
+ original_showtraceback: Callable[..., None] | None = None
848
895
  try:
849
896
  exp_id = self._sql_client.get_experiment_id(self._experiment.name)
850
897
  run_id = self._sql_client.get_run_id(
@@ -247,6 +247,100 @@ def _store_type_from_oft_show_row(row: Row) -> OnlineStoreType:
247
247
  return OnlineStoreType.HYBRID_TABLE
248
248
 
249
249
 
250
+ # Setup-readiness columns on SHOW ONLINE FEATURE TABLES. When setup-readiness
251
+ # information is not available for a table, these columns are **absent from the result
252
+ # row entirely** rather than present-and-NULL. "Column missing" is therefore an
253
+ # ordinary case and must never raise -- reads go through
254
+ # ``_oft_show_row_optional_field`` rather than the strict ``extract_field`` used for
255
+ # always-present columns such as ``target_lag``.
256
+ _OFT_SETUP_STATUS_FIELDS = ("setup_status", "setup_error_msg", "setup_time")
257
+
258
+
259
+ def _oft_show_row_optional_field(row: Row, field_name: str) -> tuple[bool, Any]:
260
+ """Read a possibly-absent SHOW ONLINE FEATURE TABLES column.
261
+
262
+ Same defensive shape as :func:`_store_type_from_oft_show_row`: try the
263
+ lowercase key, then the uppercase key, then report absence instead of
264
+ raising. Absence and a present NULL are reported distinctly because they
265
+ mean different things for the setup-readiness columns (no setup-readiness
266
+ information is available at all vs. the table is not Postgres-backed, so
267
+ readiness does not apply to it).
268
+
269
+ Args:
270
+ row: A Snowpark ``Row`` from SHOW ONLINE FEATURE TABLES.
271
+ field_name: Lowercase column name to read.
272
+
273
+ Returns:
274
+ ``(present, value)``. ``present`` is False only when the column is
275
+ absent under both casings, in which case ``value`` is ``None``.
276
+ """
277
+ if field_name in row:
278
+ return True, row[field_name]
279
+ upper = field_name.upper()
280
+ if upper in row:
281
+ return True, row[upper]
282
+ return False, None
283
+
284
+
285
+ def _json_safe_oft_value(value: Any) -> Any:
286
+ """Coerce a SHOW column value into something ``json.dumps`` accepts.
287
+
288
+ Strings (``setup_status``, ``setup_error_msg``) and ``None`` pass through
289
+ untouched so statuses stay verbatim; ``setup_time`` arrives as a
290
+ ``datetime`` and is rendered ISO-8601.
291
+
292
+ Args:
293
+ value: Raw value read off the SHOW row.
294
+
295
+ Returns:
296
+ The value unchanged when it is already JSON-encodable as-is, its
297
+ ``isoformat()`` when it is date-like, otherwise ``str(value)``.
298
+ """
299
+ if value is None or isinstance(value, (str, bool, int, float)):
300
+ return value
301
+ isoformat = getattr(value, "isoformat", None)
302
+ if callable(isoformat):
303
+ return isoformat()
304
+ return str(value)
305
+
306
+
307
+ def _oft_setup_status_display_fields(row: Row) -> dict[str, Any]:
308
+ """Extract the OFT setup-readiness fields from a SHOW row for display.
309
+
310
+ Keys are emitted verbatim (``setup_status``, ``setup_error_msg``,
311
+ ``setup_time``) and only for columns actually present on the row, so the
312
+ resulting dict is empty when the row does not carry those columns. A column
313
+ that is present but NULL is emitted as ``None`` -- for ``setup_status`` that
314
+ means the online feature table is not Postgres-backed, so setup readiness
315
+ does not apply to it.
316
+
317
+ ``setup_status`` values, passed through exactly as SHOW reports them:
318
+
319
+ - ``SETUP_READY``: setup concluded successfully, **or** nothing has ever been
320
+ reported for the table. These two are indistinguishable here, and in the
321
+ never-reported case a first report can still arrive later, so a
322
+ ``SETUP_READY`` may subsequently read ``SETUP_FAILED``.
323
+ - ``SETUP_NOTREADY``: setup has not concluded yet. This is **not** a failure
324
+ and it may persist. ``SETUP_FAILED`` is the failure signal.
325
+ - ``SETUP_FAILED``: setup failed; ``setup_error_msg`` carries the reason.
326
+
327
+ ``setup_error_msg`` is NULL for both non-failure statuses.
328
+
329
+ Args:
330
+ row: A Snowpark ``Row`` from SHOW ONLINE FEATURE TABLES.
331
+
332
+ Returns:
333
+ Mapping of present setup columns to JSON-encodable values; empty when
334
+ none of the columns are present on the row.
335
+ """
336
+ fields: dict[str, Any] = {}
337
+ for field_name in _OFT_SETUP_STATUS_FIELDS:
338
+ present, value = _oft_show_row_optional_field(row, field_name)
339
+ if present:
340
+ fields[field_name] = _json_safe_oft_value(value)
341
+ return fields
342
+
343
+
250
344
  # Module-local alias kept so existing callers continue to reference
251
345
  # ``_ENTITY_TAG_PREFIX``; the canonical value lives in
252
346
  # :data:`snowflake.ml.feature_store.spec.enums.ENTITY_TAG_PREFIX`.
@@ -2074,6 +2168,29 @@ class FeatureStore:
2074
2168
  ``append_only`` (bool). ``initialization_warehouse``, ``source_refs`` and
2075
2169
  ``backup_source`` (all string, nullable) are included only when ``verbose=True``.
2076
2170
 
2171
+ The JSON in the ``online_config`` column may additionally carry the online feature
2172
+ table's setup-readiness fields:
2173
+
2174
+ - ``setup_status``: one of ``SETUP_READY``, ``SETUP_NOTREADY`` or ``SETUP_FAILED``.
2175
+ - ``setup_error_msg``: the failure reason when ``setup_status`` is ``SETUP_FAILED``;
2176
+ null otherwise.
2177
+ - ``setup_time``: ISO-8601 timestamp for when setup concluded.
2178
+
2179
+ These keys are absent from the JSON when no setup-readiness information is available
2180
+ for the online feature table, and are present-but-null when the table is not
2181
+ Postgres-backed, since readiness does not apply to it. Test for key presence rather
2182
+ than assuming the keys exist.
2183
+
2184
+ ``SETUP_FAILED`` is the failure signal, and ``setup_error_msg`` carries the reason.
2185
+ ``SETUP_NOTREADY`` means setup has not concluded yet; it is not a failure, and it may
2186
+ persist.
2187
+
2188
+ Known limitation: ``SETUP_READY`` is reported both when setup concluded successfully
2189
+ and when nothing has ever been reported for the table, and callers cannot distinguish
2190
+ the two. In the second case a first report can still arrive later, so ``setup_status``
2191
+ may change from ``SETUP_READY`` to ``SETUP_FAILED``. Treat ``SETUP_READY`` as "no
2192
+ failure has been reported" rather than as confirmation that setup completed.
2193
+
2077
2194
  Example::
2078
2195
 
2079
2196
  >>> fs = FeatureStore(...)
@@ -8235,6 +8352,10 @@ FROM {batch_cte_names[0]}{''.join(merge_join_parts)}
8235
8352
  version: Feature view version
8236
8353
  include_online_service_metadata: If True, includes additional Online Service metadata
8237
8354
  (refresh_mode, scheduling_state) in the JSON for display purposes.
8355
+ Also includes the setup-readiness fields (``setup_status``, ``setup_error_msg``,
8356
+ ``setup_time``) when SHOW returns those columns; see
8357
+ ``_oft_setup_status_display_fields`` for their meaning. They are omitted
8358
+ entirely when SHOW does not return those columns.
8238
8359
  If False, returns only OnlineConfig-compatible JSON.
8239
8360
 
8240
8361
  Returns:
@@ -8294,6 +8415,8 @@ FROM {batch_cte_names[0]}{''.join(merge_join_parts)}
8294
8415
 
8295
8416
  display_data["refresh_mode"] = extract_field(oft_row, "refresh_mode")
8296
8417
  display_data["scheduling_state"] = extract_field(oft_row, "scheduling_state")
8418
+ # Setup-readiness columns: omitted entirely when SHOW does not return them.
8419
+ display_data.update(_oft_setup_status_display_fields(oft_row))
8297
8420
 
8298
8421
  return json.dumps(display_data)
8299
8422
  else: