├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── dist ├── notion-cli-list-manager-0.1.4.tar.gz ├── notion-cli-list-manager-0.1.5.tar.gz ├── notion-cli-list-manager-0.1.6.tar.gz ├── notion_cli_list_manager-0.1.4-py3-none-any.whl ├── notion_cli_list_manager-0.1.5-py3-none-any.whl └── notion_cli_list_manager-0.1.6-py3-none-any.whl ├── notion_cli_list_manager ├── __init__.py ├── __init__.pyc ├── main.py ├── page.py └── utils.py ├── poetry.lock ├── pyproject.toml ├── readme.md ├── requirements.txt ├── showcase.gif └── test ├── __init__.py ├── config.toml └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | /vir_env 2 | .vscode 3 | /notion_cli_list_manager/token.json 4 | /notion_cli_list_manager/config.json 5 | /notion_cli_list_manager/pages.json 6 | /notion_cli_list_manager/config.toml 7 | /notion_cli_list_manager/pages.toml 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [0.1.6] - 2022-01-05 7 | ### Fixed 8 | - Formula type property 9 | - Properties not unique name runtime bug 10 | 11 | ## [0.1.5] - 2022-01-05 12 | ### Added 13 | - Properties support! 🌈 At the present, properties are fully supported (except Relations and Rolls up that are __NS__ - Not supported) but read-only. Writeable ones will be supported in the next versions. 14 | - Use `list db --prop [LABEL]` to edit the displayed properties of an already saved database labeled [LABEL] 15 | - Error display color (yellow/orange) 16 | - Added CHANGELOG file. 17 | 18 | ### Changed 19 | - After a database submission, a prompt will ask you which properties you want to display. 20 | 21 | ### Fixed 22 | - Some typos 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Giacomo Salici 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 | -------------------------------------------------------------------------------- /dist/notion-cli-list-manager-0.1.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/dist/notion-cli-list-manager-0.1.4.tar.gz -------------------------------------------------------------------------------- /dist/notion-cli-list-manager-0.1.5.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/dist/notion-cli-list-manager-0.1.5.tar.gz -------------------------------------------------------------------------------- /dist/notion-cli-list-manager-0.1.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/dist/notion-cli-list-manager-0.1.6.tar.gz -------------------------------------------------------------------------------- /dist/notion_cli_list_manager-0.1.4-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/dist/notion_cli_list_manager-0.1.4-py3-none-any.whl -------------------------------------------------------------------------------- /dist/notion_cli_list_manager-0.1.5-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/dist/notion_cli_list_manager-0.1.5-py3-none-any.whl -------------------------------------------------------------------------------- /dist/notion_cli_list_manager-0.1.6-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/dist/notion_cli_list_manager-0.1.6-py3-none-any.whl -------------------------------------------------------------------------------- /notion_cli_list_manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/notion_cli_list_manager/__init__.py -------------------------------------------------------------------------------- /notion_cli_list_manager/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/notion_cli_list_manager/__init__.pyc -------------------------------------------------------------------------------- /notion_cli_list_manager/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import typer 3 | from .page import page 4 | 5 | def string_parser(string: str): 6 | 7 | pars_raw = string.split(',') 8 | 9 | indexes = [] 10 | 11 | for pars_temp in pars_raw: 12 | pars = pars_temp.split(':') 13 | 14 | if len(pars) == 1: 15 | indexes.append(int(pars[0])) 16 | else: 17 | 18 | if len(pars) == 2: 19 | pars.append('1') 20 | 21 | pars = [int(i) for i in pars] 22 | for i in range(pars[0], pars[1], pars[2]): 23 | indexes.append(i) 24 | 25 | return indexes 26 | 27 | def set_properties_list(properties, label): 28 | try: 29 | typer.echo(page.show_properties(properties)) 30 | 31 | typer.secho("Insert the ordered list of indexes of the properties you want to display.", bold=True ) 32 | indexes_string = typer.prompt("Eg: \"2,3,0\". Default:", 33 | default="{}:{}".format(0, len(properties)), type=str) 34 | indexes = string_parser(indexes_string) 35 | new_prop=[] 36 | for index in indexes: 37 | if properties[index] == "Index": 38 | typer.secho("Property cannot be named 'Index', please change it.", err= True, fg='yellow') 39 | elif properties[index] in new_prop: 40 | typer.secho("Property names must be unique.", err= True, fg='yellow') 41 | else: 42 | new_prop.append(properties[index]) 43 | page.set_properties(label, new_prop) 44 | except: 45 | typer.secho("Error during property set.", err= True, fg='yellow') 46 | 47 | app = typer.Typer() 48 | 49 | @app.command() 50 | def add(title: str = typer.Argument(..., help="The title of the page."), 51 | db: str = typer.Option("Default", help="To add the page to a custom database (Leave the option blank and the default database will be used.)", show_default=False)): 52 | """ 53 | Add an entry to your database. 54 | """ 55 | page.add(title, db) 56 | 57 | @app.command() 58 | def rm(string_of_index: str = typer.Argument(..., metavar="INDEX", help="The index of the page to remove.")): 59 | """ 60 | Remove a database entry. Use this after the "list" command. 61 | """ 62 | page.remove(string_parser(string_of_index)) 63 | 64 | @app.command() 65 | def set( 66 | token: str = typer.Option("", help="The API token"), 67 | id: str = typer.Option("", help="The database id") 68 | ): 69 | """ 70 | Set the personal Notion API Token and the default Database id. 71 | """ 72 | 73 | 74 | if token != "": 75 | page.set_token(token) 76 | 77 | if id != "": 78 | properties = page.set_database("Default", id) 79 | set_properties_list(properties, "Default") 80 | 81 | 82 | @app.command() 83 | def db( 84 | rm: str = typer.Option("", help="Remove a database from the manager.", metavar="LABEL"), 85 | label: str = typer.Option("", help="Add a database from the manager.", metavar="LABEL"), 86 | id: str = typer.Option("", help="The database id."), 87 | prop: str = typer.Option("", help="Set the database properties.", metavar="LABEL") 88 | ): 89 | """ 90 | Display the databases saved on the manager. To add or to remove a database here does not cause the actual creation or deletion on Notion. 91 | """ 92 | if prop != "": 93 | properties = page.get_properties(prop) 94 | set_properties_list(properties, prop) 95 | elif rm == "" and label == "" and id == "" and prop=="": 96 | page.show_databases() 97 | elif rm != "" and (label != "" or id != "") and prop=="": 98 | typer.secho("You cannot remove and add a databese at the same time.", err= True, fg='yellow') 99 | elif rm != "" and prop=="": 100 | page.rm_database(rm) 101 | elif prop=="": 102 | properties = page.set_database(label, id) 103 | set_properties_list(properties, label) 104 | 105 | 106 | 107 | 108 | 109 | 110 | @app.callback(invoke_without_command=True) 111 | def main(ctx: typer.Context, 112 | db: str = typer.Option("Default", help="Display the list for a specific database"), 113 | all: bool = typer.Option(False, help="Display all the lists.")): 114 | """ 115 | The Notion Cli List Manager allows you to manage - as you can imagine - Notion Lists. 116 | Use the set command to initialize the token and the default database id. 117 | """ 118 | if ctx.invoked_subcommand is None: 119 | if(all): 120 | page.all_by_all() 121 | else: 122 | page.all(db) 123 | 124 | 125 | if __name__=="__main__": 126 | app() 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /notion_cli_list_manager/page.py: -------------------------------------------------------------------------------- 1 | from os import get_terminal_size 2 | import typer 3 | from .utils import api_helper, toml_helper 4 | from prettytable import PrettyTable 5 | 6 | 7 | 8 | class page(): 9 | 10 | def add(title, database): 11 | api_helper.new_page(api_helper, title, database) 12 | 13 | def all_by_all(): 14 | index = 0 15 | x = PrettyTable() 16 | x.max_table_width=round(get_terminal_size().columns*0.8) 17 | x.field_names = ["Index", "Name"] 18 | x.align["Name"] = "l" 19 | pages_dict = {} 20 | for key in toml_helper.get_db_keys(): 21 | index, x, pages_dict = page.query(key, index, x, pages_dict) 22 | typer.echo(x) 23 | 24 | toml_helper.set_dict(pages_dict, toml_helper.pages_file) 25 | 26 | 27 | def all(database): 28 | 29 | properties_list = ["Index"] + toml_helper.get_db_prop(database) 30 | if len(properties_list) ==1: 31 | properties_list.append("Name") 32 | 33 | x = PrettyTable() 34 | x.max_table_width=round(get_terminal_size().columns*0.95) 35 | x.field_names = properties_list 36 | x.align = "l" 37 | 38 | index, x, pages_dict = page.query(database, x = x, properties_list=properties_list) 39 | 40 | typer.echo(x) 41 | 42 | toml_helper.set_dict(pages_dict, toml_helper.pages_file) 43 | 44 | def query(database, index = 0, x = PrettyTable, pages_dict = {}, show_properties=True, properties_list = ["Index", "Name"]): 45 | r = api_helper.get_pages(api_helper, database) 46 | 47 | if type(r) is dict: 48 | if r["object"] == "list": 49 | for page in r["results"]: 50 | properties = page.get("properties") 51 | prop_types = { 52 | "title": "title", 53 | "rich": "rich_text", 54 | "select": "select", 55 | "multi": "multi_select", 56 | "created_time": "created_time", 57 | "number": "number", 58 | "url": "url", 59 | "last_edited_time": "last_edited_time", 60 | "checkbox":"checkbox", 61 | "phone":"phone_number", 62 | "mail":"email", 63 | "formula":"formula", 64 | "created_by":"created_by", 65 | "last_edit": "last_edited_by", 66 | "date":"date", 67 | "people":"people", 68 | "files":"files" 69 | } 70 | row={p:"" for p in properties_list} 71 | row["Index"]= (index) 72 | for property in properties: 73 | if property in row.keys(): 74 | type_of = properties.get(property).get("type") 75 | #title 76 | if type_of == prop_types["title"]: 77 | path=properties.get(property).get(prop_types["title"]) 78 | if len(path)>0: 79 | row[property] = path[0].get("text").get("content") 80 | #rich-text 81 | elif type_of == prop_types["rich"]: 82 | path=properties.get(property).get(prop_types["rich"]) 83 | full_string = "" 84 | for p in path: 85 | full_string += p.get("plain_text") 86 | row[property] = full_string 87 | #select 88 | elif type_of == prop_types["select"]: 89 | path=properties.get(property).get(prop_types["select"]) 90 | if path is not None: 91 | row[property] = path.get("name") 92 | #created_by 93 | elif type_of == prop_types["created_by"]: 94 | path=properties.get(property).get(prop_types["created_by"]) 95 | if path is not None: 96 | row[property] = path.get("name") 97 | #date 98 | elif type_of == prop_types["date"]: 99 | path=properties.get(property).get(prop_types["date"]) 100 | full_string = "" 101 | if path is not None: 102 | start = path.get("start") 103 | end = path.get("end") 104 | if start is not None: 105 | full_string += str(start) 106 | if end is not None: 107 | full_string += " -> " 108 | 109 | if end is not None: 110 | full_string += str(end) 111 | 112 | row[property] = full_string 113 | #last_edit 114 | elif type_of == prop_types["last_edit"]: 115 | path=properties.get(property).get(prop_types["last_edit"]) 116 | if path is not None: 117 | row[property] = path.get("name") 118 | #formula 119 | elif type_of == prop_types["formula"]: 120 | path=properties.get(property).get(prop_types["formula"]) 121 | if path is not None: 122 | row[property] = path.get("string") 123 | #multi-select 124 | elif type_of == prop_types["multi"]: 125 | path=properties.get(property).get(prop_types["multi"]) 126 | full_string = "" 127 | for p in path: 128 | full_string += p.get("name") + " " 129 | row[property] = full_string 130 | #files 131 | elif type_of == prop_types["files"]: 132 | path=properties.get(property).get(prop_types["files"]) 133 | full_string = "" 134 | for p in path: 135 | full_string += p.get("name") + " " 136 | row[property] = full_string 137 | #people 138 | elif type_of == prop_types["people"]: 139 | path=properties.get(property).get(prop_types["people"]) 140 | full_string = "" 141 | for p in path: 142 | full_string += p.get("name") + " " 143 | row[property] = full_string 144 | #created_time 145 | elif type_of == prop_types["created_time"]: 146 | path=properties.get(property).get(prop_types["created_time"]) 147 | row[property] = path 148 | #last_edited_time 149 | elif type_of == prop_types["last_edited_time"]: 150 | path=properties.get(property).get(prop_types["last_edited_time"]) 151 | row[property] = path 152 | #phone 153 | elif type_of == prop_types["phone"]: 154 | path=properties.get(property).get(prop_types["phone"]) 155 | row[property] = path 156 | #email 157 | elif type_of == prop_types["mail"]: 158 | path=properties.get(property).get(prop_types["mail"]) 159 | row[property] = path 160 | #number 161 | elif type_of == prop_types["number"]: 162 | path=properties.get(property).get(prop_types["number"]) 163 | if path is None: 164 | row[property] = "" 165 | else: 166 | row[property] = path 167 | #checkbox 168 | elif type_of == prop_types["checkbox"]: 169 | path=properties.get(property).get(prop_types["checkbox"]) 170 | if path is True: 171 | row[property] = "[x]" 172 | else: 173 | row[property] = "[ ]" 174 | #url 175 | elif type_of == prop_types["url"]: 176 | path=properties.get(property).get(prop_types["url"]) 177 | if path is None: 178 | row[property] = "" 179 | else: 180 | row[property] = path 181 | else: 182 | row[property] = "NS" 183 | 184 | x.add_row(row.values()) 185 | pages_dict[str(index)] = page 186 | index+=1 187 | 188 | elif r["object"] == "error": 189 | typer.echo("Error:\t" + str(r["message"])) 190 | 191 | 192 | return index, x, pages_dict 193 | 194 | 195 | def remove(list_of_indexes): 196 | pages_dict = toml_helper.get_dict(toml_helper.pages_file) 197 | for index in list_of_indexes: 198 | p = pages_dict.get(str(index)) 199 | if type(p) == dict: 200 | api_helper.remove_page(api_helper, p.get("id")) 201 | 202 | def set_token(token): 203 | dict = toml_helper.get_dict(toml_helper.config_file) 204 | dict["token"] = token 205 | toml_helper.set_dict(dict, toml_helper.config_file) 206 | 207 | def set_database(key, id): 208 | db_dict = toml_helper.get_dict(toml_helper.config_file) 209 | 210 | if "databases" not in db_dict.keys(): 211 | db_dict["databases"] = {} 212 | 213 | db_dict.get("databases")[key] = {"id": id} 214 | 215 | toml_helper.set_dict(db_dict, toml_helper.config_file) 216 | 217 | properties = api_helper.get_db_props(api_helper, id) 218 | 219 | return list(properties) 220 | 221 | 222 | 223 | def show_databases(): 224 | list = toml_helper.get_db_keys() 225 | x = PrettyTable() 226 | x.field_names = ["Label"] 227 | for label in list: 228 | x.add_row([label]) 229 | typer.echo(x) 230 | 231 | def rm_database(label): 232 | dict = toml_helper.get_dict(toml_helper.config_file) 233 | r = dict.get("databases").pop(label) 234 | toml_helper.set_dict(dict, toml_helper.config_file) 235 | 236 | def get_properties(label): 237 | return list(api_helper.get_db_props(api_helper, toml_helper.get_db_id(label))) 238 | 239 | def show_properties(properties): 240 | x = PrettyTable() 241 | x.field_names = [i for i in range (0, len(properties))] 242 | x.add_row(properties) 243 | return x 244 | 245 | def set_properties(label, properties): 246 | db_dict = toml_helper.get_dict(toml_helper.config_file) 247 | 248 | 249 | db_dict.get("databases")[label]["properties"] = properties 250 | 251 | toml_helper.set_dict(db_dict, toml_helper.config_file) 252 | -------------------------------------------------------------------------------- /notion_cli_list_manager/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests.api import request 3 | import typer 4 | import toml 5 | from pathlib import Path 6 | 7 | 8 | class toml_helper(): 9 | script_location = Path(__file__).absolute().parent 10 | config_file = script_location / "config.toml" 11 | pages_file = script_location / "pages.toml" 12 | 13 | def get_dict(f, verbose=False): 14 | try: 15 | with open(f, "r+") as file_text: 16 | r = toml.load(file_text) 17 | return r 18 | except FileNotFoundError: 19 | if verbose: 20 | typer.secho("File not found.", err= True, fg='yellow') 21 | except toml.TomlDecodeError: 22 | typer.secho("File encoding error.", err= True, fg='yellow') 23 | 24 | return {} 25 | 26 | def set_dict(dict, f): 27 | with open(f, "w") as file_text: 28 | return file_text.write(toml.dumps(dict)) 29 | 30 | 31 | def get_db_id (database): 32 | try: 33 | dict = toml_helper.get_dict(toml_helper.config_file).get("databases") 34 | return str(dict.get(database).get("id")) 35 | except: 36 | return "" 37 | 38 | 39 | def get_db_keys(): 40 | try: 41 | r = toml_helper.get_dict(toml_helper.config_file).get("databases") 42 | return r.keys() 43 | except: 44 | return [] 45 | 46 | def get_db_prop(database): 47 | try: 48 | r = toml_helper.get_dict(toml_helper.config_file).get("databases").get(database)["properties"] 49 | return r 50 | except: 51 | return [] 52 | 53 | class api_helper(): 54 | api_url_pre = "https://api.notion.com/v1/" 55 | NOTION_API_KEY = str(toml_helper.get_dict(toml_helper.config_file).get("token")) 56 | payload = { 57 | "Authorization": NOTION_API_KEY, 58 | "Notion-Version": "2021-08-16", 59 | "Content-Type": "application/json" 60 | } 61 | 62 | def get_db_props(self, database): 63 | r = self.get_database(self, database) 64 | 65 | try: 66 | r = r.get("properties") 67 | if type(r) is dict: 68 | return r.keys() 69 | else: 70 | return [] 71 | except: 72 | typer.echo(r) 73 | return [] 74 | 75 | def get_database(self, database): 76 | return self.api(self, requests.get, "databases/" + database, "") 77 | 78 | 79 | def new_page(self, title, database): 80 | data = '{"parent":{"database_id":"'+ toml_helper.get_db_id(database) + \ 81 | '"},"properties":{"Name":{"title":[{"text":{"content":"'+title+'"}}]}}}' 82 | return self.api_post(self, "pages", data) 83 | 84 | 85 | def get_pages(self, database): 86 | return self.api_post(self, "databases/" + toml_helper.get_db_id(database) + "/query", "") 87 | 88 | 89 | 90 | def remove_page(self, page_id): 91 | return self.api_delete(self, "blocks/" + page_id, "") 92 | 93 | 94 | 95 | def api_post(self, api_url_post, data): 96 | return self.api(self, requests.post, api_url_post, data) 97 | 98 | 99 | def api_delete(self, api_url_post, data): 100 | return self.api(self, requests.delete, api_url_post, data) 101 | 102 | def api(self, function, api_url_post, data): 103 | try: 104 | r = function(self.api_url_pre + api_url_post, headers=self.payload, data=data) 105 | if (r.status_code == 200): 106 | return r.json() 107 | else: 108 | typer.secho("Request post error", err= True, fg='yellow') 109 | return "" 110 | except: 111 | typer.secho("Request post error", err= True, fg='yellow') 112 | return "" 113 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.4.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 22 | 23 | [[package]] 24 | name = "certifi" 25 | version = "2021.10.8" 26 | description = "Python package for providing Mozilla's CA Bundle." 27 | category = "main" 28 | optional = false 29 | python-versions = "*" 30 | 31 | [[package]] 32 | name = "charset-normalizer" 33 | version = "2.0.10" 34 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 35 | category = "main" 36 | optional = false 37 | python-versions = ">=3.5.0" 38 | 39 | [package.extras] 40 | unicode_backport = ["unicodedata2"] 41 | 42 | [[package]] 43 | name = "click" 44 | version = "8.0.3" 45 | description = "Composable command line interface toolkit" 46 | category = "main" 47 | optional = false 48 | python-versions = ">=3.6" 49 | 50 | [package.dependencies] 51 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 52 | 53 | [[package]] 54 | name = "colorama" 55 | version = "0.4.4" 56 | description = "Cross-platform colored terminal text." 57 | category = "main" 58 | optional = false 59 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 60 | 61 | [[package]] 62 | name = "idna" 63 | version = "3.3" 64 | description = "Internationalized Domain Names in Applications (IDNA)" 65 | category = "main" 66 | optional = false 67 | python-versions = ">=3.5" 68 | 69 | [[package]] 70 | name = "more-itertools" 71 | version = "8.12.0" 72 | description = "More routines for operating on iterables, beyond itertools" 73 | category = "dev" 74 | optional = false 75 | python-versions = ">=3.5" 76 | 77 | [[package]] 78 | name = "packaging" 79 | version = "21.3" 80 | description = "Core utilities for Python packages" 81 | category = "dev" 82 | optional = false 83 | python-versions = ">=3.6" 84 | 85 | [package.dependencies] 86 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 87 | 88 | [[package]] 89 | name = "pluggy" 90 | version = "0.13.1" 91 | description = "plugin and hook calling mechanisms for python" 92 | category = "dev" 93 | optional = false 94 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 95 | 96 | [package.extras] 97 | dev = ["pre-commit", "tox"] 98 | 99 | [[package]] 100 | name = "prettytable" 101 | version = "2.5.0" 102 | description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" 103 | category = "main" 104 | optional = false 105 | python-versions = ">=3.6" 106 | 107 | [package.dependencies] 108 | wcwidth = "*" 109 | 110 | [package.extras] 111 | tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"] 112 | 113 | [[package]] 114 | name = "py" 115 | version = "1.11.0" 116 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 117 | category = "dev" 118 | optional = false 119 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 120 | 121 | [[package]] 122 | name = "pyparsing" 123 | version = "3.0.6" 124 | description = "Python parsing module" 125 | category = "dev" 126 | optional = false 127 | python-versions = ">=3.6" 128 | 129 | [package.extras] 130 | diagrams = ["jinja2", "railroad-diagrams"] 131 | 132 | [[package]] 133 | name = "pytest" 134 | version = "5.4.3" 135 | description = "pytest: simple powerful testing with Python" 136 | category = "dev" 137 | optional = false 138 | python-versions = ">=3.5" 139 | 140 | [package.dependencies] 141 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 142 | attrs = ">=17.4.0" 143 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 144 | more-itertools = ">=4.0.0" 145 | packaging = "*" 146 | pluggy = ">=0.12,<1.0" 147 | py = ">=1.5.0" 148 | wcwidth = "*" 149 | 150 | [package.extras] 151 | checkqa-mypy = ["mypy (==v0.761)"] 152 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 153 | 154 | [[package]] 155 | name = "requests" 156 | version = "2.27.0" 157 | description = "Python HTTP for Humans." 158 | category = "main" 159 | optional = false 160 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 161 | 162 | [package.dependencies] 163 | certifi = ">=2017.4.17" 164 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 165 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 166 | urllib3 = ">=1.21.1,<1.27" 167 | 168 | [package.extras] 169 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 170 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 171 | 172 | [[package]] 173 | name = "toml" 174 | version = "0.10.2" 175 | description = "Python Library for Tom's Obvious, Minimal Language" 176 | category = "main" 177 | optional = false 178 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 179 | 180 | [[package]] 181 | name = "typer" 182 | version = "0.4.0" 183 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 184 | category = "main" 185 | optional = false 186 | python-versions = ">=3.6" 187 | 188 | [package.dependencies] 189 | click = ">=7.1.1,<9.0.0" 190 | 191 | [package.extras] 192 | all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] 193 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] 194 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"] 195 | test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)"] 196 | 197 | [[package]] 198 | name = "urllib3" 199 | version = "1.26.7" 200 | description = "HTTP library with thread-safe connection pooling, file post, and more." 201 | category = "main" 202 | optional = false 203 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 204 | 205 | [package.extras] 206 | brotli = ["brotlipy (>=0.6.0)"] 207 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 208 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 209 | 210 | [[package]] 211 | name = "wcwidth" 212 | version = "0.2.5" 213 | description = "Measures the displayed width of unicode strings in a terminal" 214 | category = "main" 215 | optional = false 216 | python-versions = "*" 217 | 218 | [metadata] 219 | lock-version = "1.1" 220 | python-versions = "^3.8" 221 | content-hash = "c102a1fd90b0ef4dedd7953bd5f74c885b71e0980c06648be2aec0b72988601a" 222 | 223 | [metadata.files] 224 | atomicwrites = [ 225 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 226 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 227 | ] 228 | attrs = [ 229 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 230 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 231 | ] 232 | certifi = [ 233 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 234 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 235 | ] 236 | charset-normalizer = [ 237 | {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, 238 | {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, 239 | ] 240 | click = [ 241 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 242 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 243 | ] 244 | colorama = [ 245 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 246 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 247 | ] 248 | idna = [ 249 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 250 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 251 | ] 252 | more-itertools = [ 253 | {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, 254 | {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, 255 | ] 256 | packaging = [ 257 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 258 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 259 | ] 260 | pluggy = [ 261 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 262 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 263 | ] 264 | prettytable = [ 265 | {file = "prettytable-2.5.0-py3-none-any.whl", hash = "sha256:1411c65d21dca9eaa505ba1d041bed75a6d629ae22f5109a923f4e719cfecba4"}, 266 | {file = "prettytable-2.5.0.tar.gz", hash = "sha256:f7da57ba63d55116d65e5acb147bfdfa60dceccabf0d607d6817ee2888a05f2c"}, 267 | ] 268 | py = [ 269 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 270 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 271 | ] 272 | pyparsing = [ 273 | {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, 274 | {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, 275 | ] 276 | pytest = [ 277 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 278 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 279 | ] 280 | requests = [ 281 | {file = "requests-2.27.0-py2.py3-none-any.whl", hash = "sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"}, 282 | {file = "requests-2.27.0.tar.gz", hash = "sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5"}, 283 | ] 284 | toml = [ 285 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 286 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 287 | ] 288 | typer = [ 289 | {file = "typer-0.4.0-py3-none-any.whl", hash = "sha256:d81169725140423d072df464cad1ff25ee154ef381aaf5b8225352ea187ca338"}, 290 | {file = "typer-0.4.0.tar.gz", hash = "sha256:63c3aeab0549750ffe40da79a1b524f60e08a2cbc3126c520ebf2eeaf507f5dd"}, 291 | ] 292 | urllib3 = [ 293 | {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, 294 | {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, 295 | ] 296 | wcwidth = [ 297 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 298 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 299 | ] 300 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "notion-cli-list-manager" 3 | version = "0.1.6" 4 | description = "A simple command-line tool for managing Notion databases." 5 | authors = ["Jack Salici"] 6 | repository = "https://github.com/jacksalici/notion-cli-list-manager" 7 | keywords = ["Notion", "CLI", "todo manager"] 8 | license = "MIT" 9 | readme = "README.md" 10 | documentation = "https://github.com/jacksalici/notion-cli-list-manager/wiki" 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.8" 14 | requests = "^2.26.0" 15 | typer = "^0.4.0" 16 | colorama = "^0.4.4" 17 | toml = "^0.10.2" 18 | prettytable = "^2.5.0" 19 | 20 | 21 | [tool.poetry.scripts] 22 | list = "notion_cli_list_manager.main:app" 23 | 24 | [tool.poetry.dev-dependencies] 25 | pytest = "^5.2" 26 | 27 | [build-system] 28 | requires = ["poetry-core>=1.0.0"] 29 | build-backend = "poetry.core.masonry.api" 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | ##### ⚠️ This project is still in work in progress, please forgive any little flaw here and there. 3 | # Notion CLI List Manager 🗂 4 | A simple command-line tool for managing [Notion](http://notion.so) ___List___ databases. ✨ 5 | 6 | ### Increase your productivity with a simple command. 🛋 7 | 8 | ![](showcase.gif) 9 | 10 | ## 📺 Features: 11 | - fast and clear; saving your idea is as simple as type `add "get money"` 💆‍♂️ 12 | - tables are pretty-printed with fab ASCII tables 🌈 13 | - parameters are now supported [^3] 🎻 14 | 15 | 16 | ## 👾 Get Started: 17 | - Create a new internal api integration [here](https://www.notion.so/my-integrations). 18 | - ❗️ Share the default database you want to use with your integration. 19 | You can copy [my free simple template](https://jacksalici.notion.site/d75c9590dc8b4d62a6c65cbf3fdd1dfb?v=0e3782222f014d7bb3e44a87376e3cfb). 20 | - Download the tool: [^1] 21 | ``` 22 | pip install notion-cli-list-manager 23 | ``` 24 | - Set the token and your default database id: 25 | ``` 26 | list set --token [token] --id [database-id] 27 | ``` 28 | - You're done! 29 | 30 | ## 🧰 Syntax: 31 | TL;DR: `list` is the keyword for activating this tool from the terminal. Typing just `list`, the list of your default database's items will be shown. Other commands can be used typing `list [command]` 32 | 33 | | Commands:| | Args and options:| 34 | |---|---|---| 35 | | `list` | to display all the ___List___ items. | `--db [id] ` to display a specific database. Otherwise the default database will be shown.
`--all` to display all the lists. 36 | | `list add [title]` | to add a new ___List___ item called `title`. | `[title]` will be the text of the ___List___ item (and the title of the associated Notion database page)
`--db [id] ` to add the entry to a specific database. Otherwise, the default database will be used.| 37 | | `list rm [index]` | to remove the ___List___ item with the index `index`.
_(Command to call after `list`)_| `[index]` has to be formatted either like a range or a list, or a combination of these. E.g.: 3,4,6:10:2 will remove pages 3, 4, 6, 8. 38 | | `list db` | to display all the notion display saved in the manager. | `--label [LABEL] --id [ID]` to add a database to the manager. A prompt will then ask you the ordered indexes list.
`--rm [LABEL]` to remove a database named [LABEL] from the manager. Note that adding or removing a database to the manager does not cause the actual creation or deletion on Notion.
`--prop [LABEL]` to set which and in which order display the properties of an already saved database labeled [LABEL]. A prompt will then ask you the ordered indexes list[^3]. 39 | | `list set --token [token] --id [database_id]` | to set the token and the ID of the Notion Database you want as default. _This must be executed as the first command_. | You can get the `[token]` as internal api integration [here](https://www.notion.so/my-integrations).
You can get the database id from the database url: notion.so/[username]/`[database_id]`?v=[view_id].
You can also use separately `--token` and `--id` to set just one parameter. After the `--id` command, a prompt will then ask you the ordered indexes list. | 40 | 41 | ## 🛒 Still to do: 42 | See the [project tab](https://github.com/jacksalici/notion-cli-list-manager/projects/1) for a complete and real-time-updated list. 43 | Issues and PRs are appreciated. 🤝 44 | 45 | 46 | [^3]: At the present, properties are fully supported (except Relations and Rolls up that are __NS__ - Not supported) but read-only. Writeable ones will be supported in the next versions. 47 | 48 | [^1]: You can also clone the repo to have always the very last version. 49 | Having installed Python3 and Pip3 on your machine, write on the terminal: 50 | `git clone https://github.com/jacksalici/notion-cli-list-manager.git notion-cli-list-manager` 51 | `pip3 install notion-cli-list-manager/dist/notion-cli-list-manager-[last-version].tar.gz` 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | typer 3 | colorama 4 | toml 5 | prettytable 6 | -------------------------------------------------------------------------------- /showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/showcase.gif -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksalici/notion-cli-list-manager/68125834f99939680e9c6de4c04183efc51bc31a/test/__init__.py -------------------------------------------------------------------------------- /test/config.toml: -------------------------------------------------------------------------------- 1 | token = "secret_XgNTCXRenJtCIx7h4Ii79gOEtWaFXwc1KNtWFfOcHGX" 2 | 3 | [databases] 4 | [databases.Default] 5 | id = "70df07a1b4cd4208a93533ec30c21298" 6 | 7 | -------------------------------------------------------------------------------- /test/main.py: -------------------------------------------------------------------------------- 1 | import toml 2 | 3 | def toml_parser(): 4 | with open ("test/config.toml", "r") as f: 5 | dict = toml.load(f) 6 | 7 | return dict 8 | 9 | 10 | 11 | 12 | toml_parser() --------------------------------------------------------------------------------