notionhelper 0.2.3__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
notionhelper/helper.py CHANGED
@@ -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,211 @@ 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_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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionhelper
3
- Version: 0.2.3
3
+ Version: 0.3.1
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
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  # NotionHelper
29
29
 
30
- ![NotionHelper](https://github.com/janduplessis883/notionhelper/blob/master/images/helper_logo.png?raw=true)
30
+ ![NotionHelper](https://github.com/janduplessis883/notionhelper/blob/master/images/notionh3.png?raw=true)
31
31
 
32
32
  `NotionHelper` is a Python library that provides a convenient interface for interacting with the Notion API, specifically designed to leverage the **Notion API Version 2025-09-03**. It simplifies common tasks such as managing databases, data sources, pages, and file uploads, allowing you to integrate Notion's powerful features into your applications with ease.
33
33
 
@@ -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+
@@ -0,0 +1,5 @@
1
+ notionhelper/__init__.py,sha256=_ShvAiiI4rspEoAjP71AHLPL1wrmcIlDBn0YUSqSMi8,61
2
+ notionhelper/helper.py,sha256=NJdthOSNIqp1ESaDkV-V1Kb2x35LcQlAfG-KTyDnEl8,36279
3
+ notionhelper-0.3.1.dist-info/METADATA,sha256=NcMT0DgyLX096LdLa5UGlUOg184bmGuWT2eiJkwxkGo,19906
4
+ notionhelper-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ notionhelper-0.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
notionhelper/helper1.8.py DELETED
@@ -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
@@ -1,6 +0,0 @@
1
- notionhelper/__init__.py,sha256=_ShvAiiI4rspEoAjP71AHLPL1wrmcIlDBn0YUSqSMi8,61
2
- notionhelper/helper.py,sha256=WwiyRk3rvrcUg9w0HMIXHm5AKQBwh6yhlVx6wueiv_o,27181
3
- notionhelper/helper1.8.py,sha256=ubR55j97JDduxVipe3yBqHMWfVO9EKhe48ZWCPOq938,23456
4
- notionhelper-0.2.3.dist-info/METADATA,sha256=tizGtzPAzxQIcsJx5K6eUzamHsWh1QF8wMaEXa3_lnI,13892
5
- notionhelper-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- notionhelper-0.2.3.dist-info/RECORD,,