workbench 0.8.177__py3-none-any.whl → 0.8.227__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.

Potentially problematic release.


This version of workbench might be problematic. Click here for more details.

Files changed (140) hide show
  1. workbench/__init__.py +1 -0
  2. workbench/algorithms/dataframe/__init__.py +1 -2
  3. workbench/algorithms/dataframe/compound_dataset_overlap.py +321 -0
  4. workbench/algorithms/dataframe/feature_space_proximity.py +168 -75
  5. workbench/algorithms/dataframe/fingerprint_proximity.py +422 -86
  6. workbench/algorithms/dataframe/projection_2d.py +44 -21
  7. workbench/algorithms/dataframe/proximity.py +259 -305
  8. workbench/algorithms/graph/light/proximity_graph.py +12 -11
  9. workbench/algorithms/models/cleanlab_model.py +382 -0
  10. workbench/algorithms/models/noise_model.py +388 -0
  11. workbench/algorithms/sql/column_stats.py +0 -1
  12. workbench/algorithms/sql/correlations.py +0 -1
  13. workbench/algorithms/sql/descriptive_stats.py +0 -1
  14. workbench/algorithms/sql/outliers.py +3 -3
  15. workbench/api/__init__.py +5 -1
  16. workbench/api/df_store.py +17 -108
  17. workbench/api/endpoint.py +14 -12
  18. workbench/api/feature_set.py +117 -11
  19. workbench/api/meta.py +0 -1
  20. workbench/api/meta_model.py +289 -0
  21. workbench/api/model.py +52 -21
  22. workbench/api/parameter_store.py +3 -52
  23. workbench/cached/cached_meta.py +0 -1
  24. workbench/cached/cached_model.py +49 -11
  25. workbench/core/artifacts/__init__.py +11 -2
  26. workbench/core/artifacts/artifact.py +5 -5
  27. workbench/core/artifacts/df_store_core.py +114 -0
  28. workbench/core/artifacts/endpoint_core.py +319 -204
  29. workbench/core/artifacts/feature_set_core.py +249 -45
  30. workbench/core/artifacts/model_core.py +135 -82
  31. workbench/core/artifacts/parameter_store_core.py +98 -0
  32. workbench/core/cloud_platform/cloud_meta.py +0 -1
  33. workbench/core/pipelines/pipeline_executor.py +1 -1
  34. workbench/core/transforms/features_to_model/features_to_model.py +60 -44
  35. workbench/core/transforms/model_to_endpoint/model_to_endpoint.py +43 -10
  36. workbench/core/transforms/pandas_transforms/pandas_to_features.py +38 -2
  37. workbench/core/views/training_view.py +113 -42
  38. workbench/core/views/view.py +53 -3
  39. workbench/core/views/view_utils.py +4 -4
  40. workbench/model_script_utils/model_script_utils.py +339 -0
  41. workbench/model_script_utils/pytorch_utils.py +405 -0
  42. workbench/model_script_utils/uq_harness.py +277 -0
  43. workbench/model_scripts/chemprop/chemprop.template +774 -0
  44. workbench/model_scripts/chemprop/generated_model_script.py +774 -0
  45. workbench/model_scripts/chemprop/model_script_utils.py +339 -0
  46. workbench/model_scripts/chemprop/requirements.txt +3 -0
  47. workbench/model_scripts/custom_models/chem_info/fingerprints.py +175 -0
  48. workbench/model_scripts/custom_models/chem_info/mol_descriptors.py +0 -1
  49. workbench/model_scripts/custom_models/chem_info/molecular_descriptors.py +0 -1
  50. workbench/model_scripts/custom_models/chem_info/morgan_fingerprints.py +1 -2
  51. workbench/model_scripts/custom_models/proximity/feature_space_proximity.py +194 -0
  52. workbench/model_scripts/custom_models/proximity/feature_space_proximity.template +8 -10
  53. workbench/model_scripts/custom_models/uq_models/bayesian_ridge.template +7 -8
  54. workbench/model_scripts/custom_models/uq_models/ensemble_xgb.template +20 -21
  55. workbench/model_scripts/custom_models/uq_models/feature_space_proximity.py +194 -0
  56. workbench/model_scripts/custom_models/uq_models/gaussian_process.template +5 -11
  57. workbench/model_scripts/custom_models/uq_models/ngboost.template +15 -16
  58. workbench/model_scripts/ensemble_xgb/ensemble_xgb.template +15 -17
  59. workbench/model_scripts/meta_model/generated_model_script.py +209 -0
  60. workbench/model_scripts/meta_model/meta_model.template +209 -0
  61. workbench/model_scripts/pytorch_model/generated_model_script.py +443 -499
  62. workbench/model_scripts/pytorch_model/model_script_utils.py +339 -0
  63. workbench/model_scripts/pytorch_model/pytorch.template +440 -496
  64. workbench/model_scripts/pytorch_model/pytorch_utils.py +405 -0
  65. workbench/model_scripts/pytorch_model/requirements.txt +1 -1
  66. workbench/model_scripts/pytorch_model/uq_harness.py +277 -0
  67. workbench/model_scripts/scikit_learn/generated_model_script.py +7 -12
  68. workbench/model_scripts/scikit_learn/scikit_learn.template +4 -9
  69. workbench/model_scripts/script_generation.py +15 -12
  70. workbench/model_scripts/uq_models/generated_model_script.py +248 -0
  71. workbench/model_scripts/xgb_model/generated_model_script.py +371 -403
  72. workbench/model_scripts/xgb_model/model_script_utils.py +339 -0
  73. workbench/model_scripts/xgb_model/uq_harness.py +277 -0
  74. workbench/model_scripts/xgb_model/xgb_model.template +367 -399
  75. workbench/repl/workbench_shell.py +18 -14
  76. workbench/resources/open_source_api.key +1 -1
  77. workbench/scripts/endpoint_test.py +162 -0
  78. workbench/scripts/lambda_test.py +73 -0
  79. workbench/scripts/meta_model_sim.py +35 -0
  80. workbench/scripts/ml_pipeline_sqs.py +122 -6
  81. workbench/scripts/training_test.py +85 -0
  82. workbench/themes/dark/custom.css +59 -0
  83. workbench/themes/dark/plotly.json +5 -5
  84. workbench/themes/light/custom.css +153 -40
  85. workbench/themes/light/plotly.json +9 -9
  86. workbench/themes/midnight_blue/custom.css +59 -0
  87. workbench/utils/aws_utils.py +0 -1
  88. workbench/utils/chem_utils/fingerprints.py +87 -46
  89. workbench/utils/chem_utils/mol_descriptors.py +0 -1
  90. workbench/utils/chem_utils/projections.py +16 -6
  91. workbench/utils/chem_utils/vis.py +25 -27
  92. workbench/utils/chemprop_utils.py +141 -0
  93. workbench/utils/config_manager.py +2 -6
  94. workbench/utils/endpoint_utils.py +5 -7
  95. workbench/utils/license_manager.py +2 -6
  96. workbench/utils/markdown_utils.py +57 -0
  97. workbench/utils/meta_model_simulator.py +499 -0
  98. workbench/utils/metrics_utils.py +256 -0
  99. workbench/utils/model_utils.py +260 -76
  100. workbench/utils/pipeline_utils.py +0 -1
  101. workbench/utils/plot_utils.py +159 -34
  102. workbench/utils/pytorch_utils.py +87 -0
  103. workbench/utils/shap_utils.py +11 -57
  104. workbench/utils/theme_manager.py +95 -30
  105. workbench/utils/xgboost_local_crossfold.py +267 -0
  106. workbench/utils/xgboost_model_utils.py +127 -220
  107. workbench/web_interface/components/experiments/outlier_plot.py +0 -1
  108. workbench/web_interface/components/model_plot.py +16 -2
  109. workbench/web_interface/components/plugin_unit_test.py +5 -3
  110. workbench/web_interface/components/plugins/ag_table.py +2 -4
  111. workbench/web_interface/components/plugins/confusion_matrix.py +3 -6
  112. workbench/web_interface/components/plugins/model_details.py +48 -80
  113. workbench/web_interface/components/plugins/scatter_plot.py +192 -92
  114. workbench/web_interface/components/settings_menu.py +184 -0
  115. workbench/web_interface/page_views/main_page.py +0 -1
  116. {workbench-0.8.177.dist-info → workbench-0.8.227.dist-info}/METADATA +31 -17
  117. {workbench-0.8.177.dist-info → workbench-0.8.227.dist-info}/RECORD +121 -106
  118. {workbench-0.8.177.dist-info → workbench-0.8.227.dist-info}/entry_points.txt +4 -0
  119. {workbench-0.8.177.dist-info → workbench-0.8.227.dist-info}/licenses/LICENSE +1 -1
  120. workbench/core/cloud_platform/aws/aws_df_store.py +0 -404
  121. workbench/core/cloud_platform/aws/aws_parameter_store.py +0 -280
  122. workbench/model_scripts/custom_models/meta_endpoints/example.py +0 -53
  123. workbench/model_scripts/custom_models/proximity/generated_model_script.py +0 -138
  124. workbench/model_scripts/custom_models/proximity/proximity.py +0 -384
  125. workbench/model_scripts/custom_models/uq_models/generated_model_script.py +0 -494
  126. workbench/model_scripts/custom_models/uq_models/mapie.template +0 -494
  127. workbench/model_scripts/custom_models/uq_models/meta_uq.template +0 -386
  128. workbench/model_scripts/custom_models/uq_models/proximity.py +0 -384
  129. workbench/model_scripts/ensemble_xgb/generated_model_script.py +0 -279
  130. workbench/model_scripts/quant_regression/quant_regression.template +0 -279
  131. workbench/model_scripts/quant_regression/requirements.txt +0 -1
  132. workbench/themes/quartz/base_css.url +0 -1
  133. workbench/themes/quartz/custom.css +0 -117
  134. workbench/themes/quartz/plotly.json +0 -642
  135. workbench/themes/quartz_dark/base_css.url +0 -1
  136. workbench/themes/quartz_dark/custom.css +0 -131
  137. workbench/themes/quartz_dark/plotly.json +0 -642
  138. workbench/utils/resource_utils.py +0 -39
  139. {workbench-0.8.177.dist-info → workbench-0.8.227.dist-info}/WHEEL +0 -0
  140. {workbench-0.8.177.dist-info → workbench-0.8.227.dist-info}/top_level.txt +0 -0
@@ -17,18 +17,28 @@ log = logging.getLogger("workbench")
17
17
 
18
18
  def fingerprints_to_matrix(fingerprints, dtype=np.uint8):
19
19
  """
20
- Convert bitstring fingerprints to numpy matrix.
20
+ Convert fingerprints to numpy matrix.
21
+
22
+ Supports two formats (auto-detected):
23
+ - Bitstrings: "10110010..." → matrix of 0s and 1s
24
+ - Count vectors: "0,3,0,1,5,..." → matrix of counts (or binary if dtype=np.bool_)
21
25
 
22
26
  Args:
23
- fingerprints: pandas Series or list of bitstring fingerprints
24
- dtype: numpy data type (uint8 is default: np.bool_ is good for Jaccard computations
27
+ fingerprints: pandas Series or list of fingerprints
28
+ dtype: numpy data type (uint8 is default; np.bool_ for Jaccard computations)
25
29
 
26
30
  Returns:
27
31
  dense numpy array of shape (n_molecules, n_bits)
28
32
  """
29
-
30
- # Dense matrix representation (we might support sparse in the future)
31
- return np.array([list(fp) for fp in fingerprints], dtype=dtype)
33
+ # Auto-detect format based on first fingerprint
34
+ sample = str(fingerprints.iloc[0] if hasattr(fingerprints, "iloc") else fingerprints[0])
35
+ if "," in sample:
36
+ # Count vector format: comma-separated integers
37
+ matrix = np.array([list(map(int, fp.split(","))) for fp in fingerprints], dtype=dtype)
38
+ else:
39
+ # Bitstring format: each character is a bit
40
+ matrix = np.array([list(fp) for fp in fingerprints], dtype=dtype)
41
+ return matrix
32
42
 
33
43
 
34
44
  def project_fingerprints(df: pd.DataFrame, projection: str = "UMAP") -> pd.DataFrame:
@@ -2,34 +2,18 @@
2
2
 
3
3
  import logging
4
4
  import base64
5
- import re
6
5
  from typing import Optional, Tuple
7
6
  from rdkit import Chem
8
7
  from rdkit.Chem import AllChem, Draw
9
8
  from rdkit.Chem.Draw import rdMolDraw2D
10
9
 
10
+ # Workbench Imports
11
+ from workbench.utils.color_utils import is_dark
12
+
11
13
  # Set up the logger
12
14
  log = logging.getLogger("workbench")
13
15
 
14
16
 
15
- def _is_dark(color: str) -> bool:
16
- """Determine if an rgba color is dark based on RGB average.
17
-
18
- Args:
19
- color: Color in rgba(...) format
20
-
21
- Returns:
22
- True if the color is dark, False otherwise
23
- """
24
- match = re.match(r"rgba?\((\d+),\s*(\d+),\s*(\d+)", color)
25
- if not match:
26
- log.warning(f"Invalid color format: {color}, defaulting to dark")
27
- return True # Default to dark mode on error
28
-
29
- r, g, b = map(int, match.groups())
30
- return (r + g + b) / 3 < 128
31
-
32
-
33
17
  def _rgba_to_tuple(rgba: str) -> Tuple[float, float, float, float]:
34
18
  """Convert rgba string to normalized tuple (R, G, B, A).
35
19
 
@@ -75,7 +59,13 @@ def _configure_draw_options(options: Draw.MolDrawOptions, background: str) -> No
75
59
  options: RDKit drawing options object
76
60
  background: Background color string
77
61
  """
78
- if _is_dark(background):
62
+ try:
63
+ if is_dark(background):
64
+ rdMolDraw2D.SetDarkMode(options)
65
+ # Light backgrounds use RDKit defaults (no action needed)
66
+ except ValueError:
67
+ # Default to dark mode if color format is invalid
68
+ log.warning(f"Invalid color format: {background}, defaulting to dark mode")
79
69
  rdMolDraw2D.SetDarkMode(options)
80
70
  options.setBackgroundColour(_rgba_to_tuple(background))
81
71
 
@@ -137,7 +127,7 @@ def svg_from_smiles(
137
127
  drawer.DrawMolecule(mol)
138
128
  drawer.FinishDrawing()
139
129
 
140
- # Encode SVG
130
+ # Encode SVG as base64 data URI
141
131
  svg = drawer.GetDrawingText()
142
132
  encoded_svg = base64.b64encode(svg.encode("utf-8")).decode("utf-8")
143
133
  return f"data:image/svg+xml;base64,{encoded_svg}"
@@ -222,7 +212,7 @@ if __name__ == "__main__":
222
212
  # Test 6: Color parsing functions
223
213
  print("\n6. Testing color utility functions...")
224
214
  test_colors = [
225
- ("invalid_color", True, (0.25, 0.25, 0.25, 1.0)), # Should use defaults
215
+ ("invalid_color", None, (0.25, 0.25, 0.25, 1.0)), # Should raise ValueError
226
216
  ("rgba(255, 255, 255, 1)", False, (1.0, 1.0, 1.0, 1.0)),
227
217
  ("rgba(0, 0, 0, 1)", True, (0.0, 0.0, 0.0, 1.0)),
228
218
  ("rgba(64, 64, 64, 0.5)", True, (0.251, 0.251, 0.251, 0.5)),
@@ -230,12 +220,20 @@ if __name__ == "__main__":
230
220
  ]
231
221
 
232
222
  for color, expected_dark, expected_tuple in test_colors:
233
- is_dark_result = _is_dark(color)
234
- tuple_result = _rgba_to_tuple(color)
235
-
236
- dark_status = "✓" if is_dark_result == expected_dark else "✗"
237
- print(f" {dark_status} is_dark('{color[:20]}...'): {is_dark_result} == {expected_dark}")
223
+ try:
224
+ is_dark_result = is_dark(color)
225
+ if expected_dark is None:
226
+ print(f" is_dark('{color[:20]}...'): Expected ValueError but got {is_dark_result}")
227
+ else:
228
+ dark_status = "✓" if is_dark_result == expected_dark else "✗"
229
+ print(f" {dark_status} is_dark('{color[:20]}...'): {is_dark_result} == {expected_dark}")
230
+ except ValueError:
231
+ if expected_dark is None:
232
+ print(f" ✓ is_dark('{color[:20]}...'): Correctly raised ValueError")
233
+ else:
234
+ print(f" ✗ is_dark('{color[:20]}...'): Unexpected ValueError")
238
235
 
236
+ tuple_result = _rgba_to_tuple(color)
239
237
  # Check tuple values with tolerance for floating point
240
238
  tuple_match = all(abs(a - b) < 0.01 for a, b in zip(tuple_result, expected_tuple))
241
239
  tuple_status = "✓" if tuple_match else "✗"
@@ -0,0 +1,141 @@
1
+ """ChemProp utilities for Workbench models."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import Any, Tuple
6
+
7
+ import pandas as pd
8
+
9
+ from workbench.utils.aws_utils import pull_s3_data
10
+ from workbench.utils.metrics_utils import compute_metrics_from_predictions
11
+ from workbench.utils.model_utils import safe_extract_tarfile
12
+
13
+ log = logging.getLogger("workbench")
14
+
15
+
16
+ def download_and_extract_model(s3_uri: str, model_dir: str) -> None:
17
+ """Download model artifact from S3 and extract it.
18
+
19
+ Args:
20
+ s3_uri: S3 URI to the model artifact (model.tar.gz)
21
+ model_dir: Directory to extract model artifacts to
22
+ """
23
+ import awswrangler as wr
24
+
25
+ log.info(f"Downloading model from {s3_uri}...")
26
+
27
+ # Download to temp file
28
+ local_tar_path = os.path.join(model_dir, "model.tar.gz")
29
+ wr.s3.download(path=s3_uri, local_file=local_tar_path)
30
+
31
+ # Extract using safe extraction
32
+ log.info(f"Extracting to {model_dir}...")
33
+ safe_extract_tarfile(local_tar_path, model_dir)
34
+
35
+ # Cleanup tar file
36
+ os.unlink(local_tar_path)
37
+
38
+
39
+ def load_chemprop_model_artifacts(model_dir: str) -> Tuple[Any, dict]:
40
+ """Load ChemProp MPNN model and artifacts from an extracted model directory.
41
+
42
+ Args:
43
+ model_dir: Directory containing extracted model artifacts
44
+
45
+ Returns:
46
+ Tuple of (MPNN model, artifacts_dict).
47
+ artifacts_dict contains 'label_encoder' and 'feature_metadata' if present.
48
+ """
49
+ import joblib
50
+ from chemprop import models
51
+
52
+ model_path = os.path.join(model_dir, "chemprop_model.pt")
53
+ if not os.path.exists(model_path):
54
+ raise FileNotFoundError(f"No chemprop_model.pt found in {model_dir}")
55
+
56
+ model = models.MPNN.load_from_file(model_path)
57
+ model.eval()
58
+
59
+ # Load additional artifacts
60
+ artifacts = {}
61
+
62
+ label_encoder_path = os.path.join(model_dir, "label_encoder.joblib")
63
+ if os.path.exists(label_encoder_path):
64
+ artifacts["label_encoder"] = joblib.load(label_encoder_path)
65
+
66
+ feature_metadata_path = os.path.join(model_dir, "feature_metadata.joblib")
67
+ if os.path.exists(feature_metadata_path):
68
+ artifacts["feature_metadata"] = joblib.load(feature_metadata_path)
69
+
70
+ return model, artifacts
71
+
72
+
73
+ def pull_cv_results(workbench_model: Any) -> Tuple[pd.DataFrame, pd.DataFrame]:
74
+ """Pull cross-validation results from AWS training artifacts.
75
+
76
+ This retrieves the validation predictions saved during model training and
77
+ computes metrics directly from them.
78
+
79
+ Note:
80
+ - Regression: Supports both single-target and multi-target models
81
+ - Classification: Only single-target is supported (with any number of classes)
82
+
83
+ Args:
84
+ workbench_model: Workbench model object
85
+
86
+ Returns:
87
+ Tuple of:
88
+ - DataFrame with computed metrics
89
+ - DataFrame with validation predictions
90
+ """
91
+
92
+ # Get the validation predictions from S3
93
+ s3_path = f"{workbench_model.model_training_path}/validation_predictions.csv"
94
+ predictions_df = pull_s3_data(s3_path)
95
+
96
+ if predictions_df is None:
97
+ raise ValueError(f"No validation predictions found at {s3_path}")
98
+
99
+ log.info(f"Pulled {len(predictions_df)} validation predictions from {s3_path}")
100
+
101
+ # Get target and class labels
102
+ target = workbench_model.target()
103
+ class_labels = workbench_model.class_labels()
104
+
105
+ # If single target just use the "prediction" column
106
+ if isinstance(target, str):
107
+ metrics_df = compute_metrics_from_predictions(predictions_df, target, class_labels)
108
+ return metrics_df, predictions_df
109
+
110
+ # Multi-target regression
111
+ metrics_list = []
112
+ for t in target:
113
+ # Prediction will be {target}_pred in multi-target case
114
+ pred_col = f"{t}_pred"
115
+
116
+ # Drop NaNs for this target
117
+ target_preds_df = predictions_df.dropna(subset=[t, pred_col])
118
+ metrics_df = compute_metrics_from_predictions(target_preds_df, t, class_labels, prediction_col=pred_col)
119
+ metrics_df.insert(0, "target", t)
120
+ metrics_list.append(metrics_df)
121
+ metrics_df = pd.concat(metrics_list, ignore_index=True) if metrics_list else pd.DataFrame()
122
+
123
+ return metrics_df, predictions_df
124
+
125
+
126
+ if __name__ == "__main__":
127
+
128
+ # Tests for the ChemProp utilities
129
+ from workbench.api import Model
130
+
131
+ # Initialize Workbench model
132
+ model_name = "open-admet-chemprop-mt"
133
+ print(f"Loading Workbench model: {model_name}")
134
+ model = Model(model_name)
135
+ print(f"Model Framework: {model.model_framework}")
136
+
137
+ # Pull CV results
138
+ metrics_df, predictions_df = pull_cv_results(model)
139
+ print("\nTraining Metrics:")
140
+ print(metrics_df.to_string(index=False))
141
+ print(f"\nSample Predictions:\n{predictions_df.head().to_string(index=False)}")
@@ -4,16 +4,13 @@ import os
4
4
  import sys
5
5
  import platform
6
6
  import logging
7
- import importlib.resources as resources # noqa: F401 Python 3.9 compatibility
8
7
  from typing import Any, Dict
8
+ from importlib.resources import files, as_file
9
9
 
10
10
  # Workbench imports
11
11
  from workbench.utils.license_manager import LicenseManager
12
12
  from workbench_bridges.utils.execution_environment import running_as_service
13
13
 
14
- # Python 3.9 compatibility
15
- from workbench.utils.resource_utils import get_resource_path
16
-
17
14
 
18
15
  class FatalConfigError(Exception):
19
16
  """Exception raised for errors in the configuration."""
@@ -172,8 +169,7 @@ class ConfigManager:
172
169
  Returns:
173
170
  str: The open source API key.
174
171
  """
175
- # Python 3.9 compatibility
176
- with get_resource_path("workbench.resources", "open_source_api.key") as open_source_key_path:
172
+ with as_file(files("workbench.resources").joinpath("open_source_api.key")) as open_source_key_path:
177
173
  with open(open_source_key_path, "r") as key_file:
178
174
  return key_file.read().strip()
179
175
 
@@ -7,9 +7,7 @@ from typing import Union, Optional
7
7
  import pandas as pd
8
8
 
9
9
  # Workbench Imports
10
- from workbench.api.feature_set import FeatureSet
11
- from workbench.api.model import Model
12
- from workbench.api.endpoint import Endpoint
10
+ from workbench.api import FeatureSet, Model, Endpoint
13
11
 
14
12
  # Set up the log
15
13
  log = logging.getLogger("workbench")
@@ -77,7 +75,7 @@ def internal_model_data_url(endpoint_config_name: str, session: boto3.Session) -
77
75
  return None
78
76
 
79
77
 
80
- def fs_training_data(end: Endpoint) -> pd.DataFrame:
78
+ def get_training_data(end: Endpoint) -> pd.DataFrame:
81
79
  """Code to get the training data from the FeatureSet used to train the Model
82
80
 
83
81
  Args:
@@ -100,7 +98,7 @@ def fs_training_data(end: Endpoint) -> pd.DataFrame:
100
98
  return train_df
101
99
 
102
100
 
103
- def fs_evaluation_data(end: Endpoint) -> pd.DataFrame:
101
+ def get_evaluation_data(end: Endpoint) -> pd.DataFrame:
104
102
  """Code to get the evaluation data from the FeatureSet NOT used for training
105
103
 
106
104
  Args:
@@ -178,11 +176,11 @@ if __name__ == "__main__":
178
176
  print(model_data_url)
179
177
 
180
178
  # Get the training data
181
- my_train_df = fs_training_data(my_endpoint)
179
+ my_train_df = get_training_data(my_endpoint)
182
180
  print(my_train_df)
183
181
 
184
182
  # Get the evaluation data
185
- my_eval_df = fs_evaluation_data(my_endpoint)
183
+ my_eval_df = get_evaluation_data(my_endpoint)
186
184
  print(my_eval_df)
187
185
 
188
186
  # Backtrack to the FeatureSet
@@ -6,15 +6,12 @@ import json
6
6
  import logging
7
7
  import requests
8
8
  from typing import Union
9
- import importlib.resources as resources # noqa: F401 Python 3.9 compatibility
10
9
  from datetime import datetime
11
10
  from cryptography.hazmat.primitives import hashes
12
11
  from cryptography.hazmat.primitives.asymmetric import padding
13
12
  from cryptography.hazmat.primitives import serialization
14
13
  from cryptography.hazmat.backends import default_backend
15
-
16
- # Python 3.9 compatibility
17
- from workbench.utils.resource_utils import get_resource_path
14
+ from importlib.resources import files, as_file
18
15
 
19
16
 
20
17
  class FatalLicenseError(Exception):
@@ -140,8 +137,7 @@ class LicenseManager:
140
137
  Returns:
141
138
  The public key as an object.
142
139
  """
143
- # Python 3.9 compatibility
144
- with get_resource_path("workbench.resources", "signature_verify_pub.pem") as public_key_path:
140
+ with as_file(files("workbench.resources").joinpath("signature_verify_pub.pem")) as public_key_path:
145
141
  with open(public_key_path, "rb") as key_file:
146
142
  public_key_data = key_file.read()
147
143
 
@@ -185,6 +185,63 @@ def dict_to_collapsible_html(data: dict, title: str = None, collapse_all: bool =
185
185
  return result
186
186
 
187
187
 
188
+ def df_to_html_table(df, round_digits: int = 2, margin_bottom: int = 30) -> str:
189
+ """Convert a DataFrame to a compact styled HTML table (horizontal layout).
190
+
191
+ Args:
192
+ df: DataFrame with metrics (can be single or multi-row)
193
+ round_digits: Number of decimal places to round to (default: 2)
194
+ margin_bottom: Bottom margin in pixels (default: 30)
195
+
196
+ Returns:
197
+ str: HTML table string
198
+ """
199
+ # Handle index: reset if named (keeps as column), otherwise drop
200
+ if df.index.name:
201
+ df = df.reset_index()
202
+ else:
203
+ df = df.reset_index(drop=True)
204
+
205
+ # Round numeric columns
206
+ df = df.round(round_digits)
207
+
208
+ # Table styles
209
+ container_style = f"display: flex; justify-content: center; margin-top: 10px; margin-bottom: {margin_bottom}px;"
210
+ table_style = "border-collapse: collapse; width: 100%; font-size: 15px;"
211
+ header_style = (
212
+ "background: linear-gradient(to bottom, #4a4a4a 0%, #2d2d2d 100%); "
213
+ "color: white; padding: 4px 8px; text-align: center;"
214
+ )
215
+ cell_style = "padding: 3px 8px; text-align: center; border-bottom: 1px solid #444;"
216
+
217
+ # Build the HTML table (wrapped in centered container)
218
+ html = f'<div style="{container_style}"><table style="{table_style}">'
219
+
220
+ # Header row
221
+ html += "<tr>"
222
+ for col in df.columns:
223
+ html += f'<th style="{header_style}">{col}</th>'
224
+ html += "</tr>"
225
+
226
+ # Data rows
227
+ for _, row in df.iterrows():
228
+ html += "<tr>"
229
+ for val in row:
230
+ # Format value: integers without decimal, floats rounded
231
+ if isinstance(val, float):
232
+ if val == int(val):
233
+ formatted_val = int(val)
234
+ else:
235
+ formatted_val = round(val, round_digits)
236
+ else:
237
+ formatted_val = val
238
+ html += f'<td style="{cell_style}">{formatted_val}</td>'
239
+ html += "</tr>"
240
+
241
+ html += "</table></div>"
242
+ return html
243
+
244
+
188
245
  if __name__ == "__main__":
189
246
  """Exercise the Markdown Utilities"""
190
247
  from workbench.api.model import Model