├── assets ├── Friday.png ├── Monday.png ├── Sunday.png ├── Tuesday.png ├── Saturday.png ├── Thursday.png ├── Wednesday.png └── notion_habit_tracker_public_page.png ├── requirements.txt ├── Pipfile ├── LICENSE ├── remove_page.py ├── .gitignore ├── Pipfile.lock ├── README.md └── main.py /assets/Friday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Friday.png -------------------------------------------------------------------------------- /assets/Monday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Monday.png -------------------------------------------------------------------------------- /assets/Sunday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Sunday.png -------------------------------------------------------------------------------- /assets/Tuesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Tuesday.png -------------------------------------------------------------------------------- /assets/Saturday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Saturday.png -------------------------------------------------------------------------------- /assets/Thursday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Thursday.png -------------------------------------------------------------------------------- /assets/Wednesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Wednesday.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2022.9.24 2 | charset-normalizer==2.1.1 3 | idna==3.4 4 | python-dotenv==0.21.0 5 | requests==2.28.1 6 | urllib3==1.26.13 7 | -------------------------------------------------------------------------------- /assets/notion_habit_tracker_public_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/notion_habit_tracker_public_page.png -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | python-dotenv = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.10" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anurag Singh 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 | -------------------------------------------------------------------------------- /remove_page.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import datetime 4 | from datetime import date, timedelta 5 | import requests 6 | import json 7 | from json import JSONEncoder 8 | 9 | # load environment variables from .env 10 | load_dotenv() 11 | 12 | base_db_url = "https://api.notion.com/v1/databases/" 13 | base_pg_url = "https://api.notion.com/v1/pages/" 14 | 15 | header = {"Authorization": os.getenv('NOTION_SECRET_KEY'), 16 | "Notion-Version": "2021-05-13", "Content-Type": "application/json"} 17 | 18 | response_habits_db = requests.post( 19 | base_db_url + os.getenv('NOTION_HABIT_DB') + "/query", headers=header) 20 | 21 | 22 | # subclass JSONEncoder 23 | class DateTimeEncoder(JSONEncoder): 24 | # Override the default method 25 | def default(self, obj): 26 | if isinstance(obj, (datetime.date, datetime.datetime)): 27 | return obj.isoformat() 28 | 29 | 30 | # Deleting previous year data, else it will mess up with monthly analytics 31 | for page in response_habits_db.json()['results']: 32 | page_id = page['id'] 33 | props = page['properties'] 34 | current_month = props['Date']['date']['start'] 35 | date_object = date.fromisoformat(current_month) 36 | 37 | if date_object.year < date.today().year: 38 | 39 | payload = { 40 | "archived": True 41 | } 42 | 43 | remove_page = requests.patch( 44 | base_pg_url + page_id, headers=header, json=payload) 45 | 46 | if remove_page.status_code == 200: 47 | print( 48 | f"Page removed, Status code: {remove_page.status_code}, Reason: {remove_page.reason}") 49 | else: 50 | print( 51 | f"Something went wrong, Status code: {remove_page.status_code}, Reason: {remove_page.reason}") 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS / VScode 2 | .vscode 3 | .DS_Store 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | test/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | env_vars.py 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "163fc9c6876d17e5df8202b86c3119d7c9709505f8c51552563e9ce41d3410dd" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", 22 | "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" 23 | ], 24 | "markers": "python_version >= '3.6'", 25 | "version": "==2022.9.24" 26 | }, 27 | "charset-normalizer": { 28 | "hashes": [ 29 | "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", 30 | "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" 31 | ], 32 | "markers": "python_full_version >= '3.6.0'", 33 | "version": "==2.1.1" 34 | }, 35 | "idna": { 36 | "hashes": [ 37 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 38 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 39 | ], 40 | "markers": "python_version >= '3.5'", 41 | "version": "==3.4" 42 | }, 43 | "python-dotenv": { 44 | "hashes": [ 45 | "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5", 46 | "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045" 47 | ], 48 | "index": "pypi", 49 | "version": "==0.21.0" 50 | }, 51 | "requests": { 52 | "hashes": [ 53 | "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", 54 | "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" 55 | ], 56 | "index": "pypi", 57 | "version": "==2.28.1" 58 | }, 59 | "urllib3": { 60 | "hashes": [ 61 | "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", 62 | "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" 63 | ], 64 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 65 | "version": "==1.26.13" 66 | } 67 | }, 68 | "develop": {} 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
](https://youtu.be/tDbUbqv4S0c)
20 |
21 | ## Setup Pre-requisites
22 | - Duplicate this [Notion Template](https://ashleymavericks.gumroad.com/l/notion-habit-tracker) to get started.
23 | - Use services like [ChartBase](https://www.chartbase.so/) and [Data Jumbo](https://www.datajumbo.co/) to have the monthly analytics chart for your habits database.
24 |
25 | ## No Code Approach
26 | - With the new notion recurring template update, you can simply set an individual weekday template for a weekly repeat and it will automatically add a new page to the database.
27 |
28 | ## Find Database ID
29 | Login Notion in a browser and viewing the database as a full page, the database ID is the part of the URL after your workspace name and the slash (acme/) and before the question mark (?). The ID is 32 characters long, containing numbers and letters. Copy the ID and paste it somewhere you can easily find later.
30 |
31 | Repeat this process for both Habits DB and Analytics DB, and take a note of these Database IDs
32 |
33 | ```
34 | https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
35 | |--------- Database ID --------|
36 | ```
37 |
38 | ## Script Usage
39 | 1. Clone the repo
40 | ```bash
41 | git clone https://github.com/ashleymavericks/notion-habit-tracker.git
42 | ```
43 | 2. Create a .env file in the project folder
44 | 3. Add NOTION_HABIT_DB, NOTION_ANALYTICS_DB and [NOTION_SECRET_KEY](https://syncwith.com/p/notion-api-key-qrsJHMnH5LuHUjDqvZnmWC) in .env file
45 | ```bash
46 | NOTION_SECRET_KEY=PASTE_KEY_HERE
47 | NOTION_HABIT_DB=PASTE_KEY_HERE
48 | NOTION_ANALYTICS_DB=PASTE_KEY_HERE
49 | ```
50 | 4. Install project dependencies
51 | ```bash
52 | pipenv install
53 | ```
54 | 5. Run the Script called `main.py`, it may take some time to complete
55 | ```bash
56 | python3 main.py
57 | ```
58 | 6. Due to Notion API rate throttling, the script might not able to remove a huge number of previous year records at once, in that scenario kindly run `remove_page.py` until all records get purged
59 | ```bash
60 | python3 remove_page.py
61 | ```
62 |
63 | ## Contributing
64 | Feel free to reach out, if you want to further improve the template.
65 |
66 | ## License
67 | This project is licensed under the MIT license
68 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dotenv import load_dotenv
3 | import datetime
4 | from datetime import date, timedelta
5 | import requests
6 | import json
7 | from json import JSONEncoder
8 |
9 | # load environment variables from .env
10 | load_dotenv()
11 |
12 | base_db_url = "https://api.notion.com/v1/databases/"
13 | base_pg_url = "https://api.notion.com/v1/pages/"
14 |
15 | header = {"Authorization": os.getenv('NOTION_SECRET_KEY'),
16 | "Notion-Version": "2021-05-13", "Content-Type": "application/json"}
17 |
18 | notion_habit_db = os.getenv('NOTION_HABIT_DB')
19 | notion_analytics_db = os.getenv('NOTION_ANALYTICS_DB')
20 |
21 | response_habits_db = requests.post(
22 | base_db_url + notion_habit_db + "/query", headers=header)
23 |
24 | response_analytics_db = requests.post(
25 | base_db_url + notion_analytics_db + "/query", headers=header)
26 |
27 | # define no. of new pages/records to be added in Tracker
28 | days_count = 365
29 |
30 | # subclass JSONEncoder
31 | class DateTimeEncoder(JSONEncoder):
32 | # Override the default method
33 | def default(self, obj):
34 | if isinstance(obj, (datetime.date, datetime.datetime)):
35 | return obj.isoformat()
36 |
37 | date_list = list()
38 | month_dict = dict()
39 |
40 | # Deleting previous year data, else it will mess up with monthly analytics
41 | for page in response_habits_db.json()['results']:
42 | page_id = page['id']
43 | props = page['properties']
44 | current_month = props['Date']['date']['start']
45 | date_object = date.fromisoformat(current_month)
46 |
47 | if date_object.year < date.today().year:
48 |
49 | payload = {
50 | "archived": True
51 | }
52 |
53 | remove_page = requests.patch(
54 | base_pg_url + page_id, headers=header, json=payload)
55 |
56 | if remove_page.status_code == 200:
57 | print(
58 | f"Page removed, Status code: {remove_page.status_code}, Reason: {remove_page.reason}")
59 | else:
60 | print(
61 | f"Something went wrong, Status code: {remove_page.status_code}, Reason: {remove_page.reason}")
62 |
63 |
64 | # Creating list of datetime.date objects for current month pages/records
65 | for page in response_habits_db.json()['results']:
66 | page_id = page['id']
67 | props = page['properties']
68 | current_month = props['Date']['date']['start']
69 | date_object = date.fromisoformat(current_month)
70 | date_list.append(date_object)
71 |
72 | if not date_list:
73 | start_date = date.today()
74 | else:
75 | start_date = max(date_list) + timedelta(days=1)
76 |
77 |
78 | # Creating dict of page name and id for monthly analytics db
79 | for page in response_analytics_db.json()['results']:
80 | page_id = page['id']
81 | props = page['properties']
82 | page_name = props['Name']['title'][0]['text']['content']
83 | month_dict[page_name] = page_id
84 |
85 | for date in (start_date + timedelta(n) for n in range(days_count)):
86 | day = date.strftime('%A')
87 | month = date.strftime('%B')
88 | relation_id = month_dict[month]
89 | date_string = date.strftime('%B %d, %Y').replace(' 0', ' ')
90 |
91 | # To make datetime.date object JSON serializable
92 | date_json = json.dumps(date, indent=4, cls=DateTimeEncoder)
93 | date_modified = date_json[1:11]
94 |
95 | payload = {
96 | "parent": {
97 | "database_id": notion_habit_db
98 | },
99 | "cover": {
100 | "type": "external",
101 | "external": {
102 | "url": "https://github.com/ashleymavericks/notion-habit-tracker/blob/main/assets/" + day + ".png?raw=true"
103 | }
104 | },
105 | "properties": {
106 | "Date": {
107 | "date": {"start": date_modified}
108 | },
109 | "Name": {
110 | "title": [
111 | {
112 | "text": {
113 | "content": date_string
114 | }
115 | }
116 | ]
117 | },
118 | "Relation": {
119 | "relation": [
120 | {
121 | "id": relation_id
122 | }
123 | ]
124 | }
125 | }
126 | }
127 |
128 | add_page = requests.post(
129 | base_pg_url, headers=header, json=payload)
130 |
131 | if add_page.status_code == 200:
132 | print(
133 | f"Page added, Status code: {add_page.status_code}, Reason: {add_page.reason}")
134 | else:
135 | print(
136 | f"Something went wrong, Status code: {add_page.status_code}, Reason: {add_page.reason}")
137 |
--------------------------------------------------------------------------------