├── .gitignore ├── LICENSE ├── README.md ├── poetry.lock ├── poetry.toml ├── purepress ├── __init__.py ├── __main__.py └── __meta__.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # ----- Python ----- 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # ----- Project ----- 109 | 110 | .vscode 111 | .idea 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 Richard Chien 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PurePress 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/purepress.svg)](https://pypi.python.org/pypi/purepress/) 4 | 5 | **PurePress** is a very simple static blog generator. 6 | 7 | ## Usage 8 | 9 | ```sh 10 | pip install purepress 11 | 12 | mkdir my-blog 13 | cd my-blog 14 | 15 | purepress init # init the blog 16 | git clone https://github.com/verilab/purepress-theme-default.git theme # install a theme 17 | 18 | purepress preview # preview the blog 19 | purepress build # build the blog 20 | ``` 21 | 22 | See [richardchien/blog](https://github.com/richardchien/blog) for more. 23 | 24 | ## Minimality 25 | 26 | ```sh 27 | ❯ cloc purepress 28 | 3 text files. 29 | 3 unique files. 30 | 3 files ignored. 31 | 32 | github.com/AlDanial/cloc v 1.94 T=0.01 s (346.3 files/s, 74444.9 lines/s) 33 | ------------------------------------------------------------------------------- 34 | Language files blank comment code 35 | ------------------------------------------------------------------------------- 36 | Python 3 91 51 503 37 | ------------------------------------------------------------------------------- 38 | SUM: 3 91 51 503 39 | ------------------------------------------------------------------------------- 40 | ``` 41 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "blinker" 5 | version = "1.9.0" 6 | description = "Fast, simple object-to-object and broadcast signaling" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["main"] 10 | files = [ 11 | {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, 12 | {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, 13 | ] 14 | 15 | [[package]] 16 | name = "click" 17 | version = "8.1.8" 18 | description = "Composable command line interface toolkit" 19 | optional = false 20 | python-versions = ">=3.7" 21 | groups = ["main"] 22 | files = [ 23 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 24 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 25 | ] 26 | 27 | [package.dependencies] 28 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 29 | 30 | [[package]] 31 | name = "colorama" 32 | version = "0.4.6" 33 | description = "Cross-platform colored terminal text." 34 | optional = false 35 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 36 | groups = ["main"] 37 | files = [ 38 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 39 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 40 | ] 41 | 42 | [[package]] 43 | name = "feedgen" 44 | version = "1.0.0" 45 | description = "Feed Generator (ATOM, RSS, Podcasts)" 46 | optional = false 47 | python-versions = "*" 48 | groups = ["main"] 49 | files = [ 50 | {file = "feedgen-1.0.0.tar.gz", hash = "sha256:d9bd51c3b5e956a2a52998c3708c4d2c729f2fcc311188e1e5d3b9726393546a"}, 51 | ] 52 | 53 | [package.dependencies] 54 | lxml = "*" 55 | python-dateutil = "*" 56 | 57 | [[package]] 58 | name = "flask" 59 | version = "3.1.0" 60 | description = "A simple framework for building complex web applications." 61 | optional = false 62 | python-versions = ">=3.9" 63 | groups = ["main"] 64 | files = [ 65 | {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, 66 | {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, 67 | ] 68 | 69 | [package.dependencies] 70 | blinker = ">=1.9" 71 | click = ">=8.1.3" 72 | itsdangerous = ">=2.2" 73 | Jinja2 = ">=3.1.2" 74 | Werkzeug = ">=3.1" 75 | 76 | [package.extras] 77 | async = ["asgiref (>=3.2)"] 78 | dotenv = ["python-dotenv"] 79 | 80 | [[package]] 81 | name = "html-toc" 82 | version = "0.1.2" 83 | description = "Generate TOC from HTML." 84 | optional = false 85 | python-versions = ">=3.6,<4.0" 86 | groups = ["main"] 87 | files = [ 88 | {file = "html_toc-0.1.2-py3-none-any.whl", hash = "sha256:75312f385bd160df1ea19d457e9b7926979be243d8db810da66d9f2c27ef14fd"}, 89 | {file = "html_toc-0.1.2.tar.gz", hash = "sha256:135c3002b582bae72ff2849c872b1e10c1b8e5838cc6b5ce3f34033e7de49609"}, 90 | ] 91 | 92 | [[package]] 93 | name = "itsdangerous" 94 | version = "2.2.0" 95 | description = "Safely pass data to untrusted environments and back." 96 | optional = false 97 | python-versions = ">=3.8" 98 | groups = ["main"] 99 | files = [ 100 | {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, 101 | {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, 102 | ] 103 | 104 | [[package]] 105 | name = "jinja2" 106 | version = "3.1.6" 107 | description = "A very fast and expressive template engine." 108 | optional = false 109 | python-versions = ">=3.7" 110 | groups = ["main"] 111 | files = [ 112 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 113 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 114 | ] 115 | 116 | [package.dependencies] 117 | MarkupSafe = ">=2.0" 118 | 119 | [package.extras] 120 | i18n = ["Babel (>=2.7)"] 121 | 122 | [[package]] 123 | name = "lxml" 124 | version = "5.3.1" 125 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 126 | optional = false 127 | python-versions = ">=3.6" 128 | groups = ["main"] 129 | files = [ 130 | {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b"}, 131 | {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b"}, 132 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5"}, 133 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3"}, 134 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c"}, 135 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2"}, 136 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac"}, 137 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9"}, 138 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9"}, 139 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79"}, 140 | {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2"}, 141 | {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51"}, 142 | {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406"}, 143 | {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5"}, 144 | {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0"}, 145 | {file = "lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23"}, 146 | {file = "lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c"}, 147 | {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f"}, 148 | {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607"}, 149 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8"}, 150 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a"}, 151 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27"}, 152 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666"}, 153 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79"}, 154 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65"}, 155 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709"}, 156 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629"}, 157 | {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33"}, 158 | {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295"}, 159 | {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c"}, 160 | {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c"}, 161 | {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff"}, 162 | {file = "lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"}, 163 | {file = "lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6"}, 164 | {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"}, 165 | {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"}, 166 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"}, 167 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"}, 168 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"}, 169 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"}, 170 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"}, 171 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"}, 172 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"}, 173 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"}, 174 | {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"}, 175 | {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"}, 176 | {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"}, 177 | {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"}, 178 | {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"}, 179 | {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"}, 180 | {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"}, 181 | {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e"}, 182 | {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd"}, 183 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7"}, 184 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414"}, 185 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e"}, 186 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1"}, 187 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5"}, 188 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423"}, 189 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20"}, 190 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8"}, 191 | {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9"}, 192 | {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c"}, 193 | {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b"}, 194 | {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5"}, 195 | {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252"}, 196 | {file = "lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78"}, 197 | {file = "lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332"}, 198 | {file = "lxml-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81"}, 199 | {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a"}, 200 | {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439"}, 201 | {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac"}, 202 | {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d"}, 203 | {file = "lxml-5.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a"}, 204 | {file = "lxml-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576"}, 205 | {file = "lxml-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9"}, 206 | {file = "lxml-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32"}, 207 | {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61"}, 208 | {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19"}, 209 | {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307"}, 210 | {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70"}, 211 | {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9"}, 212 | {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53"}, 213 | {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce"}, 214 | {file = "lxml-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407"}, 215 | {file = "lxml-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5"}, 216 | {file = "lxml-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e"}, 217 | {file = "lxml-5.3.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098"}, 218 | {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5"}, 219 | {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7"}, 220 | {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf"}, 221 | {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2"}, 222 | {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb"}, 223 | {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73"}, 224 | {file = "lxml-5.3.1-cp38-cp38-win32.whl", hash = "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc"}, 225 | {file = "lxml-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7"}, 226 | {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f"}, 227 | {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8"}, 228 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a"}, 229 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125"}, 230 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd"}, 231 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549"}, 232 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3"}, 233 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e"}, 234 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914"}, 235 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b"}, 236 | {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be"}, 237 | {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e"}, 238 | {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675"}, 239 | {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2"}, 240 | {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af"}, 241 | {file = "lxml-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0"}, 242 | {file = "lxml-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2"}, 243 | {file = "lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725"}, 244 | {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d"}, 245 | {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84"}, 246 | {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2"}, 247 | {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877"}, 248 | {file = "lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499"}, 249 | {file = "lxml-5.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131"}, 250 | {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3"}, 251 | {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50"}, 252 | {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394"}, 253 | {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98"}, 254 | {file = "lxml-5.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314"}, 255 | {file = "lxml-5.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1"}, 256 | {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615"}, 257 | {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1"}, 258 | {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de"}, 259 | {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac"}, 260 | {file = "lxml-5.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde"}, 261 | {file = "lxml-5.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270"}, 262 | {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca"}, 263 | {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6"}, 264 | {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a"}, 265 | {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe"}, 266 | {file = "lxml-5.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2"}, 267 | {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"}, 268 | ] 269 | 270 | [package.extras] 271 | cssselect = ["cssselect (>=0.7)"] 272 | html-clean = ["lxml_html_clean"] 273 | html5 = ["html5lib"] 274 | htmlsoup = ["BeautifulSoup4"] 275 | source = ["Cython (>=3.0.11,<3.1.0)"] 276 | 277 | [[package]] 278 | name = "markdown" 279 | version = "3.7" 280 | description = "Python implementation of John Gruber's Markdown." 281 | optional = false 282 | python-versions = ">=3.8" 283 | groups = ["main"] 284 | files = [ 285 | {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, 286 | {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, 287 | ] 288 | 289 | [package.extras] 290 | docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] 291 | testing = ["coverage", "pyyaml"] 292 | 293 | [[package]] 294 | name = "markupsafe" 295 | version = "3.0.2" 296 | description = "Safely add untrusted strings to HTML/XML markup." 297 | optional = false 298 | python-versions = ">=3.9" 299 | groups = ["main"] 300 | files = [ 301 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 302 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 303 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 304 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 305 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 306 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 307 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 308 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 309 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 310 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 311 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 312 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 313 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 314 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 315 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 316 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 317 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 318 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 319 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 320 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 321 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 322 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 323 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 324 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 325 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 326 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 327 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 328 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 329 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 330 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 331 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 332 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 333 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 334 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 335 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 336 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 337 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 338 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 339 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 340 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 341 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 342 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 343 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 344 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 345 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 346 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 347 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 348 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 349 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 350 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 351 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 352 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 353 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 354 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 355 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 356 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 357 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 358 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 359 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 360 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 361 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 362 | ] 363 | 364 | [[package]] 365 | name = "py-gfm" 366 | version = "2.0.0" 367 | description = "An implementation of Github-Flavored Markdown written as an extension to the Python Markdown library." 368 | optional = false 369 | python-versions = ">=3.8" 370 | groups = ["main"] 371 | files = [ 372 | {file = "py-gfm-2.0.0.linux-x86_64.tar.gz", hash = "sha256:8768b31bbfda8e1d52e2f32b363c138eedf52b1a81c06036b60ece576b2a652f"}, 373 | {file = "py_gfm-2.0.0-py2.py3-none-any.whl", hash = "sha256:c49f43b584e15bdbe569141c92aefc00542289b6d88d95b38117e3359a35cdfe"}, 374 | ] 375 | 376 | [package.dependencies] 377 | markdown = ">=3.3,<4" 378 | 379 | [[package]] 380 | name = "python-dateutil" 381 | version = "2.9.0.post0" 382 | description = "Extensions to the standard Python datetime module" 383 | optional = false 384 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 385 | groups = ["main"] 386 | files = [ 387 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 388 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 389 | ] 390 | 391 | [package.dependencies] 392 | six = ">=1.5" 393 | 394 | [[package]] 395 | name = "pytz" 396 | version = "2025.2" 397 | description = "World timezone definitions, modern and historical" 398 | optional = false 399 | python-versions = "*" 400 | groups = ["main"] 401 | files = [ 402 | {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, 403 | {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, 404 | ] 405 | 406 | [[package]] 407 | name = "pyyaml" 408 | version = "6.0.2" 409 | description = "YAML parser and emitter for Python" 410 | optional = false 411 | python-versions = ">=3.8" 412 | groups = ["main"] 413 | files = [ 414 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 415 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 416 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 417 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 418 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 419 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 420 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 421 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 422 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 423 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 424 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 425 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 426 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 427 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 428 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 429 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 430 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 431 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 432 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 433 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 434 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 435 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 436 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 437 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 438 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 439 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 440 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 441 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 442 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 443 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 444 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 445 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 446 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 447 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 448 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 449 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 450 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 451 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 452 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 453 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 454 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 455 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 456 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 457 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 458 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 459 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 460 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 461 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 462 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 463 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 464 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 465 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 466 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 467 | ] 468 | 469 | [[package]] 470 | name = "six" 471 | version = "1.17.0" 472 | description = "Python 2 and 3 compatibility utilities" 473 | optional = false 474 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 475 | groups = ["main"] 476 | files = [ 477 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 478 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 479 | ] 480 | 481 | [[package]] 482 | name = "toml" 483 | version = "0.10.2" 484 | description = "Python Library for Tom's Obvious, Minimal Language" 485 | optional = false 486 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 487 | groups = ["main"] 488 | files = [ 489 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 490 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 491 | ] 492 | 493 | [[package]] 494 | name = "werkzeug" 495 | version = "3.1.3" 496 | description = "The comprehensive WSGI web application library." 497 | optional = false 498 | python-versions = ">=3.9" 499 | groups = ["main"] 500 | files = [ 501 | {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, 502 | {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, 503 | ] 504 | 505 | [package.dependencies] 506 | MarkupSafe = ">=2.1.1" 507 | 508 | [package.extras] 509 | watchdog = ["watchdog (>=2.3)"] 510 | 511 | [metadata] 512 | lock-version = "2.1" 513 | python-versions = ">=3.12,<3.14" 514 | content-hash = "3240e9dfc90222660c680657a3528512fbba2f16cebe092ccd8e817d4dee8430" 515 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /purepress/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import functools 4 | import xml.etree.ElementTree as etree 5 | from datetime import date, datetime, timedelta, timezone 6 | from typing import Any, Callable, Dict, List, Optional 7 | 8 | import yaml 9 | import pytz 10 | import toml 11 | import markdown.extensions 12 | import markdown.treeprocessors 13 | from markdown import Markdown 14 | from mdx_gfm import GithubFlavoredMarkdownExtension 15 | from flask import ( 16 | Flask, 17 | render_template, 18 | abort, 19 | redirect, 20 | url_for, 21 | Blueprint, 22 | send_from_directory, 23 | request, 24 | make_response, 25 | ) 26 | from werkzeug.security import safe_join 27 | from feedgen.feed import FeedGenerator 28 | from html_toc import HtmlTocParser 29 | 30 | # calculate some folder path 31 | root_folder = os.getenv("PUREPRESS_INSTANCE", os.getcwd()) 32 | static_folder = os.path.join(root_folder, "static") 33 | template_folder = os.path.join(root_folder, "theme", "templates") 34 | theme_static_folder = os.path.join(root_folder, "theme", "static") 35 | posts_folder = os.path.join(root_folder, "posts") 36 | pages_folder = os.path.join(root_folder, "pages") 37 | raw_folder = os.path.join(root_folder, "raw") 38 | 39 | # load configurations 40 | try: 41 | purepress_config = toml.load(os.path.join(root_folder, "purepress.toml")) 42 | except FileNotFoundError: 43 | purepress_config = {"site": {}, "config": {}} 44 | site, config = purepress_config["site"], purepress_config["config"] 45 | 46 | app = Flask( 47 | __name__, 48 | instance_path=root_folder, 49 | template_folder=template_folder, 50 | static_folder=static_folder, 51 | instance_relative_config=True, 52 | ) 53 | 54 | # handle static files for theme 55 | theme_bp = Blueprint( 56 | "theme", 57 | __name__, 58 | static_url_path="/static/theme", 59 | static_folder=theme_static_folder, 60 | ) 61 | app.register_blueprint(theme_bp) 62 | 63 | # prepare markdown parser 64 | class HookImageSrcProcessor(markdown.treeprocessors.Treeprocessor): 65 | def run(self, root: etree.Element): 66 | static_url = url_for("static", filename="") 67 | for el in root.iter("img"): 68 | src = el.get("src", "") 69 | if src.startswith("/static/"): 70 | el.set("src", re.sub(r"^/static/", static_url, src)) 71 | 72 | 73 | class HookLinkHrefProcessor(markdown.treeprocessors.Treeprocessor): 74 | @staticmethod 75 | def path_to_url(path: str) -> str: 76 | root = url_for("index").rstrip("/") 77 | url = path 78 | if path.startswith("/posts/"): 79 | # /posts/2021-08-23-hello-world.md -> /post/2021/08/23/hello-world/ 80 | # /posts/2021-08-23-hello-world.md#anchor1 -> /post/2021/08/23/hello-world/#anchor1 81 | url = re.sub(r"^/posts/", f"{root}/post/", url) 82 | url = re.sub(r"-", "/", url, count=3) 83 | url = re.sub(r"\.md(#.*)?$", r"/\1", url) 84 | elif path.startswith("/pages/"): 85 | # /pages/about/ -> /about/ 86 | # /pages/about/index.md -> /about/ 87 | # /pages/foo/bar.md -> /foo/bar.html 88 | # /pages/foo/bar.md#anchor1 -> /foo/bar.html#anchor1 89 | url = re.sub(r"^/pages/", f"{root}/", url) 90 | url = re.sub(r"index\.md(#.*)?$", r"\1", url) 91 | url = re.sub(r"\.md(#.*)?$", r".html\1", url) 92 | elif path.startswith("/raw/"): 93 | # /raw/foo/baz.html -> /foo/baz.html 94 | url = re.sub(r"^/raw/", f"{root}/", url) 95 | return url 96 | 97 | def run(self, root: etree.Element): 98 | for el in root.iter("a"): 99 | href = el.get("href", "") 100 | if href.startswith("/"): 101 | el.set("href", self.path_to_url(href)) 102 | 103 | 104 | class Extension(markdown.extensions.Extension): 105 | def extendMarkdown(self, md) -> None: 106 | md.treeprocessors.register(HookImageSrcProcessor(), "hook-image-src", 5) 107 | md.treeprocessors.register(HookLinkHrefProcessor(), "hook-link-href", 5) 108 | 109 | 110 | _md = Markdown(extensions=[GithubFlavoredMarkdownExtension(), Extension(), "footnotes"]) 111 | 112 | 113 | def markdown_convert(text: str) -> str: 114 | _md.reset() 115 | return _md.convert(text) 116 | 117 | 118 | # inject site and config into template context 119 | @app.context_processor 120 | def inject_objects() -> Dict[str, Any]: 121 | return {"global": {"site": site, "config": config}} 122 | 123 | 124 | def load_entry(fullpath: str, *, meta_only: bool, parse_toc: bool) -> Optional[Dict[str, Any]]: 125 | # read frontmatter and content 126 | frontmatter, content = "", "" 127 | try: 128 | with open(fullpath, mode="r", encoding="utf-8") as f: 129 | firstline = f.readline().strip() 130 | remained = f.read().strip() 131 | if firstline == "---": 132 | frontmatter, remained = remained.split("---", maxsplit=1) 133 | content = remained.strip() 134 | else: 135 | content = "\n\n".join([firstline, remained]) 136 | except FileNotFoundError: 137 | return None 138 | # construct the entry object 139 | entry: Dict[str, Any] = yaml.load(frontmatter, Loader=yaml.FullLoader) or {} 140 | # ensure datetime fields are real datetime 141 | for k in ("created", "updated"): 142 | if isinstance(entry.get(k), date) and not isinstance(entry.get(k), datetime): 143 | entry[k] = datetime.combine(entry[k], datetime.min.time()) 144 | # ensure tags and categories are lists 145 | for k in ("categories", "tags"): 146 | if isinstance(entry.get(k), str): 147 | entry[k] = [entry[k]] 148 | # if should, convert markdown content to html 149 | if not meta_only: 150 | entry["content"] = markdown_convert(content) 151 | if parse_toc: 152 | parser = HtmlTocParser() 153 | parser.feed(entry["content"]) 154 | entry["content"] = parser.html 155 | depth = entry.get("toc_depth", config.get("toc_depth")) or 0 156 | entry["toc"] = parser.toc(depth=depth) 157 | entry["toc_html"] = parser.toc_html(depth=depth) 158 | return entry 159 | 160 | 161 | def load_post(filename: str, *, meta_only: bool = False, parse_toc: bool = False) -> Optional[Dict[str, Any]]: 162 | # parse the filename (yyyy-MM-dd-post-title.md) 163 | try: 164 | year, month, day, name = os.path.splitext(filename)[0].split("-", maxsplit=3) 165 | year, month, day = int(year), int(month), int(day) 166 | except ValueError: 167 | return None 168 | # load post entry 169 | fullpath = safe_join(posts_folder, filename) 170 | if fullpath is None: 171 | return None 172 | post = load_entry(fullpath, meta_only=meta_only, parse_toc=parse_toc) 173 | if post is None: # note that post may be {} 174 | return None 175 | # add some fields 176 | post["filename"] = filename 177 | post["url"] = url_for( 178 | "post", 179 | year=f"{year:0>4d}", 180 | month=f"{month:0>2d}", 181 | day=f"{day:0>2d}", 182 | name=name, 183 | ) 184 | # ensure *title* field 185 | if "title" not in post: 186 | post["title"] = " ".join(name.split("-")) 187 | # ensure *created* field 188 | if "created" not in post: 189 | post["created"] = datetime(year=year, month=month, day=day) 190 | return post 191 | 192 | 193 | def load_posts(*, meta_only: bool = False) -> List[Dict[str, Any]]: 194 | try: 195 | post_files = os.listdir(posts_folder) 196 | except FileNotFoundError: 197 | return [] 198 | 199 | def gen_posts(): 200 | for post_file in post_files: 201 | if not post_file.endswith(".md"): 202 | continue 203 | yield load_post(post_file, meta_only=meta_only) 204 | 205 | posts = list(filter(lambda x: x and not x.get("hide", False), gen_posts())) 206 | posts.sort(key=lambda x: x.get("created"), reverse=True) 207 | return posts 208 | 209 | 210 | def load_page(rel_url: str, *, parse_toc: bool = False) -> Optional[Dict[str, Any]]: 211 | # convert relative url to full file path 212 | pathnames = rel_url.split("/") 213 | fullpath = safe_join(pages_folder, *pathnames) 214 | if fullpath is None: 215 | return None 216 | if fullpath.endswith(os.path.sep): # /foo/bar/ 217 | fullpath = os.path.join(fullpath, "index.md") 218 | elif fullpath.endswith(".html"): # /foo/bar.html 219 | fullpath = os.path.splitext(fullpath)[0] + ".md" 220 | else: # /foo/bar 221 | fullpath += ".md" 222 | # load page entry 223 | page = load_entry(fullpath, meta_only=False, parse_toc=parse_toc) 224 | if page is None: 225 | return None 226 | page["url"] = url_for("page", rel_url=rel_url) 227 | # ensure *title* field 228 | if "title" not in page: 229 | name = os.path.splitext(os.path.basename(fullpath))[0] 230 | page["title"] = " ".join(name.split("-")) 231 | return page 232 | 233 | 234 | def templated(template: str) -> Callable: 235 | if not template.endswith(".html"): 236 | template += ".html" 237 | 238 | def decorator(func: Callable) -> Callable: 239 | @functools.wraps(func) 240 | def wrapper(*args, **kwargs): 241 | res = func(*args, **kwargs) 242 | if isinstance(res, dict): 243 | return render_template([f"custom/{template}", template], **res) 244 | return res 245 | 246 | return wrapper 247 | 248 | return decorator 249 | 250 | 251 | @app.route("/") 252 | def index(): 253 | # the logic is the same as /page/1/, just reuse it 254 | return index_page(1, from_index=True) 255 | 256 | 257 | @app.route("/page//") 258 | @templated("index") 259 | def index_page(page_num, *, from_index: bool = False): 260 | # do some calculation and handle unexpected cases 261 | posts_per_page = config["posts_per_index_page"] 262 | posts = load_posts(meta_only=True) # just load meta data quickly 263 | post_count = len(posts) 264 | page_count = (post_count + posts_per_page - 1) // posts_per_page 265 | if page_num == 1 and not from_index: 266 | # redirect /page/1/ to / 267 | return redirect(url_for("index"), 302) 268 | if page_num < 1 or page_num > page_count: 269 | abort(404) 270 | 271 | # prepare pager links 272 | prev_url, next_url = None, None 273 | if page_num == 2: 274 | prev_url = url_for("index") 275 | elif page_num > 2: 276 | prev_url = url_for("index_page", page_num=page_num - 1) 277 | if page_num < page_count: 278 | next_url = url_for("index_page", page_num=page_num + 1) 279 | 280 | # load posts in the specified range 281 | begin = (page_num - 1) * posts_per_page 282 | end = min(post_count, begin + posts_per_page) 283 | posts_to_render = [] 284 | for i in range(begin, end): 285 | posts_to_render.append(load_post(posts[i]["filename"])) 286 | return { 287 | "entries": posts_to_render, 288 | "pager": {"prev_url": prev_url, "next_url": next_url}, 289 | } 290 | 291 | 292 | @app.route("/post/////") 293 | @templated("post") 294 | def post(year: str, month: str, day: str, name: str): 295 | # use secure_filename to avoid filename attacks 296 | post = load_post(f"{year}-{month}-{day}-{name}.md", parse_toc=True) 297 | if not post: 298 | abort(404) 299 | return {"entry": post} 300 | 301 | 302 | @app.route("/archive/") 303 | @templated("archive") 304 | def archive(): 305 | posts = load_posts(meta_only=True) 306 | return {"entries": posts, "archive": {"type": "Archive", "name": "All"}} 307 | 308 | 309 | @app.route("/category//") 310 | @templated("archive") 311 | def category(name: str): 312 | posts = list(filter(lambda p: name in p.get("categories", []), load_posts(meta_only=True))) 313 | return {"entries": posts, "archive": {"type": "Category", "name": name}} 314 | 315 | 316 | @app.route("/tag//") 317 | @templated("archive") 318 | def tag(name: str): 319 | posts = list(filter(lambda p: name in p.get("tags", []), load_posts(meta_only=True))) 320 | return {"entries": posts, "archive": {"type": "Tag", "name": name}} 321 | 322 | 323 | @app.route("/") 324 | @templated("page") 325 | def page(rel_url: str): 326 | page = load_page(rel_url, parse_toc=True) 327 | if not page: 328 | if rel_url.endswith("/"): 329 | rel_url += "/index.html" 330 | return send_from_directory(raw_folder, rel_url) 331 | return {"entry": page} 332 | 333 | 334 | @app.errorhandler(404) 335 | @app.route("/404.html") 336 | def page_not_found(e=None): 337 | return render_template("404.html"), 404 338 | 339 | 340 | def s2tz(tz_str): 341 | m = re.match(r"UTC([+|-]\d{1,2}):(\d{2})", tz_str) 342 | if m: # in format 'UTC±[hh]:[mm]' 343 | delta_h = int(m.group(1)) 344 | delta_m = int(m.group(2)) if delta_h >= 0 else -int(m.group(2)) 345 | return timezone(timedelta(hours=delta_h, minutes=delta_m)) 346 | try: # in format 'Asia/Shanghai' 347 | return pytz.timezone(tz_str) 348 | except pytz.UnknownTimeZoneError: 349 | return None 350 | 351 | 352 | @app.route("/feed.xml") 353 | def feed(): 354 | root_url = request.url_root.rstrip("/") 355 | home_full_url = root_url + url_for("index") 356 | feed_full_url = root_url + url_for("feed") 357 | site_tz = s2tz(site.get("timezone", "")) or timezone(timedelta()) 358 | # set feed info 359 | feed_gen = FeedGenerator() 360 | feed_gen.id(home_full_url) 361 | feed_gen.title(site.get("title", "")) 362 | feed_gen.subtitle(site.get("subtitle", "")) 363 | if "author" in site: 364 | feed_gen.author(name=site["author"]) 365 | feed_gen.link(href=home_full_url, rel="alternate") 366 | feed_gen.link(href=feed_full_url, rel="self") 367 | # add feed entries 368 | posts = load_posts(meta_only=True)[:10] 369 | for i in range(len(posts)): 370 | p = load_post(posts[i]["filename"]) 371 | if not p: 372 | continue 373 | feed_entry = feed_gen.add_entry() 374 | feed_entry.id(root_url + p["url"]) 375 | feed_entry.link(href=root_url + p["url"]) 376 | feed_entry.title(p["title"]) 377 | feed_entry.content(p["content"], type="CDATA") 378 | feed_entry.published(p["created"].replace(tzinfo=site_tz)) 379 | feed_entry.updated(p.get("updated", p["created"]).replace(tzinfo=site_tz)) 380 | if "author" in p: 381 | feed_entry.author(name=p["author"]) 382 | # make http response 383 | resp = make_response(feed_gen.rss_str(pretty=True)) 384 | resp.content_type = "application/rss+xml" 385 | return resp 386 | -------------------------------------------------------------------------------- /purepress/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import functools 5 | import traceback 6 | from urllib.parse import urlparse 7 | from contextlib import contextmanager 8 | 9 | import click 10 | from flask import url_for 11 | 12 | from .__meta__ import __version__ 13 | from . import ( 14 | app, 15 | root_folder, 16 | static_folder, 17 | theme_static_folder, 18 | pages_folder, 19 | posts_folder, 20 | raw_folder, 21 | load_posts, 22 | ) 23 | 24 | 25 | echo = click.echo 26 | echo_green = functools.partial(click.secho, fg="green") 27 | echo_red = functools.partial(click.secho, fg="red") 28 | echo_yellow = functools.partial(click.secho, fg="yellow") 29 | 30 | 31 | @contextmanager 32 | def step(op_name: str): 33 | echo(f"{op_name}...", nl=False) 34 | yield 35 | echo_green("OK") 36 | 37 | 38 | @click.group(name="purepress", short_help="A simple static blog generator.") 39 | @click.version_option(version=__version__) 40 | def cli(): 41 | pass 42 | 43 | 44 | DEFAULT_PUREPRESS_TOML = """\ 45 | [site] 46 | title = "My Blog" 47 | subtitle = "Here is my blog" 48 | author = "My Name" 49 | timezone = "Asia/Shanghai" 50 | 51 | [config] 52 | posts_per_index_page = 5 53 | """ 54 | 55 | DEFAULT_POST_TEMPLATE = """\ 56 | --- 57 | title: A demo {0} 58 | --- 59 | 60 | This is a demo {0}. 61 | """ 62 | 63 | 64 | @cli.command("init", short_help="Initialize an instance.") 65 | def init_command(): 66 | if os.listdir(root_folder): 67 | echo_red(f'The instance folder "{root_folder}" is not empty') 68 | exit(1) 69 | with step("Creating folders"): 70 | os.makedirs(posts_folder, exist_ok=True) 71 | os.makedirs(pages_folder, exist_ok=True) 72 | os.makedirs(static_folder, exist_ok=True) 73 | os.makedirs(raw_folder, exist_ok=True) 74 | with step("Creating default purepress.toml"): 75 | with open(os.path.join(root_folder, "purepress.toml"), mode="w", encoding="utf-8") as f: 76 | f.write(DEFAULT_PUREPRESS_TOML) 77 | with step("Createing demo page"): 78 | with open(os.path.join(pages_folder, "demo.md"), mode="w", encoding="utf-8") as f: 79 | f.write(DEFAULT_POST_TEMPLATE.format("page")) 80 | with step("Createing demo post"): 81 | with open(os.path.join(posts_folder, "1970-01-01-demo.md"), mode="w", encoding="utf-8") as f: 82 | f.write(DEFAULT_POST_TEMPLATE.format("post")) 83 | echo_green("OK! Now you can install a theme and preview the site.") 84 | 85 | 86 | @cli.command("preview", short_help="Preview the site.") 87 | @click.option("--host", "-h", default="127.0.0.1", help="Host to preview the site.") 88 | @click.option("--port", "-p", default=8080, help="Port to preview the site.") 89 | @click.option("--no-debug", is_flag=True, default=False, help="Do not preview in debug mode.") 90 | def preview_command(host, port, no_debug): 91 | app.config["ENV"] = "development" 92 | app.config["TEMPLATES_AUTO_RELOAD"] = True 93 | app.run(host=host, port=port, debug=not no_debug, use_reloader=False) 94 | 95 | 96 | @cli.command("build", short_help="Build the site.") 97 | @click.option( 98 | "--url-root", 99 | prompt="Please enter the url root (used as prefix of generated url)", 100 | help='The url root of your site, e.g. "http://example.com/blog/".', 101 | ) 102 | def build_command(url_root): 103 | res = urlparse(url_root) 104 | app_root = res.path or "/" 105 | app.config["PREFERRED_URL_SCHEME"] = res.scheme or "http" 106 | app.config["SERVER_NAME"] = res.netloc or "localhost" 107 | if not res.netloc: 108 | echo_yellow('The url root does not contain a valid server name, "localhost" will be used.') 109 | app.config["APPLICATION_ROOT"] = app_root 110 | # mark as 'BUILDING' status, so that templates can react properly, 111 | app.config["BUILDING"] = True 112 | 113 | try: 114 | with app.test_client() as client: 115 | build(lambda url: client.get(re.sub(r"^" + app_root, "/", url))) 116 | echo_green('OK! Now you can find the built site in the "build" folder.') 117 | except Exception: 118 | traceback.print_exc() 119 | echo_red("Failed to build the site.") 120 | exit(1) 121 | 122 | 123 | def build(get): 124 | # prepare folder paths 125 | build_folder = os.path.join(root_folder, "build") 126 | build_static_folder = os.path.join(build_folder, "static") 127 | build_static_theme_folder = os.path.join(build_static_folder, "theme") 128 | build_pages_folder = build_folder 129 | build_posts_folder = os.path.join(build_folder, "post") 130 | build_categories_folder = os.path.join(build_folder, "category") 131 | build_tags_folder = os.path.join(build_folder, "tag") 132 | build_archive_folder = os.path.join(build_folder, "archive") 133 | build_index_page_folder = os.path.join(build_folder, "page") 134 | 135 | with step("Creating build folder"): 136 | if os.path.isdir(build_folder): 137 | shutil.rmtree(build_folder) 138 | elif os.path.exists(build_folder): 139 | os.remove(build_folder) 140 | os.mkdir(build_folder) 141 | 142 | with step("Copying raw files"): 143 | copy_folder_content(raw_folder, build_folder) 144 | 145 | with step("Copying theme static files"): 146 | os.makedirs(build_static_theme_folder, exist_ok=True) 147 | copy_folder_content(theme_static_folder, build_static_theme_folder) 148 | 149 | with step("Copying static files"): 150 | copy_folder_content(static_folder, build_static_folder) 151 | 152 | with step("Building custom pages"): 153 | for dirname, _, files in os.walk(pages_folder): 154 | if os.path.basename(dirname).startswith("."): 155 | continue 156 | rel_dirname = os.path.relpath(dirname, pages_folder) 157 | os.makedirs(os.path.join(build_pages_folder, rel_dirname), exist_ok=True) 158 | for file in filter(lambda f: not f.startswith("."), files): 159 | rel_path = os.path.join(rel_dirname, file) 160 | dst_rel_path = re.sub(r".md$", ".html", rel_path) 161 | dst_path = os.path.join(build_pages_folder, dst_rel_path) 162 | rel_url = "/".join(os.path.split(dst_rel_path)) 163 | with app.test_request_context(): 164 | url = url_for("page", rel_url=rel_url) 165 | res = get(url) 166 | with open(dst_path, "wb") as f: 167 | f.write(res.data) 168 | 169 | with app.test_request_context(): 170 | posts = load_posts(meta_only=True) 171 | 172 | with step("Building posts"): 173 | for post in posts: 174 | filename = post["filename"] 175 | year, month, day, name = os.path.splitext(filename)[0].split("-", maxsplit=3) 176 | dst_dirname = os.path.join(build_posts_folder, year, month, day, name) 177 | os.makedirs(dst_dirname, exist_ok=True) 178 | dst_path = os.path.join(dst_dirname, "index.html") 179 | with app.test_request_context(): 180 | url = url_for("post", year=year, month=month, day=day, name=name) 181 | res = get(url) 182 | with open(dst_path, "wb") as f: 183 | f.write(res.data) 184 | 185 | with step("Building categories"): 186 | categories = set(functools.reduce(lambda c, p: c + p.get("categories", []), posts, [])) 187 | for category in categories: 188 | category_folder = os.path.join(build_categories_folder, category) 189 | os.makedirs(category_folder, exist_ok=True) 190 | with app.test_request_context(): 191 | url = url_for("category", name=category) 192 | res = get(url) 193 | with open(os.path.join(category_folder, "index.html"), "wb") as f: 194 | f.write(res.data) 195 | 196 | with step("Building tags"): 197 | tags = set(functools.reduce(lambda t, p: t + p.get("tags", []), posts, [])) 198 | for tag in tags: 199 | tag_folder = os.path.join(build_tags_folder, tag) 200 | os.makedirs(tag_folder, exist_ok=True) 201 | with app.test_request_context(): 202 | url = url_for("tag", name=tag) 203 | res = get(url) 204 | with open(os.path.join(tag_folder, "index.html"), "wb") as f: 205 | f.write(res.data) 206 | 207 | with step("Building archive"): 208 | os.makedirs(build_archive_folder, exist_ok=True) 209 | with app.test_request_context(): 210 | url = url_for("archive") 211 | res = get(url) 212 | with open(os.path.join(build_archive_folder, "index.html"), "wb") as f: 213 | f.write(res.data) 214 | 215 | with step("Building index"): 216 | with app.test_request_context(): 217 | url = url_for("index") 218 | res = get(url) 219 | with open(os.path.join(build_folder, "index.html"), "wb") as f: 220 | f.write(res.data) 221 | page_num = 2 222 | while True: 223 | page_folder = os.path.join(build_index_page_folder, str(page_num)) 224 | os.makedirs(page_folder, exist_ok=True) 225 | with app.test_request_context(): 226 | url = url_for("index_page", page_num=page_num) 227 | res = get(url) 228 | if res.status_code != 200: 229 | break 230 | with open(os.path.join(page_folder, "index.html"), "wb") as f: 231 | f.write(res.data) 232 | page_num += 1 233 | 234 | with step("Building feed"): 235 | with app.test_request_context(): 236 | url = url_for("feed") 237 | res = get(url) 238 | with open(os.path.join(build_folder, "feed.xml"), "wb") as f: 239 | f.write(res.data) 240 | 241 | with step("Building 404"): 242 | with app.test_request_context(): 243 | url = url_for("page_not_found") 244 | res = get(url) 245 | with open(os.path.join(build_folder, "404.html"), "wb") as f: 246 | f.write(res.data) 247 | 248 | 249 | def copy_folder_content(src, dst): 250 | """ 251 | Copy all content in src directory to dst directory. 252 | The src and dst must exist. 253 | """ 254 | for file in os.listdir(src): 255 | file_path = os.path.join(src, file) 256 | dst_file_path = os.path.join(dst, file) 257 | if os.path.isdir(file_path): 258 | shutil.copytree(file_path, dst_file_path) 259 | else: 260 | shutil.copy(file_path, dst_file_path) 261 | -------------------------------------------------------------------------------- /purepress/__meta__.py: -------------------------------------------------------------------------------- 1 | __version_info__ = (0, 10, 0) 2 | __version__ = ".".join([str(v) for v in __version_info__]) 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "purepress" 3 | version = "0.10.0" 4 | description = "A simple static blog generator." 5 | license = "MIT" 6 | authors = ["Richard Chien "] 7 | readme = "README.md" 8 | repository = "https://github.com/verilab/purepress" 9 | keywords = ["Static Blog Generator", "Static Blog", "Blog", "Blog Engine"] 10 | classifiers = [ 11 | "Development Status :: 4 - Beta", 12 | "Framework :: Flask", 13 | "Operating System :: OS Independent", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3 :: Only", 16 | ] 17 | 18 | [tool.poetry.scripts] 19 | purepress = "purepress.__main__:cli.main" 20 | 21 | [tool.poetry.dependencies] 22 | python = ">=3.12,<3.14" 23 | Werkzeug = "~3.1.3" 24 | Flask = "~3.1.0" 25 | PyYAML = "~6.0.2" 26 | click = "~8.1.8" 27 | colorama = "~0.4.6" 28 | feedgen = "~1.0.0" 29 | pytz = "~2025.2" 30 | Markdown = "~3.7" 31 | py-gfm = "~2.0.0" 32 | toml = "~0.10.2" 33 | html-toc = "~0.1.2" 34 | MarkupSafe = "~3.0.2" 35 | 36 | [build-system] 37 | requires = ["poetry>=0.12"] 38 | build-backend = "poetry.masonry.api" 39 | 40 | [tool.black] 41 | line-length = 120 42 | --------------------------------------------------------------------------------