snowflake-ml-python 1.8.5__py3-none-any.whl → 1.9.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 (49) hide show
  1. snowflake/ml/_internal/telemetry.py +6 -9
  2. snowflake/ml/_internal/utils/connection_params.py +196 -0
  3. snowflake/ml/_internal/utils/identifier.py +1 -1
  4. snowflake/ml/_internal/utils/mixins.py +61 -0
  5. snowflake/ml/jobs/__init__.py +2 -0
  6. snowflake/ml/jobs/_utils/constants.py +3 -2
  7. snowflake/ml/jobs/_utils/function_payload_utils.py +43 -0
  8. snowflake/ml/jobs/_utils/interop_utils.py +63 -4
  9. snowflake/ml/jobs/_utils/payload_utils.py +89 -40
  10. snowflake/ml/jobs/_utils/query_helper.py +9 -0
  11. snowflake/ml/jobs/_utils/scripts/constants.py +19 -3
  12. snowflake/ml/jobs/_utils/scripts/mljob_launcher.py +8 -26
  13. snowflake/ml/jobs/_utils/spec_utils.py +29 -5
  14. snowflake/ml/jobs/_utils/stage_utils.py +119 -0
  15. snowflake/ml/jobs/_utils/types.py +5 -1
  16. snowflake/ml/jobs/decorators.py +20 -28
  17. snowflake/ml/jobs/job.py +197 -61
  18. snowflake/ml/jobs/manager.py +253 -121
  19. snowflake/ml/model/_client/model/model_impl.py +58 -0
  20. snowflake/ml/model/_client/model/model_version_impl.py +90 -0
  21. snowflake/ml/model/_client/ops/model_ops.py +18 -6
  22. snowflake/ml/model/_client/ops/service_ops.py +23 -6
  23. snowflake/ml/model/_client/service/model_deployment_spec_schema.py +2 -0
  24. snowflake/ml/model/_client/sql/service.py +68 -20
  25. snowflake/ml/model/_client/sql/stage.py +5 -2
  26. snowflake/ml/model/_model_composer/model_manifest/model_manifest.py +38 -10
  27. snowflake/ml/model/_packager/model_env/model_env.py +35 -27
  28. snowflake/ml/model/_packager/model_handlers/pytorch.py +5 -1
  29. snowflake/ml/model/_packager/model_handlers/snowmlmodel.py +103 -73
  30. snowflake/ml/model/_packager/model_meta/model_meta.py +3 -1
  31. snowflake/ml/model/_signatures/core.py +24 -0
  32. snowflake/ml/model/_signatures/snowpark_handler.py +55 -3
  33. snowflake/ml/model/target_platform.py +11 -0
  34. snowflake/ml/model/task.py +9 -0
  35. snowflake/ml/model/type_hints.py +5 -13
  36. snowflake/ml/modeling/metrics/metrics_utils.py +2 -0
  37. snowflake/ml/monitoring/explain_visualize.py +2 -2
  38. snowflake/ml/monitoring/model_monitor.py +0 -4
  39. snowflake/ml/registry/_manager/model_manager.py +30 -15
  40. snowflake/ml/registry/registry.py +144 -47
  41. snowflake/ml/utils/connection_params.py +1 -1
  42. snowflake/ml/utils/html_utils.py +263 -0
  43. snowflake/ml/version.py +1 -1
  44. {snowflake_ml_python-1.8.5.dist-info → snowflake_ml_python-1.9.0.dist-info}/METADATA +64 -19
  45. {snowflake_ml_python-1.8.5.dist-info → snowflake_ml_python-1.9.0.dist-info}/RECORD +48 -41
  46. snowflake/ml/monitoring/model_monitor_version.py +0 -1
  47. {snowflake_ml_python-1.8.5.dist-info → snowflake_ml_python-1.9.0.dist-info}/WHEEL +0 -0
  48. {snowflake_ml_python-1.8.5.dist-info → snowflake_ml_python-1.9.0.dist-info}/licenses/LICENSE.txt +0 -0
  49. {snowflake_ml_python-1.8.5.dist-info → snowflake_ml_python-1.9.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ import logging
2
+ import os
1
3
  import warnings
2
4
  from types import ModuleType
3
5
  from typing import Any, Optional, Union, overload
@@ -11,10 +13,11 @@ from snowflake.ml.model import (
11
13
  Model,
12
14
  ModelVersion,
13
15
  model_signature,
16
+ task,
14
17
  type_hints as model_types,
15
18
  )
16
19
  from snowflake.ml.model._client.model import model_version_impl
17
- from snowflake.ml.monitoring import model_monitor, model_monitor_version
20
+ from snowflake.ml.monitoring import model_monitor
18
21
  from snowflake.ml.monitoring._manager import model_monitor_manager
19
22
  from snowflake.ml.monitoring.entities import model_monitor_config
20
23
  from snowflake.ml.registry._manager import model_manager
@@ -29,7 +32,54 @@ _MODEL_MONITORING_DISABLED_ERROR = (
29
32
  )
30
33
 
31
34
 
35
+ class _NullStatusContext:
36
+ """A fallback context manager that logs status updates."""
37
+
38
+ def __init__(self, label: str) -> None:
39
+ self._label = label
40
+
41
+ def __enter__(self) -> "_NullStatusContext":
42
+ logging.info(f"Starting: {self._label}")
43
+ return self
44
+
45
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
46
+ pass
47
+
48
+ def update(self, label: str, *, state: str = "running", expanded: bool = True) -> None:
49
+ """Update the status by logging the message."""
50
+ logging.info(f"Status update: {label} (state: {state})")
51
+
52
+
53
+ class RegistryEventHandler:
54
+ def __init__(self) -> None:
55
+ try:
56
+ import streamlit as st
57
+
58
+ if not st.runtime.exists():
59
+ self._streamlit = None
60
+ else:
61
+ self._streamlit = st
62
+ USE_STREAMLIT_WIDGETS = os.getenv("USE_STREAMLIT_WIDGETS", "1") == "1"
63
+ if not USE_STREAMLIT_WIDGETS:
64
+ self._streamlit = None
65
+ except ImportError:
66
+ self._streamlit = None
67
+
68
+ def update(self, message: str) -> None:
69
+ """Write a message using streamlit if available, otherwise do nothing."""
70
+ if self._streamlit is not None:
71
+ self._streamlit.write(message)
72
+
73
+ def status(self, label: str, *, state: str = "running", expanded: bool = True) -> Any:
74
+ """Context manager that provides status updates with optional enhanced display capabilities."""
75
+ if self._streamlit is None:
76
+ return _NullStatusContext(label)
77
+ else:
78
+ return self._streamlit.status(label, state=state, expanded=expanded)
79
+
80
+
32
81
  class Registry:
82
+ @telemetry.send_api_usage_telemetry(project=_TELEMETRY_PROJECT, subproject=_MODEL_TELEMETRY_SUBPROJECT)
33
83
  def __init__(
34
84
  self,
35
85
  session: session.Session,
@@ -74,6 +124,22 @@ class Registry:
74
124
  else sql_identifier.SqlIdentifier("PUBLIC")
75
125
  )
76
126
 
127
+ database_exists = session.sql(
128
+ f"""SELECT 1 FROM INFORMATION_SCHEMA.DATABASES WHERE DATABASE_NAME = '{self._database_name.resolved()}';"""
129
+ ).collect()
130
+
131
+ if not database_exists:
132
+ raise ValueError(f"Database {self._database_name} does not exist.")
133
+
134
+ schema_exists = session.sql(
135
+ f"""
136
+ SELECT 1 FROM {self._database_name.identifier()}.INFORMATION_SCHEMA.SCHEMATA
137
+ WHERE SCHEMA_NAME = '{self._schema_name.resolved()}';"""
138
+ ).collect()
139
+
140
+ if not schema_exists:
141
+ raise ValueError(f"Schema {self._schema_name} does not exist.")
142
+
77
143
  self._model_manager = model_manager.ModelManager(
78
144
  session,
79
145
  database_name=self._database_name,
@@ -119,7 +185,7 @@ class Registry:
119
185
  user_files: Optional[dict[str, list[str]]] = None,
120
186
  code_paths: Optional[list[str]] = None,
121
187
  ext_modules: Optional[list[ModuleType]] = None,
122
- task: model_types.Task = model_types.Task.UNKNOWN,
188
+ task: model_types.Task = task.Task.UNKNOWN,
123
189
  options: Optional[model_types.ModelSaveOption] = None,
124
190
  ) -> ModelVersion:
125
191
  """
@@ -142,12 +208,12 @@ class Registry:
142
208
  to specify a dependency. It is a recommended way to specify your dependencies using conda. When channel
143
209
  is not specified, Snowflake Anaconda Channel will be used. Defaults to None.
144
210
  pip_requirements: List of Pip package specifications. Defaults to None.
145
- Models with pip requirements are currently only runnable in Snowpark Container Services.
146
- See https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/container for more.
147
- Models with pip requirements specified will not be executable in Snowflake Warehouse where all
148
- dependencies must be retrieved from Snowflake Anaconda Channel.
211
+ Models running in a Snowflake Warehouse must also specify a pip artifact repository (see
212
+ `artifact_repository_map`). Otherwise, models with pip requirements are runnable only in Snowpark
213
+ Container Services. See
214
+ https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/container for more.
149
215
  artifact_repository_map: Specifies a mapping of package channels or platforms to custom artifact
150
- repositories. Defaults to None. Currently, the mapping applies only to warehouse execution.
216
+ repositories. Defaults to None. Currently, the mapping applies only to Warehouse execution.
151
217
  Note : This feature is currently in Public Preview.
152
218
  Format: {channel_name: artifact_repository_name}, where:
153
219
  - channel_name: Currently must be 'pip'.
@@ -155,7 +221,14 @@ class Registry:
155
221
  `snowflake.snowpark.pypi_shared_repository`.
156
222
  resource_constraint: Mapping of resource constraint keys and values, e.g. {"architecture": "x86"}.
157
223
  target_platforms: List of target platforms to run the model. The only acceptable inputs are a combination of
158
- {"WAREHOUSE", "SNOWPARK_CONTAINER_SERVICES"}. Defaults to None.
224
+ "WAREHOUSE" and "SNOWPARK_CONTAINER_SERVICES", or a target platform constant:
225
+ - ["WAREHOUSE"] or snowflake.ml.model.target_platform.WAREHOUSE_ONLY (Warehouse only)
226
+ - ["SNOWPARK_CONTAINER_SERVICES"] or
227
+ snowflake.ml.model.target_platform.SNOWPARK_CONTAINER_SERVICES_ONLY
228
+ (Snowpark Container Services only)
229
+ - ["WAREHOUSE", "SNOWPARK_CONTAINER_SERVICES"] or
230
+ snowflake.ml.model.target_platform.BOTH_WAREHOUSE_AND_SNOWPARK_CONTAINER_SERVICES (Both)
231
+ Defaults to None. When None, the target platforms will be both.
159
232
  python_version: Python version in which the model is run. Defaults to None.
160
233
  signatures: Model data signatures for inputs and outputs for various target methods. If it is None,
161
234
  sample_input_data would be used to infer the signatures for those models that cannot automatically
@@ -259,7 +332,7 @@ class Registry:
259
332
  user_files: Optional[dict[str, list[str]]] = None,
260
333
  code_paths: Optional[list[str]] = None,
261
334
  ext_modules: Optional[list[ModuleType]] = None,
262
- task: model_types.Task = model_types.Task.UNKNOWN,
335
+ task: model_types.Task = task.Task.UNKNOWN,
263
336
  options: Optional[model_types.ModelSaveOption] = None,
264
337
  ) -> ModelVersion:
265
338
  """
@@ -282,12 +355,12 @@ class Registry:
282
355
  to specify a dependency. It is a recommended way to specify your dependencies using conda. When channel
283
356
  is not specified, Snowflake Anaconda Channel will be used. Defaults to None.
284
357
  pip_requirements: List of Pip package specifications. Defaults to None.
285
- Models with pip requirements are currently only runnable in Snowpark Container Services.
286
- See https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/container for more.
287
- Models with pip requirements specified will not be executable in Snowflake Warehouse where all
288
- dependencies must be retrieved from Snowflake Anaconda Channel.
358
+ Models running in a Snowflake Warehouse must also specify a pip artifact repository (see
359
+ `artifact_repository_map`). Otherwise, models with pip requirements are runnable only in Snowpark
360
+ Container Services. See
361
+ https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/container for more.
289
362
  artifact_repository_map: Specifies a mapping of package channels or platforms to custom artifact
290
- repositories. Defaults to None. Currently, the mapping applies only to warehouse execution.
363
+ repositories. Defaults to None. Currently, the mapping applies only to Warehouse execution.
291
364
  Note : This feature is currently in Public Preview.
292
365
  Format: {channel_name: artifact_repository_name}, where:
293
366
  - channel_name: Currently must be 'pip'.
@@ -295,8 +368,14 @@ class Registry:
295
368
  `snowflake.snowpark.pypi_shared_repository`.
296
369
  resource_constraint: Mapping of resource constraint keys and values, e.g. {"architecture": "x86"}.
297
370
  target_platforms: List of target platforms to run the model. The only acceptable inputs are a combination of
298
- ["WAREHOUSE", "SNOWPARK_CONTAINER_SERVICES"]. Defaults to None. When None, the target platforms will be
299
- both.
371
+ "WAREHOUSE" and "SNOWPARK_CONTAINER_SERVICES", or a target platform constant:
372
+ - ["WAREHOUSE"] or snowflake.ml.model.target_platform.WAREHOUSE_ONLY (Warehouse only)
373
+ - ["SNOWPARK_CONTAINER_SERVICES"] or
374
+ snowflake.ml.model.target_platform.SNOWPARK_CONTAINER_SERVICES_ONLY
375
+ (Snowpark Container Services only)
376
+ - ["WAREHOUSE", "SNOWPARK_CONTAINER_SERVICES"] or
377
+ snowflake.ml.model.target_platform.BOTH_WAREHOUSE_AND_SNOWPARK_CONTAINER_SERVICES (Both)
378
+ Defaults to None. When None, the target platforms will be both.
300
379
  python_version: Python version in which the model is run. Defaults to None.
301
380
  signatures: Model data signatures for inputs and outputs for various target methods. If it is None,
302
381
  sample_input_data would be used to infer the signatures for those models that cannot automatically
@@ -342,6 +421,7 @@ class Registry:
342
421
 
343
422
  Raises:
344
423
  ValueError: If extra arguments are specified ModelVersion is provided.
424
+ Exception: If the model logging fails.
345
425
 
346
426
  Returns:
347
427
  ModelVersion: ModelVersion object corresponding to the model just logged.
@@ -397,35 +477,47 @@ class Registry:
397
477
  if task is not model_types.Task.UNKNOWN:
398
478
  raise ValueError("`task` cannot be specified when calling log_model with a ModelVersion.")
399
479
 
400
- if pip_requirements:
480
+ if pip_requirements and not artifact_repository_map and self._targets_warehouse(target_platforms):
401
481
  warnings.warn(
402
- "Models logged specifying `pip_requirements` can not be executed "
403
- "in Snowflake Warehouse where all dependencies are required to be retrieved "
404
- "from Snowflake Anaconda Channel.",
482
+ "Models logged specifying `pip_requirements` cannot be executed in a Snowflake Warehouse "
483
+ "without specifying `artifact_repository_map`. This model can be run in Snowpark Container "
484
+ "Services. See https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/container.",
405
485
  category=UserWarning,
406
486
  stacklevel=1,
407
487
  )
408
- return self._model_manager.log_model(
409
- model=model,
410
- model_name=model_name,
411
- version_name=version_name,
412
- comment=comment,
413
- metrics=metrics,
414
- conda_dependencies=conda_dependencies,
415
- pip_requirements=pip_requirements,
416
- artifact_repository_map=artifact_repository_map,
417
- resource_constraint=resource_constraint,
418
- target_platforms=target_platforms,
419
- python_version=python_version,
420
- signatures=signatures,
421
- sample_input_data=sample_input_data,
422
- user_files=user_files,
423
- code_paths=code_paths,
424
- ext_modules=ext_modules,
425
- task=task,
426
- options=options,
427
- statement_params=statement_params,
428
- )
488
+
489
+ event_handler = RegistryEventHandler()
490
+ with event_handler.status("Logging model to registry...") as status:
491
+ # Perform the actual model logging
492
+ try:
493
+ result = self._model_manager.log_model(
494
+ model=model,
495
+ model_name=model_name,
496
+ version_name=version_name,
497
+ comment=comment,
498
+ metrics=metrics,
499
+ conda_dependencies=conda_dependencies,
500
+ pip_requirements=pip_requirements,
501
+ artifact_repository_map=artifact_repository_map,
502
+ resource_constraint=resource_constraint,
503
+ target_platforms=target_platforms,
504
+ python_version=python_version,
505
+ signatures=signatures,
506
+ sample_input_data=sample_input_data,
507
+ user_files=user_files,
508
+ code_paths=code_paths,
509
+ ext_modules=ext_modules,
510
+ task=task,
511
+ options=options,
512
+ statement_params=statement_params,
513
+ event_handler=event_handler,
514
+ )
515
+ status.update(label="Model logged successfully!", state="complete", expanded=False)
516
+ return result
517
+ except Exception as e:
518
+ event_handler.update("❌ Model logging failed!")
519
+ status.update(label="Model logging failed!", state="error", expanded=False)
520
+ raise e
429
521
 
430
522
  @telemetry.send_api_usage_telemetry(
431
523
  project=_TELEMETRY_PROJECT,
@@ -500,7 +592,6 @@ class Registry:
500
592
  project=telemetry.TelemetryProject.MLOPS.value,
501
593
  subproject=telemetry.TelemetrySubProject.MONITORING.value,
502
594
  )
503
- @snowpark._internal.utils.private_preview(version=model_monitor_version.SNOWFLAKE_ML_MONITORING_MIN_VERSION)
504
595
  def add_monitor(
505
596
  self,
506
597
  name: str,
@@ -525,7 +616,7 @@ class Registry:
525
616
  return self._model_monitor_manager.add_monitor(name, source_config, model_monitor_config)
526
617
 
527
618
  @overload
528
- def get_monitor(self, model_version: model_version_impl.ModelVersion) -> model_monitor.ModelMonitor:
619
+ def get_monitor(self, *, model_version: model_version_impl.ModelVersion) -> model_monitor.ModelMonitor:
529
620
  """Get a Model Monitor on a Model Version from the Registry.
530
621
 
531
622
  Args:
@@ -534,7 +625,7 @@ class Registry:
534
625
  ...
535
626
 
536
627
  @overload
537
- def get_monitor(self, name: str) -> model_monitor.ModelMonitor:
628
+ def get_monitor(self, *, name: str) -> model_monitor.ModelMonitor:
538
629
  """Get a Model Monitor by name from the Registry.
539
630
 
540
631
  Args:
@@ -546,7 +637,6 @@ class Registry:
546
637
  project=telemetry.TelemetryProject.MLOPS.value,
547
638
  subproject=telemetry.TelemetrySubProject.MONITORING.value,
548
639
  )
549
- @snowpark._internal.utils.private_preview(version=model_monitor_version.SNOWFLAKE_ML_MONITORING_MIN_VERSION)
550
640
  def get_monitor(
551
641
  self, *, name: Optional[str] = None, model_version: Optional[model_version_impl.ModelVersion] = None
552
642
  ) -> model_monitor.ModelMonitor:
@@ -575,7 +665,6 @@ class Registry:
575
665
  project=telemetry.TelemetryProject.MLOPS.value,
576
666
  subproject=telemetry.TelemetrySubProject.MONITORING.value,
577
667
  )
578
- @snowpark._internal.utils.private_preview(version=model_monitor_version.SNOWFLAKE_ML_MONITORING_MIN_VERSION)
579
668
  def show_model_monitors(self) -> list[snowpark.Row]:
580
669
  """Show all model monitors in the registry.
581
670
 
@@ -593,7 +682,6 @@ class Registry:
593
682
  project=telemetry.TelemetryProject.MLOPS.value,
594
683
  subproject=telemetry.TelemetrySubProject.MONITORING.value,
595
684
  )
596
- @snowpark._internal.utils.private_preview(version=model_monitor_version.SNOWFLAKE_ML_MONITORING_MIN_VERSION)
597
685
  def delete_monitor(self, name: str) -> None:
598
686
  """Delete a Model Monitor by name from the Registry.
599
687
 
@@ -606,3 +694,12 @@ class Registry:
606
694
  if not self.enable_monitoring:
607
695
  raise ValueError(_MODEL_MONITORING_DISABLED_ERROR)
608
696
  self._model_monitor_manager.delete_monitor(name)
697
+
698
+ @staticmethod
699
+ def _targets_warehouse(target_platforms: Optional[list[model_types.SupportedTargetPlatformType]]) -> bool:
700
+ """Returns True if warehouse is a target platform (None defaults to True)."""
701
+ return (
702
+ target_platforms is None
703
+ or model_types.TargetPlatform.WAREHOUSE in target_platforms
704
+ or "WAREHOUSE" in target_platforms
705
+ )
@@ -136,7 +136,7 @@ def _load_from_snowsql_config_file(connection_name: str, login_file: str = "") -
136
136
  return conn_params
137
137
 
138
138
 
139
- @snowpark._internal.utils.private_preview(version="0.2.0")
139
+ @snowpark._internal.utils.deprecated(version="1.8.5")
140
140
  def SnowflakeLoginOptions(connection_name: str = "", login_file: Optional[str] = None) -> dict[str, Union[str, bytes]]:
141
141
  """Returns a dict that can be used directly into snowflake python connector or Snowpark session config.
142
142
 
@@ -0,0 +1,263 @@
1
+ """Utilities for generating consistent HTML representations across model classes."""
2
+
3
+ from typing import Any, Sequence
4
+
5
+ # Common CSS styles used across model classes
6
+ BASE_CONTAINER_STYLE = (
7
+ "font-family: Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; "
8
+ "color: #333; background-color: #f9f9f9; border: 1px solid #ddd; "
9
+ "border-radius: 4px; padding: 15px; margin-bottom: 15px;"
10
+ )
11
+
12
+ HEADER_STYLE = "margin-top: 0; margin-bottom: 10px; font-size: 16px; color: #007bff;"
13
+
14
+ SECTION_HEADER_STYLE = "margin: 15px 0 10px; color: #007bff;"
15
+
16
+ GRID_CONTAINER_STYLE = "display: grid; grid-template-columns: 150px 1fr; gap: 10px;"
17
+
18
+ GRID_LABEL_STYLE = "font-weight: bold;"
19
+
20
+ DETAILS_STYLE = "margin: 5px 0; border: 1px solid #e0e0e0; border-radius: 4px;"
21
+
22
+ SUMMARY_STYLE = "padding: 8px; cursor: pointer; background-color: #f5f5f5; border-radius: 3px;"
23
+
24
+ SUMMARY_LABEL_STYLE = "font-weight: bold; color: #007bff;"
25
+
26
+ CONTENT_SECTION_STYLE = "margin-left: 10px;"
27
+
28
+ VERSION_ITEM_STYLE = "margin: 5px 0; padding: 5px; background-color: #f5f5f5; border-radius: 3px;"
29
+
30
+ NESTED_GROUP_STYLE = "border-left: 2px solid #e0e0e0; margin-left: 8px;"
31
+
32
+ ERROR_STYLE = "color: #888; font-style: italic;"
33
+
34
+
35
+ def create_base_container(title: str, content: str) -> str:
36
+ """Create a base HTML container with consistent styling.
37
+
38
+ Args:
39
+ title: The main title for the container.
40
+ content: The HTML content to include in the container.
41
+
42
+ Returns:
43
+ HTML string with the base container structure.
44
+ """
45
+ return f"""
46
+ <div style="{BASE_CONTAINER_STYLE}">
47
+ <h3 style="{HEADER_STYLE}">
48
+ {title}
49
+ </h3>
50
+ {content}
51
+ </div>
52
+ """
53
+
54
+
55
+ def create_grid_section(items: list[tuple[str, str]]) -> str:
56
+ """Create a grid layout section for key-value pairs.
57
+
58
+ Args:
59
+ items: List of (label, value) tuples to display in the grid.
60
+
61
+ Returns:
62
+ HTML string with grid layout.
63
+ """
64
+ grid_items = ""
65
+ for label, value in items:
66
+ grid_items += f"""
67
+ <div style="{GRID_LABEL_STYLE}">{label}:</div>
68
+ <div>{value}</div>
69
+ """
70
+
71
+ return f"""
72
+ <div style="{GRID_CONTAINER_STYLE}">
73
+ {grid_items}
74
+ </div>
75
+ """
76
+
77
+
78
+ def create_section_header(title: str) -> str:
79
+ """Create a section header with consistent styling.
80
+
81
+ Args:
82
+ title: The section title.
83
+
84
+ Returns:
85
+ HTML string for the section header.
86
+ """
87
+ return f'<h4 style="{SECTION_HEADER_STYLE}">{title}</h4>'
88
+
89
+
90
+ def create_content_section(content: str) -> str:
91
+ """Create a content section with consistent indentation.
92
+
93
+ Args:
94
+ content: The content to include in the section.
95
+
96
+ Returns:
97
+ HTML string with content section styling.
98
+ """
99
+ return f"""
100
+ <div style="{CONTENT_SECTION_STYLE}">
101
+ {content}
102
+ </div>
103
+ """
104
+
105
+
106
+ def create_collapsible_section(title: str, content: str, open_by_default: bool = True) -> str:
107
+ """Create a collapsible section with consistent styling.
108
+
109
+ Args:
110
+ title: The title for the collapsible section.
111
+ content: The content to include in the collapsible section.
112
+ open_by_default: Whether the section should be open by default.
113
+
114
+ Returns:
115
+ HTML string for the collapsible section.
116
+ """
117
+ open_attr = "open" if open_by_default else ""
118
+
119
+ return f"""
120
+ <details style="{DETAILS_STYLE}" {open_attr}>
121
+ <summary style="{SUMMARY_STYLE}">
122
+ <span style="{SUMMARY_LABEL_STYLE}">{title}</span>
123
+ </summary>
124
+ <div style="padding: 10px; border-top: 1px solid #e0e0e0;">
125
+ {content}
126
+ </div>
127
+ </details>
128
+ """
129
+
130
+
131
+ def create_version_item(version_name: str, created_on: str, comment: str, is_default: bool = False) -> str:
132
+ """Create a version item with consistent styling.
133
+
134
+ Args:
135
+ version_name: The name of the version.
136
+ created_on: The creation timestamp.
137
+ comment: The version comment.
138
+ is_default: Whether this is the default version.
139
+
140
+ Returns:
141
+ HTML string for the version item.
142
+ """
143
+ default_text = " (Default)" if is_default else ""
144
+
145
+ return f"""
146
+ <div style="{VERSION_ITEM_STYLE}">
147
+ <strong>Version:</strong> {version_name}{default_text}<br/>
148
+ <strong>Created:</strong> {created_on}<br/>
149
+ <strong>Comment:</strong> {comment}<br/>
150
+ </div>
151
+ """
152
+
153
+
154
+ def create_tag_item(tag_name: str, tag_value: str) -> str:
155
+ """Create a tag item with consistent styling.
156
+
157
+ Args:
158
+ tag_name: The name of the tag.
159
+ tag_value: The value of the tag.
160
+
161
+ Returns:
162
+ HTML string for the tag item.
163
+ """
164
+ return f"""
165
+ <div style="margin: 5px 0;">
166
+ <strong>{tag_name}:</strong> {tag_value}
167
+ </div>
168
+ """
169
+
170
+
171
+ def create_metric_item(metric_name: str, value: Any) -> str:
172
+ """Create a metric item with consistent styling.
173
+
174
+ Args:
175
+ metric_name: The name of the metric.
176
+ value: The value of the metric.
177
+
178
+ Returns:
179
+ HTML string for the metric item.
180
+ """
181
+ value_str = "N/A" if value is None else str(value)
182
+ return f"""
183
+ <div style="margin: 5px 0;">
184
+ <strong>{metric_name}:</strong> {value_str}
185
+ </div>
186
+ """
187
+
188
+
189
+ def create_error_message(message: str) -> str:
190
+ """Create an error message with consistent styling.
191
+
192
+ Args:
193
+ message: The error message to display.
194
+
195
+ Returns:
196
+ HTML string for the error message.
197
+ """
198
+ return f'<em style="{ERROR_STYLE}">{message}</em>'
199
+
200
+
201
+ def create_feature_spec_html(spec: Any, indent: int = 0) -> str:
202
+ """Create HTML representation for a feature specification.
203
+
204
+ Args:
205
+ spec: The feature specification (FeatureSpec or FeatureGroupSpec).
206
+ indent: The indentation level for nested features.
207
+
208
+ Returns:
209
+ HTML string for the feature specification.
210
+ """
211
+ # Import here to avoid circular imports
212
+ from snowflake.ml.model._signatures import core
213
+
214
+ if isinstance(spec, core.FeatureSpec):
215
+ shape_str = f" shape={spec._shape}" if spec._shape else ""
216
+ nullable_str = "" if spec._nullable else ", not nullable"
217
+ return f"""
218
+ <div style="margin: 3px 0; padding-left: {indent * 16}px;">
219
+ <strong>{spec.name}</strong>: {spec._dtype}{shape_str}{nullable_str}
220
+ </div>
221
+ """
222
+ elif isinstance(spec, core.FeatureGroupSpec):
223
+ group_html = f"""
224
+ <div style="margin: 8px 0;">
225
+ <div style="margin: 3px 0; padding-left: {indent * 16}px;">
226
+ <strong>{spec.name}</strong> <span style="color: #666;">(group)</span>
227
+ </div>
228
+ <div style="border-left: 2px solid #e0e0e0; margin-left: {indent * 16 + 8}px;">
229
+ """
230
+ for sub_spec in spec._specs:
231
+ group_html += create_feature_spec_html(sub_spec, indent + 1)
232
+ group_html += """
233
+ </div>
234
+ </div>
235
+ """
236
+ return group_html
237
+ return ""
238
+
239
+
240
+ def create_features_html(features: Sequence[Any], title: str) -> str:
241
+ """Create HTML representation for a collection of features.
242
+
243
+ Args:
244
+ features: The sequence of feature specifications.
245
+ title: The title for the feature collection.
246
+
247
+ Returns:
248
+ HTML string for the features collection.
249
+ """
250
+ if not features:
251
+ return f"""
252
+ <div style="margin: 5px 0; padding: 5px;">
253
+ <em>No {title.lower()} features defined</em>
254
+ </div>
255
+ """
256
+
257
+ html = """
258
+ <div style="margin: 5px 0; padding: 5px;">
259
+ """
260
+ for feature in features:
261
+ html += create_feature_spec_html(feature)
262
+ html += "</div>"
263
+ return html
snowflake/ml/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # This is parsed by regex in conda recipe meta file. Make sure not to break it.
2
- VERSION = "1.8.5"
2
+ VERSION = "1.9.0"