notionhelper 0.2.3__tar.gz → 0.3.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.
- {notionhelper-0.2.3 → notionhelper-0.3.0}/PKG-INFO +171 -1
- {notionhelper-0.2.3 → notionhelper-0.3.0}/README.md +170 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/pyproject.toml +1 -1
- {notionhelper-0.2.3 → notionhelper-0.3.0}/src/notionhelper/helper.py +178 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/uv.lock +1 -1
- notionhelper-0.2.3/.claude/settings.local.json +0 -12
- notionhelper-0.2.3/.python-version +0 -1
- notionhelper-0.2.3/src/notionhelper/helper1.8.py +0 -546
- {notionhelper-0.2.3 → notionhelper-0.3.0}/.coverage +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/.github/workflows/claude-code-review.yml +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/.github/workflows/claude.yml +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/.gitignore +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/images/helper_logo.png +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/images/json_builder.png.png +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/images/logo.png +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/images/pillio.png +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/images/pillio2.png +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/notion_api_examples.md +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/notionapi_md_info.md +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/pytest.ini +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/src/notionhelper/__init__.py +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/tests/README.md +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/tests/__init__.py +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/tests/conftest.py +0 -0
- {notionhelper-0.2.3 → notionhelper-0.3.0}/tests/test_helper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: notionhelper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -288,6 +288,170 @@ print(f"Successfully attached file to property: {response}")
|
|
|
288
288
|
|
|
289
289
|
These methods handle all the intermediate steps automatically, making file operations with Notion much simpler.
|
|
290
290
|
|
|
291
|
+
### Machine Learning Experiment Tracking
|
|
292
|
+
|
|
293
|
+
NotionHelper includes specialized functions for tracking machine learning experiments, making it easy to log configurations, metrics, plots, and output files to Notion databases. These functions automatically handle leaderboard tracking and provide a structured way to organize ML workflows.
|
|
294
|
+
|
|
295
|
+
#### create_ml_database()
|
|
296
|
+
Creates a new Notion database specifically designed for ML experiment tracking by analyzing your config and metrics dictionaries to automatically generate the appropriate schema.
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
# Define your typical experiment configuration and metrics
|
|
300
|
+
config = {
|
|
301
|
+
"Experiment Name": "LSTM Forecast v1",
|
|
302
|
+
"model_type": "LSTM",
|
|
303
|
+
"learning_rate": 0.001,
|
|
304
|
+
"batch_size": 32
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
metrics = {
|
|
308
|
+
"sMAPE": 12.5,
|
|
309
|
+
"MAE": 0.85,
|
|
310
|
+
"training_time": 45.2
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Create a new ML tracking database
|
|
314
|
+
parent_page_id = "your_parent_page_id"
|
|
315
|
+
data_source_id = helper.create_ml_database(
|
|
316
|
+
parent_page_id=parent_page_id,
|
|
317
|
+
db_title="ML Experiments - Time Series",
|
|
318
|
+
config=config,
|
|
319
|
+
metrics=metrics,
|
|
320
|
+
file_property_name="Output Files" # Optional, defaults to "Output Files"
|
|
321
|
+
)
|
|
322
|
+
print(f"Created ML database with data source ID: {data_source_id}")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The function automatically:
|
|
326
|
+
- Maps numeric values to Number properties
|
|
327
|
+
- Maps booleans to Checkbox properties
|
|
328
|
+
- Maps strings to Rich Text properties
|
|
329
|
+
- Uses the first config key as the Title property
|
|
330
|
+
- Adds a "Run Status" property for tracking improvements
|
|
331
|
+
- Adds a Files & Media property for attaching output files
|
|
332
|
+
|
|
333
|
+
#### log_ml_experiment()
|
|
334
|
+
Logs a complete ML experiment run including configuration, metrics, plots, and output files. It automatically compares metrics against previous runs to identify improvements and track the best performing models.
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
# Experiment configuration
|
|
338
|
+
config = {
|
|
339
|
+
"Experiment Name": "LSTM Forecast v2",
|
|
340
|
+
"model_type": "LSTM",
|
|
341
|
+
"layers": 3,
|
|
342
|
+
"learning_rate": 0.001,
|
|
343
|
+
"dropout": 0.2
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Training metrics
|
|
347
|
+
metrics = {
|
|
348
|
+
"sMAPE": 11.8,
|
|
349
|
+
"MAE": 0.78,
|
|
350
|
+
"RMSE": 1.23,
|
|
351
|
+
"training_time": 52.1
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Paths to plots and output files
|
|
355
|
+
plots = [
|
|
356
|
+
"path/to/training_loss.png",
|
|
357
|
+
"path/to/predictions.png"
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
output_files = [
|
|
361
|
+
"path/to/model.h5",
|
|
362
|
+
"path/to/scaler.pkl",
|
|
363
|
+
"path/to/results.csv"
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
# Log the experiment
|
|
367
|
+
page_id = helper.log_ml_experiment(
|
|
368
|
+
data_source_id=data_source_id,
|
|
369
|
+
config=config,
|
|
370
|
+
metrics=metrics,
|
|
371
|
+
plots=plots, # Will be embedded in page body
|
|
372
|
+
target_metric="sMAPE", # Metric to track for improvements
|
|
373
|
+
higher_is_better=False, # Lower sMAPE is better
|
|
374
|
+
file_paths=output_files, # Will be attached to Files & Media property
|
|
375
|
+
file_property_name="Output Files"
|
|
376
|
+
)
|
|
377
|
+
print(f"Logged experiment to page: {page_id}")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Features:**
|
|
381
|
+
- **Automatic Leaderboard Tracking**: Compares new results against previous runs
|
|
382
|
+
- **Champion Detection**: Automatically tags new best scores with 🏆
|
|
383
|
+
- **Performance Comparison**: Shows delta from current best when not improving
|
|
384
|
+
- **Plot Embedding**: Embeds visualization plots directly in the page body
|
|
385
|
+
- **File Attachments**: Attaches model files, scalers, and other outputs
|
|
386
|
+
- **Timestamp Tracking**: Automatically adds timestamps to experiment names
|
|
387
|
+
|
|
388
|
+
**Run Status Examples:**
|
|
389
|
+
- `🏆 NEW BEST sMAPE (Prev: 12.50)` - New champion found
|
|
390
|
+
- `No Improvement (+0.70 sMAPE)` - Score wasn't better
|
|
391
|
+
- `Standard Run` - First run or metric tracking disabled
|
|
392
|
+
|
|
393
|
+
#### upload_multiple_files_to_property()
|
|
394
|
+
Uploads multiple files and attaches them all to a single Files & Media property on a page.
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
page_id = "your_page_id"
|
|
398
|
+
property_name = "Output Files"
|
|
399
|
+
file_paths = [
|
|
400
|
+
"path/to/model.h5",
|
|
401
|
+
"path/to/scaler.pkl",
|
|
402
|
+
"path/to/predictions.csv"
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
response = helper.upload_multiple_files_to_property(page_id, property_name, file_paths)
|
|
406
|
+
print(f"Successfully attached {len(file_paths)} files to property")
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
#### dict_to_notion_props()
|
|
410
|
+
Converts a Python dictionary to Notion property format, handling type conversions automatically.
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
data = {
|
|
414
|
+
"Experiment Name": "Model v1",
|
|
415
|
+
"accuracy": 0.95,
|
|
416
|
+
"epochs": 100,
|
|
417
|
+
"is_best": True
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
properties = helper.dict_to_notion_props(data, title_key="Experiment Name")
|
|
421
|
+
# Properties are now formatted for Notion API
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Example ML Workflow:**
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
# 1. Create ML tracking database (one-time setup)
|
|
428
|
+
data_source_id = helper.create_ml_database(
|
|
429
|
+
parent_page_id="parent_page_id",
|
|
430
|
+
db_title="Computer Vision Experiments",
|
|
431
|
+
config={"Model Name": "ResNet50", "dataset": "ImageNet"},
|
|
432
|
+
metrics={"accuracy": 0.0, "f1_score": 0.0}
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# 2. Run multiple experiments
|
|
436
|
+
for lr in [0.001, 0.01, 0.1]:
|
|
437
|
+
# Train your model
|
|
438
|
+
model, metrics, plots = train_model(learning_rate=lr)
|
|
439
|
+
|
|
440
|
+
# Log to Notion
|
|
441
|
+
helper.log_ml_experiment(
|
|
442
|
+
data_source_id=data_source_id,
|
|
443
|
+
config={"Model Name": f"ResNet50_lr{lr}", "learning_rate": lr},
|
|
444
|
+
metrics=metrics,
|
|
445
|
+
plots=plots,
|
|
446
|
+
target_metric="accuracy",
|
|
447
|
+
higher_is_better=True
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# 3. Review results in Notion
|
|
451
|
+
df = helper.get_data_source_pages_as_dataframe(data_source_id)
|
|
452
|
+
print(df[["Model Name", "accuracy", "Run Status"]].sort_values("accuracy", ascending=False))
|
|
453
|
+
```
|
|
454
|
+
|
|
291
455
|
## Code Quality
|
|
292
456
|
|
|
293
457
|
The NotionHelper library includes several quality improvements:
|
|
@@ -329,6 +493,12 @@ The `NotionHelper` class provides the following methods:
|
|
|
329
493
|
- **`one_step_file_to_page(page_id, file_path)`** - Uploads and attaches a file to a page in one operation
|
|
330
494
|
- **`one_step_file_to_page_property(page_id, property_name, file_path, file_name)`** - Uploads and attaches a file to a page property in one operation
|
|
331
495
|
|
|
496
|
+
### Machine Learning Experiment Tracking
|
|
497
|
+
- **`create_ml_database(parent_page_id, db_title, config, metrics, file_property_name="Output Files")`** - Creates a new Notion database specifically designed for ML experiment tracking with automatic schema generation
|
|
498
|
+
- **`log_ml_experiment(data_source_id, config, metrics, plots=None, target_metric="sMAPE", higher_is_better=False, file_paths=None, file_property_name="Output Files")`** - Logs a complete ML experiment run including configuration, metrics, plots, and output files with automatic leaderboard tracking
|
|
499
|
+
- **`upload_multiple_files_to_property(page_id, property_name, file_paths)`** - Uploads multiple files and attaches them all to a single Files & Media property
|
|
500
|
+
- **`dict_to_notion_props(data, title_key)`** - Converts a Python dictionary to Notion property format with automatic type handling
|
|
501
|
+
|
|
332
502
|
## Requirements
|
|
333
503
|
|
|
334
504
|
- Python 3.10+
|
|
@@ -261,6 +261,170 @@ print(f"Successfully attached file to property: {response}")
|
|
|
261
261
|
|
|
262
262
|
These methods handle all the intermediate steps automatically, making file operations with Notion much simpler.
|
|
263
263
|
|
|
264
|
+
### Machine Learning Experiment Tracking
|
|
265
|
+
|
|
266
|
+
NotionHelper includes specialized functions for tracking machine learning experiments, making it easy to log configurations, metrics, plots, and output files to Notion databases. These functions automatically handle leaderboard tracking and provide a structured way to organize ML workflows.
|
|
267
|
+
|
|
268
|
+
#### create_ml_database()
|
|
269
|
+
Creates a new Notion database specifically designed for ML experiment tracking by analyzing your config and metrics dictionaries to automatically generate the appropriate schema.
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# Define your typical experiment configuration and metrics
|
|
273
|
+
config = {
|
|
274
|
+
"Experiment Name": "LSTM Forecast v1",
|
|
275
|
+
"model_type": "LSTM",
|
|
276
|
+
"learning_rate": 0.001,
|
|
277
|
+
"batch_size": 32
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
metrics = {
|
|
281
|
+
"sMAPE": 12.5,
|
|
282
|
+
"MAE": 0.85,
|
|
283
|
+
"training_time": 45.2
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Create a new ML tracking database
|
|
287
|
+
parent_page_id = "your_parent_page_id"
|
|
288
|
+
data_source_id = helper.create_ml_database(
|
|
289
|
+
parent_page_id=parent_page_id,
|
|
290
|
+
db_title="ML Experiments - Time Series",
|
|
291
|
+
config=config,
|
|
292
|
+
metrics=metrics,
|
|
293
|
+
file_property_name="Output Files" # Optional, defaults to "Output Files"
|
|
294
|
+
)
|
|
295
|
+
print(f"Created ML database with data source ID: {data_source_id}")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The function automatically:
|
|
299
|
+
- Maps numeric values to Number properties
|
|
300
|
+
- Maps booleans to Checkbox properties
|
|
301
|
+
- Maps strings to Rich Text properties
|
|
302
|
+
- Uses the first config key as the Title property
|
|
303
|
+
- Adds a "Run Status" property for tracking improvements
|
|
304
|
+
- Adds a Files & Media property for attaching output files
|
|
305
|
+
|
|
306
|
+
#### log_ml_experiment()
|
|
307
|
+
Logs a complete ML experiment run including configuration, metrics, plots, and output files. It automatically compares metrics against previous runs to identify improvements and track the best performing models.
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
# Experiment configuration
|
|
311
|
+
config = {
|
|
312
|
+
"Experiment Name": "LSTM Forecast v2",
|
|
313
|
+
"model_type": "LSTM",
|
|
314
|
+
"layers": 3,
|
|
315
|
+
"learning_rate": 0.001,
|
|
316
|
+
"dropout": 0.2
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Training metrics
|
|
320
|
+
metrics = {
|
|
321
|
+
"sMAPE": 11.8,
|
|
322
|
+
"MAE": 0.78,
|
|
323
|
+
"RMSE": 1.23,
|
|
324
|
+
"training_time": 52.1
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Paths to plots and output files
|
|
328
|
+
plots = [
|
|
329
|
+
"path/to/training_loss.png",
|
|
330
|
+
"path/to/predictions.png"
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
output_files = [
|
|
334
|
+
"path/to/model.h5",
|
|
335
|
+
"path/to/scaler.pkl",
|
|
336
|
+
"path/to/results.csv"
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
# Log the experiment
|
|
340
|
+
page_id = helper.log_ml_experiment(
|
|
341
|
+
data_source_id=data_source_id,
|
|
342
|
+
config=config,
|
|
343
|
+
metrics=metrics,
|
|
344
|
+
plots=plots, # Will be embedded in page body
|
|
345
|
+
target_metric="sMAPE", # Metric to track for improvements
|
|
346
|
+
higher_is_better=False, # Lower sMAPE is better
|
|
347
|
+
file_paths=output_files, # Will be attached to Files & Media property
|
|
348
|
+
file_property_name="Output Files"
|
|
349
|
+
)
|
|
350
|
+
print(f"Logged experiment to page: {page_id}")
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Features:**
|
|
354
|
+
- **Automatic Leaderboard Tracking**: Compares new results against previous runs
|
|
355
|
+
- **Champion Detection**: Automatically tags new best scores with 🏆
|
|
356
|
+
- **Performance Comparison**: Shows delta from current best when not improving
|
|
357
|
+
- **Plot Embedding**: Embeds visualization plots directly in the page body
|
|
358
|
+
- **File Attachments**: Attaches model files, scalers, and other outputs
|
|
359
|
+
- **Timestamp Tracking**: Automatically adds timestamps to experiment names
|
|
360
|
+
|
|
361
|
+
**Run Status Examples:**
|
|
362
|
+
- `🏆 NEW BEST sMAPE (Prev: 12.50)` - New champion found
|
|
363
|
+
- `No Improvement (+0.70 sMAPE)` - Score wasn't better
|
|
364
|
+
- `Standard Run` - First run or metric tracking disabled
|
|
365
|
+
|
|
366
|
+
#### upload_multiple_files_to_property()
|
|
367
|
+
Uploads multiple files and attaches them all to a single Files & Media property on a page.
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
page_id = "your_page_id"
|
|
371
|
+
property_name = "Output Files"
|
|
372
|
+
file_paths = [
|
|
373
|
+
"path/to/model.h5",
|
|
374
|
+
"path/to/scaler.pkl",
|
|
375
|
+
"path/to/predictions.csv"
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
response = helper.upload_multiple_files_to_property(page_id, property_name, file_paths)
|
|
379
|
+
print(f"Successfully attached {len(file_paths)} files to property")
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### dict_to_notion_props()
|
|
383
|
+
Converts a Python dictionary to Notion property format, handling type conversions automatically.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
data = {
|
|
387
|
+
"Experiment Name": "Model v1",
|
|
388
|
+
"accuracy": 0.95,
|
|
389
|
+
"epochs": 100,
|
|
390
|
+
"is_best": True
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
properties = helper.dict_to_notion_props(data, title_key="Experiment Name")
|
|
394
|
+
# Properties are now formatted for Notion API
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Example ML Workflow:**
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
# 1. Create ML tracking database (one-time setup)
|
|
401
|
+
data_source_id = helper.create_ml_database(
|
|
402
|
+
parent_page_id="parent_page_id",
|
|
403
|
+
db_title="Computer Vision Experiments",
|
|
404
|
+
config={"Model Name": "ResNet50", "dataset": "ImageNet"},
|
|
405
|
+
metrics={"accuracy": 0.0, "f1_score": 0.0}
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# 2. Run multiple experiments
|
|
409
|
+
for lr in [0.001, 0.01, 0.1]:
|
|
410
|
+
# Train your model
|
|
411
|
+
model, metrics, plots = train_model(learning_rate=lr)
|
|
412
|
+
|
|
413
|
+
# Log to Notion
|
|
414
|
+
helper.log_ml_experiment(
|
|
415
|
+
data_source_id=data_source_id,
|
|
416
|
+
config={"Model Name": f"ResNet50_lr{lr}", "learning_rate": lr},
|
|
417
|
+
metrics=metrics,
|
|
418
|
+
plots=plots,
|
|
419
|
+
target_metric="accuracy",
|
|
420
|
+
higher_is_better=True
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# 3. Review results in Notion
|
|
424
|
+
df = helper.get_data_source_pages_as_dataframe(data_source_id)
|
|
425
|
+
print(df[["Model Name", "accuracy", "Run Status"]].sort_values("accuracy", ascending=False))
|
|
426
|
+
```
|
|
427
|
+
|
|
264
428
|
## Code Quality
|
|
265
429
|
|
|
266
430
|
The NotionHelper library includes several quality improvements:
|
|
@@ -302,6 +466,12 @@ The `NotionHelper` class provides the following methods:
|
|
|
302
466
|
- **`one_step_file_to_page(page_id, file_path)`** - Uploads and attaches a file to a page in one operation
|
|
303
467
|
- **`one_step_file_to_page_property(page_id, property_name, file_path, file_name)`** - Uploads and attaches a file to a page property in one operation
|
|
304
468
|
|
|
469
|
+
### Machine Learning Experiment Tracking
|
|
470
|
+
- **`create_ml_database(parent_page_id, db_title, config, metrics, file_property_name="Output Files")`** - Creates a new Notion database specifically designed for ML experiment tracking with automatic schema generation
|
|
471
|
+
- **`log_ml_experiment(data_source_id, config, metrics, plots=None, target_metric="sMAPE", higher_is_better=False, file_paths=None, file_property_name="Output Files")`** - Logs a complete ML experiment run including configuration, metrics, plots, and output files with automatic leaderboard tracking
|
|
472
|
+
- **`upload_multiple_files_to_property(page_id, property_name, file_paths)`** - Uploads multiple files and attaches them all to a single Files & Media property
|
|
473
|
+
- **`dict_to_notion_props(data, title_key)`** - Converts a Python dictionary to Notion property format with automatic type handling
|
|
474
|
+
|
|
305
475
|
## Requirements
|
|
306
476
|
|
|
307
477
|
- Python 3.10+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "notionhelper"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.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 = [
|
|
@@ -4,6 +4,9 @@ import os
|
|
|
4
4
|
import requests
|
|
5
5
|
import mimetypes
|
|
6
6
|
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
# NotionHelper can be used in conjunction with the Streamlit APP: (Notion API JSON)[https://notioinapiassistant.streamlit.app]
|
|
9
12
|
|
|
@@ -94,6 +97,7 @@ class NotionHelper:
|
|
|
94
97
|
response.raise_for_status() # Raise an exception for HTTP errors
|
|
95
98
|
return response.json()
|
|
96
99
|
except requests.exceptions.HTTPError as http_err:
|
|
100
|
+
print(f"❌ NOTION API ERROR DETAILS: {response.text}")
|
|
97
101
|
print(f"HTTP error occurred: {http_err}")
|
|
98
102
|
if response is not None: # Check if response was assigned before accessing .text
|
|
99
103
|
print(f"Response Body: {response.text}")
|
|
@@ -616,3 +620,177 @@ class NotionHelper:
|
|
|
616
620
|
|
|
617
621
|
# Attach the file to the page property
|
|
618
622
|
return self.attach_file_to_page_property(page_id, property_name, file_upload_id, file_name)
|
|
623
|
+
|
|
624
|
+
def upload_multiple_files_to_property(self, page_id: str, property_name: str, file_paths: List[str]) -> Dict[str, Any]:
|
|
625
|
+
"""Uploads multiple files and attaches them all to a single Notion property."""
|
|
626
|
+
file_assets = []
|
|
627
|
+
|
|
628
|
+
for path in file_paths:
|
|
629
|
+
if os.path.exists(path):
|
|
630
|
+
# 1. Upload each file individually
|
|
631
|
+
upload_resp = self.upload_file(path)
|
|
632
|
+
file_upload_id = upload_resp["id"]
|
|
633
|
+
|
|
634
|
+
# 2. Build the 'files' array for the Notion request
|
|
635
|
+
file_assets.append({
|
|
636
|
+
"type": "file_upload",
|
|
637
|
+
"file_upload": {"id": file_upload_id},
|
|
638
|
+
"name": os.path.basename(path)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
# 3. Update the page property with the full list
|
|
642
|
+
update_url = f"https://api.notion.com/v1/pages/{page_id}"
|
|
643
|
+
headers = {
|
|
644
|
+
"Authorization": f"Bearer {self.notion_token}",
|
|
645
|
+
"Content-Type": "application/json",
|
|
646
|
+
"Notion-Version": "2025-09-03",
|
|
647
|
+
}
|
|
648
|
+
data = {
|
|
649
|
+
"properties": {
|
|
650
|
+
property_name: {
|
|
651
|
+
"files": file_assets # This array contains all your files
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
response = requests.patch(update_url, headers=headers, json=data)
|
|
656
|
+
return response.json()
|
|
657
|
+
|
|
658
|
+
def dict_to_notion_props(self, data: Dict[str, Any], title_key: str) -> Dict[str, Any]:
|
|
659
|
+
notion_props = {}
|
|
660
|
+
for key, value in data.items():
|
|
661
|
+
# Handle NumPy types
|
|
662
|
+
if hasattr(value, "item"):
|
|
663
|
+
value = value.item()
|
|
664
|
+
|
|
665
|
+
if key == title_key:
|
|
666
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
667
|
+
notion_props[key] = {"title": [{"text": {"content": f"{value} ({ts})"}}]}
|
|
668
|
+
|
|
669
|
+
# FIX: Handle Booleans
|
|
670
|
+
elif isinstance(value, bool):
|
|
671
|
+
# Option A: Map to a Checkbox column in Notion
|
|
672
|
+
# notion_props[key] = {"checkbox": value}
|
|
673
|
+
|
|
674
|
+
# Option B: Map to a Rich Text column as a string (since you added a rich text field)
|
|
675
|
+
notion_props[key] = {"rich_text": [{"text": {"content": str(value)}}]}
|
|
676
|
+
|
|
677
|
+
elif isinstance(value, (int, float)):
|
|
678
|
+
if pd.isna(value) or np.isinf(value): continue
|
|
679
|
+
notion_props[key] = {"number": float(value)}
|
|
680
|
+
else:
|
|
681
|
+
notion_props[key] = {"rich_text": [{"text": {"content": str(value)}}]}
|
|
682
|
+
return notion_props
|
|
683
|
+
|
|
684
|
+
def log_ml_experiment(
|
|
685
|
+
self,
|
|
686
|
+
data_source_id: str,
|
|
687
|
+
config: Dict,
|
|
688
|
+
metrics: Dict,
|
|
689
|
+
plots: List[str] = None,
|
|
690
|
+
target_metric: str = "sMAPE", # Re-added these
|
|
691
|
+
higher_is_better: bool = False, # to fix the error
|
|
692
|
+
file_paths: Optional[List[str]] = None, # Changed to list
|
|
693
|
+
file_property_name: str = "Output Files"
|
|
694
|
+
):
|
|
695
|
+
"""Logs ML experiment and compares metrics with multiple file support."""
|
|
696
|
+
improvement_tag = "Standard Run"
|
|
697
|
+
new_score = metrics.get(target_metric)
|
|
698
|
+
|
|
699
|
+
# 1. Leaderboard Logic (Champions)
|
|
700
|
+
if new_score is not None:
|
|
701
|
+
try:
|
|
702
|
+
df = self.get_data_source_pages_as_dataframe(data_source_id, limit=100)
|
|
703
|
+
if not df.empty and target_metric in df.columns:
|
|
704
|
+
valid_scores = pd.to_numeric(df[target_metric], errors='coerce').dropna()
|
|
705
|
+
if not valid_scores.empty:
|
|
706
|
+
current_best = valid_scores.max() if higher_is_better else valid_scores.min()
|
|
707
|
+
is_improvement = (new_score > current_best) if higher_is_better else (new_score < current_best)
|
|
708
|
+
if is_improvement:
|
|
709
|
+
improvement_tag = f"🏆 NEW BEST {target_metric} (Prev: {current_best:.2f})"
|
|
710
|
+
else:
|
|
711
|
+
diff = abs(new_score - current_best)
|
|
712
|
+
improvement_tag = f"No Improvement (+{diff:.2f} {target_metric})"
|
|
713
|
+
except Exception as e:
|
|
714
|
+
print(f"Leaderboard check skipped: {e}")
|
|
715
|
+
|
|
716
|
+
# 2. Prepare Notion Properties
|
|
717
|
+
data_for_notion = metrics.copy()
|
|
718
|
+
data_for_notion["Run Status"] = improvement_tag
|
|
719
|
+
combined_payload = {**config, **data_for_notion}
|
|
720
|
+
title_key = list(config.keys())[0]
|
|
721
|
+
properties = self.dict_to_notion_props(combined_payload, title_key)
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
# 3. Create the row
|
|
725
|
+
new_page = self.new_page_to_data_source(data_source_id, properties)
|
|
726
|
+
page_id = new_page["id"]
|
|
727
|
+
|
|
728
|
+
# 4. Handle Plots (Body)
|
|
729
|
+
if plots:
|
|
730
|
+
for plot_path in plots:
|
|
731
|
+
if os.path.exists(plot_path):
|
|
732
|
+
self.one_step_image_embed(page_id, plot_path)
|
|
733
|
+
|
|
734
|
+
# 5. Handle Multiple File Uploads (Property)
|
|
735
|
+
if file_paths:
|
|
736
|
+
file_assets = []
|
|
737
|
+
for path in file_paths:
|
|
738
|
+
if os.path.exists(path):
|
|
739
|
+
print(f"Uploading {path}...")
|
|
740
|
+
upload_resp = self.upload_file(path)
|
|
741
|
+
file_assets.append({
|
|
742
|
+
"type": "file_upload",
|
|
743
|
+
"file_upload": {"id": upload_resp["id"]},
|
|
744
|
+
"name": os.path.basename(path),
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
if file_assets:
|
|
748
|
+
# Attach all files in one request
|
|
749
|
+
update_url = f"https://api.notion.com/v1/pages/{page_id}"
|
|
750
|
+
file_payload = {"properties": {file_property_name: {"files": file_assets}}}
|
|
751
|
+
self._make_request("PATCH", update_url, file_payload)
|
|
752
|
+
print(f"✅ {len(file_assets)} files attached to {file_property_name}")
|
|
753
|
+
|
|
754
|
+
return page_id
|
|
755
|
+
except Exception as e:
|
|
756
|
+
print(f"Log error: {e}")
|
|
757
|
+
return None
|
|
758
|
+
|
|
759
|
+
def create_ml_database(self, parent_page_id: str, db_title: str, config: Dict, metrics: Dict, file_property_name: str = "Output Files") -> str:
|
|
760
|
+
"""
|
|
761
|
+
Analyzes dicts to create a new Notion Database with the correct schema.
|
|
762
|
+
"""
|
|
763
|
+
# --- ENSURE ALL LINES BELOW ARE INDENTED BY 8 SPACES (assuming 4 for class) ---
|
|
764
|
+
combined = {**config, **metrics}
|
|
765
|
+
title_key = list(config.keys())[0]
|
|
766
|
+
|
|
767
|
+
properties = {}
|
|
768
|
+
|
|
769
|
+
# 1. Map dictionary keys to Notion Property Types
|
|
770
|
+
for key, value in combined.items():
|
|
771
|
+
if key == title_key:
|
|
772
|
+
properties[key] = {"title": {}}
|
|
773
|
+
elif isinstance(value, (int, float)):
|
|
774
|
+
properties[key] = {"number": {"format": "number"}}
|
|
775
|
+
elif isinstance(value, bool):
|
|
776
|
+
properties[key] = {"checkbox": {}}
|
|
777
|
+
else:
|
|
778
|
+
properties[key] = {"rich_text": {}}
|
|
779
|
+
|
|
780
|
+
# 2. Add 'Run Status'
|
|
781
|
+
if "Run Status" not in properties:
|
|
782
|
+
properties["Run Status"] = {"rich_text": {}}
|
|
783
|
+
|
|
784
|
+
# 3. Add the Multi-file property
|
|
785
|
+
properties[file_property_name] = {"files": {}}
|
|
786
|
+
|
|
787
|
+
print(f"Creating database '{db_title}' with {len(properties)} columns...")
|
|
788
|
+
|
|
789
|
+
response = self.create_database(
|
|
790
|
+
parent_page_id=parent_page_id,
|
|
791
|
+
database_title=db_title,
|
|
792
|
+
initial_data_source_properties=properties
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
data_source_id = response.get("initial_data_source", {}).get("id")
|
|
796
|
+
return data_source_id if data_source_id else response.get("id")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.12
|
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
from typing import Optional, Dict, List, Any
|
|
2
|
-
from notion_client import Client
|
|
3
|
-
import pandas as pd
|
|
4
|
-
import os
|
|
5
|
-
import requests
|
|
6
|
-
import mimetypes
|
|
7
|
-
|
|
8
|
-
# NotionHelper can be used in conjunction with the Streamlit APP: (Notion API JSON)[https://notioinapiassistant.streamlit.app]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class NotionHelper:
|
|
12
|
-
"""
|
|
13
|
-
A helper class to interact with the Notion API.
|
|
14
|
-
|
|
15
|
-
Methods
|
|
16
|
-
-------
|
|
17
|
-
__init__():
|
|
18
|
-
Initializes the NotionHelper instance and authenticates with the Notion API.
|
|
19
|
-
|
|
20
|
-
authenticate():
|
|
21
|
-
Authenticates with the Notion API using a token from environment variables.
|
|
22
|
-
|
|
23
|
-
get_database(database_id):
|
|
24
|
-
Fetches the schema of a Notion database given its database_id.
|
|
25
|
-
|
|
26
|
-
notion_search_db(database_id, query=""):
|
|
27
|
-
Searches for pages in a Notion database that contain the specified query in their title.
|
|
28
|
-
|
|
29
|
-
notion_get_page(page_id):
|
|
30
|
-
Returns the JSON of the page properties and an array of blocks on a Notion page given its page_id.
|
|
31
|
-
|
|
32
|
-
create_database(parent_page_id, database_title, properties):
|
|
33
|
-
Creates a new database in Notion under the specified parent page with the given title and properties.
|
|
34
|
-
|
|
35
|
-
new_page_to_db(database_id, page_properties):
|
|
36
|
-
Adds a new page to a Notion database with the specified properties.
|
|
37
|
-
|
|
38
|
-
append_page_body(page_id, blocks):
|
|
39
|
-
Appends blocks of text to the body of a Notion page.
|
|
40
|
-
|
|
41
|
-
get_all_page_ids(database_id):
|
|
42
|
-
Returns the IDs of all pages in a given Notion database.
|
|
43
|
-
|
|
44
|
-
get_all_pages_as_json(database_id, limit=None):
|
|
45
|
-
Returns a list of JSON objects representing all pages in the given database, with all properties.
|
|
46
|
-
|
|
47
|
-
get_all_pages_as_dataframe(database_id, limit=None):
|
|
48
|
-
Returns a Pandas DataFrame representing all pages in the given database, with selected properties.
|
|
49
|
-
|
|
50
|
-
upload_file(file_path):
|
|
51
|
-
Uploads a file to Notion and returns the file upload object.
|
|
52
|
-
|
|
53
|
-
attach_file_to_page(page_id, file_upload_id):
|
|
54
|
-
Attaches an uploaded file to a specific page.
|
|
55
|
-
|
|
56
|
-
embed_image_to_page(page_id, file_upload_id):
|
|
57
|
-
Embeds an uploaded image to a specific page.
|
|
58
|
-
|
|
59
|
-
attach_file_to_page_property(page_id, property_name, file_upload_id, file_name):
|
|
60
|
-
Attaches a file to a Files & Media property on a specific page.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
def __init__(self, notion_token: str):
|
|
64
|
-
"""Initializes the NotionHelper instance and authenticates with the Notion API
|
|
65
|
-
using the provided token."""
|
|
66
|
-
self.notion_token = notion_token
|
|
67
|
-
self.notion = Client(auth=self.notion_token)
|
|
68
|
-
|
|
69
|
-
def get_database(self, database_id: str) -> Dict[str, Any]:
|
|
70
|
-
"""Retrieves the schema of a Notion database given its database_id.
|
|
71
|
-
|
|
72
|
-
Parameters
|
|
73
|
-
----------
|
|
74
|
-
database_id : str
|
|
75
|
-
The unique identifier of the Notion database.
|
|
76
|
-
|
|
77
|
-
Returns
|
|
78
|
-
-------
|
|
79
|
-
dict
|
|
80
|
-
A dictionary representing the database schema.
|
|
81
|
-
"""
|
|
82
|
-
try:
|
|
83
|
-
response = self.notion.databases.retrieve(database_id=database_id)
|
|
84
|
-
return response
|
|
85
|
-
except Exception as e:
|
|
86
|
-
raise Exception(f"Failed to retrieve database {database_id}: {str(e)}")
|
|
87
|
-
|
|
88
|
-
def notion_search_db(
|
|
89
|
-
self, database_id: str, query: str = ""
|
|
90
|
-
) -> None:
|
|
91
|
-
"""Searches for pages in a Notion database that contain the specified query in their title."""
|
|
92
|
-
my_pages = self.notion.databases.query(
|
|
93
|
-
database_id=database_id,
|
|
94
|
-
filter={
|
|
95
|
-
"property": "title",
|
|
96
|
-
"rich_text": {"contains": query},
|
|
97
|
-
},
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
page_title = my_pages["results"][0]["properties"]["Code / Notebook Description"]["title"][0]["plain_text"]
|
|
101
|
-
page_url = my_pages["results"][0]["url"]
|
|
102
|
-
|
|
103
|
-
page_list = my_pages["results"]
|
|
104
|
-
count = 1
|
|
105
|
-
for page in page_list:
|
|
106
|
-
try:
|
|
107
|
-
print(
|
|
108
|
-
count,
|
|
109
|
-
page["properties"]["Code / Notebook Description"]["title"][0]["plain_text"],
|
|
110
|
-
)
|
|
111
|
-
except IndexError:
|
|
112
|
-
print("No results found.")
|
|
113
|
-
|
|
114
|
-
print(page["url"])
|
|
115
|
-
print()
|
|
116
|
-
count += 1
|
|
117
|
-
|
|
118
|
-
# pprint.pprint(page)
|
|
119
|
-
|
|
120
|
-
def notion_get_page(self, page_id: str) -> Dict[str, Any]:
|
|
121
|
-
"""Retrieves the JSON of the page properties and an array of blocks on a Notion page given its page_id."""
|
|
122
|
-
|
|
123
|
-
# Retrieve the page and block data
|
|
124
|
-
page = self.notion.pages.retrieve(page_id)
|
|
125
|
-
blocks = self.notion.blocks.children.list(page_id)
|
|
126
|
-
|
|
127
|
-
# Extract all properties as a JSON object
|
|
128
|
-
properties = page.get("properties", {})
|
|
129
|
-
content = [block for block in blocks["results"]]
|
|
130
|
-
|
|
131
|
-
# Print the full JSON of the properties
|
|
132
|
-
print(properties)
|
|
133
|
-
|
|
134
|
-
# Return the properties JSON and blocks content
|
|
135
|
-
return {"properties": properties, "content": content}
|
|
136
|
-
|
|
137
|
-
def create_database(self, parent_page_id: str, database_title: str, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
138
|
-
"""Creates a new database in Notion.
|
|
139
|
-
|
|
140
|
-
This method creates a new database under a specified parent page with the provided title and property definitions.
|
|
141
|
-
|
|
142
|
-
Parameters:
|
|
143
|
-
parent_page_id (str): The unique identifier of the parent page.
|
|
144
|
-
database_title (str): The title for the new database.
|
|
145
|
-
properties (dict): A dictionary defining the property schema for the database.
|
|
146
|
-
|
|
147
|
-
Returns:
|
|
148
|
-
dict: The JSON response from the Notion API containing details about the created database.
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
# Define the properties for the database
|
|
152
|
-
new_database = {
|
|
153
|
-
"parent": {"type": "page_id", "page_id": parent_page_id},
|
|
154
|
-
"title": [{"type": "text", "text": {"content": database_title}}],
|
|
155
|
-
"properties": properties,
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
response = self.notion.databases.create(**new_database)
|
|
159
|
-
return response
|
|
160
|
-
|
|
161
|
-
def new_page_to_db(self, database_id: str, page_properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
162
|
-
"""Adds a new page to a Notion database."""
|
|
163
|
-
|
|
164
|
-
new_page = {
|
|
165
|
-
"parent": {"database_id": database_id},
|
|
166
|
-
"properties": page_properties,
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
response = self.notion.pages.create(**new_page)
|
|
170
|
-
return response
|
|
171
|
-
|
|
172
|
-
def append_page_body(self, page_id: str, blocks: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
173
|
-
"""Appends blocks of text to the body of a Notion page."""
|
|
174
|
-
|
|
175
|
-
new_blocks = {"children": blocks}
|
|
176
|
-
|
|
177
|
-
response = self.notion.blocks.children.append(block_id=page_id, **new_blocks)
|
|
178
|
-
return response
|
|
179
|
-
|
|
180
|
-
def get_all_page_ids(self, database_id: str) -> List[str]:
|
|
181
|
-
"""Returns the IDs of all pages in a given database."""
|
|
182
|
-
|
|
183
|
-
my_pages = self.notion.databases.query(database_id=database_id)
|
|
184
|
-
page_ids = [page["id"] for page in my_pages["results"]]
|
|
185
|
-
return page_ids
|
|
186
|
-
|
|
187
|
-
def get_all_pages_as_json(self, database_id: str, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
188
|
-
"""Returns a list of JSON objects representing all pages in the given database, with all properties.
|
|
189
|
-
You can specify the number of entries to be loaded using the `limit` parameter.
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
# Use pagination to remove any limits on number of entries, optionally limited by `limit` argument
|
|
193
|
-
pages_json = []
|
|
194
|
-
has_more = True
|
|
195
|
-
start_cursor = None
|
|
196
|
-
count = 0
|
|
197
|
-
|
|
198
|
-
while has_more:
|
|
199
|
-
my_pages = self.notion.databases.query(
|
|
200
|
-
**{
|
|
201
|
-
"database_id": database_id,
|
|
202
|
-
"start_cursor": start_cursor,
|
|
203
|
-
}
|
|
204
|
-
)
|
|
205
|
-
pages_json.extend([page["properties"] for page in my_pages["results"]])
|
|
206
|
-
has_more = my_pages.get("has_more", False)
|
|
207
|
-
start_cursor = my_pages.get("next_cursor", None)
|
|
208
|
-
count += len(my_pages["results"])
|
|
209
|
-
|
|
210
|
-
if limit is not None and count >= limit:
|
|
211
|
-
pages_json = pages_json[:limit]
|
|
212
|
-
break
|
|
213
|
-
|
|
214
|
-
return pages_json
|
|
215
|
-
|
|
216
|
-
def get_all_pages_as_dataframe(self, database_id: str, limit: Optional[int] = None, include_page_ids: bool = True) -> pd.DataFrame:
|
|
217
|
-
"""Retrieves all pages from a Notion database and returns them as a Pandas DataFrame.
|
|
218
|
-
|
|
219
|
-
This method collects pages from the specified Notion database, optionally including the page IDs,
|
|
220
|
-
and extracts a predefined set of allowed properties from each page to form a structured DataFrame.
|
|
221
|
-
Numeric values are formatted to avoid scientific notation.
|
|
222
|
-
|
|
223
|
-
Parameters:
|
|
224
|
-
database_id (str): The identifier of the Notion database.
|
|
225
|
-
limit (int, optional): Maximum number of page entries to include. If None, all pages are retrieved.
|
|
226
|
-
include_page_ids (bool, optional): If True, includes an additional column 'notion_page_id' in the DataFrame.
|
|
227
|
-
Defaults to True.
|
|
228
|
-
|
|
229
|
-
Returns:
|
|
230
|
-
pandas.DataFrame: A DataFrame where each row represents a page with columns corresponding to page properties.
|
|
231
|
-
If include_page_ids is True, an additional column 'notion_page_id' is included.
|
|
232
|
-
"""
|
|
233
|
-
# Retrieve pages with or without page IDs based on the flag
|
|
234
|
-
if include_page_ids:
|
|
235
|
-
pages_json = []
|
|
236
|
-
has_more = True
|
|
237
|
-
start_cursor = None
|
|
238
|
-
count = 0
|
|
239
|
-
# Retrieve pages with pagination including the page ID in properties
|
|
240
|
-
while has_more:
|
|
241
|
-
my_pages = self.notion.databases.query(
|
|
242
|
-
database_id=database_id,
|
|
243
|
-
start_cursor=start_cursor,
|
|
244
|
-
)
|
|
245
|
-
for page in my_pages["results"]:
|
|
246
|
-
props = page["properties"]
|
|
247
|
-
props["notion_page_id"] = page.get("id", "")
|
|
248
|
-
pages_json.append(props)
|
|
249
|
-
has_more = my_pages.get("has_more", False)
|
|
250
|
-
start_cursor = my_pages.get("next_cursor", None)
|
|
251
|
-
count += len(my_pages["results"])
|
|
252
|
-
if limit is not None and count >= limit:
|
|
253
|
-
pages_json = pages_json[:limit]
|
|
254
|
-
break
|
|
255
|
-
else:
|
|
256
|
-
pages_json = self.get_all_pages_as_json(database_id, limit=limit)
|
|
257
|
-
|
|
258
|
-
data = []
|
|
259
|
-
# Define the list of allowed property types that we want to extract
|
|
260
|
-
allowed_properties = [
|
|
261
|
-
"title",
|
|
262
|
-
"status",
|
|
263
|
-
"number",
|
|
264
|
-
"date",
|
|
265
|
-
"url",
|
|
266
|
-
"checkbox",
|
|
267
|
-
"rich_text",
|
|
268
|
-
"email",
|
|
269
|
-
"select",
|
|
270
|
-
"people",
|
|
271
|
-
"phone_number",
|
|
272
|
-
"multi_select",
|
|
273
|
-
"created_time",
|
|
274
|
-
"created_by",
|
|
275
|
-
"rollup",
|
|
276
|
-
"relation",
|
|
277
|
-
"last_edited_by",
|
|
278
|
-
"last_edited_time",
|
|
279
|
-
"formula",
|
|
280
|
-
"file",
|
|
281
|
-
]
|
|
282
|
-
if include_page_ids:
|
|
283
|
-
allowed_properties.append("notion_page_id")
|
|
284
|
-
|
|
285
|
-
for page in pages_json:
|
|
286
|
-
row = {}
|
|
287
|
-
for key, value in page.items():
|
|
288
|
-
if key == "notion_page_id":
|
|
289
|
-
row[key] = value
|
|
290
|
-
continue
|
|
291
|
-
property_type = value.get("type", "")
|
|
292
|
-
if property_type in allowed_properties:
|
|
293
|
-
if property_type == "title":
|
|
294
|
-
row[key] = value.get("title", [{}])[0].get("plain_text", "")
|
|
295
|
-
elif property_type == "status":
|
|
296
|
-
row[key] = value.get("status", {}).get("name", "")
|
|
297
|
-
elif property_type == "number":
|
|
298
|
-
number_value = value.get("number", None)
|
|
299
|
-
row[key] = float(number_value) if isinstance(number_value, (int, float)) else None
|
|
300
|
-
elif property_type == "date":
|
|
301
|
-
date_field = value.get("date", {})
|
|
302
|
-
row[key] = date_field.get("start", "") if date_field else ""
|
|
303
|
-
elif property_type == "url":
|
|
304
|
-
row[key] = value.get("url", "")
|
|
305
|
-
elif property_type == "checkbox":
|
|
306
|
-
row[key] = value.get("checkbox", False)
|
|
307
|
-
elif property_type == "rich_text":
|
|
308
|
-
rich_text_field = value.get("rich_text", [])
|
|
309
|
-
row[key] = rich_text_field[0].get("plain_text", "") if rich_text_field else ""
|
|
310
|
-
elif property_type == "email":
|
|
311
|
-
row[key] = value.get("email", "")
|
|
312
|
-
elif property_type == "select":
|
|
313
|
-
select_field = value.get("select", {})
|
|
314
|
-
row[key] = select_field.get("name", "") if select_field else ""
|
|
315
|
-
elif property_type == "people":
|
|
316
|
-
people_list = value.get("people", [])
|
|
317
|
-
if people_list:
|
|
318
|
-
person = people_list[0]
|
|
319
|
-
row[key] = {"name": person.get("name", ""), "email": person.get("person", {}).get("email", "")}
|
|
320
|
-
elif property_type == "phone_number":
|
|
321
|
-
row[key] = value.get("phone_number", "")
|
|
322
|
-
elif property_type == "multi_select":
|
|
323
|
-
multi_select_field = value.get("multi_select", [])
|
|
324
|
-
row[key] = [item.get("name", "") for item in multi_select_field]
|
|
325
|
-
elif property_type == "created_time":
|
|
326
|
-
row[key] = value.get("created_time", "")
|
|
327
|
-
elif property_type == "created_by":
|
|
328
|
-
created_by = value.get("created_by", {})
|
|
329
|
-
row[key] = created_by.get("name", "")
|
|
330
|
-
elif property_type == "rollup":
|
|
331
|
-
rollup_field = value.get("rollup", {}).get("array", [])
|
|
332
|
-
row[key] = [item.get("date", {}).get("start", "") for item in rollup_field]
|
|
333
|
-
elif property_type == "relation":
|
|
334
|
-
relation_list = value.get("relation", [])
|
|
335
|
-
row[key] = [relation.get("id", "") for relation in relation_list]
|
|
336
|
-
elif property_type == "last_edited_by":
|
|
337
|
-
last_edited_by = value.get("last_edited_by", {})
|
|
338
|
-
row[key] = last_edited_by.get("name", "")
|
|
339
|
-
elif property_type == "last_edited_time":
|
|
340
|
-
row[key] = value.get("last_edited_time", "")
|
|
341
|
-
elif property_type == "formula":
|
|
342
|
-
formula_value = value.get("formula", {})
|
|
343
|
-
row[key] = formula_value.get(formula_value.get("type", ""), "")
|
|
344
|
-
elif property_type == "file":
|
|
345
|
-
files = value.get("files", [])
|
|
346
|
-
row[key] = [file.get("name", "") for file in files]
|
|
347
|
-
data.append(row)
|
|
348
|
-
|
|
349
|
-
df = pd.DataFrame(data)
|
|
350
|
-
pd.options.display.float_format = "{:.3f}".format
|
|
351
|
-
return df
|
|
352
|
-
|
|
353
|
-
def upload_file(self, file_path: str) -> Dict[str, Any]:
|
|
354
|
-
"""Uploads a file to Notion and returns the file upload object."""
|
|
355
|
-
if not os.path.exists(file_path):
|
|
356
|
-
raise FileNotFoundError(f"File not found: {file_path}")
|
|
357
|
-
|
|
358
|
-
try:
|
|
359
|
-
# Step 1: Create a File Upload object
|
|
360
|
-
create_upload_url = "https://api.notion.com/v1/file_uploads"
|
|
361
|
-
headers = {
|
|
362
|
-
"Authorization": f"Bearer {self.notion_token}",
|
|
363
|
-
"Content-Type": "application/json",
|
|
364
|
-
"Notion-Version": "2022-06-28",
|
|
365
|
-
}
|
|
366
|
-
response = requests.post(create_upload_url, headers=headers, json={})
|
|
367
|
-
response.raise_for_status()
|
|
368
|
-
upload_data = response.json()
|
|
369
|
-
upload_url = upload_data["upload_url"]
|
|
370
|
-
|
|
371
|
-
# Step 2: Upload file contents
|
|
372
|
-
with open(file_path, "rb") as f:
|
|
373
|
-
upload_headers = {
|
|
374
|
-
"Authorization": f"Bearer {self.notion_token}",
|
|
375
|
-
"Notion-Version": "2022-06-28",
|
|
376
|
-
}
|
|
377
|
-
files = {'file': (os.path.basename(file_path), f, mimetypes.guess_type(file_path)[0] or 'application/octet-stream')}
|
|
378
|
-
upload_response = requests.post(upload_url, headers=upload_headers, files=files)
|
|
379
|
-
upload_response.raise_for_status()
|
|
380
|
-
|
|
381
|
-
return upload_response.json()
|
|
382
|
-
except requests.RequestException as e:
|
|
383
|
-
raise Exception(f"Failed to upload file {file_path}: {str(e)}")
|
|
384
|
-
except Exception as e:
|
|
385
|
-
raise Exception(f"Error uploading file {file_path}: {str(e)}")
|
|
386
|
-
|
|
387
|
-
def attach_file_to_page(self, page_id: str, file_upload_id: str) -> Dict[str, Any]:
|
|
388
|
-
"""Attaches an uploaded file to a specific page."""
|
|
389
|
-
attach_url = f"https://api.notion.com/v1/blocks/{page_id}/children"
|
|
390
|
-
headers = {
|
|
391
|
-
"Authorization": f"Bearer {self.notion_token}",
|
|
392
|
-
"Content-Type": "application/json",
|
|
393
|
-
"Notion-Version": "2022-06-28",
|
|
394
|
-
}
|
|
395
|
-
data = {
|
|
396
|
-
"children": [
|
|
397
|
-
{
|
|
398
|
-
"type": "file",
|
|
399
|
-
"file": {
|
|
400
|
-
"type": "file_upload",
|
|
401
|
-
"file_upload": {
|
|
402
|
-
"id": file_upload_id
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
]
|
|
407
|
-
}
|
|
408
|
-
response = requests.patch(attach_url, headers=headers, json=data)
|
|
409
|
-
return response.json()
|
|
410
|
-
|
|
411
|
-
def embed_image_to_page(self, page_id: str, file_upload_id: str) -> Dict[str, Any]:
|
|
412
|
-
"""Embeds an uploaded image to a specific page."""
|
|
413
|
-
attach_url = f"https://api.notion.com/v1/blocks/{page_id}/children"
|
|
414
|
-
headers = {
|
|
415
|
-
"Authorization": f"Bearer {self.notion_token}",
|
|
416
|
-
"Content-Type": "application/json",
|
|
417
|
-
"Notion-Version": "2022-06-28",
|
|
418
|
-
}
|
|
419
|
-
data = {
|
|
420
|
-
"children": [
|
|
421
|
-
{
|
|
422
|
-
"type": "image",
|
|
423
|
-
"image": {
|
|
424
|
-
"type": "file_upload",
|
|
425
|
-
"file_upload": {
|
|
426
|
-
"id": file_upload_id
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
]
|
|
431
|
-
}
|
|
432
|
-
response = requests.patch(attach_url, headers=headers, json=data)
|
|
433
|
-
return response.json()
|
|
434
|
-
|
|
435
|
-
def attach_file_to_page_property(
|
|
436
|
-
self, page_id: str, property_name: str, file_upload_id: str, file_name: str
|
|
437
|
-
) -> Dict[str, Any]:
|
|
438
|
-
"""Attaches a file to a Files & Media property on a specific page."""
|
|
439
|
-
update_url = f"https://api.notion.com/v1/pages/{page_id}"
|
|
440
|
-
headers = {
|
|
441
|
-
"Authorization": f"Bearer {self.notion_token}",
|
|
442
|
-
"Content-Type": "application/json",
|
|
443
|
-
"Notion-Version": "2022-06-28",
|
|
444
|
-
}
|
|
445
|
-
data = {
|
|
446
|
-
"properties": {
|
|
447
|
-
property_name: {
|
|
448
|
-
"files": [
|
|
449
|
-
{
|
|
450
|
-
"type": "file_upload",
|
|
451
|
-
"file_upload": {"id": file_upload_id},
|
|
452
|
-
"name": file_name,
|
|
453
|
-
}
|
|
454
|
-
]
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
response = requests.patch(update_url, headers=headers, json=data)
|
|
459
|
-
return response.json()
|
|
460
|
-
|
|
461
|
-
def one_step_image_embed(self, page_id: str, file_path: str) -> Dict[str, Any]:
|
|
462
|
-
"""Uploads an image and embeds it in a Notion page in one step."""
|
|
463
|
-
|
|
464
|
-
# Upload the file
|
|
465
|
-
file_upload = self.upload_file(file_path)
|
|
466
|
-
file_upload_id = file_upload["id"]
|
|
467
|
-
|
|
468
|
-
# Embed the image in the page
|
|
469
|
-
return self.embed_image_to_page(page_id, file_upload_id)
|
|
470
|
-
|
|
471
|
-
def one_step_file_to_page(self, page_id: str, file_path: str) -> Dict[str, Any]:
|
|
472
|
-
"""Uploads a file and attaches it to a Notion page in one step."""
|
|
473
|
-
|
|
474
|
-
# Upload the file
|
|
475
|
-
file_upload = self.upload_file(file_path)
|
|
476
|
-
file_upload_id = file_upload["id"]
|
|
477
|
-
|
|
478
|
-
# Attach the file to the page
|
|
479
|
-
return self.attach_file_to_page(page_id, file_upload_id)
|
|
480
|
-
|
|
481
|
-
def one_step_file_to_page_property(self, page_id: str, property_name: str, file_path: str, file_name: str) -> Dict[str, Any]:
|
|
482
|
-
"""Uploads a file and attaches it to a Notion page property in one step."""
|
|
483
|
-
|
|
484
|
-
# Upload the file
|
|
485
|
-
file_upload = self.upload_file(file_path)
|
|
486
|
-
file_upload_id = file_upload["id"]
|
|
487
|
-
|
|
488
|
-
# Attach the file to the page property
|
|
489
|
-
return self.attach_file_to_page_property(page_id, property_name, file_upload_id, file_name)
|
|
490
|
-
|
|
491
|
-
def info(self) -> Optional[Any]:
|
|
492
|
-
"""Displays comprehensive library information in a Jupyter notebook.
|
|
493
|
-
|
|
494
|
-
Shows:
|
|
495
|
-
- Library name and description
|
|
496
|
-
- Complete list of all available methods with descriptions
|
|
497
|
-
- Version information
|
|
498
|
-
- Optional logo display (if available)
|
|
499
|
-
|
|
500
|
-
Returns:
|
|
501
|
-
IPython.display.HTML: An HTML display object or None if IPython is not available.
|
|
502
|
-
"""
|
|
503
|
-
try:
|
|
504
|
-
from IPython.display import HTML
|
|
505
|
-
import base64
|
|
506
|
-
import inspect
|
|
507
|
-
|
|
508
|
-
# Get logo image data
|
|
509
|
-
logo_path = os.path.join(os.path.dirname(__file__), '../images/helper_logo.png')
|
|
510
|
-
if os.path.exists(logo_path):
|
|
511
|
-
with open(logo_path, "rb") as image_file:
|
|
512
|
-
encoded_logo = base64.b64encode(image_file.read()).decode('utf-8')
|
|
513
|
-
else:
|
|
514
|
-
encoded_logo = ""
|
|
515
|
-
|
|
516
|
-
# Get all methods and their docstrings
|
|
517
|
-
methods = []
|
|
518
|
-
for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
|
|
519
|
-
if not name.startswith('_'):
|
|
520
|
-
doc = inspect.getdoc(method) or "No description available"
|
|
521
|
-
methods.append(f"<li><code>{name}()</code>: {doc.splitlines()[0]}</li>")
|
|
522
|
-
|
|
523
|
-
# Create HTML content
|
|
524
|
-
html_content = f"""
|
|
525
|
-
<div style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; max-width: 800px; margin: 0 auto; color: #1e293b;">
|
|
526
|
-
<h1 style="color: #1e293b;">NotionHelper Library</h1>
|
|
527
|
-
{f'<img src="data:image/png;base64,{encoded_logo}" style="max-width: 200px; margin: 20px 0; display: block;">' if encoded_logo else ''}
|
|
528
|
-
<p style="font-size: 1.1rem;">A Python helper class for interacting with the Notion API.</p>
|
|
529
|
-
<h3 style="color: #1e293b;">All Available Methods:</h3>
|
|
530
|
-
<ul style="list-style-type: none; padding-left: 0;">
|
|
531
|
-
{''.join(methods)}
|
|
532
|
-
</ul>
|
|
533
|
-
<h3 style="color: #1e293b;">Features:</h3>
|
|
534
|
-
<ul style="list-style-type: none; padding-left: 0;">
|
|
535
|
-
<li style="margin-bottom: 8px;">Database querying and manipulation</li>
|
|
536
|
-
<li style="margin-bottom: 8px;">Page creation and editing</li>
|
|
537
|
-
<li style="margin-bottom: 8px;">File uploads and attachments</li>
|
|
538
|
-
<li style="margin-bottom: 8px;">Data conversion to Pandas DataFrames</li>
|
|
539
|
-
</ul>
|
|
540
|
-
<p style="font-size: 0.9rem; color: #64748b;">Version: {getattr(self, '__version__', '1.0.0')}</p>
|
|
541
|
-
</div>
|
|
542
|
-
"""
|
|
543
|
-
return HTML(html_content)
|
|
544
|
-
except ImportError:
|
|
545
|
-
print("IPython is required for this functionality. Please install it with: pip install ipython")
|
|
546
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|