├── .coveragerc ├── .flake8 ├── .github └── workflows │ └── cd.yml ├── .gitignore ├── LICENSE ├── README.md ├── lambda_function.py ├── main.py ├── pyproject.toml ├── src ├── __init__.py └── task_syncer.py └── tests ├── conftest.py └── test_task_syncer.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = src 3 | omit = 4 | tests/* -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = tests/*, 4 | src/__init__.py 5 | max-complexity = 10 6 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: [push, workflow_dispatch] 12 | 13 | env: 14 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 15 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 16 | NT_AUTH: ${{ secrets.NT_AUTH }} 17 | NT_TASKS_DB_ID: ${{ secrets.NT_TASKS_DB_ID }} 18 | NT_NOTES_DB_ID: ${{ secrets.NT_NOTES_DB_ID }} 19 | NT_STATS_DB_ID: ${{ secrets.NT_STATS_DB_ID }} 20 | NT_EXPENSES_DB_ID: ${{ secrets.NT_EXPENSES_DB_ID }} 21 | TT_USER: ${{ secrets.TT_USER }} 22 | TT_PASS: ${{ secrets.TT_PASS }} 23 | TICKTICK_IDS: ${{ secrets.TICKTICK_IDS }} 24 | 25 | permissions: 26 | contents: read 27 | 28 | jobs: 29 | test-repo: 30 | 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: '3.10' 39 | 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install -e .[dev] 44 | 45 | - name: Lint with flake8 46 | run: flake8 src 47 | 48 | - name: Lint with mypy 49 | run: mypy src 50 | 51 | - name: Test with pytest 52 | run: pytest --cov 53 | 54 | update-lambda: 55 | needs: test-repo 56 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 57 | runs-on: ubuntu-latest 58 | 59 | steps: 60 | - uses: actions/checkout@v4 61 | - name: Set up Python 62 | uses: actions/setup-python@v5 63 | with: 64 | python-version: '3.10' 65 | 66 | - name: Install dependencies 67 | run: | 68 | python -m pip install --upgrade pip 69 | pip install build awscli 70 | 71 | - name: Create lambda package 72 | run: | 73 | mkdir lambda-deployment-package 74 | cp -r src main.py lambda_function.py lambda-deployment-package 75 | pip install -t lambda-deployment-package . 76 | cd lambda-deployment-package 77 | zip -r ../lambda-deployment-package.zip . 78 | cd .. 79 | 80 | - name: Upload lambda package 81 | run: aws lambda update-function-code --function-name ticktick-notion-connector --zip-file fileb://lambda-deployment-package.zip --region us-east-1 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Auth files 2 | .token-oauth 3 | 4 | # try code 5 | scratch.py 6 | 7 | # IDE 8 | .vscode/ 9 | .idea/ 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/ 35 | Scripts/ 36 | Include/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | *.ipynb 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | pyvenv.cfg 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ 139 | .dmypy.json 140 | dmypy.json 141 | 142 | # Pyre type checker 143 | .pyre/ 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Angelo Mosquera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notion-ticktick Connector 2 | Script that uses Notion's api and the unofficial Ticktick api to sync tasks between the two applications. 3 | -------------------------------------------------------------------------------- /lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | from main import main 3 | 4 | 5 | def lambda_handler(event, conntext): 6 | main() 7 | 8 | return { 9 | 'statusCode': 200, 10 | 'headers': { 11 | 'Access-Control-Allow-Headers': 'Content-Type, Access-Control-Allow-Headers,' 12 | 'Authorization, X-Requested-With', 13 | 'Access-Control-Allow-Origin': 'https://upbeat-jackson-774e87.netlify.app', 14 | 'Access-Control-Allow-Methods': 'OPTIONS,POST,GET' 15 | }, 16 | 'body': json.dumps('Tasks synced!') 17 | } 18 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from src.task_syncer import TaskSyncer 4 | 5 | from tickthon import TicktickClient 6 | from nothion import NotionClient 7 | 8 | logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)") 9 | 10 | 11 | def main(): 12 | ticktick = TicktickClient(os.getenv("TT_user"), os.getenv("TT_pass")) 13 | notion = NotionClient(os.getenv("NT_auth")) 14 | 15 | task_syncer = TaskSyncer(ticktick, notion) 16 | task_syncer.sync_expenses() 17 | task_syncer.sync_highlights() 18 | task_syncer.sync_tasks() 19 | task_syncer.add_work_task_tag() 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project.urls] 6 | repository = "https://github.com/anggelomos/notion-ticktick-connector" 7 | 8 | [tool.setuptools.package-data] 9 | nothion = ["py.typed"] 10 | 11 | [project] 12 | name = "notion-ticktick-connector" 13 | version = "0.2.0" 14 | description = "Script that uses Notion's api and the unofficial Ticktick api to sync tasks between the two applications." 15 | readme = "README.md" 16 | authors = [{name = "anggelomos", email = "anggelomos@outlook.com"}] 17 | requires-python = ">=3.10" 18 | classifiers = [ 19 | "Programming Language :: Python :: 3.10", 20 | ] 21 | dependencies = [ 22 | "tickthon>=0.2.8", 23 | "nothion", 24 | ] 25 | 26 | [project.optional-dependencies] 27 | dev = [ 28 | "pytest", 29 | "pytest-cov", 30 | "twine", 31 | "flake8", 32 | "mypy", 33 | "types-requests" 34 | ] -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anggelomos/notion-ticktick-connector/4bc60a5599229bb6cd5466ec0564dd688fce82fc/src/__init__.py -------------------------------------------------------------------------------- /src/task_syncer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Set, Optional, Iterable, List 3 | 4 | from nothion import NotionClient 5 | from tickthon import Task, TicktickClient 6 | from tickthon._config import get_ticktick_ids 7 | from tickthon.data.ticktick_id_keys import TicktickIdKeys as tik, TicktickFolderKeys as tfK 8 | 9 | 10 | class TaskSyncer: 11 | 12 | def __init__(self, ticktick: TicktickClient, notion: NotionClient): 13 | self._ticktick = ticktick 14 | self._notion = notion 15 | self._ticktick_tasks: Set[Task] = set(self._ticktick.get_active_tasks()) 16 | self._notion_tasks: Set[Task] = set(self._notion.get_active_tasks()) 17 | self.deleted_ticktick_tasks = self._ticktick.get_deleted_tasks() 18 | 19 | def sync_tasks(self): 20 | self._get_unsync_tasks() 21 | self.sync_ticktick_tasks() 22 | self.sync_notion_tasks() 23 | 24 | def sync_expenses(self) -> List[dict]: 25 | expenses_synced = [] 26 | for expense_task, expense_log in self._ticktick.get_expense_logs(): 27 | expenses_synced.append(self._notion.add_expense_log(expense_log)) 28 | self._ticktick.complete_task(expense_task) 29 | return expenses_synced 30 | 31 | def sync_highlights(self) -> List[dict]: 32 | highlights_synced = [] 33 | for log in self._ticktick.get_day_logs(): 34 | highlight_create = None 35 | if "highlight" in log.tags: 36 | highlight_create = self._notion.add_highlight_log(log) 37 | 38 | if highlight_create: 39 | highlights_synced.append(highlight_create) 40 | return highlights_synced 41 | 42 | def _get_unsync_tasks(self): 43 | logging.info("Getting unsync tasks.") 44 | ticktick_base_unsync_tasks = self._ticktick_tasks - self._notion_tasks 45 | notion_base_unsync_tasks = self._notion_tasks - self._ticktick_tasks 46 | 47 | ticktick_etags = {t.ticktick_etag for t in ticktick_base_unsync_tasks} 48 | notion_etags_dict = {t.ticktick_etag: t for t in notion_base_unsync_tasks} 49 | notion_clean_etags = set(notion_etags_dict.keys()) - ticktick_etags 50 | 51 | ticktick_ids = {t.ticktick_id for t in ticktick_base_unsync_tasks} 52 | notion_ids_dict = {t.ticktick_id: t for t in notion_base_unsync_tasks} 53 | notion_clean_ids = set(notion_ids_dict.keys()) - ticktick_ids 54 | 55 | self._ticktick_unsync_tasks = ticktick_base_unsync_tasks 56 | self._notion_unsync_tasks = set([notion_etags_dict[etag] for etag in notion_clean_etags] + 57 | [notion_ids_dict[task_id] for task_id in notion_clean_ids]) 58 | 59 | def create_notion_tasks(self, task: Task): 60 | self._notion.create_task(task) 61 | if self.is_task_note(task): 62 | self._notion.create_task_note(task) 63 | 64 | def update_notion_tasks(self, task: Task): 65 | self._notion.update_task(task) 66 | if self.is_task_note(task): 67 | if not self._notion.is_task_note_already_created(task): 68 | self._notion.create_task_note(task) 69 | else: 70 | self._notion.update_task_note(task) 71 | 72 | def delete_notion_tasks(self, task: Task): 73 | self._notion.delete_task(task) 74 | if self.is_task_note(task): 75 | self._notion.delete_task_note(task) 76 | 77 | def complete_notion_tasks(self, task: Task): 78 | self._notion.complete_task(task) 79 | if self.is_task_note(task): 80 | self._notion.complete_task_note(task) 81 | 82 | def sync_ticktick_tasks(self): 83 | logging.info(f"Syncing ticktick tasks: {self._ticktick_unsync_tasks}") 84 | for task in self._ticktick_unsync_tasks: 85 | notion_task = self.search_for_task(task, self._notion_tasks) 86 | if notion_task is None: 87 | self.create_notion_tasks(task) 88 | elif task != notion_task: 89 | self.update_notion_tasks(task) 90 | else: 91 | logging.warning(f"TICKTICK TASK {task.ticktick_id} WITH ETAG {task.ticktick_etag} " 92 | f"WAS NOT SYNCED") 93 | 94 | def sync_notion_tasks(self): 95 | logging.info("Syncing notion tasks") 96 | for task in self._notion_unsync_tasks: 97 | was_task_deleted = self.search_for_task(task, self.deleted_ticktick_tasks) 98 | was_task_updated = self.search_for_task(task, self._ticktick_unsync_tasks) 99 | 100 | if was_task_deleted: 101 | self.delete_notion_tasks(task) 102 | elif was_task_updated: 103 | continue 104 | else: 105 | self.complete_notion_tasks(task) 106 | 107 | @staticmethod 108 | def is_task_note(tasks: Task) -> bool: 109 | return "notes" in tasks.tags 110 | 111 | @staticmethod 112 | def search_for_task(search_task: Task, tasks: Iterable[Task]) -> Optional[Task]: 113 | logging.info(f"Searching task with id {search_task.ticktick_id} and etag {search_task.ticktick_etag}") 114 | for task in tasks: 115 | if task.ticktick_etag == search_task.ticktick_etag or task.ticktick_id == search_task.ticktick_id: 116 | return task 117 | return None 118 | 119 | def add_work_task_tag(self): 120 | work_task_list_id = get_ticktick_ids()[tik.LIST_IDS.value][tfK.WORK_TASKS.value] 121 | work_reminders_list_id = get_ticktick_ids()[tik.LIST_IDS.value][tfK.WORK_REMINDERS.value] 122 | work_tasks = [task for task in self._ticktick_tasks if 123 | task.project_id in [work_task_list_id, work_reminders_list_id]] 124 | 125 | for task in work_tasks: 126 | if "work-task" not in task.tags: 127 | add_tags = list(task.tags) 128 | 129 | if "task-active" in add_tags: 130 | add_tags.remove("task-active") 131 | 132 | add_tags.append("work-task") 133 | self._ticktick.replace_task_tags(task, tuple(add_tags)) 134 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from nothion import NotionClient 5 | from tickthon import TicktickClient 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def notion_client(): 10 | return NotionClient(os.getenv("NT_AUTH")) 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def ticktick_client(): 15 | return TicktickClient(os.getenv("TT_USER"), os.getenv("TT_PASS")) 16 | -------------------------------------------------------------------------------- /tests/test_task_syncer.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from random import random 3 | 4 | import pytest 5 | from dateutil.tz import tz 6 | from nothion._notion_payloads import NotionPayloads 7 | from tickthon import Task, ExpenseLog 8 | 9 | from task_syncer import TaskSyncer 10 | 11 | timezone = "America/Bogota" 12 | STATIC_NON_SYNCED_TASK = Task(title="Test task", due_date="2099-09-09", ticktick_id="test-id", 13 | ticktick_etag="test-etag", created_date="2099-09-09", focus_time=9.99, 14 | tags=("test", "integration", "notion", "notes"), project_id="inbox114478622", 15 | timezone=timezone) 16 | TEST_TICKTICK_TASKS = [STATIC_NON_SYNCED_TASK, 17 | # Task to update 18 | Task(title="Test Existing Task", due_date="9999-09-09", ticktick_id="a7f9b3d2c8e60f1472065ac4", 19 | ticktick_etag="muu17zqq", created_date="9999-09-09", focus_time=random(), 20 | tags=("test", "existing", "notes"), project_id="4a72b6d8e9f2103c5d6e7f8a9b0c", 21 | timezone=timezone, status=2), 22 | # Test task to create 23 | Task(title="Test Create Task", due_date="2099-09-09", ticktick_id="kj39rudnsakl49fht83ksio5", 24 | ticktick_etag="w8dhr428", created_date="2099-09-09", focus_time=9.99, 25 | tags=("test", "created", "notion", "notes"), project_id="jr83utdnsakl49fh8h28dfht", 26 | timezone=timezone) 27 | ] 28 | TEST_NOTION_TASKS = [STATIC_NON_SYNCED_TASK, 29 | # Task to update 30 | Task(title="Test Existing Task", due_date="9999-09-09", ticktick_id="a7f9b3d2c8e60f1472065ac4", 31 | ticktick_etag="muu17zqq", created_date="9999-09-09", focus_time=9.99, 32 | tags=("test", "existing", "notes"), project_id="4a72b6d8e9f2103c5d6e7f8a9b0c", 33 | timezone=timezone), 34 | # Task to complete 35 | Task(title="Test Complete Task", due_date="9999-09-09", ticktick_id="b2c3d4e5f6a7b8c9d0e1f2a3", 36 | ticktick_etag="5hu47d83", created_date="9999-09-09", focus_time=0, 37 | tags=("test", "complete", "notes"), project_id="c3d4e5f6a7b8c9d0e1f2a3b4", timezone=timezone), 38 | # Task to delete 39 | Task(title="Test Delete Task", due_date="9999-09-09", ticktick_id="d4e5f6a7b8c9d0e1f2a3b4c5", 40 | ticktick_etag="8ru3n28d", created_date="9999-09-09", focus_time=0, 41 | tags=("test", "delete", "notes"), project_id="e5f6a7b8c9d0e1f2a3b4c5d6", 42 | timezone=timezone, deleted=1) 43 | ] 44 | 45 | 46 | @pytest.fixture() 47 | def task_syncer(ticktick_client, notion_client): 48 | task_syncer = TaskSyncer(ticktick_client, notion_client) 49 | task_syncer._ticktick_tasks = set(TEST_TICKTICK_TASKS) 50 | task_syncer._notion_tasks = set(TEST_NOTION_TASKS) 51 | task_syncer._get_unsync_tasks() 52 | return task_syncer 53 | 54 | 55 | def test_get_unsync_tasks(task_syncer): 56 | expected_ticktick_unsync_tasks = set(TEST_TICKTICK_TASKS[1:]) 57 | expected_notion_unsync_tasks = set(TEST_NOTION_TASKS[2:]) 58 | 59 | assert task_syncer._ticktick_unsync_tasks == expected_ticktick_unsync_tasks 60 | assert task_syncer._notion_unsync_tasks == expected_notion_unsync_tasks 61 | 62 | 63 | def test_sync_ticktick_tasks(task_syncer): 64 | task_syncer.sync_ticktick_tasks() 65 | 66 | created_task_search = Task(ticktick_etag="w8dhr428", created_date="", title="", ticktick_id="", tags=tuple("notes")) 67 | updated_task_search = Task(ticktick_etag="muu17zqq", created_date="", title="", ticktick_id="", tags=tuple("notes")) 68 | 69 | created_task = task_syncer._notion.get_notion_task(created_task_search) 70 | updated_task = task_syncer._notion.get_notion_task(updated_task_search) 71 | 72 | assert created_task == TEST_TICKTICK_TASKS[2] 73 | assert updated_task == TEST_TICKTICK_TASKS[1] 74 | 75 | task_syncer._notion.delete_task(created_task) 76 | task_syncer._notion.delete_task_note(created_task) 77 | 78 | 79 | def test_sync_notion_tasks(task_syncer): 80 | task_syncer.deleted_ticktick_tasks.append(TEST_NOTION_TASKS[3]) 81 | task_syncer._notion.create_task(TEST_NOTION_TASKS[3]) 82 | task_syncer._notion.create_task(TEST_NOTION_TASKS[2]) 83 | 84 | task_syncer.sync_notion_tasks() 85 | 86 | completed_task_search = Task(ticktick_etag="5hu47d83", created_date="", title="", ticktick_id="") 87 | completed_task = task_syncer._notion.get_notion_task(completed_task_search) 88 | assert completed_task.status == 2 89 | assert task_syncer._notion.is_task_already_created(TEST_NOTION_TASKS[3]) is False 90 | 91 | task_syncer._notion.delete_task(completed_task) 92 | 93 | 94 | def test_sync_expenses(task_syncer): 95 | task_syncer._ticktick.expense_logs.append((Task(title="$9.9 Test product", due_date="9999-09-09", 96 | ticktick_id="a7b8c9d0e1f2a3b4c5d6e7f8", created_date="9999-09-09", 97 | ticktick_etag="a9b0c1d2"), 98 | ExpenseLog(date="9999-09-09", expense=9.9, 99 | product="Test product integration tests notion-ticktick"))) 100 | 101 | expenses_synced = task_syncer.sync_expenses() 102 | 103 | test_expense = next((expense for expense in expenses_synced if 104 | expense['properties']['product']['title'][0]['plain_text'] == 105 | "Test product integration tests notion-ticktick"), None) 106 | 107 | task_syncer._notion.notion_api.get_table_entry(test_expense["id"]) 108 | task_syncer._notion.notion_api.update_table_entry(test_expense["id"], NotionPayloads.delete_table_entry()) 109 | 110 | 111 | def test_sync_highlights(task_syncer): 112 | task_syncer._ticktick.all_day_logs.append((Task(title="Tested highlight syncer", due_date="9999-09-09", 113 | ticktick_id="726db85349f01aec349fdb83", 114 | created_date=datetime.now(tz.gettz("America/New_York")).isoformat(), 115 | ticktick_etag="3c02ab1d", tags=("highlight",), 116 | timezone="America/New_York"))) 117 | 118 | highlights_synced = task_syncer.sync_highlights() 119 | 120 | test_expense = [highlight for highlight in highlights_synced if 121 | highlight['properties']['Note']['title'][0]['plain_text'] == "Tested highlight syncer"] 122 | 123 | assert len(test_expense) == 1 124 | task_syncer._notion.notion_api.get_table_entry(test_expense[0]["id"]) 125 | task_syncer._notion.notion_api.update_table_entry(test_expense[0]["id"], NotionPayloads.delete_table_entry()) 126 | --------------------------------------------------------------------------------