notion2pg-bulk 0.1.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.
- notion2pg_bulk-0.1.0/PKG-INFO +152 -0
- notion2pg_bulk-0.1.0/README.md +123 -0
- notion2pg_bulk-0.1.0/pyproject.toml +43 -0
- notion2pg_bulk-0.1.0/src/notion2pg_bulk/__init__.py +6 -0
- notion2pg_bulk-0.1.0/src/notion2pg_bulk/cli.py +69 -0
- notion2pg_bulk-0.1.0/src/notion2pg_bulk/migrator.py +902 -0
- notion2pg_bulk-0.1.0/src/notion2pg_bulk/progress_tracker.py +71 -0
- notion2pg_bulk-0.1.0/src/notion2pg_bulk/rate_limiter.py +70 -0
- notion2pg_bulk-0.1.0/src/notion2pg_bulk/schema_mapper.py +180 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: notion2pg-bulk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package that automatically finds and migrates to PostgreSQL all databases in a Notion workspace.
|
|
5
|
+
Keywords: notion,postgresql,migration,database,export,workspace,notion export,notion migrate
|
|
6
|
+
Author: benvigano
|
|
7
|
+
License: GPL-3.0-or-later
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Dist: notion-client>=2.2.1
|
|
20
|
+
Requires-Dist: psycopg2-binary>=2.9.5
|
|
21
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
22
|
+
Requires-Dist: tabulate>=0.9.0
|
|
23
|
+
Requires-Dist: tqdm>=4.64.0
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Project-URL: Homepage, https://github.com/benvigano/notion2pg-bulk
|
|
26
|
+
Project-URL: Issues, https://github.com/benvigano/notion2pg-bulk/issues
|
|
27
|
+
Project-URL: Repository, https://github.com/benvigano/notion2pg-bulk
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# notion2pg_bulk
|
|
31
|
+
|
|
32
|
+
- Automatically discovers and migrates **all databases in a workspace** regardless of their views or position in the page structure
|
|
33
|
+
- Preserves **database descriptions and property descriptions**
|
|
34
|
+
- Supports **relation properties** using PostgreSQL arrays
|
|
35
|
+
- **Select field support**: Single and multi-select options stored in separate lookup tables with foreign key constraints
|
|
36
|
+
- Complies with Notion API limits (3 requests/second average)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
```bash
|
|
40
|
+
pip install notion2pg-bulk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Setup and Usage
|
|
44
|
+
|
|
45
|
+
### Requirements
|
|
46
|
+
- a **Notion API key** connected to all the databased that you wish to migrate (note: Notion now allows to connect databases ditectly from the integration configuration page, under the 'Access' tab. For quickly connecting **all databases** in a workspace, just tick all top level pages.
|
|
47
|
+
- PostgreSQL database connection URL
|
|
48
|
+
|
|
49
|
+
### Interactive Mode
|
|
50
|
+
**On (Default):**
|
|
51
|
+
- Dispays detected databases and fields and asks for confirmation before running the migration
|
|
52
|
+
- Progress bar
|
|
53
|
+
- Shows post-migration notes
|
|
54
|
+
|
|
55
|
+
**Off:**
|
|
56
|
+
(`--quiet` flag in CLI or set `interactive_mode=False` in Python API)
|
|
57
|
+
- `.run()` runs migration directly and silently
|
|
58
|
+
|
|
59
|
+
### CLI
|
|
60
|
+
**Options:**
|
|
61
|
+
- `--notion-token`: Notion integration token (or set `NOTION_TOKEN` env var)
|
|
62
|
+
- `--database-url`: PostgreSQL connection string (or set `DATABASE_URL` env var)
|
|
63
|
+
- `--quiet`: Run in non-interactive mode (skip validation steps and progress bars)
|
|
64
|
+
- `--extract-page-content`: Extract free-form content from page bodies (slower migration)
|
|
65
|
+
|
|
66
|
+
**Examples:**
|
|
67
|
+
```bash
|
|
68
|
+
# Interactive mode (default)
|
|
69
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..."
|
|
70
|
+
|
|
71
|
+
# Non-interactive mode (skip validation steps)
|
|
72
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..." --quiet
|
|
73
|
+
|
|
74
|
+
# Extract page content (slower but includes free-form content)
|
|
75
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..." --extract-page-content
|
|
76
|
+
|
|
77
|
+
# Non-interactive with page content extraction
|
|
78
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..." --quiet --extract-page-content
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Python API:
|
|
82
|
+
```python
|
|
83
|
+
from notion2pg_bulk import NotionMigrator
|
|
84
|
+
import sqlalchemy as sa
|
|
85
|
+
|
|
86
|
+
engine = sa.create_engine('postgresql://user:password@localhost/dbname')
|
|
87
|
+
migrator = NotionMigrator(
|
|
88
|
+
notion_token="your_notion_integration_token",
|
|
89
|
+
db_connection=engine,
|
|
90
|
+
interactive_mode=True,
|
|
91
|
+
extract_page_content=True,
|
|
92
|
+
)
|
|
93
|
+
migrator.run()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Property Type Mapping
|
|
97
|
+
|
|
98
|
+
| Notion Property | PostgreSQL Type | Notes |
|
|
99
|
+
|----------------|-----------------|-------|
|
|
100
|
+
| Title | `TEXT` | Primary identifier |
|
|
101
|
+
| Rich Text | `TEXT` | Preserved as markdown with formatting |
|
|
102
|
+
| Number | `NUMERIC` | |
|
|
103
|
+
| Select | `TEXT` | Single option value + lookup table with foreign key |
|
|
104
|
+
| Multi-select | `TEXT[]` | Array of option values + lookup table with check constraints |
|
|
105
|
+
| Date | `TIMESTAMP` | |
|
|
106
|
+
| Checkbox | `BOOLEAN` | |
|
|
107
|
+
| URL | `TEXT` | |
|
|
108
|
+
| Email | `TEXT` | |
|
|
109
|
+
| Phone | `TEXT` | |
|
|
110
|
+
| Relation | `TEXT[]` | Array of related page IDs |
|
|
111
|
+
| People | `TEXT[]` | Array of user IDs |
|
|
112
|
+
| Files | `TEXT[]` | Array of file URLs |
|
|
113
|
+
| Created time | `TIMESTAMP` | |
|
|
114
|
+
| Created by | `TEXT` | User ID |
|
|
115
|
+
| Last edited time | `TIMESTAMP` | |
|
|
116
|
+
| Last edited by | `TEXT` | User ID |
|
|
117
|
+
| Formula | - | ⚠️ Skipped (not supported by Notion API) |
|
|
118
|
+
| Rollup | - | ⚠️ Skipped (not supported by Notion API) |
|
|
119
|
+
|
|
120
|
+
## Additional Page Content Mode
|
|
121
|
+
|
|
122
|
+
The migrator can optionally extract free-form content from database pages. Extracted content is stored in the `additional_page_content` column as plain text.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
migrator = NotionMigrator(
|
|
126
|
+
notion_token=token,
|
|
127
|
+
db_connection=engine,
|
|
128
|
+
extract_page_content=True # Default is false
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Supported blocks:**
|
|
133
|
+
- Paragraph text
|
|
134
|
+
- Headings (all levels)
|
|
135
|
+
- Bulleted and numbered lists
|
|
136
|
+
- Code blocks
|
|
137
|
+
- To-do items
|
|
138
|
+
- Callouts
|
|
139
|
+
- Toggle blocks
|
|
140
|
+
- Dividers
|
|
141
|
+
- Embedded databases (ID reference only)
|
|
142
|
+
|
|
143
|
+
**Unsupported blocks:**
|
|
144
|
+
- Images and media files
|
|
145
|
+
- Complex block types (equations, embeds, etc.)
|
|
146
|
+
- Block formatting and styling
|
|
147
|
+
- Page hierarchies and relationships
|
|
148
|
+
|
|
149
|
+
**Post-migration analysis:**
|
|
150
|
+
When page content extraction is enabled, the migrator provides a comprehensive summary of:
|
|
151
|
+
- Unsupported block types found and their locations (grouped by record)
|
|
152
|
+
- Embedded databases that were referenced in page content but not connected to the integration (with full database IDs)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# notion2pg_bulk
|
|
2
|
+
|
|
3
|
+
- Automatically discovers and migrates **all databases in a workspace** regardless of their views or position in the page structure
|
|
4
|
+
- Preserves **database descriptions and property descriptions**
|
|
5
|
+
- Supports **relation properties** using PostgreSQL arrays
|
|
6
|
+
- **Select field support**: Single and multi-select options stored in separate lookup tables with foreign key constraints
|
|
7
|
+
- Complies with Notion API limits (3 requests/second average)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
```bash
|
|
11
|
+
pip install notion2pg-bulk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Setup and Usage
|
|
15
|
+
|
|
16
|
+
### Requirements
|
|
17
|
+
- a **Notion API key** connected to all the databased that you wish to migrate (note: Notion now allows to connect databases ditectly from the integration configuration page, under the 'Access' tab. For quickly connecting **all databases** in a workspace, just tick all top level pages.
|
|
18
|
+
- PostgreSQL database connection URL
|
|
19
|
+
|
|
20
|
+
### Interactive Mode
|
|
21
|
+
**On (Default):**
|
|
22
|
+
- Dispays detected databases and fields and asks for confirmation before running the migration
|
|
23
|
+
- Progress bar
|
|
24
|
+
- Shows post-migration notes
|
|
25
|
+
|
|
26
|
+
**Off:**
|
|
27
|
+
(`--quiet` flag in CLI or set `interactive_mode=False` in Python API)
|
|
28
|
+
- `.run()` runs migration directly and silently
|
|
29
|
+
|
|
30
|
+
### CLI
|
|
31
|
+
**Options:**
|
|
32
|
+
- `--notion-token`: Notion integration token (or set `NOTION_TOKEN` env var)
|
|
33
|
+
- `--database-url`: PostgreSQL connection string (or set `DATABASE_URL` env var)
|
|
34
|
+
- `--quiet`: Run in non-interactive mode (skip validation steps and progress bars)
|
|
35
|
+
- `--extract-page-content`: Extract free-form content from page bodies (slower migration)
|
|
36
|
+
|
|
37
|
+
**Examples:**
|
|
38
|
+
```bash
|
|
39
|
+
# Interactive mode (default)
|
|
40
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..."
|
|
41
|
+
|
|
42
|
+
# Non-interactive mode (skip validation steps)
|
|
43
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..." --quiet
|
|
44
|
+
|
|
45
|
+
# Extract page content (slower but includes free-form content)
|
|
46
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..." --extract-page-content
|
|
47
|
+
|
|
48
|
+
# Non-interactive with page content extraction
|
|
49
|
+
notion2pg-bulk --notion-token "your_token" --database-url "postgresql://..." --quiet --extract-page-content
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Python API:
|
|
53
|
+
```python
|
|
54
|
+
from notion2pg_bulk import NotionMigrator
|
|
55
|
+
import sqlalchemy as sa
|
|
56
|
+
|
|
57
|
+
engine = sa.create_engine('postgresql://user:password@localhost/dbname')
|
|
58
|
+
migrator = NotionMigrator(
|
|
59
|
+
notion_token="your_notion_integration_token",
|
|
60
|
+
db_connection=engine,
|
|
61
|
+
interactive_mode=True,
|
|
62
|
+
extract_page_content=True,
|
|
63
|
+
)
|
|
64
|
+
migrator.run()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Property Type Mapping
|
|
68
|
+
|
|
69
|
+
| Notion Property | PostgreSQL Type | Notes |
|
|
70
|
+
|----------------|-----------------|-------|
|
|
71
|
+
| Title | `TEXT` | Primary identifier |
|
|
72
|
+
| Rich Text | `TEXT` | Preserved as markdown with formatting |
|
|
73
|
+
| Number | `NUMERIC` | |
|
|
74
|
+
| Select | `TEXT` | Single option value + lookup table with foreign key |
|
|
75
|
+
| Multi-select | `TEXT[]` | Array of option values + lookup table with check constraints |
|
|
76
|
+
| Date | `TIMESTAMP` | |
|
|
77
|
+
| Checkbox | `BOOLEAN` | |
|
|
78
|
+
| URL | `TEXT` | |
|
|
79
|
+
| Email | `TEXT` | |
|
|
80
|
+
| Phone | `TEXT` | |
|
|
81
|
+
| Relation | `TEXT[]` | Array of related page IDs |
|
|
82
|
+
| People | `TEXT[]` | Array of user IDs |
|
|
83
|
+
| Files | `TEXT[]` | Array of file URLs |
|
|
84
|
+
| Created time | `TIMESTAMP` | |
|
|
85
|
+
| Created by | `TEXT` | User ID |
|
|
86
|
+
| Last edited time | `TIMESTAMP` | |
|
|
87
|
+
| Last edited by | `TEXT` | User ID |
|
|
88
|
+
| Formula | - | ⚠️ Skipped (not supported by Notion API) |
|
|
89
|
+
| Rollup | - | ⚠️ Skipped (not supported by Notion API) |
|
|
90
|
+
|
|
91
|
+
## Additional Page Content Mode
|
|
92
|
+
|
|
93
|
+
The migrator can optionally extract free-form content from database pages. Extracted content is stored in the `additional_page_content` column as plain text.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
migrator = NotionMigrator(
|
|
97
|
+
notion_token=token,
|
|
98
|
+
db_connection=engine,
|
|
99
|
+
extract_page_content=True # Default is false
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Supported blocks:**
|
|
104
|
+
- Paragraph text
|
|
105
|
+
- Headings (all levels)
|
|
106
|
+
- Bulleted and numbered lists
|
|
107
|
+
- Code blocks
|
|
108
|
+
- To-do items
|
|
109
|
+
- Callouts
|
|
110
|
+
- Toggle blocks
|
|
111
|
+
- Dividers
|
|
112
|
+
- Embedded databases (ID reference only)
|
|
113
|
+
|
|
114
|
+
**Unsupported blocks:**
|
|
115
|
+
- Images and media files
|
|
116
|
+
- Complex block types (equations, embeds, etc.)
|
|
117
|
+
- Block formatting and styling
|
|
118
|
+
- Page hierarchies and relationships
|
|
119
|
+
|
|
120
|
+
**Post-migration analysis:**
|
|
121
|
+
When page content extraction is enabled, the migrator provides a comprehensive summary of:
|
|
122
|
+
- Unsupported block types found and their locations (grouped by record)
|
|
123
|
+
- Embedded databases that were referenced in page content but not connected to the integration (with full database IDs)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "notion2pg-bulk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A Python package that automatically finds and migrates to PostgreSQL all databases in a Notion workspace."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"notion-client>=2.2.1",
|
|
9
|
+
"psycopg2-binary>=2.9.5",
|
|
10
|
+
"sqlalchemy>=2.0.0",
|
|
11
|
+
"tabulate>=0.9.0",
|
|
12
|
+
"tqdm>=4.64.0",
|
|
13
|
+
]
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "benvigano" }
|
|
16
|
+
]
|
|
17
|
+
license = { text = "GPL-3.0-or-later" }
|
|
18
|
+
keywords = ["notion", "postgresql", "migration", "database", "export", "workspace", "notion export", "notion migrate"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.8",
|
|
25
|
+
"Programming Language :: Python :: 3.9",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Database",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/benvigano/notion2pg-bulk"
|
|
35
|
+
Repository = "https://github.com/benvigano/notion2pg-bulk"
|
|
36
|
+
Issues = "https://github.com/benvigano/notion2pg-bulk/issues"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
notion2pg-bulk = "notion2pg_bulk.cli:main"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build>=0.8.13,<0.9.0"]
|
|
43
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command line interface for the Notion to PostgreSQL migration tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import sqlalchemy as sa
|
|
9
|
+
from .migrator import NotionMigrator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
"""Main CLI entry point."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Migrate Notion workspace to PostgreSQL database"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--notion-token",
|
|
20
|
+
help="Notion integration token (or set NOTION_TOKEN env var)"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--database-url",
|
|
25
|
+
help="PostgreSQL connection string (or set DATABASE_URL env var)"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--extract-page-content",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Extract free-form content from page bodies (slower migration)"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--quiet",
|
|
36
|
+
action="store_true",
|
|
37
|
+
help="Run in non-interactive mode (skip validation steps and progress bars)"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
|
|
42
|
+
# Get credentials
|
|
43
|
+
notion_token = args.notion_token or os.getenv("NOTION_TOKEN")
|
|
44
|
+
if not notion_token:
|
|
45
|
+
print("❌ Error: Notion token required. Set --notion-token or NOTION_TOKEN env var")
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
database_url = args.database_url or os.getenv("DATABASE_URL")
|
|
49
|
+
if not database_url:
|
|
50
|
+
print("❌ Error: Database URL required. Set --database-url or DATABASE_URL env var")
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
# Create database connection
|
|
54
|
+
engine = sa.create_engine(database_url)
|
|
55
|
+
|
|
56
|
+
# Initialize and run migrator
|
|
57
|
+
migrator = NotionMigrator(
|
|
58
|
+
notion_token=notion_token,
|
|
59
|
+
db_connection=engine,
|
|
60
|
+
interactive_mode=not args.quiet,
|
|
61
|
+
extract_page_content=args.extract_page_content
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
migrator.run()
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
sys.exit(main())
|