├── .gitignore ├── README.md ├── notion-blog.py ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README.md 2 | 3 | I host [my blog](https://nikvdp.com/post) as a static site powered by Hugo, but got tired of writing in markdown files directly. 4 | 5 | This tool lets you extracts any pages nested under a page/block of your choice in Notion and converts them into Hugo compatible markdown files. 6 | 7 | I use it with a cronjob to automatically post any new or updated blog posts from Notion automatically. 8 | 9 | Proper documentation coming soon. In the meantime, here's how to use it: 10 | 11 | You'll need to retrieve your Notion token by inspecting cookies via Chrome dev tools while browsing [notion.so](https://notion.so). The short version: pick a request in the network tab, check the headers it sent, look for the cookie header, and look for `token_v2=` followed by 150 or so letters and numbers. 12 | 13 | You'll also need to get the block/page ID for the block or page in notion that you'll be nesting your posts under. 14 | 15 | - Then, tell `notion-blog-exporter` how to use them by exporting env vars: 16 | 17 | ```bash 18 | export NOTION_TOKEN="" 19 | 20 | export HUGO_POSTS_FOLDER="" 21 | 22 | export NOTION_ROOT_BLOCK="" 23 | ``` 24 | 25 | - Install deps using [poetry](https://python-poetry.org/) and activate the virtualenv: 26 | 27 | ```bash 28 | poetry install && poetry shell 29 | ``` 30 | 31 | Now you can run it with `python notion-blog.py`. Markdown files will be written to `HUGO_POSTS_FOLDER`, which you can then deploy however you normally deploy your hugo site. 32 | -------------------------------------------------------------------------------- /notion-blog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import click 3 | import re 4 | from dataclasses import dataclass, asdict 5 | import yaml 6 | from datetime import datetime 7 | import os 8 | from notion.client import NotionClient 9 | from notion.block import ( 10 | PageBlock, 11 | ImageBlock, 12 | TextBlock, 13 | CodeBlock, 14 | ImageBlock, 15 | NumberedListBlock, 16 | BulletedListBlock, 17 | QuoteBlock, 18 | HeaderBlock, 19 | SubheaderBlock, 20 | SubsubheaderBlock, 21 | CalloutBlock, 22 | ) 23 | 24 | from textwrap import dedent 25 | from os.path import join as join_path 26 | from typing import List, Optional 27 | import sys 28 | 29 | 30 | # TODO: maybe use puppeteer to grab this going forward? 31 | config = None 32 | 33 | 34 | @dataclass 35 | class GlobalConfigContainer: 36 | notion_token: str 37 | hugo_posts_location: str 38 | notion_client: NotionClient 39 | 40 | 41 | @dataclass 42 | class BlogPost: 43 | title: str 44 | date: datetime 45 | body: str 46 | draft: Optional[bool] = None 47 | last_edited: Optional[datetime] = None 48 | metadata: Optional[any] = None 49 | 50 | 51 | @dataclass 52 | class HugoPost: 53 | title: str 54 | date: datetime 55 | body: str 56 | draft: Optional[bool] = False 57 | aliases: Optional[List[str]] = None 58 | 59 | @classmethod 60 | def from_blog_post(cls, post: BlogPost): 61 | field_map = dict( 62 | title="title", date="date", draft="draft", body="body" 63 | ) 64 | 65 | output = {} 66 | for k, v in asdict(post).items(): 67 | if k in field_map: 68 | output[field_map[k]] = v 69 | 70 | return cls(**output) 71 | 72 | def to_hugo(self) -> str: 73 | front_matter = { 74 | k: v 75 | for k, v in asdict(self).items() 76 | if v is not None and k != "body" 77 | } 78 | # TODO: consider supporting non-yaml frontmatter 79 | as_yaml = yaml.dump(front_matter, default_flow_style=False) 80 | output = f"---\n{as_yaml}\n---\n{self.body}" 81 | return output 82 | 83 | 84 | def to_valid_filename(string): 85 | """Converts a string into something usable as a filename 86 | 87 | Adapted from django's get_valid_filename. 88 | 89 | see: https://github.com/django/django/blob/0382ecfe020b4c51b4c01e4e9a21892771e66941/django/utils/text.py#L221-L232 90 | """ 91 | 92 | string = str(string).strip().replace(" ", "_") 93 | return re.sub(r"(?u)[^-\w.]", "", string) 94 | 95 | 96 | def collect_notion_posts( 97 | source_block_or_page_id: str, 98 | ) -> List[BlogPost]: 99 | """Retrieve the list of published posts from Notion 100 | 101 | Args: 102 | source_block_or_page_id (str): A block or page whose children are the 103 | blog posts you wish to publish. 104 | 105 | Returns: 106 | List[BlogPost]: A list of blog posts 107 | """ 108 | 109 | blog_posts_page = config.notion_client.get_block(source_block_or_page_id) 110 | 111 | notion_posts = [] 112 | for post in blog_posts_page.children: 113 | notion_record = post.get() 114 | notion_posts.append( 115 | BlogPost( 116 | title=post.title, 117 | body=blocks_to_markdown(post.children), 118 | date=datetime.fromtimestamp( 119 | int(notion_record.get("created_time")) / 1000 120 | ), 121 | last_edited=datetime.fromtimestamp( 122 | int(notion_record.get("last_edited_time")) / 1000 123 | ), 124 | ) 125 | ) 126 | 127 | return notion_posts 128 | 129 | 130 | def listblock_to_markdown_handler(block): 131 | prefix = "- " if isinstance(block, BulletedListBlock) else "" 132 | prefix = "1. " if isinstance(block, NumberedListBlock) else prefix 133 | 134 | lines = block.title.split("\n") 135 | output = "" 136 | for idx, line in enumerate(lines): 137 | if idx == 0: 138 | output = f"{output}{prefix}{line}\n" 139 | else: 140 | output = f"{output}{' ' * len(prefix)}{line}\n" 141 | 142 | return f"{output}\n" 143 | 144 | 145 | def blocks_to_markdown(root_block): 146 | markdown_out = "" 147 | for block in root_block: 148 | markdown_out += block_to_markdown(block) or "" 149 | 150 | return markdown_out 151 | 152 | 153 | def block_to_markdown(block): 154 | default_handler = lambda block: f"{block.title}\n" 155 | 156 | handlers = { 157 | QuoteBlock: lambda block: "> " 158 | + "> ".join([f"{x}\n" for x in block.title.split("\n")]) 159 | + "\n", 160 | NumberedListBlock: listblock_to_markdown_handler, 161 | BulletedListBlock: listblock_to_markdown_handler, 162 | HeaderBlock: lambda block: f"\n# {block.title}\n", 163 | SubheaderBlock: lambda block: f"\n## {block.title}\n", 164 | SubsubheaderBlock: lambda block: f"\n### {block.title}\n", 165 | CalloutBlock: lambda block: f"> {block.icon} " 166 | + "> ".join([f"{x}\n" for x in block.title.split("\n")]), 167 | CodeBlock: lambda block: f"\n```{block.language.lower()}\n{block.title}\n```\n", 168 | ImageBlock: lambda block: f"\n `` \n", 169 | } 170 | 171 | return handlers.get(type(block), default_handler)(block) 172 | 173 | 174 | def collect_hugo_posts(): 175 | md_post_files = [ 176 | p 177 | for p in os.listdir(config.hugo_posts_location) 178 | if p.lower().endswith(".md") 179 | ] 180 | 181 | posts = [] 182 | for post_file in md_post_files: 183 | with open(join_path(config.hugo_posts_location, post_file)) as pf: 184 | content = pf.read() 185 | posts.append(parse_post(content)) 186 | return posts 187 | 188 | 189 | def parse_post(content) -> HugoPost: 190 | content = content.split("---") 191 | 192 | front_matter = yaml.safe_load(content[1]) 193 | 194 | # join in case there were any other '---' later in file 195 | body = "\n".join(content[2:]) 196 | 197 | return HugoPost(**front_matter, body=body) 198 | 199 | 200 | class ClickTokenType(click.types.StringParamType): 201 | """helper to make click show NOTION_TOKEN for 202 | the argument type when called with -h/--help""" 203 | 204 | name = "notion_token" 205 | 206 | 207 | @click.command(context_settings=dict(help_option_names=["-h", "--help"])) 208 | @click.option( 209 | "-p", 210 | "--hugo-posts-folder", 211 | prompt="Hugo posts folder", 212 | envvar="HUGO_POSTS_FOLDER", 213 | type=click.Path(exists=True, file_okay=False), 214 | ) 215 | @click.option( 216 | "-n", 217 | "--notion-token", 218 | prompt="Notion token", 219 | type=ClickTokenType(), 220 | envvar="NOTION_TOKEN", 221 | ) 222 | @click.option( 223 | "--notion-root-block", 224 | prompt="Notion root block to collect blog posts from:", 225 | envvar="NOTION_ROOT_BLOCK", 226 | ) 227 | def main(hugo_posts_folder, notion_token, notion_root_block): 228 | 229 | notion_client = NotionClient(token_v2=notion_token) 230 | 231 | global config 232 | config = GlobalConfigContainer( 233 | hugo_posts_location=hugo_posts_folder, 234 | notion_token=notion_token, 235 | notion_client=notion_client, 236 | ) 237 | 238 | # TODO: store notion ids as extra metadata in the hugo frontmatter 239 | # and use them to to intelligently update even if the title has 240 | # changed 241 | hugo_posts = collect_hugo_posts() 242 | hugo_published = [p for p in hugo_posts if not p.draft] 243 | 244 | notion_posts = collect_notion_posts(notion_root_block) 245 | 246 | for blog_post in notion_posts: 247 | 248 | # skip empty blocks 249 | if not blog_post.title: 250 | continue 251 | 252 | hugo_post = HugoPost.from_blog_post(blog_post) 253 | blog_output = hugo_post.to_hugo() 254 | 255 | safe_title = f"{to_valid_filename(hugo_post.title)}.md" 256 | with open(join_path(hugo_posts_folder, safe_title), "w") as fp: 257 | fp.write(blog_output) 258 | 259 | print(f"Wrote '{safe_title}' to '{hugo_posts_folder}'") 260 | 261 | 262 | if __name__ == "__main__": 263 | main() 264 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appnope" 3 | version = "0.1.0" 4 | description = "Disable App Nap on OS X 10.9" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "backcall" 11 | version = "0.2.0" 12 | description = "Specifications for callback functions passed in to an API" 13 | category = "dev" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [[package]] 18 | name = "beautifulsoup4" 19 | version = "4.9.3" 20 | description = "Screen-scraping library" 21 | category = "main" 22 | optional = false 23 | python-versions = "*" 24 | 25 | [package.dependencies] 26 | soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} 27 | 28 | [package.extras] 29 | html5lib = ["html5lib"] 30 | lxml = ["lxml"] 31 | 32 | [[package]] 33 | name = "bs4" 34 | version = "0.0.1" 35 | description = "Dummy package for Beautiful Soup" 36 | category = "main" 37 | optional = false 38 | python-versions = "*" 39 | 40 | [package.dependencies] 41 | beautifulsoup4 = "*" 42 | 43 | [[package]] 44 | name = "cached-property" 45 | version = "1.5.2" 46 | description = "A decorator for caching properties in classes." 47 | category = "main" 48 | optional = false 49 | python-versions = "*" 50 | 51 | [[package]] 52 | name = "certifi" 53 | version = "2020.6.20" 54 | description = "Python package for providing Mozilla's CA Bundle." 55 | category = "main" 56 | optional = false 57 | python-versions = "*" 58 | 59 | [[package]] 60 | name = "chardet" 61 | version = "3.0.4" 62 | description = "Universal encoding detector for Python 2 and 3" 63 | category = "main" 64 | optional = false 65 | python-versions = "*" 66 | 67 | [[package]] 68 | name = "click" 69 | version = "7.1.2" 70 | description = "Composable command line interface toolkit" 71 | category = "main" 72 | optional = false 73 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 74 | 75 | [[package]] 76 | name = "colorama" 77 | version = "0.4.4" 78 | description = "Cross-platform colored terminal text." 79 | category = "dev" 80 | optional = false 81 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 82 | 83 | [[package]] 84 | name = "commonmark" 85 | version = "0.9.1" 86 | description = "Python parser for the CommonMark Markdown spec" 87 | category = "main" 88 | optional = false 89 | python-versions = "*" 90 | 91 | [package.extras] 92 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 93 | 94 | [[package]] 95 | name = "decorator" 96 | version = "4.4.2" 97 | description = "Decorators for Humans" 98 | category = "dev" 99 | optional = false 100 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 101 | 102 | [[package]] 103 | name = "dictdiffer" 104 | version = "0.8.1" 105 | description = "Dictdiffer is a library that helps you to diff and patch dictionaries." 106 | category = "main" 107 | optional = false 108 | python-versions = "*" 109 | 110 | [package.extras] 111 | all = ["Sphinx (>=1.4.4)", "sphinx-rtd-theme (>=0.1.9)", "check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "mock (>=1.3.0)", "pydocstyle (>=1.0.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)", "tox (>=3.7.0)", "numpy (>=1.11.0)"] 112 | docs = ["Sphinx (>=1.4.4)", "sphinx-rtd-theme (>=0.1.9)"] 113 | numpy = ["numpy (>=1.11.0)"] 114 | tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "mock (>=1.3.0)", "pydocstyle (>=1.0.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)", "tox (>=3.7.0)"] 115 | 116 | [[package]] 117 | name = "idna" 118 | version = "2.10" 119 | description = "Internationalized Domain Names in Applications (IDNA)" 120 | category = "main" 121 | optional = false 122 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 123 | 124 | [[package]] 125 | name = "ipdb" 126 | version = "0.13.4" 127 | description = "IPython-enabled pdb" 128 | category = "dev" 129 | optional = false 130 | python-versions = ">=2.7" 131 | 132 | [package.dependencies] 133 | ipython = {version = ">=5.1.0", markers = "python_version >= \"3.4\""} 134 | 135 | [[package]] 136 | name = "ipython" 137 | version = "7.18.1" 138 | description = "IPython: Productive Interactive Computing" 139 | category = "dev" 140 | optional = false 141 | python-versions = ">=3.7" 142 | 143 | [package.dependencies] 144 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 145 | backcall = "*" 146 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 147 | decorator = "*" 148 | jedi = ">=0.10" 149 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 150 | pickleshare = "*" 151 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 152 | pygments = "*" 153 | traitlets = ">=4.2" 154 | 155 | [package.extras] 156 | all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"] 157 | doc = ["Sphinx (>=1.3)"] 158 | kernel = ["ipykernel"] 159 | nbconvert = ["nbconvert"] 160 | nbformat = ["nbformat"] 161 | notebook = ["notebook", "ipywidgets"] 162 | parallel = ["ipyparallel"] 163 | qtconsole = ["qtconsole"] 164 | test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] 165 | 166 | [[package]] 167 | name = "ipython-genutils" 168 | version = "0.2.0" 169 | description = "Vestigial utilities from IPython" 170 | category = "dev" 171 | optional = false 172 | python-versions = "*" 173 | 174 | [[package]] 175 | name = "jedi" 176 | version = "0.17.2" 177 | description = "An autocompletion tool for Python that can be used for text editors." 178 | category = "dev" 179 | optional = false 180 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 181 | 182 | [package.dependencies] 183 | parso = ">=0.7.0,<0.8.0" 184 | 185 | [package.extras] 186 | qa = ["flake8 (==3.7.9)"] 187 | testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] 188 | 189 | [[package]] 190 | name = "notion" 191 | version = "0.0.25" 192 | description = "Unofficial Python API client for Notion.so" 193 | category = "main" 194 | optional = false 195 | python-versions = ">=3.5" 196 | 197 | [package.dependencies] 198 | bs4 = "*" 199 | cached-property = "*" 200 | commonmark = "*" 201 | dictdiffer = "*" 202 | python-slugify = "*" 203 | requests = "*" 204 | tzlocal = "*" 205 | 206 | [[package]] 207 | name = "parso" 208 | version = "0.7.1" 209 | description = "A Python Parser" 210 | category = "dev" 211 | optional = false 212 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 213 | 214 | [package.extras] 215 | testing = ["docopt", "pytest (>=3.0.7)"] 216 | 217 | [[package]] 218 | name = "pexpect" 219 | version = "4.8.0" 220 | description = "Pexpect allows easy control of interactive console applications." 221 | category = "dev" 222 | optional = false 223 | python-versions = "*" 224 | 225 | [package.dependencies] 226 | ptyprocess = ">=0.5" 227 | 228 | [[package]] 229 | name = "pickleshare" 230 | version = "0.7.5" 231 | description = "Tiny 'shelve'-like database with concurrency support" 232 | category = "dev" 233 | optional = false 234 | python-versions = "*" 235 | 236 | [[package]] 237 | name = "prompt-toolkit" 238 | version = "3.0.8" 239 | description = "Library for building powerful interactive command lines in Python" 240 | category = "dev" 241 | optional = false 242 | python-versions = ">=3.6.1" 243 | 244 | [package.dependencies] 245 | wcwidth = "*" 246 | 247 | [[package]] 248 | name = "ptyprocess" 249 | version = "0.6.0" 250 | description = "Run a subprocess in a pseudo terminal" 251 | category = "dev" 252 | optional = false 253 | python-versions = "*" 254 | 255 | [[package]] 256 | name = "pygments" 257 | version = "2.7.1" 258 | description = "Pygments is a syntax highlighting package written in Python." 259 | category = "dev" 260 | optional = false 261 | python-versions = ">=3.5" 262 | 263 | [[package]] 264 | name = "python-slugify" 265 | version = "4.0.1" 266 | description = "A Python Slugify application that handles Unicode" 267 | category = "main" 268 | optional = false 269 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 270 | 271 | [package.dependencies] 272 | text-unidecode = ">=1.3" 273 | 274 | [package.extras] 275 | unidecode = ["Unidecode (>=1.1.1)"] 276 | 277 | [[package]] 278 | name = "pytz" 279 | version = "2020.1" 280 | description = "World timezone definitions, modern and historical" 281 | category = "main" 282 | optional = false 283 | python-versions = "*" 284 | 285 | [[package]] 286 | name = "pyyaml" 287 | version = "5.3.1" 288 | description = "YAML parser and emitter for Python" 289 | category = "main" 290 | optional = false 291 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 292 | 293 | [[package]] 294 | name = "requests" 295 | version = "2.24.0" 296 | description = "Python HTTP for Humans." 297 | category = "main" 298 | optional = false 299 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 300 | 301 | [package.dependencies] 302 | certifi = ">=2017.4.17" 303 | chardet = ">=3.0.2,<4" 304 | idna = ">=2.5,<3" 305 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 306 | 307 | [package.extras] 308 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 309 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 310 | 311 | [[package]] 312 | name = "soupsieve" 313 | version = "2.0.1" 314 | description = "A modern CSS selector implementation for Beautiful Soup." 315 | category = "main" 316 | optional = false 317 | python-versions = ">=3.5" 318 | 319 | [[package]] 320 | name = "text-unidecode" 321 | version = "1.3" 322 | description = "The most basic Text::Unidecode port" 323 | category = "main" 324 | optional = false 325 | python-versions = "*" 326 | 327 | [[package]] 328 | name = "traitlets" 329 | version = "5.0.5" 330 | description = "Traitlets Python configuration system" 331 | category = "dev" 332 | optional = false 333 | python-versions = ">=3.7" 334 | 335 | [package.dependencies] 336 | ipython-genutils = "*" 337 | 338 | [package.extras] 339 | test = ["pytest"] 340 | 341 | [[package]] 342 | name = "tzlocal" 343 | version = "2.1" 344 | description = "tzinfo object for the local timezone" 345 | category = "main" 346 | optional = false 347 | python-versions = "*" 348 | 349 | [package.dependencies] 350 | pytz = "*" 351 | 352 | [[package]] 353 | name = "urllib3" 354 | version = "1.25.11" 355 | description = "HTTP library with thread-safe connection pooling, file post, and more." 356 | category = "main" 357 | optional = false 358 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 359 | 360 | [package.extras] 361 | brotli = ["brotlipy (>=0.6.0)"] 362 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 363 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 364 | 365 | [[package]] 366 | name = "wcwidth" 367 | version = "0.2.5" 368 | description = "Measures the displayed width of unicode strings in a terminal" 369 | category = "dev" 370 | optional = false 371 | python-versions = "*" 372 | 373 | [metadata] 374 | lock-version = "1.1" 375 | python-versions = "^3.8" 376 | content-hash = "ddcb99e7de9bbb3c7bff56af631dd208d3c0ec97e74c7d1bbcc6f1f5a0e5f8c4" 377 | 378 | [metadata.files] 379 | appnope = [ 380 | {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, 381 | {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, 382 | ] 383 | backcall = [ 384 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 385 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 386 | ] 387 | beautifulsoup4 = [ 388 | {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, 389 | {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, 390 | {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, 391 | ] 392 | bs4 = [ 393 | {file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"}, 394 | ] 395 | cached-property = [ 396 | {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, 397 | {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, 398 | ] 399 | certifi = [ 400 | {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, 401 | {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, 402 | ] 403 | chardet = [ 404 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 405 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 406 | ] 407 | click = [ 408 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 409 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 410 | ] 411 | colorama = [ 412 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 413 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 414 | ] 415 | commonmark = [ 416 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 417 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 418 | ] 419 | decorator = [ 420 | {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, 421 | {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, 422 | ] 423 | dictdiffer = [ 424 | {file = "dictdiffer-0.8.1-py2.py3-none-any.whl", hash = "sha256:d79d9a39e459fe33497c858470ca0d2e93cb96621751de06d631856adfd9c390"}, 425 | {file = "dictdiffer-0.8.1.tar.gz", hash = "sha256:1adec0d67cdf6166bda96ae2934ddb5e54433998ceab63c984574d187cc563d2"}, 426 | ] 427 | idna = [ 428 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 429 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 430 | ] 431 | ipdb = [ 432 | {file = "ipdb-0.13.4.tar.gz", hash = "sha256:c85398b5fb82f82399fc38c44fe3532c0dde1754abee727d8f5cfcc74547b334"}, 433 | ] 434 | ipython = [ 435 | {file = "ipython-7.18.1-py3-none-any.whl", hash = "sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8"}, 436 | {file = "ipython-7.18.1.tar.gz", hash = "sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"}, 437 | ] 438 | ipython-genutils = [ 439 | {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, 440 | {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, 441 | ] 442 | jedi = [ 443 | {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, 444 | {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, 445 | ] 446 | notion = [ 447 | {file = "notion-0.0.25-py3-none-any.whl", hash = "sha256:d6ad33fab45fbf31bfe5186a3b0dd50dc88893a252f4ba45f4ddf6a8a467237f"}, 448 | {file = "notion-0.0.25.tar.gz", hash = "sha256:96b1e5ed495b6b0d6ace21fbf49c409d3c46be710d08cecaee12cb364b8d0049"}, 449 | ] 450 | parso = [ 451 | {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, 452 | {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, 453 | ] 454 | pexpect = [ 455 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 456 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 457 | ] 458 | pickleshare = [ 459 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 460 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 461 | ] 462 | prompt-toolkit = [ 463 | {file = "prompt_toolkit-3.0.8-py3-none-any.whl", hash = "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"}, 464 | {file = "prompt_toolkit-3.0.8.tar.gz", hash = "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c"}, 465 | ] 466 | ptyprocess = [ 467 | {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, 468 | {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, 469 | ] 470 | pygments = [ 471 | {file = "Pygments-2.7.1-py3-none-any.whl", hash = "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998"}, 472 | {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"}, 473 | ] 474 | python-slugify = [ 475 | {file = "python-slugify-4.0.1.tar.gz", hash = "sha256:69a517766e00c1268e5bbfc0d010a0a8508de0b18d30ad5a1ff357f8ae724270"}, 476 | ] 477 | pytz = [ 478 | {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, 479 | {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, 480 | ] 481 | pyyaml = [ 482 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 483 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 484 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 485 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 486 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 487 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 488 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 489 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 490 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 491 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 492 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 493 | ] 494 | requests = [ 495 | {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, 496 | {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, 497 | ] 498 | soupsieve = [ 499 | {file = "soupsieve-2.0.1-py3-none-any.whl", hash = "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55"}, 500 | {file = "soupsieve-2.0.1.tar.gz", hash = "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"}, 501 | ] 502 | text-unidecode = [ 503 | {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, 504 | {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, 505 | ] 506 | traitlets = [ 507 | {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, 508 | {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, 509 | ] 510 | tzlocal = [ 511 | {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, 512 | {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, 513 | ] 514 | urllib3 = [ 515 | {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, 516 | {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, 517 | ] 518 | wcwidth = [ 519 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 520 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 521 | ] 522 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "notion-hugo" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Nik V "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | notion = "^0.0.25" 10 | PyYAML = "^5.3.1" 11 | click = "^7.1.2" 12 | 13 | [tool.poetry.dev-dependencies] 14 | ipython = "^7.18.1" 15 | ipdb = "^0.13.4" 16 | 17 | [build-system] 18 | requires = ["poetry>=0.12"] 19 | build-backend = "poetry.masonry.api" 20 | --------------------------------------------------------------------------------