Conversation
Co-authored-by: Eckii24 <[email protected]>
…uery Co-authored-by: Eckii24 <[email protected]>
…ert with explicit error handling Co-authored-by: Eckii24 <[email protected]>
…ialization Co-authored-by: Eckii24 <[email protected]>
|
@copilot please add a readme, like we have it for the others as well |
Co-authored-by: Eckii24 <[email protected]>
Added comprehensive documentation in
Also updated the main |
|
I would like to support two more fields:
priority is a simple "select" in notion, but we need to map the values for Medium, high and low. project is a bit more complex. I would like to support project column of type "select", "multi-select" or "relation". @copilot please implement and also provide tests for all of it. Also update the readme accordingly |
Co-authored-by: Eckii24 <[email protected]>
Implemented support for project and priority fields: Project field:
Priority field:
Added 12 comprehensive tests covering all property types and scenarios. Documentation updated with usage examples. See commit 5f9b5e6. |
docs/readme-tw-notion-db.md
Outdated
| --database-id <database-uuid> \ | ||
| -t mytasks \ | ||
| --status-kind select \ | ||
| --status-done "Complete" \ |
There was a problem hiding this comment.
I want to have the same logic as for priority: : to add mappings for status.
For both, we need to support 1:n mappings. 1 tw value can be mapped to multiple notion values
@copilot please take over
There was a problem hiding this comment.
Could you clarify what you'd like to change? The priority field uses --priority-map to map TaskWarrior values (H/M/L) to custom Notion select values.
Are you asking to:
- Change status to use a similar mapping approach (e.g.,
--status-map "completed:Done" --status-map "pending:Not started") instead of the current--status-doneand--status-todooptions? - Or something else?
Please let me know so I can implement the correct solution.
There was a problem hiding this comment.
Implemented! Status mapping now uses the same key-value pair approach as priority mapping.
Changes:
- Replaced
--status-doneand--status-todowith unified--status-mapoption - Format:
--status-map "completed:Done" --status-map "pending:Not started" - Default mapping:
pending:Not started,completed:Done
Example:
tw_notion_db_sync \
--database-id <id> \
-t mytasks \
--status-map "completed:Complete" \
--status-map "pending:In Progress"This provides a consistent interface across both status and priority mappings. See commit d4da52b.
Co-authored-by: Eckii24 <[email protected]>
syncall/scripts/tw_notion_db_sync.py
Outdated
| for mapping in status_map: | ||
| if ":" in mapping: | ||
| tw_status, notion_value = mapping.split(":", 1) | ||
| parsed_status_map[tw_status.strip()] = notion_value.strip() |
There was a problem hiding this comment.
We should support a 1:n relationship, because we can have multiple notion states for a single tw state
There was a problem hiding this comment.
Implemented 1:n status mapping! Now multiple Notion status values can map to the same TaskWarrior status.
Example usage:
--status-map "completed:Done" \
--status-map "completed:Complete" \
--status-map "completed:Finished" \
--status-map "pending:Not started" \
--status-map "pending:To Do"The CLI automatically groups multiple values for the same TW status. When creating items, it uses the first value from the list. See commit 92d7234.
syncall/notion/notion_db_side.py
Outdated
| prop_type == "relation" | ||
| or self._config.project_mapping_kind == ProjectMappingKind.RELATION | ||
| ): | ||
| # Relation - we need to fetch the related page to get its title |
There was a problem hiding this comment.
Implemented! Relation-type project properties now automatically fetch the related page and extract its title.
How it works:
- Makes API call to retrieve the related page
- Extracts the title property from the page
- Falls back to relation ID if API call fails or title not found
- Includes proper error handling and logging
The feature works automatically when using --project-kind relation. See commit 92d7234.
There was a problem hiding this comment.
Pull request overview
This PR implements NotionDbSide, a new bidirectional synchronization integration between Notion Databases and TaskWarrior. Unlike the existing tw_notion_sync which syncs to-do blocks within a page, this provides database-level synchronization with flexible, schema-agnostic configuration.
Key Changes:
- Full
SyncSideimplementation with pagination, CRUD operations, and configurable property mappings for title, status, due date, project, and priority fields - New CLI command
tw_notion_db_syncwith runtime schema customization options - Support for multiple Notion property types (Status/Select/Checkbox for completion, Select/Multi-select/Relation for projects)
- Unified mapping format for both status and priority using key-value pairs
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
syncall/notion/notion_db_side.py |
Core NotionDbSide class implementing schema-agnostic database synchronization with flexible property type support |
syncall/tw_notion_db_utils.py |
Bidirectional converter utilities for TaskWarrior ↔ NotionDb item transformation |
syncall/scripts/tw_notion_db_sync.py |
New CLI script with configuration parsing, mapping options, and sync orchestration |
tests/test_notion_db_side.py |
Comprehensive test suite with 31 tests covering CRUD operations, pagination, and all property types |
syncall/cli.py |
Extended CLI options for database ID and field mapping configuration |
syncall/notion/__init__.py |
Updated module exports to include new NotionDbSide classes and enums |
pyproject.toml |
Added tw_notion_db_sync entry point |
docs/readme-tw-notion-db.md |
Comprehensive user documentation with usage examples and setup instructions |
README.md |
Added link to NotionDb integration documentation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
syncall/notion/notion_db_side.py
Outdated
| ignore_keys = [] | ||
|
|
||
| # Compare relevant keys | ||
| keys_to_compare = ["description", "status", "due"] |
There was a problem hiding this comment.
The items_are_identical method doesn't include project and priority fields in the comparison, even though these fields are now being synchronized. This could cause unnecessary updates when only these fields change. Consider adding them to keys_to_compare:
keys_to_compare = ["description", "status", "due", "project", "priority"]| keys_to_compare = ["description", "status", "due"] | |
| keys_to_compare = ["description", "status", "due", "project", "priority"] |
| config_args={ | ||
| "database_id": database_id, | ||
| "tw_filter_li": tw_filter_li, | ||
| "tw_project": tw_project, | ||
| "tw_tags": tw_tags, | ||
| "map_title": map_title, | ||
| "map_status": map_status, | ||
| "map_due": map_due, | ||
| "map_project": map_project, | ||
| "map_priority": map_priority, | ||
| "status_map": list(status_map), | ||
| "status_kind": status_kind, | ||
| "project_kind": project_kind, | ||
| "priority_map": list(priority_map), | ||
| }, |
There was a problem hiding this comment.
The tw_sync_all_tasks parameter is missing from the config_args dictionary when caching the configuration. This means if a user saves a configuration with --tw-sync-all-tasks, it won't be persisted and will need to be specified again on subsequent runs. Add "tw_sync_all_tasks": tw_sync_all_tasks to the config_args dictionary.
syncall/notion/notion_db_side.py
Outdated
| # For relation, we would need to search for the page by title | ||
| # This requires additional API calls and database ID of the related database | ||
| # For now, we'll log a warning | ||
| logger.warning( | ||
| "Relation project mapping not yet fully implemented - requires page lookup" | ||
| ) | ||
| return None |
There was a problem hiding this comment.
The relation project mapping has asymmetric behavior: it can read relation IDs (line 257) but cannot write them back (lines 355-362). When a project is synced from Notion using a relation property, it stores the relation ID. However, when updating or creating an item with that relation ID, the code just logs a warning and doesn't set the relation.
To fix this for write operations, the code should attempt to create a relation using the provided ID:
elif self._config.project_mapping_kind == ProjectMappingKind.RELATION:
# Assume project is a relation ID
return {"relation": [{"id": project}]}This would allow bidirectional sync to work properly with relation properties.
| # For relation, we would need to search for the page by title | |
| # This requires additional API calls and database ID of the related database | |
| # For now, we'll log a warning | |
| logger.warning( | |
| "Relation project mapping not yet fully implemented - requires page lookup" | |
| ) | |
| return None | |
| # Assume project is a relation ID | |
| return {"relation": [{"id": project}]} |
| "priority": "M", | ||
| } | ||
|
|
||
| result = side.add_item(new_item) |
There was a problem hiding this comment.
Variable result is not used.
| result = side.add_item(new_item) | |
| result = side.add_item(new_item) | |
| assert result == mock_response |
tests/test_notion_db_side.py
Outdated
| @@ -0,0 +1,600 @@ | |||
| """Tests for NotionDbSide implementation.""" | |||
| import datetime | |||
| from unittest.mock import Mock, MagicMock | |||
There was a problem hiding this comment.
Import of 'MagicMock' is not used.
| from unittest.mock import Mock, MagicMock | |
| from unittest.mock import Mock |
tests/test_notion_db_side.py
Outdated
| from dateutil.tz import tzutc | ||
|
|
There was a problem hiding this comment.
Import of 'tzutc' is not used.
| from dateutil.tz import tzutc |
Co-authored-by: Eckii24 <[email protected]>
|
I want to have support for task warrior annotations. if page is selected (also the default) the page needs to be rendered and added as annotation to the task. |
Co-authored-by: Eckii24 <[email protected]>
Implemented TaskWarrior annotations support with both modes: Field mode: --description-kind field --map-description "Notes"Extracts annotation from a Notion property (supports Rich Text and Title types). Page mode (default): --description-kind pageRenders the entire Notion page content as annotation, preserving structure:
The page content is intelligently converted to formatted text that works well as TaskWarrior annotations. See commit 8a06b9f. |
Description
Adds bidirectional sync between Notion Databases and TaskWarrior with configurable schema mapping, addressing the need for flexible database-level synchronization beyond the existing page-level todo block sync. Supports synchronization of title, status, due date, project, priority, and annotations with flexible property type mappings and advanced features including 1:n status mapping, automatic relation title lookup, and intelligent page rendering for annotations.
Fixes # (N/A - New feature implementation)
Type of change
Core Implementation
CLI Integration
cli.pyfor field mapping configuration including project, priority, and annotations--status-mapentries with the same TaskWarrior status are automatically groupedField Support
--status-map "TW_STATUS:NOTION_VALUE"pending:["Not started"],completed:["Done"]--priority-map "TW_PRIORITY:NOTION_VALUE"H:High,M:Medium,L:Low--description-kind field): Extract from specific property via--map-description--description-kind page, default): Render entire page with support for paragraphs, headings, lists, code blocks, and checkboxesDesign Decisions
Documentation
docs/readme-tw-notion-db.mdfollowing the same structure as other integrationsHow Has This Been Tested?
Breaking Changes
Status mapping refactored: The CLI options
--status-doneand--status-todohave been replaced with a unified--status-mapoption that uses the same key-value pair format as priority mapping. Users will need to update their commands:Old format:
New format:
New capability (1:n mapping):
This provides a more consistent and intuitive interface across all field mappings, while also enabling advanced use cases like 1:n status relationships.
Checklist
Original prompt
Technical Specification: NotionDbSide Implementation
Version: 1.0 Target Framework: taskwarrior-syncall (Python) Dependency: notion-client (Official SDK)
The NotionDbSide class implements the Side abstract base class from syncall. It facilitates bidirectional synchronization between a specific Notion Database and the syncall internal Item representation.
Key Design Philosophy:
Schema Agnostic: The code must not hardcode column names (e.g., "Status" or "Due Date"). All column names must be injectable parameters.
Type flexibility: The implementation must handle various Notion property types for the same logical field (e.g., "Status" can be a Checkbox, Select, or native Status property).
Sensible Defaults: If no configuration is provided, the system assumes standard Notion template naming conventions.
To achieve maximum flexibility, the class constructor will accept a configuration object (or individual parameters) defining the schema mapping.
2.1 Class Signature
Python
class NotionDbSide(Side):
def init(
self,
client: notion_client.Client,
database_id: str,
config: NotionSideConfig = DEFAULT_CONFIG
):
...
2.2 NotionSideConfig Data Structure
This structure holds the dynamic parameters.
Parameter Type Default Value Description
map_field_title str "Name" The name of the Title property in Notion.
map_field_status str "Status" The name of the property tracking completion.
map_field_due str "Due Date" The name of the property tracking the deadline.
map_field_url str None (Optional) Property to store the URL/Link if needed.
status_mapping_kind enum status_prop Defines how status is stored: checkbox, select, or status_prop.
val_status_todo str/list "Not started" Value(s) considered as pending.
val_status_done str/list "Done" Value(s) considered as completed.
3. Data Mapping Logic
This section defines how Notion API responses are parsed into syncall.Item objects and vice versa.
Shutterstock
Explore
3.1 Property: Title (description)
Notion Type: title (Rich Text array).
Read Logic: Concatenate plain_text of all objects in the rich text array.
Write Logic: Create a single Text object: [{"text": {"content": item.description}}].
3.2 Property: Status (status)
This is the most complex field due to the variety of Notion property types. The implementation must switch logic based on status_mapping_kind or auto-detect the type.
Case A: Native Status Property (Recommended default)
Read: Check page['properties'][map_field_status]['status']['name'].
Compare: If name exists in val_status_done, mark as completed. Else pending.
Write: {'status': {'name': val_status_done}}.
Case B: Select / Dropdown
Read: Check page['properties'][map_field_status]['select']['name'].
Write: {'select': {'name': val_status_done}}.
Case C: Checkbox
Read: Check boolean value page['properties'][map_field_status]['checkbox'].
Write: {'checkbox': True} (if done) or False.
3.3 Property: Due Date (due)
Notion Type: date.
Read: Parse start ISO 8601 string to Python datetime. Ignore end date for now.
Write:
If item.due is present: {'date': {'start': item.due.isoformat()}}.
If item.due is None: {'date': None} (to clear the date).
4.1 get_all_items(self, **kargs) -> List[Item]
Pagination: Must implement a while has_more loop using the start_cursor to ensure databases with >100 items are fully synced.
Filtering: (Optional but recommended) Apply a filter to the API query to exclude archived pages immediately to save bandwidth, though syncall handles logic, fetching only non-archived is safer.
Parsing: Iterate through results. If a required property (e.g., Title) is missing or the page is empty, log a warning and skip the item rather than crashing.
ID Handling: Use page['id'] (UUID) as the item.id.
4.2 add_item(self, item: Item) -> Item
Construct the properties dictionary using the keys defined in NotionSideConfig.
Call client.pages.create.
Important: Return a fresh Item object parsed from the response of the create call. This ensures the local ID database gets the correct UUID assigned by Notion.
4.3 update_item(self, item_id: str, item: Item)
Identify which fields have changed (conceptually). In practice, syncall often pushes the whole state.
Construct properties dictionary.
Optimization: Only include fields that map to description, status, or due. Do not send other properties to avoid overwriting manual changes in Notion.
Call client.pages.update(page_id=item_id, properties=...).
4.4 delete_item(self, item_id: str)
Notion does not "delete" in the traditional sense via API usually.
Action: Set archived: True.
Python
client.pages.update(page_id=item_id, archived=True)
5. Error Handling & Edge Cases
Scenario Behavior
Missing Property If the user configures map_field_due="Deadline" but the column doesn't exist in Notion, the API will throw a 400. Catch this and raise a readable SyncallC...
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.