├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .pylintrc
├── CONTRIBUTING.md
├── LICENSE.txt
├── Pipfile
├── Pipfile.lock
├── README.md
├── media
├── folders.png
└── where-to-export.png
├── notion_export_enhancer
├── __init__.py
├── __main__.py
└── enhancer.py
├── setup.py
└── tests
├── __init__.py
├── conftest.py
├── test_files
├── README.md
├── merge_handle
│ ├── test 0123456789abcdef0123456789abcdef.md
│ └── test 0123456789abcdef0123456789abcdef
│ │ └── .git-commit-folder
├── zip_complex.zip
├── zip_complex
│ ├── beep 0123456789abcdef0123456789abcdef.md
│ ├── beep 0123456789abcdef0123456789abcdef
│ │ └── types 11111111111111111111111111111111.md
│ ├── device 00000000000000000000000000000000.md
│ └── something_else.csv
├── zip_simple.zip
└── zip_simple
│ └── test 0123456789abcdef0123456789abcdef.md
└── test_upload.py
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Package Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - owo
7 | pull_request:
8 | branches:
9 | - owo
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | max-parallel: 4
16 | matrix:
17 | python-version: [3.6, 3.7, 3.8]
18 |
19 | steps:
20 | - uses: actions/checkout@v1
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v1
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install pipenv
29 | pipenv install
30 | #Install manually to avoid all other dev deps
31 | - name: Test with pytest
32 | run: |
33 | pipenv install pytest
34 | pipenv run pytest -vv
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # general things to ignore
2 | build/
3 | dist/
4 | *.egg-info/
5 | *.egg
6 | *.py[cod]
7 | __pycache__/
8 | *.so
9 | *~
10 | venv/
11 | venv-ci/
12 | .pytest_cache/
13 | .cache/
14 | .coverage
15 | htmlcov
16 | zip_complex.zip.formatted
17 | zip_simple.zip.formatted
18 | pyproject.toml
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 |
2 | [BASIC]
3 | variable-naming-style=any
4 | function-naming-style=any
5 | argument-naming-style=any
6 | attr-naming-style=any
7 | method-naming-style=any
8 |
9 | [LOGGING]
10 | logging-format-style=new
11 |
12 | [STRING]
13 |
14 | # This flag controls whether inconsistent-quotes generates a warning when the
15 | # character used as a quote delimiter is used inconsistently within a module.
16 | check-quote-consistency=no
17 |
18 | # This flag controls whether the implicit-str-concat should generate a warning
19 | # on implicit string concatenation in sequences defined over several lines.
20 | check-str-concat-over-line-jumps=no
21 |
22 |
23 | [FORMAT]
24 | indent-string=' '
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Here's how to run all the development stuff.
4 |
5 | ## Setup Development Environment
6 | * `pyenv global 3.6.8-amd64`
7 | * `pipenv install --dev`
8 |
9 | ## Testing
10 | * TODO:!
11 | * `pytest -v` in the root directory
12 | * To test coverage run `pipenv run coverage run -m pytest -v`
13 | * Then run `pipenv run coverage report` or `pipenv run coverage html` and browser the coverage (TODO: Figure out a way to make a badge for this??)
14 |
15 | ## Releasing
16 | Refer to [the python docs on packaging for clarification](https://packaging.python.org/tutorials/packaging-projects/).
17 | * Make sure you've updated `setup.py`
18 | * `python setup.py sdist bdist_wheel` - Create a source distribution and a binary wheel distribution into `dist/`
19 | * `twine upload dist/notion_export_enhancer-x.x.x*` - Upload all `dist/` files to PyPI of a given version
20 | * Make sure to tag the commit you released!
21 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2020 Samantha Fornari / Cobertos
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | pytest = "*"
8 | twine = "*"
9 | setuptools = "*"
10 | wheel = "*"
11 | coverage = "*"
12 | colorama = "*"
13 |
14 | [packages]
15 | notion_export_enhancer = {editable = true, path = "."}
16 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "f00c2141c5a043ee093a5a3b065374606565e3c9415dd41170a15c9f7b46eaa0"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {},
8 | "sources": [
9 | {
10 | "name": "pypi",
11 | "url": "https://pypi.org/simple",
12 | "verify_ssl": true
13 | }
14 | ]
15 | },
16 | "default": {
17 | "backoff": {
18 | "hashes": [
19 | "sha256:48373089ccc81b094281884e48305cbf879b4fc7a80ab7c6de2011e2f741267b",
20 | "sha256:dfaca81d40554e271a53a5f0e7c920100c948d38f5448600de0eab551fcd4fed"
21 | ],
22 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
23 | "version": "==1.11.0"
24 | },
25 | "beautifulsoup4": {
26 | "hashes": [
27 | "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35",
28 | "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25",
29 | "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"
30 | ],
31 | "version": "==4.9.3"
32 | },
33 | "bs4": {
34 | "hashes": [
35 | "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"
36 | ],
37 | "version": "==0.0.1"
38 | },
39 | "cached-property": {
40 | "hashes": [
41 | "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130",
42 | "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"
43 | ],
44 | "version": "==1.5.2"
45 | },
46 | "certifi": {
47 | "hashes": [
48 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
49 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
50 | ],
51 | "version": "==2021.5.30"
52 | },
53 | "charset-normalizer": {
54 | "hashes": [
55 | "sha256:ad0da505736fc7e716a8da15bf19a985db21ac6415c26b34d2fafd3beb3d927e",
56 | "sha256:b68b38179052975093d71c1b5361bf64afd80484697c1f27056e50593e695ceb"
57 | ],
58 | "markers": "python_version >= '3'",
59 | "version": "==2.0.1"
60 | },
61 | "commonmark": {
62 | "hashes": [
63 | "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60",
64 | "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"
65 | ],
66 | "version": "==0.9.1"
67 | },
68 | "dictdiffer": {
69 | "hashes": [
70 | "sha256:1adec0d67cdf6166bda96ae2934ddb5e54433998ceab63c984574d187cc563d2",
71 | "sha256:d79d9a39e459fe33497c858470ca0d2e93cb96621751de06d631856adfd9c390"
72 | ],
73 | "version": "==0.8.1"
74 | },
75 | "emoji-extractor": {
76 | "hashes": [
77 | "sha256:23b0ca51602b15b6d3d090d76be0ee0ad95f28d7e95ed53e58da9b9e589293df"
78 | ],
79 | "version": "==1.0.19"
80 | },
81 | "idna": {
82 | "hashes": [
83 | "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
84 | "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
85 | ],
86 | "markers": "python_version >= '3'",
87 | "version": "==3.2"
88 | },
89 | "notion": {
90 | "git": "https://github.com/Cobertos/notion-py.git",
91 | "ref": "c4e67e5b2d8efeedbbc9c48d6947bab3cabca1c5"
92 | },
93 | "notion-export-enhancer": {
94 | "editable": true,
95 | "path": "."
96 | },
97 | "python-slugify": {
98 | "hashes": [
99 | "sha256:6d8c5df75cd4a7c3a2d21e257633de53f52ab0265cd2d1dc62a730e8194a7380",
100 | "sha256:f13383a0b9fcbe649a1892b9c8eb4f8eab1d6d84b84bb7a624317afa98159cab"
101 | ],
102 | "markers": "python_version >= '3.6'",
103 | "version": "==5.0.2"
104 | },
105 | "pytz": {
106 | "hashes": [
107 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
108 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
109 | ],
110 | "version": "==2021.1"
111 | },
112 | "requests": {
113 | "hashes": [
114 | "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
115 | "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
116 | ],
117 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
118 | "version": "==2.26.0"
119 | },
120 | "soupsieve": {
121 | "hashes": [
122 | "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc",
123 | "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"
124 | ],
125 | "markers": "python_version >= '3.0'",
126 | "version": "==2.2.1"
127 | },
128 | "text-unidecode": {
129 | "hashes": [
130 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
131 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
132 | ],
133 | "version": "==1.3"
134 | },
135 | "tzlocal": {
136 | "hashes": [
137 | "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44",
138 | "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"
139 | ],
140 | "version": "==2.1"
141 | },
142 | "urllib3": {
143 | "hashes": [
144 | "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
145 | "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
146 | ],
147 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
148 | "version": "==1.26.6"
149 | }
150 | },
151 | "develop": {
152 | "attrs": {
153 | "hashes": [
154 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
155 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
156 | ],
157 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
158 | "version": "==21.2.0"
159 | },
160 | "bleach": {
161 | "hashes": [
162 | "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125",
163 | "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433"
164 | ],
165 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
166 | "version": "==3.3.0"
167 | },
168 | "certifi": {
169 | "hashes": [
170 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
171 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
172 | ],
173 | "version": "==2021.5.30"
174 | },
175 | "cffi": {
176 | "hashes": [
177 | "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d",
178 | "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771",
179 | "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872",
180 | "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c",
181 | "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc",
182 | "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762",
183 | "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202",
184 | "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5",
185 | "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548",
186 | "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a",
187 | "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f",
188 | "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20",
189 | "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218",
190 | "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c",
191 | "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e",
192 | "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56",
193 | "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224",
194 | "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a",
195 | "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2",
196 | "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a",
197 | "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819",
198 | "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346",
199 | "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b",
200 | "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e",
201 | "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534",
202 | "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb",
203 | "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0",
204 | "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156",
205 | "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd",
206 | "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87",
207 | "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc",
208 | "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195",
209 | "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33",
210 | "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f",
211 | "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d",
212 | "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd",
213 | "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728",
214 | "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7",
215 | "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca",
216 | "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99",
217 | "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf",
218 | "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e",
219 | "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c",
220 | "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5",
221 | "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"
222 | ],
223 | "version": "==1.14.6"
224 | },
225 | "charset-normalizer": {
226 | "hashes": [
227 | "sha256:ad0da505736fc7e716a8da15bf19a985db21ac6415c26b34d2fafd3beb3d927e",
228 | "sha256:b68b38179052975093d71c1b5361bf64afd80484697c1f27056e50593e695ceb"
229 | ],
230 | "markers": "python_version >= '3'",
231 | "version": "==2.0.1"
232 | },
233 | "colorama": {
234 | "hashes": [
235 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
236 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
237 | ],
238 | "index": "pypi",
239 | "version": "==0.4.4"
240 | },
241 | "coverage": {
242 | "hashes": [
243 | "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
244 | "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
245 | "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
246 | "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
247 | "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
248 | "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
249 | "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
250 | "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
251 | "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
252 | "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
253 | "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
254 | "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
255 | "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
256 | "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
257 | "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
258 | "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
259 | "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
260 | "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
261 | "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
262 | "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
263 | "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
264 | "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
265 | "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
266 | "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
267 | "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
268 | "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
269 | "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
270 | "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
271 | "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
272 | "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
273 | "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
274 | "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
275 | "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
276 | "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
277 | "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
278 | "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
279 | "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
280 | "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
281 | "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
282 | "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
283 | "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
284 | "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
285 | "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
286 | "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
287 | "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
288 | "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
289 | "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
290 | "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
291 | "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
292 | "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
293 | "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
294 | "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
295 | ],
296 | "index": "pypi",
297 | "version": "==5.5"
298 | },
299 | "cryptography": {
300 | "hashes": [
301 | "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
302 | "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
303 | "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
304 | "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
305 | "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
306 | "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
307 | "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
308 | "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
309 | "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
310 | "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
311 | "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
312 | "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
313 | ],
314 | "markers": "python_version >= '3.6'",
315 | "version": "==3.4.7"
316 | },
317 | "docutils": {
318 | "hashes": [
319 | "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
320 | "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
321 | ],
322 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
323 | "version": "==0.17.1"
324 | },
325 | "idna": {
326 | "hashes": [
327 | "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
328 | "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
329 | ],
330 | "markers": "python_version >= '3'",
331 | "version": "==3.2"
332 | },
333 | "importlib-metadata": {
334 | "hashes": [
335 | "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac",
336 | "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"
337 | ],
338 | "markers": "python_version >= '3.6'",
339 | "version": "==4.6.1"
340 | },
341 | "iniconfig": {
342 | "hashes": [
343 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
344 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
345 | ],
346 | "version": "==1.1.1"
347 | },
348 | "jeepney": {
349 | "hashes": [
350 | "sha256:1237cd64c8f7ac3aa4b3f332c4d0fb4a8216f39eaa662ec904302d4d77de5a54",
351 | "sha256:71335e7a4e93817982f473f3507bffc2eff7a544119ab9b73e089c8ba1409ba3"
352 | ],
353 | "markers": "sys_platform == 'linux'",
354 | "version": "==0.7.0"
355 | },
356 | "keyring": {
357 | "hashes": [
358 | "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8",
359 | "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48"
360 | ],
361 | "markers": "python_version >= '3.6'",
362 | "version": "==23.0.1"
363 | },
364 | "packaging": {
365 | "hashes": [
366 | "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
367 | "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
368 | ],
369 | "markers": "python_version >= '3.6'",
370 | "version": "==21.0"
371 | },
372 | "pkginfo": {
373 | "hashes": [
374 | "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779",
375 | "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"
376 | ],
377 | "version": "==1.7.1"
378 | },
379 | "pluggy": {
380 | "hashes": [
381 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
382 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
383 | ],
384 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
385 | "version": "==0.13.1"
386 | },
387 | "py": {
388 | "hashes": [
389 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
390 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
391 | ],
392 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
393 | "version": "==1.10.0"
394 | },
395 | "pycparser": {
396 | "hashes": [
397 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
398 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
399 | ],
400 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
401 | "version": "==2.20"
402 | },
403 | "pygments": {
404 | "hashes": [
405 | "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
406 | "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
407 | ],
408 | "markers": "python_version >= '3.5'",
409 | "version": "==2.9.0"
410 | },
411 | "pyparsing": {
412 | "hashes": [
413 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
414 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
415 | ],
416 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
417 | "version": "==2.4.7"
418 | },
419 | "pytest": {
420 | "hashes": [
421 | "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
422 | "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
423 | ],
424 | "index": "pypi",
425 | "version": "==6.2.4"
426 | },
427 | "readme-renderer": {
428 | "hashes": [
429 | "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c",
430 | "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"
431 | ],
432 | "version": "==29.0"
433 | },
434 | "requests": {
435 | "hashes": [
436 | "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
437 | "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
438 | ],
439 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
440 | "version": "==2.26.0"
441 | },
442 | "requests-toolbelt": {
443 | "hashes": [
444 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
445 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
446 | ],
447 | "version": "==0.9.1"
448 | },
449 | "rfc3986": {
450 | "hashes": [
451 | "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
452 | "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
453 | ],
454 | "version": "==1.5.0"
455 | },
456 | "secretstorage": {
457 | "hashes": [
458 | "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f",
459 | "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"
460 | ],
461 | "markers": "sys_platform == 'linux'",
462 | "version": "==3.3.1"
463 | },
464 | "six": {
465 | "hashes": [
466 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
467 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
468 | ],
469 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
470 | "version": "==1.16.0"
471 | },
472 | "toml": {
473 | "hashes": [
474 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
475 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
476 | ],
477 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
478 | "version": "==0.10.2"
479 | },
480 | "tqdm": {
481 | "hashes": [
482 | "sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64",
483 | "sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a"
484 | ],
485 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
486 | "version": "==4.61.2"
487 | },
488 | "twine": {
489 | "hashes": [
490 | "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83",
491 | "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b"
492 | ],
493 | "index": "pypi",
494 | "version": "==3.4.1"
495 | },
496 | "urllib3": {
497 | "hashes": [
498 | "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
499 | "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
500 | ],
501 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
502 | "version": "==1.26.6"
503 | },
504 | "webencodings": {
505 | "hashes": [
506 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
507 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
508 | ],
509 | "version": "==0.5.1"
510 | },
511 | "wheel": {
512 | "hashes": [
513 | "sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e",
514 | "sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e"
515 | ],
516 | "index": "pypi",
517 | "version": "==0.36.2"
518 | },
519 | "zipp": {
520 | "hashes": [
521 | "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
522 | "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
523 | ],
524 | "markers": "python_version >= '3.6'",
525 | "version": "==3.5.0"
526 | }
527 | }
528 | }
529 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # Notion Export Enhancer
9 |
10 | Takes a [Notion.so](https://notion.so) export .zip and enhances it by:
11 |
12 | * Removing all Notion IDs from the end of folders and files
13 | * Adds Unicode Emoji to start of folder/file names if it was in your Notion notes
14 | * Retruncates note titles to 200 characters instead of 50
15 | * Applies Notion's modification time to the file data itself
16 | * Moves root md files into the folder with their name, giving them a name like `!index.md` instead so they sort to the top.
17 |
18 |
19 |
20 |
21 |
22 | TODO:
23 | * Remove empty notes (ones with only links)?
24 | * Rewrite csv + md tables into md tables where appropriate?
25 | * .exe instead of .py?
26 | * Image captions should become MD alt image text, not a separate paragraph
27 | * Would require exporting everything ourselves, paragraph after image is ambiguous
28 |
29 | Supports Python 3.6+
30 |
31 | ## Usage from CLI
32 |
33 | * Export your notion workspace
34 | * You can export a single workspace from `Settings > [Workspace] Settings > Export Content > Export all workspace content`
35 |
36 |
37 |
38 |
39 |
40 | * Choose export option `"Markdown & CSV"`
41 | * `pip install notion_export_enhancer`
42 | * Then run like `python -m notion_export_enhancer [token_v2] [path_to_zip]`
43 | * `token_v2` is your Notion.so token, which can be obtained by inspecting your browser cookies on a logged-in (non-guest) session on Notion.so
44 |
45 | There are also some configuration options:
46 |
47 | * `--output-path`: Optionally set an output path, otherwise uses the current working directory
48 | * `--remove-title`: Removes the title that Notion adds. H1s at the top of every file (default false)
49 | * `--rewrite-paths`: Rewrite the paths in the Markdown files themselves to match file renaming (default true)
50 |
51 | ## Contributing
52 | See [CONTRIBUTING.md](https://github.com/Cobertos/notion_export_enhancer/blob/master/CONTRIBUTING.md)
53 |
--------------------------------------------------------------------------------
/media/folders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/notion_export_enhancer/07a34d4b3daeb1ec69cd4253c089ba4d9dc5bbc3/media/folders.png
--------------------------------------------------------------------------------
/media/where-to-export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/notion_export_enhancer/07a34d4b3daeb1ec69cd4253c089ba4d9dc5bbc3/media/where-to-export.png
--------------------------------------------------------------------------------
/notion_export_enhancer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/notion_export_enhancer/07a34d4b3daeb1ec69cd4253c089ba4d9dc5bbc3/notion_export_enhancer/__init__.py
--------------------------------------------------------------------------------
/notion_export_enhancer/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from .enhancer import cli
3 |
4 | if __name__ == "__main__":
5 | cli(sys.argv[1:])
--------------------------------------------------------------------------------
/notion_export_enhancer/enhancer.py:
--------------------------------------------------------------------------------
1 | """
2 | Takes a [Notion.so](https://notion.so) export .zip and enhances it
3 | """
4 |
5 | import tempfile
6 | import sys
7 | import os
8 | import time
9 | import re
10 | import argparse
11 | import zipfile
12 | import urllib.parse
13 | from datetime import datetime
14 | from pathlib import Path
15 | import backoff
16 | import requests
17 | from emoji_extractor.extract import Extractor as EmojiExtractor
18 | from notion.client import NotionClient
19 | from notion.block import PageBlock
20 |
21 | def noteNameRewrite(nCl, originalNameNoExt):
22 | """
23 | Takes original name (with no extension) and renames it using the Notion ID
24 | and data from Notion itself
25 | * Removes the Notion ID
26 | * Looks up the Notion ID for it's icon, and appends if we can find it
27 | """
28 | match = re.search(r"(.+?) ([0-9a-f]{32})$", originalNameNoExt)
29 | if not match:
30 | return (None, None, None)
31 |
32 | notionId = match[2]
33 |
34 | # Query notion for the ID
35 | #print(f"Fetching Notion ID '{notionId}' for '{originalNameNoExt}'")
36 | try:
37 | pageBlock = nCl.get_block(notionId)
38 | except requests.exceptions.HTTPError:
39 | print(f"Failed to retrieve ID {notionId}")
40 | return (None, None, None)
41 |
42 | # The ID might not be a PageBlock (like when a note with no child PageBlocks
43 | # has an image in it, generating a folder, Notion uses the ID of the first
44 | # ImageBlock, maybe a bug on Notion's end? lol)
45 | if not isinstance(pageBlock, PageBlock):
46 | print(f"Block at ID {notionId}, was not PageBlock. Was {type(pageBlock).__name__}")
47 | if hasattr(pageBlock, 'parent') and pageBlock.parent is not None:
48 | # Try traversing up the parents for the first page
49 | while hasattr(pageBlock, 'parent') and not isinstance(pageBlock, PageBlock):
50 | pageBlock = pageBlock.parent
51 | if isinstance(pageBlock, PageBlock):
52 | print(f"Using some .parent as PageBlock")
53 | elif hasattr(pageBlock, 'children') and pageBlock.children is not None:
54 | # Try to find a PageBlock in the children, but only use if one single one exists
55 | pageBlockChildren = [c for c in pageBlock.children if isinstance(c, PageBlock)]
56 | if len(pageBlockChildren) != 1:
57 | print(f"Ambiguous .children, contained {len(pageBlockChildren)} chlidren PageBlocks")
58 | else:
59 | print(f"Using .children[0] as PageBlock")
60 | pageBlock = pageBlockChildren[0]
61 |
62 | if not isinstance(pageBlock, PageBlock):
63 | print(f"Failed to retrieve PageBlock for ID {notionId}")
64 | return (None, None, None)
65 |
66 |
67 | #print(f"Found parent '{type(pageBlock).__name__}' instead")
68 |
69 | # Check for name truncation
70 | newName = match[1]
71 | if len(match[1]) == 50:
72 | # Use full name instead, invalids replaced with " ", like the normal export
73 | # TODO: These are just Windows reserved characters
74 | # TODO: 200 was just a value to stop Windows from complaining
75 | newName = re.sub(r"[\\/?:*\"<>|]", " ", pageBlock.title)
76 | if len(newName) > 200:
77 | print(f"'{newName}' too long, truncating to 200")
78 | newName = newName[0:200]
79 |
80 | # Add icon to the front if it's there and usable
81 | icon = pageBlock.icon
82 | if icon and EmojiExtractor().big_regex.match(icon): # A full match of a single emoji, might be None or an https://aws.amazon uploaded icon
83 | newName = f"{icon} {newName}"
84 |
85 | # Also get the times to set the file to
86 | createdTime = datetime.fromtimestamp(int(pageBlock._get_record_data()["created_time"])/1000)
87 | lastEditedTime = datetime.fromtimestamp(int(pageBlock._get_record_data()["last_edited_time"])/1000)
88 |
89 | return (newName, createdTime, lastEditedTime)
90 |
91 | class NotionExportRenamer:
92 | """
93 | Holds state information for renaming a single Notion.so export. Allows it to avoid
94 | naming collisions and store other state
95 | """
96 | def __init__(self, notionClient, rootPath):
97 | self.notionClient = notionClient
98 | self.rootPath = rootPath
99 | # Dict containing all the paths we've renamed and what they were renamed to
100 | # (plus createdtime and lastEditedTime). Strings with relative directories to
101 | # rootPath mapped to 3 tuples returned from noteNameRewrite
102 | self._renameCache = {}
103 | # Dict containing keys where it is an unrenamed path with the last part being
104 | # renamed mapped to True. Used to see if other files in the folder might
105 | # have the same name and to act accordingly
106 | self._collisionCache = {}
107 |
108 | def renameAndTimesWithNotion(self, pathToRename):
109 | """
110 | Takes an original on file-system path and rewrites _just the basename_. It
111 | collects rename operations for speed and collision prevention (as some renames
112 | will cause the same name to occur)
113 | @param {string} realPath The path to rename the basename of. Must point to an
114 | actual unrenamed file/folder on disk rooted at self.rootPath so we can scan around it
115 | @returns {tuple} 3 tuple of new name, created time and modified time
116 | """
117 | if pathToRename in self._renameCache:
118 | return self._renameCache[pathToRename]
119 |
120 | path, name = os.path.split(pathToRename)
121 | nameNoExt, ext = os.path.splitext(name)
122 | newNameNoExt, createdTime, lastEditedTime = noteNameRewrite(self.notionClient, nameNoExt)
123 | if not newNameNoExt: # No rename happened, probably no ID in the name or not an .md file
124 | self._renameCache[pathToRename] = (name, None, None)
125 | else:
126 | # Merge files into folders in path at same name if that folder exists
127 | if ext == '.md':
128 | p = Path(os.path.join(self.rootPath, path, nameNoExt))
129 | if p.exists() and p.is_dir():
130 | # NOTE: newNameNoExt can contain a '/' for path joining later!
131 | newNameNoExt = os.path.join(newNameNoExt, "!index")
132 |
133 | # Check to see if name collides
134 | if os.path.join(path, newNameNoExt) in self._collisionCache:
135 | # If it does, try progressive (i) until a new one is found
136 | i = 1
137 | collidingNameNoExt = newNameNoExt
138 | while os.path.join(path, newNameNoExt) in self._collisionCache:
139 | newNameNoExt = f"{collidingNameNoExt} ({i})"
140 | i += 1
141 |
142 | self._renameCache[pathToRename] = (f"{newNameNoExt}{ext}", createdTime, lastEditedTime)
143 | self._collisionCache[os.path.join(path, newNameNoExt)] = True
144 |
145 | return self._renameCache[pathToRename]
146 |
147 | def renameWithNotion(self, pathToRename):
148 | """
149 | Takes an original on file-system path and rewrites _just the basename_. It
150 | collects rename operations for speed and collision prevention (as some renames
151 | will cause the same name to occur)
152 | @param {string} pathToRename The path to rename the basename of. Must point to an
153 | actual unrenamed file/folder on disk rooted at self.rootPath so we can scan around it
154 | @returns {string} The new name
155 | """
156 | return self.renameAndTimesWithNotion(pathToRename)[0]
157 |
158 | def renamePathWithNotion(self, pathToRename):
159 | """
160 | Renames all parts of a path
161 | @param {string} pathToRename A real path on disk to a file or folder root at
162 | self.rootPath. All pieces of the path will be renamed
163 | """
164 | pathToRenameSplit = re.split(r"[\\/]", pathToRename)
165 | paths = [os.path.join(*pathToRenameSplit[0:rpc + 1]) for rpc in range(len(pathToRenameSplit))]
166 | return os.path.join(*[self.renameWithNotion(rp) for rp in paths])
167 |
168 | def renamePathAndTimesWithNotion(self, pathToRename):
169 | """
170 | Renames all parts of a path and return the created and lastEditedTime for the last
171 | part of the path (the file)
172 | @param {string} pathToRename A real path on disk to a file or folder root at
173 | self.rootPath. All pieces of the path will be renamed
174 | """
175 | newPath = self.renamePathWithNotion(os.path.dirname(pathToRename))
176 | newName, createdTime, lastEditedTime = self.renameAndTimesWithNotion(pathToRename)
177 | return (os.path.join(newPath, newName), createdTime, lastEditedTime)
178 |
179 | def mdFileRewrite(renamer, mdFilePath, mdFileContents=None, removeTopH1=False, rewritePaths=False):
180 | """
181 | Takes a Notion exported md file and rewrites parts of it
182 | @param {string} mdFilePath String to the markdown file that's being editted, rooted at
183 | self.rootPath
184 | @param {string} [mdFileContents=None] The contents of the markdown file, if not provided
185 | we will read it manually
186 | @param {boolean} [removeTopH1=False] Remove the title on the first line of the MD file?
187 | @param {boolean} [rewritePaths=False] Rewrite the relative paths in the MD file (images and links)
188 | using Notion file name rewriting
189 | """
190 | if not mdFileContents:
191 | raise NotImplementedError("TODO: Not passing mdFileContents is not implemented... please pass it ;w;")
192 |
193 | newMDFileContents = mdFileContents
194 | if removeTopH1:
195 | lines = mdFileContents.split("\n")
196 | newMDFileContents = "\n".join(lines[1:])
197 |
198 | if rewritePaths:
199 | # Notion link/images use relative paths to other notes, which we can't known without
200 | # consulting the file tree and renaming (to handle duplicates and such)
201 | # Notion links are also URL encoded
202 | # Can't use finditer because we modify the string each time...
203 | searchStartIndex = 0
204 | while True:
205 | m = re.search(r"!?\[.+?\]\(([\w\d\-._~:/?=#%\]\[@!$&'\(\)*+,;]+?)\)", newMDFileContents[searchStartIndex:])
206 | if not m:
207 | break
208 |
209 | if re.search(r":/", m.group(1)):
210 | searchStartIndex = searchStartIndex + m.end(1)
211 | continue # Not a local file path
212 | relTargetFilePath = urllib.parse.unquote(m.group(1))
213 |
214 | # Convert the current MD file path and link target path to the renamed version
215 | # (also taking into account potentially mdFilePath renames moving the directory)
216 | mdDirPath = os.path.dirname(mdFilePath)
217 | newTargetFilePath = renamer.renamePathWithNotion(os.path.join(mdDirPath, relTargetFilePath))
218 | newMDDirPath = os.path.dirname(renamer.renamePathWithNotion(mdFilePath))
219 | # Find the relative path to the newly converted paths for both files
220 | newRelTargetFilePath = os.path.relpath(newTargetFilePath, newMDDirPath)
221 | # Convert back to the way markdown expects the link to be
222 | newRelTargetFilePath = re.sub(r"\\", "/", newRelTargetFilePath)
223 | newRelTargetFilePath = urllib.parse.quote(newRelTargetFilePath)
224 |
225 | # Replace the path in the original string with the new relative renamed
226 | # target path
227 | newMDFileContents = newMDFileContents[0:m.start(1) + searchStartIndex] + newRelTargetFilePath + newMDFileContents[m.end(1) + searchStartIndex:]
228 | searchStartIndex = searchStartIndex + m.start(1) + len(newRelTargetFilePath)
229 |
230 | return newMDFileContents
231 |
232 | def rewriteNotionZip(notionClient, zipPath, outputPath=".", removeTopH1=False, rewritePaths=True):
233 | """
234 | Takes a Notion .zip and prettifies the whole thing
235 | * Removes all Notion IDs from end of names, folders and files
236 | * Add icon to the start of folder/file name if Unicode character
237 | * For files had content in Notion, move them inside the folder, and set the
238 | name to something that will sort to the top
239 | * Fix links inside of files
240 | * Optionally remove titles at the tops of files
241 |
242 | @param {NotionClient} notionClient The NotionClient to use to query Notion with
243 | @param {string} zipPath The path to the Notion zip
244 | @param {string} [outputPath="."] Optional output path, otherwise will use cwd
245 | @param {boolean} [removeTopH1=False] To remove titles at the top of all the md files
246 | @param {boolean} [rewritePaths=True] To rewrite all the links and images in the Markdown files too
247 | @returns {string} Path to the output zip file
248 | """
249 | with tempfile.TemporaryDirectory() as tmpDir:
250 | # Unpack the whole thing first (probably faster than traversing it zipped, like with tar files)
251 | print(f"Extracting '{zipPath}' temporarily...")
252 | with zipfile.ZipFile(zipPath) as zf:
253 | zf.extractall(tmpDir)
254 |
255 | # Make new zip to begin filling
256 | zipName = os.path.basename(zipPath)
257 | newZipName = f"{zipName}.formatted"
258 | newZipPath = os.path.join(outputPath, newZipName)
259 | with zipfile.ZipFile(newZipPath, 'w', zipfile.ZIP_DEFLATED) as zf:
260 |
261 | #Traverse over the files, renaming, modifying, and rewriting back to the zip
262 | renamer = NotionExportRenamer(notionClient, tmpDir)
263 | for tmpWalkDir, dirs, files in os.walk(tmpDir):
264 | walkDir = os.path.relpath(tmpWalkDir, tmpDir)
265 | for name in files:
266 | realPath = os.path.join(tmpWalkDir, name)
267 | relPath = os.path.join("" if walkDir == "." else walkDir, name) # Prevent paths starting with .\\ which, when written to the tar, do annoying things
268 | # print(f"Reading '{root}' '{name}'")
269 |
270 | # Rewrite the current path and get the times from Notion
271 | print("---")
272 | print(f"Working on '{relPath}'")
273 | newPath, createdTime, lastEditedTime = renamer.renamePathAndTimesWithNotion(relPath)
274 |
275 | if os.path.splitext(name)[1] == ".md":
276 | # Grab the data from the file if md file
277 | with open(realPath, "r", encoding='utf-8') as f:
278 | mdFileData = f.read()
279 | mdFileData = mdFileRewrite(renamer, relPath, mdFileContents=mdFileData, removeTopH1=removeTopH1, rewritePaths=rewritePaths)
280 |
281 | print(f"Writing as '{newPath}' with time '{lastEditedTime}'")
282 | zi = zipfile.ZipInfo(newPath, lastEditedTime.timetuple())
283 | zf.writestr(zi, mdFileData)
284 | else:
285 | print(f"Writing as '{newPath}' with time from original export (not an .md file)")
286 | zf.write(realPath, newPath)
287 | return newZipPath
288 |
289 |
290 | def cli(argv):
291 | """
292 | CLI entrypoint, takes CLI arguments array
293 | """
294 | parser = argparse.ArgumentParser(description='Prettifies Notion .zip exports')
295 | parser.add_argument('token_v2', type=str,
296 | help='the token for your Notion.so session')
297 | parser.add_argument('zip_path', type=str,
298 | help='the path to the Notion exported .zip file')
299 | parser.add_argument('--output-path', action='store', type=str, default=".",
300 | help='The path to output to, defaults to cwd')
301 | parser.add_argument('--remove-title', action='store_true',
302 | help='Removes the title that Notion adds. H1s at the top of every file')
303 | parser.add_argument('--rewrite-paths', action='store_false', default=True,
304 | help='Rewrite the paths in the Markdown files themselves to match file renaming')
305 | args = parser.parse_args(argv)
306 |
307 | startTime = time.time()
308 | nCl = NotionClient(token_v2=args.token_v2)
309 | nCl.get_block = backoff.on_exception(backoff.expo,
310 | requests.exceptions.HTTPError,
311 | max_tries=5,
312 | )(nCl.get_block)
313 |
314 | outFileName = rewriteNotionZip(nCl, args.zip_path, outputPath=args.output_path,
315 | removeTopH1=args.remove_title, rewritePaths=args.rewrite_paths)
316 | print("--- Finished in %s seconds ---" % (time.time() - startTime))
317 | print(f"Output file written as '{outFileName}'")
318 |
319 | if __name__ == "__main__":
320 | cli(sys.argv[1:])
321 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='notion_export_enhancer',
5 | version='0.0.7',
6 | description='Export and _enhance_, takes Notion\'s export and makes it just a bit more usable.',
7 | long_description=open('README.md', 'r').read(),
8 | long_description_content_type="text/markdown",
9 | url='https://github.com/Cobertos/notion_export_enhancer/',
10 | author='Cobertos',
11 | author_email='me+python@cobertos.com',
12 | license='MIT',
13 | classifiers=[
14 | 'Development Status :: 4 - Beta',
15 | 'Intended Audience :: Developers',
16 | 'License :: OSI Approved :: MIT License',
17 | 'Programming Language :: Python :: 3.6',
18 | 'Programming Language :: Python :: 3.7',
19 | 'Programming Language :: Python :: 3.8',
20 | 'Topic :: Office/Business :: News/Diary',
21 | 'Topic :: System :: Filesystems',
22 | 'Topic :: Text Processing :: Markup :: Markdown',
23 | 'Topic :: Utilities'
24 | ],
25 | install_requires=[
26 | 'backoff>=1.11.0',
27 | 'emoji_extractor>=1.0.19',
28 | 'notion-cobertos-fork>=0.0.29',
29 | ],
30 | keywords='notion notion.so notion-py markdown md export enhance enhancer',
31 | packages=['notion_export_enhancer']
32 | )
33 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/notion_export_enhancer/07a34d4b3daeb1ec69cd4253c089ba4d9dc5bbc3/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | def pytest_generate_tests(metafunc):
2 | if "headerLevel" in metafunc.fixturenames:
3 | metafunc.parametrize("headerLevel", map(lambda n: n+1, range(6)))
--------------------------------------------------------------------------------
/tests/test_files/README.md:
--------------------------------------------------------------------------------
1 | # Test files
2 |
3 | * merge_handle - Tests the merging functionality of a file into a folder
4 | * zip_simple - Contains a single markdown file at root which should be renamed and packaged back up
5 | * `0123456789abcdef0123456789abcdef` - ID of the MD file
6 | * zip_complex - TODO document
--------------------------------------------------------------------------------
/tests/test_files/merge_handle/test 0123456789abcdef0123456789abcdef.md:
--------------------------------------------------------------------------------
1 | # Test
2 |
3 | This is a test of the merge functionality
--------------------------------------------------------------------------------
/tests/test_files/merge_handle/test 0123456789abcdef0123456789abcdef/.git-commit-folder:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/notion_export_enhancer/07a34d4b3daeb1ec69cd4253c089ba4d9dc5bbc3/tests/test_files/merge_handle/test 0123456789abcdef0123456789abcdef/.git-commit-folder
--------------------------------------------------------------------------------
/tests/test_files/zip_complex.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/notion_export_enhancer/07a34d4b3daeb1ec69cd4253c089ba4d9dc5bbc3/tests/test_files/zip_complex.zip
--------------------------------------------------------------------------------
/tests/test_files/zip_complex/beep 0123456789abcdef0123456789abcdef.md:
--------------------------------------------------------------------------------
1 | # Beep
2 |
3 | A tiny little sound
4 | it feels ever so comforting
5 | an impulse and then it's gone
6 | but the effect ripples through your body
7 |
8 | [Types](beep/types%2011111111111111111111111111111111.md)
--------------------------------------------------------------------------------
/tests/test_files/zip_complex/beep 0123456789abcdef0123456789abcdef/types 11111111111111111111111111111111.md:
--------------------------------------------------------------------------------
1 | # Types of beeps
2 |
3 | * Small
4 | * Soft
5 | * Agile
6 |
7 | [beep](../device%2000000000000000000000000000000000.md)
--------------------------------------------------------------------------------
/tests/test_files/zip_complex/device 00000000000000000000000000000000.md:
--------------------------------------------------------------------------------
1 | You come across a small device
2 | It's covered in ash and debris
3 | The smooth body has faint ridges that trace your palms
4 | Until it gives and it lets out a soft
5 | [_Beep_](beep%200123456789abcdef0123456789abcdef.md)
--------------------------------------------------------------------------------
/tests/test_files/zip_complex/something_else.csv:
--------------------------------------------------------------------------------
1 | beep,beep,beep,beep
2 | beep,beep,beep,beep
3 | beep,beep,beep,beep
4 | beep,beep,beep,beep
5 |
--------------------------------------------------------------------------------
/tests/test_files/zip_simple.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/notion_export_enhancer/07a34d4b3daeb1ec69cd4253c089ba4d9dc5bbc3/tests/test_files/zip_simple.zip
--------------------------------------------------------------------------------
/tests/test_files/zip_simple/test 0123456789abcdef0123456789abcdef.md:
--------------------------------------------------------------------------------
1 | Simple zip test
--------------------------------------------------------------------------------
/tests/test_upload.py:
--------------------------------------------------------------------------------
1 | '''
2 | Tests NotionPyRenderer parsing
3 | '''
4 | import pytest
5 | from datetime import datetime
6 | import io
7 | import sys
8 | import os
9 | import re
10 | import zipfile
11 | import requests
12 | from notion_export_enhancer.enhancer import noteNameRewrite, NotionExportRenamer, \
13 | mdFileRewrite, rewriteNotionZip
14 | from notion.block import PageBlock, ImageBlock
15 | from unittest.mock import Mock, patch
16 |
17 | #No-op, seal doesn't exist in Python 3.6
18 | if sys.version_info >= (3,7,0):
19 | from unittest.mock import seal
20 | else:
21 | seal = lambda x: x
22 |
23 | testsRoot = os.path.dirname(os.path.realpath(__file__))
24 | defaultBlockTimeNotion = "7955187742000" #2/2/2222 22:22:22
25 | defaultBlockTime = datetime.fromtimestamp(7955187742)
26 |
27 | def MockBlock(title='', icon=None, createdTime=defaultBlockTimeNotion, lastEditedTime=None, spec=PageBlock, parent=None, children=None):
28 | mockBlock = Mock(spec=spec)
29 | mockBlock._get_record_data = Mock(return_value={ "created_time": createdTime, "last_edited_time": lastEditedTime or createdTime })
30 | mockBlock.icon = icon
31 | mockBlock.title = title
32 | mockBlock.parent = parent
33 | mockBlock.children = children
34 | seal(mockBlock)
35 | return mockBlock
36 |
37 | def MockClient(blockMap={}):
38 | notionClient = Mock()
39 | notionClient.return_value = notionClient
40 |
41 | def get_block(bId):
42 | ret = blockMap[bId]
43 | if isinstance(ret, Exception):
44 | raise ret
45 | else:
46 | return ret
47 | notionClient.get_block = get_block
48 | seal(notionClient)
49 | return notionClient
50 |
51 | def test_noteNameRewrite_non_matching_names():
52 | '''it will return None tuple when not matching pattern'''
53 | #arrange
54 | nCl = MockClient()
55 |
56 | #act/assert
57 | assert noteNameRewrite(nCl, 'asdf') == (None, None, None)
58 | assert noteNameRewrite(nCl, 'asdf 4fe9r0ogij') == (None, None, None)
59 |
60 | def test_noteNameRewrite_name():
61 | '''it will return properly extracted name'''
62 | #arrange
63 | nCl = MockClient({
64 | '0123456789abcdef0123456789abcdef': MockBlock()
65 | })
66 |
67 | #act
68 | ret = noteNameRewrite(nCl, 'asdf 0123456789abcdef0123456789abcdef')
69 |
70 | #assert
71 | assert ret == ('asdf', defaultBlockTime, defaultBlockTime)
72 |
73 | @patch('sys.stdout', new_callable=io.StringIO)
74 | def test_noteNameRewrite_HTTPError(mockStdout):
75 | '''HTTPError will skip'''
76 | #arrange
77 | nCl = MockClient({
78 | '0123456789abcdef0123456789abcdef': requests.exceptions.HTTPError('asdf')
79 | })
80 |
81 | #act
82 | ret = noteNameRewrite(nCl, 'asdf 0123456789abcdef0123456789abcdef')
83 |
84 | #assert
85 | assert ret == (None, None, None)
86 | assert re.search(r"Failed", mockStdout.getvalue(), flags=re.IGNORECASE)
87 |
88 | def test_noteNameRewrite_returns_imageblock_with_parent():
89 | '''HTTPError will skip'''
90 | #arrange
91 | nCl = MockClient({
92 | '0123456789abcdef0123456789abcdef': MockBlock(spec=ImageBlock, parent=MockBlock('yyyy'))
93 | })
94 |
95 | #act
96 | ret = noteNameRewrite(nCl, 'yyyy 0123456789abcdef0123456789abcdef')
97 |
98 | #assert
99 | assert ret == ('yyyy', defaultBlockTime, defaultBlockTime)
100 |
101 | def test_noteNameRewrite_returns_imageblock_with_children():
102 | '''HTTPError will skip'''
103 | #arrange
104 | nCl = MockClient({
105 | '0123456789abcdef0123456789abcdef': MockBlock(spec=ImageBlock, children=[MockBlock('yyyy')])
106 | })
107 |
108 | #act
109 | ret = noteNameRewrite(nCl, 'yyyy 0123456789abcdef0123456789abcdef')
110 |
111 | #assert
112 | assert ret == ('yyyy', defaultBlockTime, defaultBlockTime)
113 |
114 | @patch('sys.stdout', new_callable=io.StringIO)
115 | def test_noteNameRewrite_returns_imageblock_with_multiple_children(mockStdout):
116 | '''HTTPError will skip'''
117 | #arrange
118 | nCl = MockClient({
119 | '0123456789abcdef0123456789abcdef': MockBlock(spec=ImageBlock, children=[MockBlock('aaaa'), MockBlock('yyyy')])
120 | })
121 |
122 | #act
123 | ret = noteNameRewrite(nCl, 'yyyy 0123456789abcdef0123456789abcdef')
124 |
125 | #assert
126 | assert ret == (None, None, None)
127 | assert re.search(r"Failed", mockStdout.getvalue(), flags=re.IGNORECASE)
128 | assert re.search(r"Ambiguous", mockStdout.getvalue(), flags=re.IGNORECASE)
129 |
130 | @patch('sys.stdout', new_callable=io.StringIO)
131 | def test_noteNameRewrite_returns_imageblock_with_no_connection(mockStdout):
132 | '''HTTPError will skip'''
133 | #arrange
134 | nCl = MockClient({
135 | '0123456789abcdef0123456789abcdef': MockBlock(spec=ImageBlock)
136 | })
137 |
138 | #act
139 | ret = noteNameRewrite(nCl, 'yyyy 0123456789abcdef0123456789abcdef')
140 |
141 | #assert
142 | assert ret == (None, None, None)
143 | assert re.search(r"Failed", mockStdout.getvalue(), flags=re.IGNORECASE)
144 |
145 | def test_noteNameRewrite_long_names():
146 | '''it will retruncate names from Notion'''
147 | #arrange
148 | nCl = MockClient({
149 | '0123456789abcdef0123456789abcdef': MockBlock(title="abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")
150 | })
151 |
152 | #act
153 | ret = noteNameRewrite(nCl, 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx 0123456789abcdef0123456789abcdef')
154 |
155 | #assert
156 | assert ret == ('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz', defaultBlockTime, defaultBlockTime)
157 |
158 | def test_noteNameRewrite_icon_not_emoji():
159 | '''it will not use the icon from the block if it's not an emoji'''
160 | #arrange
161 | nCl = MockClient({
162 | '0123456789abcdef0123456789abcdef': MockBlock(icon="https://example.com")
163 | })
164 |
165 | #act
166 | ret = noteNameRewrite(nCl, 'owo 0123456789abcdef0123456789abcdef')
167 |
168 | #assert
169 | assert ret == ('owo', defaultBlockTime, defaultBlockTime)
170 |
171 | def test_noteNameRewrite_icon_emoji():
172 | '''it will not use the icon if it's an emoji, even if multiple unicode characters'''
173 | #arrange
174 | nCl = MockClient({
175 | '0123456789abcdef0123456789abcdef': MockBlock(icon="🌲"), # Single code point
176 | '00000000000000000000000000000000': MockBlock(icon="🕳️") # Multiple code points "U+1F573, U+FE0F"
177 | })
178 |
179 | #act
180 | ret = noteNameRewrite(nCl, 'owo 0123456789abcdef0123456789abcdef')
181 | ret2 = noteNameRewrite(nCl, 'owo 00000000000000000000000000000000')
182 |
183 | #assert
184 | assert ret == ('🌲 owo', defaultBlockTime, defaultBlockTime)
185 | assert ret2 == ('🕳️ owo', defaultBlockTime, defaultBlockTime)
186 |
187 | def test_noteNameRewrite_times():
188 | '''it will get times from Notion as well, as datetime objects'''
189 | #arrange
190 | nCl = MockClient({
191 | '0123456789abcdef0123456789abcdef': MockBlock(createdTime="1555555555000", lastEditedTime="16666666666777")
192 | })
193 |
194 | #act
195 | ret = noteNameRewrite(nCl, 'owo 0123456789abcdef0123456789abcdef')
196 |
197 | #assert
198 | assert ret == ('owo', datetime.fromtimestamp(1555555555), datetime.fromtimestamp(16666666666.777))
199 |
200 |
201 |
202 | def test_NotionExportRewriter_renameAndTimesWithNotion_no_rename():
203 | '''it will not rename paths that dont match'''
204 | #arrange
205 | nCl = MockClient()
206 | rn = NotionExportRenamer(nCl, "")
207 |
208 | #act
209 | ret = rn.renameAndTimesWithNotion(os.path.join('a', 'b', 'c.png'))
210 |
211 | #assert
212 | assert ret == ('c.png', None, None)
213 |
214 | def test_NotionExportRewriter_renameAndTimesWithNotion_simple_rename():
215 | '''it will rename normal paths'''
216 | #arrange
217 | nCl = MockClient({
218 | '0123456789abcdef0123456789abcdef': MockBlock()
219 | })
220 | rn = NotionExportRenamer(nCl, "")
221 |
222 | #act
223 | ret = rn.renameAndTimesWithNotion(os.path.join('a', 'b', 'c 0123456789abcdef0123456789abcdef.md'))
224 |
225 | #assert
226 | assert ret == ('c.md', defaultBlockTime, defaultBlockTime)
227 |
228 | def test_NotionExportRewriter_renameAndTimesWithNotion_merge_handle():
229 | '''it will rename while handling collisions from previous conversions'''
230 | #arrange
231 | nCl = MockClient({
232 | '0123456789abcdef0123456789abcdef': MockBlock(),
233 | })
234 | rn = NotionExportRenamer(nCl, os.path.join(testsRoot, 'test_files', 'merge_handle'))
235 |
236 | #act
237 | ret = rn.renameAndTimesWithNotion('test 0123456789abcdef0123456789abcdef.md')
238 |
239 | #assert
240 | assert ret == (os.path.join('test','!index.md'), defaultBlockTime, defaultBlockTime)
241 |
242 | def test_NotionExportRewriter_renameAndTimesWithNotion_rename_collision_handle():
243 | '''it will rename while handling collisions from previous conversions'''
244 | #arrange
245 | nCl = MockClient({
246 | '0123456789abcdef0123456789abcdef': MockBlock(),
247 | '00000000000000000000000000000000': MockBlock(),
248 | '11111111111111111111111111111111': MockBlock(),
249 | })
250 | rn = NotionExportRenamer(nCl, "")
251 |
252 | #act
253 | ret = rn.renameAndTimesWithNotion(os.path.join('a', 'b', 'c 0123456789abcdef0123456789abcdef.md'))
254 | ret2 = rn.renameAndTimesWithNotion(os.path.join('a', 'b', 'c 00000000000000000000000000000000.md'))
255 | ret3 = rn.renameAndTimesWithNotion(os.path.join('a', 'b', 'c 11111111111111111111111111111111.md'))
256 |
257 | #assert
258 | assert ret == ('c.md', defaultBlockTime, defaultBlockTime)
259 | assert ret2 == ('c (1).md', defaultBlockTime, defaultBlockTime)
260 | assert ret3 == ('c (2).md', defaultBlockTime, defaultBlockTime)
261 |
262 | def test_NotionExportRewriter_renameWithNotion_simple_rename():
263 | '''it will rename if path matches and only return name'''
264 | #arrange
265 | nCl = MockClient({
266 | '0123456789abcdef0123456789abcdef': MockBlock(),
267 | })
268 | rn = NotionExportRenamer(nCl, "")
269 |
270 | #act
271 | ret = rn.renameWithNotion(os.path.join('a', 'b', 'c 0123456789abcdef0123456789abcdef.md'))
272 |
273 | #assert
274 | assert ret == 'c.md'
275 |
276 | def test_NotionExportRewriter_renamePathWithNotion_simple_rename():
277 | '''it will rename a full path'''
278 | #arrange
279 | nCl = MockClient({
280 | '0123456789abcdef0123456789abcdef': MockBlock(),
281 | '00000000000000000000000000000000': MockBlock(),
282 | '11111111111111111111111111111111': MockBlock(),
283 | })
284 | rn = NotionExportRenamer(nCl, os.path.join('x', 'y'))
285 |
286 | #act
287 | ret = rn.renamePathWithNotion( \
288 | os.path.join('a 11111111111111111111111111111111', 'b 00000000000000000000000000000000', 'c 0123456789abcdef0123456789abcdef.md'))
289 |
290 | #assert
291 | assert ret == os.path.join('a', 'b', 'c.md')
292 |
293 | def test_NotionExportRewriter_renamePathAndTimesWithNotion_simple_rename():
294 | '''it will rename a full path'''
295 | #arrange
296 | nCl = MockClient({
297 | '0123456789abcdef0123456789abcdef': MockBlock(createdTime="1000000000000", lastEditedTime="1111111111000"),
298 | '00000000000000000000000000000000': MockBlock(createdTime="1555555555000", lastEditedTime="1666666666000"),
299 | '11111111111111111111111111111111': MockBlock(createdTime="1555555555000", lastEditedTime="1666666666000"),
300 | })
301 | rn = NotionExportRenamer(nCl, os.path.join('x', 'y'))
302 |
303 | #act
304 | ret = rn.renamePathAndTimesWithNotion( \
305 | os.path.join('a 11111111111111111111111111111111', 'b 00000000000000000000000000000000', 'c 0123456789abcdef0123456789abcdef.md'))
306 |
307 | #assert
308 | assert ret == (os.path.join('a', 'b', 'c.md'), datetime.fromtimestamp(1000000000), datetime.fromtimestamp(1111111111))
309 |
310 | def test_mdFileRewrite_no_op():
311 | '''it will do nothing to md files by default'''
312 | md = """# I'm really good at taking copypastas from reddit and putting them in
313 |
314 | Now, this is a stowy aww about how My wife got fwipped-tuwned upside down And I'd wike to take a minute Just sit wight thewe I'ww teww you how I became the pwince of a town cawwed Bew Aiw In west Phiwadewphia bown and waised On the pwaygwound was whewe I spent most of my days Chiwwin' out maxin' wewaxin' aww coow And aww shootin some b-baww outside of the schoow When a coupwe of guys who wewe up to no good Stawted making twoubwe in my neighbowhood I got in one wittwe fight and my mom got scawed She said 'You'we movin' with youw auntie and uncwe in Bew Aiw'
315 |
316 | I begged and pweaded with hew day aftew day But she packed my suit case and sent me on my way She gave me a kiss and then she gave me my ticket. I put my Wawkman on and said, 'I might as weww kick it'.
317 |
318 | Fiwst cwass, yo this is bad Dwinking owange juice out of a champagne gwass. Is this what the peopwe of Bew-Aiw wiving wike? Hmmmmm this might be awwight.
319 | """
320 | nCl = MockClient()
321 | rn = NotionExportRenamer(nCl, '')
322 |
323 | #act
324 | ret = mdFileRewrite(rn, os.path.join('a', 'b', 'c.md'), mdFileContents=md)
325 |
326 | #assert
327 | assert ret == md
328 |
329 | def test_mdFileRewrite_remove_top_h1():
330 | '''it will remove the top h1 if configured to'''
331 | md = """# Copypasta
332 |
333 | Okay but for real this is not one of those copy-ma-pastas that you people are taking about. Spaghetti
334 | """
335 | nCl = MockClient()
336 | rn = NotionExportRenamer(nCl, '')
337 |
338 | #act
339 | ret = mdFileRewrite(rn, os.path.join('a', 'b', 'c.md'), mdFileContents=md, removeTopH1=True)
340 |
341 | #assert
342 | assert ret == """
343 | Okay but for real this is not one of those copy-ma-pastas that you people are taking about. Spaghetti
344 | """
345 |
346 | def test_mdFileRewrite_rewrite_paths():
347 | '''it will rewrite paths in the markdown if passed'''
348 | md = """# Things to do with my time
349 |
350 | Okay but really, do you think that me writing this was a good use of time?
351 |
352 | [not really](https://example.com). But you know what is a good use of my time? Probably going to the grocery store and getting some food.
353 |
354 | What do I need? Maybe something [off my grocery list](Grocery%20List%200123456789abcdef0123456789abcdef.md).
355 |
356 | And here's another line just for fun
357 | """
358 | nCl = MockClient({
359 | '0123456789abcdef0123456789abcdef': MockBlock(),
360 | })
361 | rn = NotionExportRenamer(nCl, '')
362 |
363 | #act
364 | ret = mdFileRewrite(rn, os.path.join('a', 'b', 'c.md'), mdFileContents=md, rewritePaths=True)
365 |
366 | #assert
367 | assert ret == """# Things to do with my time
368 |
369 | Okay but really, do you think that me writing this was a good use of time?
370 |
371 | [not really](https://example.com). But you know what is a good use of my time? Probably going to the grocery store and getting some food.
372 |
373 | What do I need? Maybe something [off my grocery list](Grocery%20List.md).
374 |
375 | And here's another line just for fun
376 | """
377 |
378 | def test_mdFileRewrite_rewrite_path_complex():
379 | '''it will rewrite paths in the markdown if passed, but more complex'''
380 | md = """# Pathssss
381 |
382 | [owo](../d%200123456789abcdef0123456789abcdef.md) [ewe](../e%2044444444444444444444444444444444.md)
383 |
384 | [uwu](im%2000000000000000000000000000000000/gay%2011111111111111111111111111111111.md)
385 |
386 | [vwv](../cute%2022222222222222222222222222222222/girls%2033333333333333333333333333333333.md)
387 | """
388 | nCl = MockClient({
389 | '0123456789abcdef0123456789abcdef': MockBlock(),
390 | '00000000000000000000000000000000': MockBlock(),
391 | '11111111111111111111111111111111': MockBlock(),
392 | '22222222222222222222222222222222': MockBlock(),
393 | '33333333333333333333333333333333': MockBlock(),
394 | '44444444444444444444444444444444': MockBlock(),
395 | })
396 | rn = NotionExportRenamer(nCl, '')
397 |
398 | #act
399 | ret = mdFileRewrite(rn, os.path.join('a', 'b', 'c.md'), mdFileContents=md, rewritePaths=True)
400 |
401 | #assert
402 | assert ret == """# Pathssss
403 |
404 | [owo](../d.md) [ewe](../e.md)
405 |
406 | [uwu](im/gay.md)
407 |
408 | [vwv](../cute/girls.md)
409 | """
410 |
411 | def test_rewriteNotionZip_simple():
412 | '''it will rewrite an entire zip file (simple, 1 file, 1 id, no special markdown)'''
413 | nCl = MockClient({
414 | '0123456789abcdef0123456789abcdef': MockBlock(createdTime="1000000000000", lastEditedTime="1609459200000"), #1/1/2021 12:00:00 AM
415 | })
416 |
417 | #act
418 | outputFilePath = rewriteNotionZip(nCl, os.path.join(testsRoot, 'test_files', 'zip_simple.zip'))
419 |
420 | #assert
421 | with zipfile.ZipFile(outputFilePath) as zf:
422 | assert zf.testzip() == None
423 | assert zf.namelist() == ['test.md']
424 | assert zf.open('test.md').read().decode('utf-8') == "Simple zip test"
425 | i = zf.getinfo('test.md')
426 | assert i.date_time == datetime.fromtimestamp(1609459200).timetuple()[0:6]
427 |
428 | def test_rewriteNotionZip_complex():
429 | '''it will rewrite an entire zip file (simple, 1 file, 1 id, no special markdown)'''
430 | nCl = MockClient({
431 | '0123456789abcdef0123456789abcdef': MockBlock(lastEditedTime="1609459200000"), #1/1/2021 12:00:00 AM
432 | '00000000000000000000000000000000': MockBlock(lastEditedTime="1609459200000"), #1/1/2021 12:00:00 AM
433 | '11111111111111111111111111111111': MockBlock(icon="📟", lastEditedTime="1609459200000"), #1/1/2021 12:00:00 AM
434 | })
435 |
436 | #act
437 | outputFilePath = rewriteNotionZip(nCl, os.path.join(testsRoot, 'test_files', 'zip_complex.zip'))
438 |
439 | #assert
440 | with zipfile.ZipFile(outputFilePath) as zf:
441 | assert zf.testzip() == None
442 | assert set(zf.namelist()) == set(['beep/!index.md', 'beep/📟 types.md', 'device.md', 'something_else.csv'])
443 | assert zf.open('beep/!index.md').read().decode('utf-8') == """# Beep
444 |
445 | A tiny little sound
446 | it feels ever so comforting
447 | an impulse and then it's gone
448 | but the effect ripples through your body
449 |
450 | [Types](%F0%9F%93%9F%20types.md)"""
451 | assert zf.open('device.md').read().decode('utf-8') == """You come across a small device
452 | It's covered in ash and debris
453 | The smooth body has faint ridges that trace your palms
454 | Until it gives and it lets out a soft
455 | [_Beep_](beep/%21index.md)"""
456 | assert zf.open('beep/📟 types.md').read().decode('utf-8') == """# Types of beeps
457 |
458 | * Small
459 | * Soft
460 | * Agile
461 |
462 | [beep](../device.md)"""
--------------------------------------------------------------------------------