notionhelper 0.3.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. notionhelper-0.4.0/ML_DEMO_README.md +317 -0
  2. {notionhelper-0.3.1 → notionhelper-0.4.0}/PKG-INFO +6 -2
  3. {notionhelper-0.3.1 → notionhelper-0.4.0}/README.md +5 -1
  4. notionhelper-0.4.0/SEPARATION_SUMMARY.md +70 -0
  5. notionhelper-0.4.0/examples/ml_demo.py +391 -0
  6. {notionhelper-0.3.1 → notionhelper-0.4.0}/pyproject.toml +1 -1
  7. notionhelper-0.4.0/src/notionhelper/__init__.py +4 -0
  8. {notionhelper-0.3.1 → notionhelper-0.4.0}/src/notionhelper/helper.py +0 -174
  9. notionhelper-0.4.0/src/notionhelper/ml_logger.py +206 -0
  10. {notionhelper-0.3.1 → notionhelper-0.4.0}/uv.lock +1 -1
  11. notionhelper-0.3.1/src/notionhelper/__init__.py +0 -3
  12. {notionhelper-0.3.1 → notionhelper-0.4.0}/.coverage +0 -0
  13. {notionhelper-0.3.1 → notionhelper-0.4.0}/.github/workflows/claude-code-review.yml +0 -0
  14. {notionhelper-0.3.1 → notionhelper-0.4.0}/.github/workflows/claude.yml +0 -0
  15. {notionhelper-0.3.1 → notionhelper-0.4.0}/.gitignore +0 -0
  16. {notionhelper-0.3.1 → notionhelper-0.4.0}/GETTING_STARTED.md +0 -0
  17. {notionhelper-0.3.1 → notionhelper-0.4.0}/images/helper_logo.png +0 -0
  18. {notionhelper-0.3.1 → notionhelper-0.4.0}/images/json_builder.png.png +0 -0
  19. {notionhelper-0.3.1 → notionhelper-0.4.0}/images/logo.png +0 -0
  20. {notionhelper-0.3.1 → notionhelper-0.4.0}/images/notionh3.png +0 -0
  21. {notionhelper-0.3.1 → notionhelper-0.4.0}/images/pillio.png +0 -0
  22. {notionhelper-0.3.1 → notionhelper-0.4.0}/images/pillio2.png +0 -0
  23. {notionhelper-0.3.1 → notionhelper-0.4.0}/notionapi_md_info.md +0 -0
  24. {notionhelper-0.3.1 → notionhelper-0.4.0}/pytest.ini +0 -0
  25. {notionhelper-0.3.1 → notionhelper-0.4.0}/tests/README.md +0 -0
  26. {notionhelper-0.3.1 → notionhelper-0.4.0}/tests/__init__.py +0 -0
  27. {notionhelper-0.3.1 → notionhelper-0.4.0}/tests/conftest.py +0 -0
  28. {notionhelper-0.3.1 → notionhelper-0.4.0}/tests/test_helper.py +0 -0
@@ -0,0 +1,317 @@
1
+ # NotionHelper ML Demo Guide
2
+
3
+ ## Overview
4
+
5
+ `ml_demo.py` is a comprehensive demonstration of how to use **MLNotionHelper** (which extends NotionHelper) to track machine learning experiments. It showcases a complete workflow from model training to Notion integration.
6
+
7
+ **Note:** The ML experiment tracking features are available in the `MLNotionHelper` class, which inherits from `NotionHelper` and adds specialized methods for logging ML experiments.
8
+
9
+ ## Features
10
+
11
+ ✨ **Complete ML Pipeline**
12
+ - Logistic Regression on sklearn's breast cancer dataset
13
+ - Train/test split with stratification
14
+ - Feature scaling
15
+ - Comprehensive metrics calculation
16
+
17
+ 📊 **Metrics Tracked**
18
+ - Accuracy
19
+ - Precision
20
+ - Recall
21
+ - F1 Score
22
+ - ROC AUC
23
+ - Training/Test sample sizes
24
+
25
+ 📈 **Visualizations**
26
+ - Confusion Matrix (heatmap)
27
+ - ROC Curve with AUC score
28
+ - Feature Importance (when scaling is disabled)
29
+
30
+ 💾 **Artifacts**
31
+ - Predictions CSV with probabilities
32
+ - Classification report
33
+ - All generated plots
34
+
35
+ ## Quick Start
36
+
37
+ ### 1. Run the Demo (without Notion)
38
+
39
+ ```bash
40
+ python ml_demo.py
41
+ ```
42
+
43
+ This will:
44
+ - Train the model
45
+ - Generate all metrics and plots
46
+ - Save artifacts to disk
47
+ - Show instructions for Notion integration
48
+
49
+ ### 2. Set Up Notion Integration
50
+
51
+ #### A. Get Your Notion API Token
52
+
53
+ 1. Go to [Notion Integrations](https://www.notion.so/my-integrations)
54
+ 2. Create a new integration
55
+ 3. Copy the "Internal Integration Token"
56
+ 4. Set it as an environment variable:
57
+
58
+ ```bash
59
+ export NOTION_TOKEN='secret_your_token_here'
60
+ ```
61
+
62
+ #### B. Create a Parent Page
63
+
64
+ 1. Create a new page in Notion (this will hold your ML experiment databases)
65
+ 2. Share the page with your integration
66
+ 3. Copy the page ID from the URL:
67
+ - URL: `https://www.notion.so/My-ML-Experiments-abc123def456...`
68
+ - Page ID: `abc123def456...`
69
+
70
+ #### C. Create the Database (First Time Only)
71
+
72
+ 1. Open `ml_demo.py`
73
+ 2. Find the "STEP 4A" section
74
+ 3. Uncomment the database creation code
75
+ 4. Set `PARENT_PAGE_ID = "your_page_id_here"`
76
+ 5. Run the script:
77
+
78
+ ```bash
79
+ python ml_demo.py
80
+ ```
81
+
82
+ 6. **IMPORTANT**: Copy the `data_source_id` from the output!
83
+
84
+ Example output:
85
+ ```
86
+ ✓ Database created! Data Source ID: 2d2fdfd6-8a97-80ba-bdd6-000b787993a4
87
+ 💡 Save this ID for future experiment logging!
88
+ ```
89
+
90
+ #### D. Log Experiments
91
+
92
+ 1. Comment out the database creation code (STEP 4A)
93
+ 2. Set `DATA_SOURCE_ID = "your_data_source_id_from_step_C"`
94
+ 3. Run experiments:
95
+
96
+ ```bash
97
+ python ml_demo.py
98
+ ```
99
+
100
+ Each run will:
101
+ - Create a new row in your Notion database
102
+ - Upload confusion matrix and ROC curve plots
103
+ - Attach CSV artifacts
104
+ - Compare metrics with previous runs
105
+ - Show 🏆 if it's a new best score!
106
+
107
+ ## Customization
108
+
109
+ ### Hyperparameters
110
+
111
+ Modify the `config` dictionary in the `main()` function:
112
+
113
+ ```python
114
+ config = {
115
+ "Experiment_Name": "Your Experiment",
116
+ "Model": "Logistic Regression",
117
+ "C_Regularization": 10.0, # Change this
118
+ "Max_Iterations": 2000, # Or this
119
+ "Solver": "saga", # Try different solvers
120
+ "Penalty": "l1", # L1 or L2 regularization
121
+ "Feature_Scaling": True # Enable/disable scaling
122
+ }
123
+ ```
124
+
125
+ ### Target Metric
126
+
127
+ Change which metric to optimize in the `log_ml_experiment()` call:
128
+
129
+ ```python
130
+ page_id = nh.log_ml_experiment(
131
+ ...
132
+ target_metric="Accuracy", # Or "Precision", "Recall", "F1_Score"
133
+ higher_is_better=True, # Higher scores are better
134
+ ...
135
+ )
136
+ ```
137
+
138
+ ## Example Workflow
139
+
140
+ ### Experiment 1: Baseline
141
+ ```python
142
+ config = {
143
+ "C_Regularization": 1.0,
144
+ "Penalty": "l2",
145
+ "Solver": "lbfgs"
146
+ }
147
+ # Results: F1 Score = 98.61%
148
+ ```
149
+
150
+ ### Experiment 2: Stronger Regularization
151
+ ```python
152
+ config = {
153
+ "C_Regularization": 0.1, # Stronger regularization
154
+ "Penalty": "l2",
155
+ "Solver": "lbfgs"
156
+ }
157
+ # Run to see if it improves performance
158
+ ```
159
+
160
+ ### Experiment 3: L1 Regularization
161
+ ```python
162
+ config = {
163
+ "C_Regularization": 1.0,
164
+ "Penalty": "l1", # Switch to L1
165
+ "Solver": "saga" # L1 requires saga or liblinear
166
+ }
167
+ # L1 can perform feature selection
168
+ ```
169
+
170
+ ## Generated Files
171
+
172
+ After running the demo, you'll find:
173
+
174
+ ```
175
+ ├── confusion_matrix.png # Confusion matrix heatmap
176
+ ├── roc_curve.png # ROC curve with AUC
177
+ ├── feature_importance.png # Feature coefficients (if no scaling)
178
+ ├── predictions.csv # Test set predictions
179
+ └── classification_report.csv # Detailed metrics per class
180
+ ```
181
+
182
+ ## Notion Database Schema
183
+
184
+ The created database will have columns for:
185
+
186
+ **Config Fields:**
187
+ - Experiment_Name (Title)
188
+ - Model
189
+ - Dataset
190
+ - Test_Size
191
+ - Random_State
192
+ - C_Regularization (Number)
193
+ - Max_Iterations (Number)
194
+ - Solver
195
+ - Penalty
196
+ - Feature_Scaling (Checkbox) ✅
197
+
198
+ **Metric Fields:**
199
+ - Accuracy (Number)
200
+ - Precision (Number)
201
+ - Recall (Number)
202
+ - F1_Score (Number)
203
+ - ROC_AUC (Number)
204
+ - Train_Samples (Number)
205
+ - Test_Samples (Number)
206
+ - Run Status (shows 🏆 for new best)
207
+
208
+ **Artifacts:**
209
+ - Plots (embedded in page body)
210
+ - Artifacts (attached CSV files)
211
+
212
+ ## Troubleshooting
213
+
214
+ ### Boolean Properties Showing as Numbers
215
+
216
+ If you see boolean values (like `Feature_Scaling`) appearing as numbers in Notion:
217
+
218
+ 1. Check the debug output in the console
219
+ 2. Ensure you're passing Python `bool` types (not 0/1 integers)
220
+ 3. The `dict_to_notion_schema()` includes debug prints to help diagnose
221
+
222
+ ### Notion API Errors
223
+
224
+ Common issues:
225
+ - **401 Unauthorized**: Check your NOTION_TOKEN
226
+ - **404 Not Found**: Verify your PARENT_PAGE_ID or DATA_SOURCE_ID
227
+ - **400 Bad Request**: Make sure the page is shared with your integration
228
+
229
+ ### Missing Plots
230
+
231
+ Ensure matplotlib and seaborn are installed:
232
+ ```bash
233
+ pip install matplotlib seaborn
234
+ ```
235
+
236
+ ## Advanced Usage
237
+
238
+ ### Use Your Own Dataset
239
+
240
+ Replace the data loading section:
241
+
242
+ ```python
243
+ # Replace this:
244
+ data = load_breast_cancer()
245
+ X = pd.DataFrame(data.data, columns=data.feature_names)
246
+ y = pd.Series(data.target, name='target')
247
+
248
+ # With your own data:
249
+ df = pd.read_csv('your_data.csv')
250
+ X = df.drop('target_column', axis=1)
251
+ y = df['target_column']
252
+ ```
253
+
254
+ ### Add More Metrics
255
+
256
+ Calculate additional metrics:
257
+
258
+ ```python
259
+ from sklearn.metrics import matthews_corrcoef, balanced_accuracy_score
260
+
261
+ metrics = {
262
+ ...
263
+ "MCC": round(matthews_corrcoef(y_test, y_pred), 4),
264
+ "Balanced_Accuracy": round(balanced_accuracy_score(y_test, y_pred) * 100, 2)
265
+ }
266
+ ```
267
+
268
+ ### Grid Search Integration
269
+
270
+ Combine with sklearn's GridSearchCV:
271
+
272
+ ```python
273
+ from sklearn.model_selection import GridSearchCV
274
+
275
+ param_grid = {
276
+ 'C': [0.1, 1.0, 10.0],
277
+ 'penalty': ['l1', 'l2']
278
+ }
279
+
280
+ grid = GridSearchCV(LogisticRegression(), param_grid, cv=5)
281
+ grid.fit(X_train, y_train)
282
+
283
+ # Log each configuration
284
+ for params, mean_score in zip(grid.cv_results_['params'],
285
+ grid.cv_results_['mean_test_score']):
286
+ config.update(params)
287
+ metrics['CV_Score'] = mean_score
288
+ nh.log_ml_experiment(...)
289
+ ```
290
+
291
+ ## Benefits of Using NotionHelper
292
+
293
+ ✅ **Centralized Tracking**: All experiments in one place
294
+ ✅ **Visual Comparison**: See which hyperparameters work best
295
+ ✅ **Automatic Leaderboard**: Highlights new best scores
296
+ ✅ **File Attachments**: Keep plots and CSVs with experiments
297
+ ✅ **Team Collaboration**: Share results with your team
298
+ ✅ **Reproducibility**: Track all hyperparameters and seeds
299
+
300
+ ## Next Steps
301
+
302
+ 1. **Run the demo** to familiarize yourself with the workflow
303
+ 2. **Create your Notion database** following the setup guide
304
+ 3. **Customize for your project** - replace with your ML model
305
+ 4. **Run multiple experiments** with different hyperparameters
306
+ 5. **Review results in Notion** - compare and analyze performance
307
+
308
+ ## Support
309
+
310
+ For issues or questions:
311
+ - Check the [NotionHelper documentation](carecast/notionhelper.py)
312
+ - Review the [Notion API docs](https://developers.notion.com/)
313
+ - Examine the debug output for type checking issues
314
+
315
+ ---
316
+
317
+ **Happy Experimenting! 🚀**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionhelper
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: NotionHelper is a Python library that simplifies interactions with the Notion API, enabling easy management of databases, pages, and files within Notion workspaces.
5
5
  Author-email: Jan du Plessis <drjanduplessis@icloud.com>
6
6
  Requires-Python: >=3.10
@@ -74,7 +74,7 @@ Here is an example of how to use the library:
74
74
 
75
75
  ```python
76
76
  import os
77
- from notionhelper import NotionHelper
77
+ from notionhelper import NotionHelper, MLNotionHelper
78
78
  ```
79
79
 
80
80
  ### Initialize the NotionHelper class
@@ -82,7 +82,11 @@ from notionhelper import NotionHelper
82
82
  ```python
83
83
  notion_token = os.getenv("NOTION_TOKEN")
84
84
 
85
+ # For core Notion operations
85
86
  helper = NotionHelper(notion_token)
87
+
88
+ # For ML experiment tracking (includes all NotionHelper methods)
89
+ ml_helper = MLNotionHelper(notion_token)
86
90
  ```
87
91
 
88
92
  ### Retrieve a Database (Container)
@@ -47,7 +47,7 @@ Here is an example of how to use the library:
47
47
 
48
48
  ```python
49
49
  import os
50
- from notionhelper import NotionHelper
50
+ from notionhelper import NotionHelper, MLNotionHelper
51
51
  ```
52
52
 
53
53
  ### Initialize the NotionHelper class
@@ -55,7 +55,11 @@ from notionhelper import NotionHelper
55
55
  ```python
56
56
  notion_token = os.getenv("NOTION_TOKEN")
57
57
 
58
+ # For core Notion operations
58
59
  helper = NotionHelper(notion_token)
60
+
61
+ # For ML experiment tracking (includes all NotionHelper methods)
62
+ ml_helper = MLNotionHelper(notion_token)
59
63
  ```
60
64
 
61
65
  ### Retrieve a Database (Container)
@@ -0,0 +1,70 @@
1
+ # ML Functions Separation - Implementation Summary
2
+
3
+ ## What Was Done
4
+
5
+ Successfully separated Machine Learning functions from the core NotionHelper class using **inheritance-based approach**.
6
+
7
+ ### File Changes
8
+
9
+ #### 1. **Created: `src/notionhelper/ml_logger.py`** (NEW)
10
+ - New `MLNotionHelper` class that **inherits from `NotionHelper`**
11
+ - Moved ML-specific methods:
12
+ - `log_ml_experiment()` - Logs experiments with metrics, plots, and artifacts
13
+ - `create_ml_database()` - Creates Notion databases optimized for ML tracking
14
+ - `dict_to_notion_schema()` - Converts dictionaries to Notion schema
15
+ - `dict_to_notion_props()` - Converts dictionaries to Notion properties
16
+
17
+ #### 2. **Modified: `src/notionhelper/helper.py`**
18
+ - Removed the 4 ML-specific methods listed above
19
+ - **Kept all core Notion API methods**:
20
+ - Database/data source operations
21
+ - Page creation and retrieval
22
+ - File upload and embedding
23
+ - Block management
24
+
25
+ #### 3. **Updated: `src/notionhelper/__init__.py`**
26
+ ```python
27
+ from .helper import NotionHelper
28
+ from .ml_logger import MLNotionHelper
29
+
30
+ __all__ = ["NotionHelper", "MLNotionHelper"]
31
+ ```
32
+
33
+ #### 4. **Updated: `examples/ml_demo.py`**
34
+ - Changed import: `from notionhelper import MLNotionHelper`
35
+ - Changed initialization: `nh = MLNotionHelper(NOTION_TOKEN)`
36
+
37
+ ## Usage
38
+
39
+ ### Simple, Single Instantiation:
40
+ ```python
41
+ from notionhelper import MLNotionHelper
42
+
43
+ # One line - that's it!
44
+ ml_tracker = MLNotionHelper(notion_token)
45
+
46
+ # Use ML methods
47
+ ml_tracker.log_ml_experiment(...)
48
+ ml_tracker.create_ml_database(...)
49
+
50
+ # Also available: all NotionHelper methods
51
+ ml_tracker.get_data_source(...)
52
+ ml_tracker.upload_file(...)
53
+ ```
54
+
55
+ ## Architecture Benefits
56
+
57
+ ✅ **Clean Separation** - ML logic isolated in dedicated module
58
+ ✅ **Single Instantiation** - No extra code needed
59
+ ✅ **Minimal Changes** - Just inherit and move methods
60
+ ✅ **Backward Compatible** - `NotionHelper` still available separately
61
+ ✅ **Extensible** - Easy to add other trackers (e.g., `ImageNotionHelper`)
62
+ ✅ **Elegant** - Inheritance makes intent clear
63
+
64
+ ## File Structure
65
+ ```
66
+ src/notionhelper/
67
+ ├── helper.py # Core Notion API methods
68
+ ├── ml_logger.py # ML experiment tracking (NEW)
69
+ └── __init__.py # Exports both classes
70
+ ```
@@ -0,0 +1,391 @@
1
+ """
2
+ NotionHelper ML Demo: Logistic Regression with sklearn
3
+ =======================================================
4
+ This demo showcases how to use NotionHelper to track ML experiments.
5
+
6
+ Features:
7
+ - Logistic regression on sklearn's breast cancer dataset
8
+ - Complete metrics tracking (accuracy, precision, recall, F1)
9
+ - Hyperparameter configuration
10
+ - Automatic Notion database creation
11
+ - Experiment logging with plots and artifacts
12
+ """
13
+
14
+ import os
15
+ import numpy as np
16
+ import pandas as pd
17
+ import matplotlib.pyplot as plt
18
+ import seaborn as sns
19
+ from sklearn.datasets import load_breast_cancer
20
+ from sklearn.model_selection import train_test_split
21
+ from sklearn.linear_model import LogisticRegression
22
+ from sklearn.metrics import (
23
+ accuracy_score,
24
+ precision_score,
25
+ recall_score,
26
+ f1_score,
27
+ confusion_matrix,
28
+ classification_report,
29
+ roc_curve,
30
+ roc_auc_score
31
+ )
32
+ from sklearn.preprocessing import StandardScaler
33
+
34
+ from notionhelper import MLNotionHelper
35
+
36
+
37
+ def train_logistic_regression(
38
+ test_size=0.2,
39
+ random_state=42,
40
+ C=1.0,
41
+ max_iter=1000,
42
+ solver='lbfgs',
43
+ penalty='l2',
44
+ scale_features=True
45
+ ):
46
+ """
47
+ Train a logistic regression model on breast cancer dataset.
48
+
49
+ Parameters:
50
+ -----------
51
+ test_size : float
52
+ Proportion of dataset to use for testing
53
+ random_state : int
54
+ Random seed for reproducibility
55
+ C : float
56
+ Inverse of regularization strength
57
+ max_iter : int
58
+ Maximum iterations for solver
59
+ solver : str
60
+ Algorithm to use in optimization
61
+ penalty : str
62
+ Regularization penalty type
63
+ scale_features : bool
64
+ Whether to standardize features
65
+
66
+ Returns:
67
+ --------
68
+ metrics : dict
69
+ Dictionary containing all evaluation metrics
70
+ plot_paths : list
71
+ List of paths to generated plots
72
+ artifacts : list
73
+ List of paths to saved artifacts
74
+ """
75
+
76
+ print("\n" + "="*60)
77
+ print("🔬 NOTIONHELPER ML DEMO: Logistic Regression")
78
+ print("="*60 + "\n")
79
+
80
+ # 1. Load Dataset
81
+ print("📊 Loading breast cancer dataset...")
82
+ data = load_breast_cancer()
83
+ X = pd.DataFrame(data.data, columns=data.feature_names)
84
+ y = pd.Series(data.target, name='target')
85
+
86
+ print(f" Dataset shape: {X.shape}")
87
+ print(f" Classes: {data.target_names}")
88
+ print(f" Features: {X.shape[1]}")
89
+
90
+ # 2. Split Data
91
+ print("\n🔀 Splitting data...")
92
+ X_train, X_test, y_train, y_test = train_test_split(
93
+ X, y, test_size=test_size, random_state=random_state, stratify=y
94
+ )
95
+ print(f" Training set: {X_train.shape[0]} samples")
96
+ print(f" Test set: {X_test.shape[0]} samples")
97
+
98
+ # 3. Feature Scaling (optional but recommended)
99
+ if scale_features:
100
+ print("\n⚖️ Scaling features...")
101
+ scaler = StandardScaler()
102
+ X_train = scaler.fit_transform(X_train)
103
+ X_test = scaler.transform(X_test)
104
+
105
+ # 4. Train Model
106
+ print("\n🤖 Training Logistic Regression model...")
107
+ model = LogisticRegression(
108
+ C=C,
109
+ max_iter=max_iter,
110
+ solver=solver,
111
+ penalty=penalty,
112
+ random_state=random_state
113
+ )
114
+ model.fit(X_train, y_train)
115
+ print(" ✓ Model trained successfully")
116
+
117
+ # 5. Make Predictions
118
+ print("\n🎯 Making predictions...")
119
+ y_pred = model.predict(X_test)
120
+ y_pred_proba = model.predict_proba(X_test)[:, 1]
121
+
122
+ # 6. Calculate Metrics
123
+ print("\n📈 Calculating metrics...")
124
+ accuracy = accuracy_score(y_test, y_pred)
125
+ precision = precision_score(y_test, y_pred)
126
+ recall = recall_score(y_test, y_pred)
127
+ f1 = f1_score(y_test, y_pred)
128
+ roc_auc = roc_auc_score(y_test, y_pred_proba)
129
+
130
+ metrics = {
131
+ "Accuracy": round(accuracy * 100, 2),
132
+ "Precision": round(precision * 100, 2),
133
+ "Recall": round(recall * 100, 2),
134
+ "F1_Score": round(f1 * 100, 2),
135
+ "ROC_AUC": round(roc_auc * 100, 2),
136
+ "Train_Samples": int(X_train.shape[0]),
137
+ "Test_Samples": int(X_test.shape[0])
138
+ }
139
+
140
+ # Print metrics
141
+ print("\n" + "="*60)
142
+ print("📊 MODEL PERFORMANCE METRICS")
143
+ print("-" * 60)
144
+ print(f"Accuracy : {metrics['Accuracy']:.2f}%")
145
+ print(f"Precision : {metrics['Precision']:.2f}%")
146
+ print(f"Recall : {metrics['Recall']:.2f}%")
147
+ print(f"F1 Score : {metrics['F1_Score']:.2f}%")
148
+ print(f"ROC AUC : {metrics['ROC_AUC']:.2f}%")
149
+ print("="*60 + "\n")
150
+
151
+ # 7. Generate Visualizations
152
+ print("📊 Generating visualizations...")
153
+ plot_paths = []
154
+
155
+ # Confusion Matrix
156
+ cm = confusion_matrix(y_test, y_pred)
157
+ plt.figure(figsize=(8, 6))
158
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
159
+ xticklabels=data.target_names,
160
+ yticklabels=data.target_names)
161
+ plt.title('Confusion Matrix', fontsize=14, fontweight='bold')
162
+ plt.ylabel('True Label')
163
+ plt.xlabel('Predicted Label')
164
+ plt.tight_layout()
165
+ cm_path = 'confusion_matrix.png'
166
+ plt.savefig(cm_path, dpi=150)
167
+ plot_paths.append(cm_path)
168
+ plt.close()
169
+ print(f" ✓ Saved: {cm_path}")
170
+
171
+ # ROC Curve
172
+ fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
173
+ plt.figure(figsize=(8, 6))
174
+ plt.plot(fpr, tpr, color='#1f77b4', lw=2,
175
+ label=f'ROC curve (AUC = {roc_auc:.3f})')
176
+ plt.plot([0, 1], [0, 1], color='gray', lw=1, linestyle='--',
177
+ label='Random Classifier')
178
+ plt.xlim([0.0, 1.0])
179
+ plt.ylim([0.0, 1.05])
180
+ plt.xlabel('False Positive Rate', fontsize=12)
181
+ plt.ylabel('True Positive Rate', fontsize=12)
182
+ plt.title('ROC Curve', fontsize=14, fontweight='bold')
183
+ plt.legend(loc="lower right")
184
+ plt.grid(alpha=0.3)
185
+ plt.tight_layout()
186
+ roc_path = 'roc_curve.png'
187
+ plt.savefig(roc_path, dpi=150)
188
+ plot_paths.append(roc_path)
189
+ plt.close()
190
+ print(f" ✓ Saved: {roc_path}")
191
+
192
+ # Feature Importance (Coefficients)
193
+ if not scale_features:
194
+ feature_importance = pd.DataFrame({
195
+ 'Feature': data.feature_names,
196
+ 'Coefficient': model.coef_[0]
197
+ }).sort_values('Coefficient', key=abs, ascending=False).head(15)
198
+
199
+ plt.figure(figsize=(10, 6))
200
+ colors = ['#d62728' if x < 0 else '#2ca02c' for x in feature_importance['Coefficient']]
201
+ plt.barh(feature_importance['Feature'], feature_importance['Coefficient'], color=colors)
202
+ plt.xlabel('Coefficient Value', fontsize=12)
203
+ plt.title('Top 15 Feature Importance (Logistic Regression Coefficients)',
204
+ fontsize=14, fontweight='bold')
205
+ plt.grid(axis='x', alpha=0.3)
206
+ plt.tight_layout()
207
+ feat_path = 'feature_importance.png'
208
+ plt.savefig(feat_path, dpi=150)
209
+ plot_paths.append(feat_path)
210
+ plt.close()
211
+ print(f" ✓ Saved: {feat_path}")
212
+
213
+ # 8. Save Artifacts
214
+ print("\n💾 Saving artifacts...")
215
+ artifacts = []
216
+
217
+ # Save predictions
218
+ predictions_df = pd.DataFrame({
219
+ 'True_Label': y_test.values,
220
+ 'Predicted_Label': y_pred,
221
+ 'Probability_Malignant': y_pred_proba,
222
+ 'Correct': (y_test.values == y_pred).astype(int)
223
+ })
224
+ pred_path = 'predictions.csv'
225
+ predictions_df.to_csv(pred_path, index=False)
226
+ artifacts.append(pred_path)
227
+ print(f" ✓ Saved: {pred_path}")
228
+
229
+ # Save classification report
230
+ report = classification_report(y_test, y_pred,
231
+ target_names=data.target_names,
232
+ output_dict=True)
233
+ report_df = pd.DataFrame(report).transpose()
234
+ report_path = 'classification_report.csv'
235
+ report_df.to_csv(report_path)
236
+ artifacts.append(report_path)
237
+ print(f" ✓ Saved: {report_path}")
238
+
239
+ # Combine plot paths and artifacts
240
+ all_artifacts = plot_paths + artifacts
241
+
242
+ return metrics, plot_paths, all_artifacts
243
+
244
+
245
+ def main():
246
+ """
247
+ Main function to demonstrate NotionHelper integration.
248
+ """
249
+
250
+ # ============================================================
251
+ # STEP 1: Define Hyperparameters Configuration
252
+ # ============================================================
253
+ config = {
254
+ "Experiment_Name": "Logistic Regression Demo",
255
+ "Model": "Logistic Regression",
256
+ "Dataset": "Breast Cancer (sklearn)",
257
+ "Test_Size": 0.2,
258
+ "Random_State": 42,
259
+ "C_Regularization": 1.0,
260
+ "Max_Iterations": 2,
261
+ "Solver": "lbfgs",
262
+ "Penalty": "l2",
263
+ "Feature_Scaling": True
264
+ }
265
+
266
+ # ============================================================
267
+ # STEP 2: Train Model and Calculate Metrics
268
+ # ============================================================
269
+ metrics, plot_paths, artifacts = train_logistic_regression(
270
+ test_size=config["Test_Size"],
271
+ random_state=config["Random_State"],
272
+ C=config["C_Regularization"],
273
+ max_iter=config["Max_Iterations"],
274
+ solver=config["Solver"],
275
+ penalty=config["Penalty"],
276
+ scale_features=config["Feature_Scaling"]
277
+ )
278
+
279
+ # ============================================================
280
+ # STEP 3: Initialize NotionHelper
281
+ # ============================================================
282
+ print("\n" + "="*60)
283
+ print("📝 NOTION INTEGRATION")
284
+ print("="*60 + "\n")
285
+
286
+ # IMPORTANT: Replace with your Notion API token
287
+ NOTION_TOKEN = os.getenv("NOTION_TOKEN", "your_notion_token_here")
288
+
289
+ if NOTION_TOKEN == "your_notion_token_here":
290
+ print("⚠️ WARNING: Please set your NOTION_TOKEN environment variable")
291
+ print(" Example: export NOTION_TOKEN='secret_...'")
292
+ print("\n✅ Demo completed successfully (without Notion logging)")
293
+ print(f"\n📁 Generated files:")
294
+ for artifact in artifacts:
295
+ print(f" • {artifact}")
296
+ return
297
+
298
+ try:
299
+ nh = MLNotionHelper(NOTION_TOKEN)
300
+ print("✓ MLNotionHelper initialized successfully")
301
+
302
+ # ============================================================
303
+ # STEP 4A: Create New Database (First time only)
304
+ # ============================================================
305
+ # Set CREATE_NEW_DB to True on first run, then set to False
306
+ CREATE_NEW_DB = False # Force creation for this run
307
+ PARENT_PAGE_ID = "your page id here"
308
+
309
+ if CREATE_NEW_DB:
310
+ print("\n🗄️ Creating new Notion database...")
311
+ data_source_id = nh.create_ml_database(
312
+ parent_page_id=PARENT_PAGE_ID,
313
+ db_title="ML Experiments - Logistic Regression Demo",
314
+ config=config,
315
+ metrics=metrics,
316
+ file_property_name="Artifacts"
317
+ )
318
+ print(f"\n✅ Database created successfully!")
319
+ print(f"📝 Data Source ID: {data_source_id}")
320
+ print("\n" + "="*60)
321
+ print("⚠️ CRITICAL: Complete these steps NOW!")
322
+ print("="*60)
323
+ print("\n1️⃣ Go to Notion and find the new database:")
324
+ print(" 'ML Experiments - Logistic Regression Demo'")
325
+ print("\n2️⃣ Click '...' (top right) → Add connections")
326
+ print(" → Select your integration")
327
+ print("\n3️⃣ Save this Data Source ID:")
328
+ print(f" DATA_SOURCE_ID = \"{data_source_id}\"")
329
+ print("\n4️⃣ Set CREATE_NEW_DB = False in this script")
330
+ print("\n5️⃣ Run the script again to log experiments")
331
+ print("="*60 + "\n")
332
+
333
+ print("⏸️ Skipping experiment logging for this run.")
334
+ print(" Complete steps above, then run again.")
335
+ return # Exit after database creation
336
+
337
+ # This else block will only be reached if CREATE_NEW_DB was initially False
338
+ # and the user has already provided a DATA_SOURCE_ID.
339
+ else:
340
+ # Replace with your actual data source ID after creating the database
341
+ DATA_SOURCE_ID = "your_data_source_id_here" # This should be updated by the user
342
+
343
+
344
+ if DATA_SOURCE_ID == "your_data_source_id_here":
345
+ print("\n💡 To log experiments:")
346
+ print(" 1. Ensure CREATE_NEW_DB is False and DATA_SOURCE_ID is set.")
347
+ print(" 2. Make sure the database is shared with your integration.")
348
+ else:
349
+ print("\n� Logging experiment to Notion...")
350
+ page_id = nh.log_ml_experiment(
351
+ data_source_id=DATA_SOURCE_ID,
352
+ config=config,
353
+ metrics=metrics,
354
+ plots=plot_paths,
355
+ target_metric="F1_Score",
356
+ higher_is_better=True,
357
+ file_paths=artifacts,
358
+ file_property_name="Artifacts"
359
+ )
360
+
361
+ if page_id:
362
+ print(f"✓ Experiment logged successfully!")
363
+ print(f" Page ID: {page_id}")
364
+ else:
365
+ print("❌ Failed to log experiment")
366
+
367
+ except Exception as e:
368
+ print(f"❌ Notion API Error: {e}")
369
+ print(" Continuing without Notion logging...")
370
+
371
+ # ============================================================
372
+ # FINAL SUMMARY
373
+ # ============================================================
374
+ print("\n" + "="*60)
375
+ print("✅ DEMO COMPLETED SUCCESSFULLY")
376
+ print("="*60)
377
+ print(f"\n📁 Generated files:")
378
+ for artifact in artifacts:
379
+ print(f" • {artifact}")
380
+
381
+ print("\n📊 Key Metrics:")
382
+ print(f" • Accuracy: {metrics['Accuracy']:.2f}%")
383
+ print(f" • F1 Score: {metrics['F1_Score']:.2f}%")
384
+ print(f" • ROC AUC: {metrics['ROC_AUC']:.2f}%")
385
+
386
+ print("\n🎉 Thank you for trying NotionHelper!")
387
+ print("="*60 + "\n")
388
+
389
+
390
+ if __name__ == "__main__":
391
+ main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "notionhelper"
3
- version = "0.3.1"
3
+ version = "0.4.0"
4
4
  description = "NotionHelper is a Python library that simplifies interactions with the Notion API, enabling easy management of databases, pages, and files within Notion workspaces."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,4 @@
1
+ from .helper import NotionHelper
2
+ from .ml_logger import MLNotionHelper
3
+
4
+ __all__ = ["NotionHelper", "MLNotionHelper"]
@@ -654,177 +654,3 @@ class NotionHelper:
654
654
  }
655
655
  response = requests.patch(update_url, headers=headers, json=data)
656
656
  return response.json()
657
-
658
- def dict_to_notion_schema(self, data: Dict[str, Any], title_key: str) -> Dict[str, Any]:
659
- """Converts a dictionary into a Notion property schema for database creation.
660
-
661
- Parameters:
662
- data (dict): Dictionary containing sample values to infer types from.
663
- title_key (str): The key that should be used as the title property.
664
-
665
- Returns:
666
- dict: A dictionary defining the Notion property schema.
667
- """
668
- properties = {}
669
-
670
- for key, value in data.items():
671
- # Handle NumPy types
672
- if hasattr(value, "item"):
673
- value = value.item()
674
-
675
- # Debug output to help diagnose type issues
676
- print(f"DEBUG: key='{key}', value={value}, type={type(value).__name__}, isinstance(bool)={isinstance(value, bool)}, isinstance(int)={isinstance(value, int)}")
677
-
678
- if key == title_key:
679
- properties[key] = {"title": {}}
680
- # IMPORTANT: Check for bool BEFORE (int, float) because bool is a subclass of int in Python
681
- elif isinstance(value, bool):
682
- properties[key] = {"checkbox": {}}
683
- print(f" → Assigned as CHECKBOX")
684
- elif isinstance(value, (int, float)):
685
- properties[key] = {"number": {"format": "number"}}
686
- print(f" → Assigned as NUMBER")
687
- else:
688
- properties[key] = {"rich_text": {}}
689
- print(f" → Assigned as RICH_TEXT")
690
-
691
- return properties
692
-
693
- def dict_to_notion_props(self, data: Dict[str, Any], title_key: str) -> Dict[str, Any]:
694
- """Converts a dictionary into Notion property values for page creation.
695
-
696
- Parameters:
697
- data (dict): Dictionary containing the values to convert.
698
- title_key (str): The key that should be used as the title property.
699
-
700
- Returns:
701
- dict: A dictionary defining the Notion property values.
702
- """
703
- notion_props = {}
704
- for key, value in data.items():
705
- # Handle NumPy types
706
- if hasattr(value, "item"):
707
- value = value.item()
708
-
709
- if key == title_key:
710
- ts = datetime.now().strftime("%Y-%m-%d %H:%M")
711
- notion_props[key] = {"title": [{"text": {"content": f"{value} ({ts})"}}]}
712
-
713
- # FIX: Handle Booleans
714
- elif isinstance(value, bool):
715
- # Option A: Map to a Checkbox column in Notion
716
- # notion_props[key] = {"checkbox": value}
717
-
718
- # Option B: Map to a Rich Text column as a string (since you added a rich text field)
719
- notion_props[key] = {"rich_text": [{"text": {"content": str(value)}}]}
720
-
721
- elif isinstance(value, (int, float)):
722
- if pd.isna(value) or np.isinf(value): continue
723
- notion_props[key] = {"number": float(value)}
724
- else:
725
- notion_props[key] = {"rich_text": [{"text": {"content": str(value)}}]}
726
- return notion_props
727
-
728
- def log_ml_experiment(
729
- self,
730
- data_source_id: str,
731
- config: Dict,
732
- metrics: Dict,
733
- plots: List[str] = None,
734
- target_metric: str = "sMAPE", # Re-added these
735
- higher_is_better: bool = False, # to fix the error
736
- file_paths: Optional[List[str]] = None, # Changed to list
737
- file_property_name: str = "Output Files"
738
- ):
739
- """Logs ML experiment and compares metrics with multiple file support."""
740
- improvement_tag = "Standard Run"
741
- new_score = metrics.get(target_metric)
742
-
743
- # 1. Leaderboard Logic (Champions)
744
- if new_score is not None:
745
- try:
746
- df = self.get_data_source_pages_as_dataframe(data_source_id, limit=100)
747
- if not df.empty and target_metric in df.columns:
748
- valid_scores = pd.to_numeric(df[target_metric], errors='coerce').dropna()
749
- if not valid_scores.empty:
750
- current_best = valid_scores.max() if higher_is_better else valid_scores.min()
751
- is_improvement = (new_score > current_best) if higher_is_better else (new_score < current_best)
752
- if is_improvement:
753
- improvement_tag = f"🏆 NEW BEST {target_metric} (Prev: {current_best:.2f})"
754
- else:
755
- diff = abs(new_score - current_best)
756
- improvement_tag = f"No Improvement (+{diff:.2f} {target_metric})"
757
- except Exception as e:
758
- print(f"Leaderboard check skipped: {e}")
759
-
760
- # 2. Prepare Notion Properties
761
- data_for_notion = metrics.copy()
762
- data_for_notion["Run Status"] = improvement_tag
763
- combined_payload = {**config, **data_for_notion}
764
- title_key = list(config.keys())[0]
765
- properties = self.dict_to_notion_props(combined_payload, title_key)
766
-
767
- try:
768
- # 3. Create the row
769
- new_page = self.new_page_to_data_source(data_source_id, properties)
770
- page_id = new_page["id"]
771
-
772
- # 4. Handle Plots (Body)
773
- if plots:
774
- for plot_path in plots:
775
- if os.path.exists(plot_path):
776
- self.one_step_image_embed(page_id, plot_path)
777
-
778
- # 5. Handle Multiple File Uploads (Property)
779
- if file_paths:
780
- file_assets = []
781
- for path in file_paths:
782
- if os.path.exists(path):
783
- print(f"Uploading {path}...")
784
- upload_resp = self.upload_file(path)
785
- file_assets.append({
786
- "type": "file_upload",
787
- "file_upload": {"id": upload_resp["id"]},
788
- "name": os.path.basename(path),
789
- })
790
-
791
- if file_assets:
792
- # Attach all files in one request
793
- update_url = f"https://api.notion.com/v1/pages/{page_id}"
794
- file_payload = {"properties": {file_property_name: {"files": file_assets}}}
795
- self._make_request("PATCH", update_url, file_payload)
796
- print(f"✅ {len(file_assets)} files attached to {file_property_name}")
797
-
798
- return page_id
799
- except Exception as e:
800
- print(f"Log error: {e}")
801
- return None
802
-
803
- def create_ml_database(self, parent_page_id: str, db_title: str, config: Dict, metrics: Dict, file_property_name: str = "Output Files") -> str:
804
- """
805
- Analyzes dicts to create a new Notion Database with the correct schema.
806
- Uses dict_to_notion_schema() for universal type conversion.
807
- """
808
- combined = {**config, **metrics}
809
- title_key = list(config.keys())[0]
810
-
811
- # Use the universal dict_to_notion_schema() method
812
- properties = self.dict_to_notion_schema(combined, title_key)
813
-
814
- # Add 'Run Status' if not already present
815
- if "Run Status" not in properties:
816
- properties["Run Status"] = {"rich_text": {}}
817
-
818
- # Add the Multi-file property
819
- properties[file_property_name] = {"files": {}}
820
-
821
- print(f"Creating database '{db_title}' with {len(properties)} columns...")
822
-
823
- response = self.create_database(
824
- parent_page_id=parent_page_id,
825
- database_title=db_title,
826
- initial_data_source_properties=properties
827
- )
828
-
829
- data_source_id = response.get("initial_data_source", {}).get("id")
830
- return data_source_id if data_source_id else response.get("id")
@@ -0,0 +1,206 @@
1
+ from typing import Optional, Dict, List, Any
2
+ import pandas as pd
3
+ import numpy as np
4
+ import os
5
+ from datetime import datetime
6
+
7
+ from .helper import NotionHelper
8
+
9
+
10
+ class MLNotionHelper(NotionHelper):
11
+ """
12
+ ML experiment tracking helper that extends NotionHelper.
13
+
14
+ Provides specialized methods for logging and tracking machine learning experiments,
15
+ automatically comparing metrics against historical runs and logging results to Notion.
16
+
17
+ Methods
18
+ -------
19
+ log_ml_experiment(data_source_id, config, metrics, plots, target_metric,
20
+ higher_is_better, file_paths, file_property_name):
21
+ Logs an ML experiment run with metrics, plots, and artifacts.
22
+
23
+ create_ml_database(parent_page_id, db_title, config, metrics, file_property_name):
24
+ Creates a new Notion database optimized for ML experiment tracking.
25
+
26
+ dict_to_notion_schema(data, title_key):
27
+ Converts a dictionary into a Notion property schema.
28
+
29
+ dict_to_notion_props(data, title_key):
30
+ Converts a dictionary into Notion property values.
31
+ """
32
+
33
+ def dict_to_notion_schema(self, data: Dict[str, Any], title_key: str) -> Dict[str, Any]:
34
+ """Converts a dictionary into a Notion property schema for database creation.
35
+
36
+ Parameters:
37
+ data (dict): Dictionary containing sample values to infer types from.
38
+ title_key (str): The key that should be used as the title property.
39
+
40
+ Returns:
41
+ dict: A dictionary defining the Notion property schema.
42
+ """
43
+ properties = {}
44
+
45
+ for key, value in data.items():
46
+ # Handle NumPy types
47
+ if hasattr(value, "item"):
48
+ value = value.item()
49
+
50
+ # Debug output to help diagnose type issues
51
+ print(f"DEBUG: key='{key}', value={value}, type={type(value).__name__}, isinstance(bool)={isinstance(value, bool)}, isinstance(int)={isinstance(value, int)}")
52
+
53
+ if key == title_key:
54
+ properties[key] = {"title": {}}
55
+ # IMPORTANT: Check for bool BEFORE (int, float) because bool is a subclass of int in Python
56
+ elif isinstance(value, bool):
57
+ properties[key] = {"checkbox": {}}
58
+ print(f" → Assigned as CHECKBOX")
59
+ elif isinstance(value, (int, float)):
60
+ properties[key] = {"number": {"format": "number"}}
61
+ print(f" → Assigned as NUMBER")
62
+ else:
63
+ properties[key] = {"rich_text": {}}
64
+ print(f" → Assigned as RICH_TEXT")
65
+
66
+ return properties
67
+
68
+ def dict_to_notion_props(self, data: Dict[str, Any], title_key: str) -> Dict[str, Any]:
69
+ """Converts a dictionary into Notion property values for page creation.
70
+
71
+ Parameters:
72
+ data (dict): Dictionary containing the values to convert.
73
+ title_key (str): The key that should be used as the title property.
74
+
75
+ Returns:
76
+ dict: A dictionary defining the Notion property values.
77
+ """
78
+ notion_props = {}
79
+ for key, value in data.items():
80
+ # Handle NumPy types
81
+ if hasattr(value, "item"):
82
+ value = value.item()
83
+
84
+ if key == title_key:
85
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M")
86
+ notion_props[key] = {"title": [{"text": {"content": f"{value} ({ts})"}}]}
87
+
88
+ # FIX: Handle Booleans
89
+ elif isinstance(value, bool):
90
+ # Option A: Map to a Checkbox column in Notion
91
+ # notion_props[key] = {"checkbox": value}
92
+
93
+ # Option B: Map to a Rich Text column as a string (since you added a rich text field)
94
+ notion_props[key] = {"rich_text": [{"text": {"content": str(value)}}]}
95
+
96
+ elif isinstance(value, (int, float)):
97
+ if pd.isna(value) or np.isinf(value):
98
+ continue
99
+ notion_props[key] = {"number": float(value)}
100
+ else:
101
+ notion_props[key] = {"rich_text": [{"text": {"content": str(value)}}]}
102
+ return notion_props
103
+
104
+ def log_ml_experiment(
105
+ self,
106
+ data_source_id: str,
107
+ config: Dict,
108
+ metrics: Dict,
109
+ plots: List[str] = None,
110
+ target_metric: str = "sMAPE",
111
+ higher_is_better: bool = False,
112
+ file_paths: Optional[List[str]] = None,
113
+ file_property_name: str = "Output Files"
114
+ ):
115
+ """Logs ML experiment and compares metrics with multiple file support."""
116
+ improvement_tag = "Standard Run"
117
+ new_score = metrics.get(target_metric)
118
+
119
+ # 1. Leaderboard Logic (Champions)
120
+ if new_score is not None:
121
+ try:
122
+ df = self.get_data_source_pages_as_dataframe(data_source_id, limit=100)
123
+ if not df.empty and target_metric in df.columns:
124
+ valid_scores = pd.to_numeric(df[target_metric], errors='coerce').dropna()
125
+ if not valid_scores.empty:
126
+ current_best = valid_scores.max() if higher_is_better else valid_scores.min()
127
+ is_improvement = (new_score > current_best) if higher_is_better else (new_score < current_best)
128
+ if is_improvement:
129
+ improvement_tag = f"🏆 NEW BEST {target_metric} (Prev: {current_best:.2f})"
130
+ else:
131
+ diff = abs(new_score - current_best)
132
+ improvement_tag = f"No Improvement (+{diff:.2f} {target_metric})"
133
+ except Exception as e:
134
+ print(f"Leaderboard check skipped: {e}")
135
+
136
+ # 2. Prepare Notion Properties
137
+ data_for_notion = metrics.copy()
138
+ data_for_notion["Run Status"] = improvement_tag
139
+ combined_payload = {**config, **data_for_notion}
140
+ title_key = list(config.keys())[0]
141
+ properties = self.dict_to_notion_props(combined_payload, title_key)
142
+
143
+ try:
144
+ # 3. Create the row
145
+ new_page = self.new_page_to_data_source(data_source_id, properties)
146
+ page_id = new_page["id"]
147
+
148
+ # 4. Handle Plots (Body)
149
+ if plots:
150
+ for plot_path in plots:
151
+ if os.path.exists(plot_path):
152
+ self.one_step_image_embed(page_id, plot_path)
153
+
154
+ # 5. Handle Multiple File Uploads (Property)
155
+ if file_paths:
156
+ file_assets = []
157
+ for path in file_paths:
158
+ if os.path.exists(path):
159
+ print(f"Uploading {path}...")
160
+ upload_resp = self.upload_file(path)
161
+ file_assets.append({
162
+ "type": "file_upload",
163
+ "file_upload": {"id": upload_resp["id"]},
164
+ "name": os.path.basename(path),
165
+ })
166
+
167
+ if file_assets:
168
+ # Attach all files in one request
169
+ update_url = f"https://api.notion.com/v1/pages/{page_id}"
170
+ file_payload = {"properties": {file_property_name: {"files": file_assets}}}
171
+ self._make_request("PATCH", update_url, file_payload)
172
+ print(f"✅ {len(file_assets)} files attached to {file_property_name}")
173
+
174
+ return page_id
175
+ except Exception as e:
176
+ print(f"Log error: {e}")
177
+ return None
178
+
179
+ def create_ml_database(self, parent_page_id: str, db_title: str, config: Dict, metrics: Dict, file_property_name: str = "Output Files") -> str:
180
+ """
181
+ Analyzes dicts to create a new Notion Database with the correct schema.
182
+ Uses dict_to_notion_schema() for universal type conversion.
183
+ """
184
+ combined = {**config, **metrics}
185
+ title_key = list(config.keys())[0]
186
+
187
+ # Use the universal dict_to_notion_schema() method
188
+ properties = self.dict_to_notion_schema(combined, title_key)
189
+
190
+ # Add 'Run Status' if not already present
191
+ if "Run Status" not in properties:
192
+ properties["Run Status"] = {"rich_text": {}}
193
+
194
+ # Add the Multi-file property
195
+ properties[file_property_name] = {"files": {}}
196
+
197
+ print(f"Creating database '{db_title}' with {len(properties)} columns...")
198
+
199
+ response = self.create_database(
200
+ parent_page_id=parent_page_id,
201
+ database_title=db_title,
202
+ initial_data_source_properties=properties
203
+ )
204
+
205
+ data_source_id = response.get("initial_data_source", {}).get("id")
206
+ return data_source_id if data_source_id else response.get("id")
@@ -1279,7 +1279,7 @@ wheels = [
1279
1279
 
1280
1280
  [[package]]
1281
1281
  name = "notionhelper"
1282
- version = "0.2.1"
1282
+ version = "0.3.2"
1283
1283
  source = { editable = "." }
1284
1284
  dependencies = [
1285
1285
  { name = "mimetype" },
@@ -1,3 +0,0 @@
1
- from .helper import NotionHelper
2
-
3
- __all__ = ["NotionHelper"]
File without changes
File without changes
File without changes