149 lines
6.0 KiB
Markdown
149 lines
6.0 KiB
Markdown
# notion2plank
|
||
|
||
One-shot Python script that imports tasks from a **Notion CSV export** into a **self-hosted Plane** instance via its REST API.
|
||
|
||
Plane's free/community tier has no built-in import UI, but the API is fully open. This script bridges the gap.
|
||
|
||
---
|
||
|
||
## Requirements
|
||
|
||
- Python 3.10+
|
||
- A Plane API key — _Settings → API Tokens_
|
||
- A Notion database exported as CSV — _··· → Export → Markdown & CSV → without subpages_
|
||
|
||
```
|
||
pip -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
---
|
||
|
||
## Setup
|
||
|
||
```bash
|
||
cp config.yaml.example config.yaml
|
||
```
|
||
|
||
Edit `config.yaml`:
|
||
|
||
```yaml
|
||
plane_url: https://your-plane-instance.com
|
||
workspace_slug: your-workspace
|
||
project_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||
api_key: your-api-key-here
|
||
|
||
# Optional — needed to import estimate points (вес column).
|
||
# Get it from browser DevTools → Application → Cookies → session-id.
|
||
# If omitted, the вес column is silently skipped.
|
||
plane_session: your-session-id-cookie-value
|
||
|
||
# Map Notion status values (exact text + emoji) → Plane state names.
|
||
# Find your state names: GET /api/v1/workspaces/<slug>/projects/<id>/states/
|
||
status_mapping:
|
||
"в работе 🔨": "In Progress"
|
||
"Готово ✅": "Done"
|
||
"На проверке 👀": "Review"
|
||
"Утверждённые задачи 📝": "Todo"
|
||
"Общие идеи (задачи) 📋": "Backlog"
|
||
```
|
||
|
||
---
|
||
|
||
## Usage
|
||
|
||
**Dry run first** — parses every row and shows what would be created, no API calls:
|
||
|
||
```bash
|
||
python import.py --dry-run
|
||
```
|
||
|
||
**Real import:**
|
||
|
||
```bash
|
||
python import.py
|
||
```
|
||
|
||
**Custom paths:**
|
||
|
||
```bash
|
||
python import.py --csv path/to/export.csv --config path/to/config.yaml
|
||
```
|
||
|
||
### Output
|
||
|
||
```
|
||
Fetching project states…
|
||
Found 6 states: ['Backlog', 'Todo', 'In Progress', 'Done', 'Cancelled', 'Review']
|
||
Fetching project labels…
|
||
Found 7 labels: ['Sound', '2d', 'Game Design', 'Org', '3d', 'Level Design', 'Devel']
|
||
Fetching estimate points…
|
||
Found 6 estimate points: ['1', '2', '3', '5', '8', '13']
|
||
|
||
Ensuring labels for disciplines: ['Game Design', 'Sound', ...]
|
||
|
||
Processing 37 rows…
|
||
|
||
[ 1] Created: #31 — 'Create Kanban Board'
|
||
[ 2] Created: #32 — 'Add Milestones'
|
||
...
|
||
|
||
Summary: 37 created, 0 failed.
|
||
```
|
||
|
||
---
|
||
|
||
## Field Mapping
|
||
|
||
| Notion column | Plane field | Notes |
|
||
| ---------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||
| `Name` | `name` | Required |
|
||
| `Status` | `state` (UUID) | Mapped via `status_mapping` in config |
|
||
| `priority` | `priority` | Emoji stripped: 🔥 Critical→`urgent`, ⭐ High→`high`, 📌 Medium→`medium`, 💤 Low→`low`, empty→`none` |
|
||
| `discipline` | `label_ids` | Auto-created as a Plane label if it doesn't exist yet |
|
||
| `Date` (single) | `target_date` | Parsed with dateutil; output `YYYY-MM-DD` |
|
||
| `Date` (range `A → B`) | `start_date` + `target_date` | Split on the `→` Unicode arrow |
|
||
| `Description` | `description_html` | Wrapped in `<p>` |
|
||
| `результат` | appended to `description_html` | Added as `<p><em>Result: N</em></p>` if non-zero |
|
||
| `вес` | `estimate_point` (UUID) | Matched by value (e.g. `"5"`) then by key position; requires `plane_session` |
|
||
| `исполнитель` | — | Skipped (no user ID mapping) |
|
||
| `Автор задачи` | — | Skipped |
|
||
| `Attachments` | — | Skipped |
|
||
| `Person` | — | Skipped |
|
||
|
||
---
|
||
|
||
## Notes
|
||
|
||
### Estimate points (`вес`)
|
||
|
||
Plane's public API (`/api/v1/`) does not expose an endpoint to list estimate point UUIDs.
|
||
The script works around this by calling an internal frontend endpoint (`/api/workspaces/…/estimates/`) using your browser session cookie.
|
||
|
||
Resolution order:
|
||
|
||
1. **Exact value match** — `вес="5"` maps to the point labeled "5" (Fibonacci-style: 1, 2, 3, 5, 8, 13)
|
||
2. **Ordinal key fallback** — `вес="4"` maps to the 4th point in the estimate scale
|
||
|
||
If `plane_session` is not set or the cookie has expired, the `вес` column is skipped and everything else still imports normally.
|
||
|
||
### Custom fields (work item properties)
|
||
|
||
Plane's custom property API (`/work-item-types/…/work-item-properties/`) requires the `is_issue_type_enabled` project flag, which is a paid-tier feature. It is not available in the community/self-hosted free edition.
|
||
|
||
### Discipline labels
|
||
|
||
All unique `discipline` values in the CSV are pre-fetched and compared against existing project labels before the import starts. Missing labels are created automatically via `POST /labels/`. Existing labels are reused by UUID.
|
||
|
||
---
|
||
|
||
## Files
|
||
|
||
```
|
||
import.py — main script
|
||
config.yaml.example — annotated config template
|
||
config.yaml — your local config (do not commit)
|
||
requirements.txt — pip dependencies
|
||
```
|