├── .gitignore ├── LICENSE ├── README.md ├── coolify ├── .gitignore ├── README.md ├── main.py └── requirements.txt ├── droplet-ssl-with-ci ├── .github │ └── workflows │ │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── README.md ├── config │ ├── docker-compose.yml │ └── nginx │ │ └── site-confs │ │ └── default.conf ├── main.py ├── poetry.lock └── pyproject.toml ├── droplet ├── .gitignore ├── README.md ├── images │ ├── 00_droplet.PNG │ ├── 01_droplet.PNG │ ├── 02_droplet.PNG │ ├── 03_web.PNG │ ├── 04_nginx.PNG │ └── 05_newuser.PNG ├── main.py └── requirements.txt ├── fly ├── Dockerfile ├── README.md ├── main.py └── requirements.txt ├── heroku ├── .gitignore ├── Procfile ├── README.md ├── main.py └── requirements.txt ├── huggingface ├── .gitignore ├── README.md ├── main.py └── requirements.txt ├── modal ├── README.md ├── app.py ├── requirements-dev.txt └── requirements.txt ├── railway ├── .gitignore ├── README.md ├── main.py └── requirements.txt ├── replit ├── .replit ├── README.md ├── main.py └── requirements.txt └── vercel ├── .gitignore ├── README.md ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fh-deploy 2 | 3 | This repo contains deployment guides for the platforms listed below. Each guide contains a "hello world" style project which you'll setup on your local device before deploying it on the platform. 4 | 5 | | Platform | Deployment Guide | 6 | |------------------------------------------|--------------------------------------------------------------------------| 7 | | [HuggingFace](https://huggingface.co/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/huggingface) | 8 | | [Railway](https://railway.app/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/railway) | 9 | | [Replit](https://replit.com/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/replit) | 10 | | [Vercel](https://vercel.com/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/vercel) | 11 | | [Heroku](https://heroku.com/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/heroku) | 12 | | [Droplet](https://www.digitalocean.com/products/droplets) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/droplet) [Docker + SSL guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/droplet-ssl-with-ci) | 13 | | [Fly.io](https://fly.io/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/fly) | 14 | | [Coolify](https://coolify.io/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/coolify) | 15 | | [Modal](https://modal.com/) | [guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/modal) | 16 | 17 | If you would like to add a guide for another platform feel free to fork this repo and submit a PR. 18 | 19 | ### Platform Agnostic Deployment 20 | 21 | - [Uvicorn](https://www.uvicorn.org/) is the server used to run FastHTML apps. For general info on uvicorn deployments see this [guide](https://www.uvicorn.org/deployment/). 22 | - The Apache HTTP Server doesn't support ASGI deployments, which includes FastHTML. We suggest running [uvicorn behind nginx](https://www.uvicorn.org/deployment/#running-behind-nginx) or [Caddy](https://caddyserver.com/) instead. 23 | -------------------------------------------------------------------------------- /coolify/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sesskey 3 | -------------------------------------------------------------------------------- /coolify/README.md: -------------------------------------------------------------------------------- 1 | ### Step 1 - Setup Coolify 2 | - Setup Coolify on your server by following the [documentation](https://coolify.io/docs/installation), and open Coolify's UI. 3 | 4 | 5 | ### Step 2 - Make a Github App to be used as your source 6 | - Click "Sources" in the left menu 7 | - Click "Add Source", name your app, and click continue. 8 | - Click "Register Now" on the Register a GitHub App screen 9 | - Sign into Github, give your app a name, and click "Create Github App". You will then be redirected back to your Coolify instance. 10 | - Click "Install Repositories on Github", and select your FastHTML app repo and click "Install". 11 | - Once redirected back to Coolify, click Save on your Github app. 12 | 13 | 14 | ### Step 3 - Deploy your Github App 15 | - In your Coolify project, click New, and then select "Private Repository (with GitHub App)" 16 | - Select the app you made in step 2. 17 | - Click "Load Repository" 18 | - Ensure your build pack is set as "Nixpacks" and click continue. 19 | - On the Configuration screen, on the General tab, under Network, and "Ports Exposes" ensure you list the ports your application uses separated by commas. 20 | - Remember to click Save at the top of the General tab when updating your ports. 21 | - Add any environment variables to the Environment Variables tab, remember to click Save. 22 | - Click Deploy 23 | - Once the deployment is finished, click Links, and click the link to your shiny new FastHTML app! -------------------------------------------------------------------------------- /coolify/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.fastapp import * 2 | 3 | app, rt = fast_app() 4 | 5 | @rt("/") 6 | def get(): 7 | return Titled("FastHTML", P("FastHTML on Coolify!")) 8 | 9 | serve() 10 | -------------------------------------------------------------------------------- /coolify/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml -------------------------------------------------------------------------------- /droplet-ssl-with-ci/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | env: 7 | REGISTRY: ${{ secrets.DOCKER_REGISTRY }} 8 | IMAGE_NAME: ${{ secrets.DOCKER_IMAGE }} 9 | PRODUCTION_DOT_ENV: ${{ secrets.PRODUCTION_DOT_ENV }} 10 | SERVER_APP_DIRECTORY: ${{ secrets.SERVER_APP_DIRECTORY }} 11 | DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} 12 | HOST_URL: ${{ secrets.HOST_URL }} 13 | jobs: 14 | build-and-push: 15 | name: Build image and push to DO registry 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout the repo 19 | uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: "3.10" 24 | - name: Install Poetry 25 | run: | 26 | curl -sSL https://install.python-poetry.org | python - 27 | - name: Install dependencies 28 | working-directory: . 29 | run: poetry install 30 | - name: Freeze dependencies 31 | run: poetry export --without-hashes -f requirements.txt --output requirements.txt 32 | - name: Install doctl 33 | uses: digitalocean/action-doctl@v2 34 | with: 35 | token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} 36 | - name: Build container image 37 | run: docker build -t $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $GITHUB_SHA | head -c7) . 38 | - name: Log in to DigitalOcean Container Registry with short-lived credentials 39 | run: doctl registry login --expiry-seconds 600 40 | - name: Push image to DigitalOcean Container Registry 41 | run: docker push $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $GITHUB_SHA | head -c7) 42 | deploy: 43 | name: Deploy 44 | runs-on: ubuntu-latest 45 | needs: build-and-push 46 | steps: 47 | - name: Checkout the repo 48 | uses: actions/checkout@v4 49 | - name: Config env 50 | run: | 51 | echo "IMAGE_TAG=$(echo $GITHUB_SHA | head -c7)" >> $GITHUB_ENV 52 | - name: Config docker compose 53 | uses: appleboy/scp-action@v0.1.7 54 | with: 55 | host: ${{ secrets.HOST_IP }} 56 | username: root 57 | key: ${{ secrets.SSHKEY }} 58 | passphrase: ${{ secrets.PASSPHRASE }} 59 | source: "config/docker-compose.yml" 60 | strip_components: 1 61 | target: ${{ secrets.SERVER_APP_DIRECTORY }} 62 | - name: Config nginx 63 | uses: appleboy/scp-action@v0.1.7 64 | with: 65 | host: ${{ secrets.HOST_IP }} 66 | username: root 67 | key: ${{ secrets.SSHKEY }} 68 | passphrase: ${{ secrets.PASSPHRASE }} 69 | source: "config/nginx/site-confs/default.conf" 70 | strip_components: 1 71 | # change this if you change volumes mapping for swag in docker-compose.yml 72 | target: /etc/config/swag 73 | - name: Deploy to Digital Ocean droplet via SSH action 74 | uses: appleboy/ssh-action@v1.0.3 75 | with: 76 | host: ${{ secrets.HOST_IP }} 77 | username: root 78 | key: ${{ secrets.SSHKEY }} 79 | passphrase: ${{ secrets.PASSPHRASE }} 80 | envs: SERVER_APP_DIRECTORY,IMAGE_NAME,REGISTRY,DIGITALOCEAN_ACCESS_TOKEN,GITHUB_SHA,PRODUCTION_DOT_ENV,IMAGE_TAG,HOST_URL 81 | script: | 82 | # Login to registry 83 | docker login -u $(echo DIGITALOCEAN_ACCESS_TOKEN) -p $(echo DIGITALOCEAN_ACCESS_TOKEN) registry.digitalocean.com 84 | 85 | # Stop running container 86 | if [ "$(docker ps -q -f name=$(echo $IMAGE_NAME))" ]; then 87 | docker stop $(echo $IMAGE_NAME) 88 | fi 89 | 90 | # Remove old container 91 | if [ "$(docker ps -aq -f status=exited -f name=$(echo $IMAGE_NAME))" ]; then 92 | docker rm $(echo $IMAGE_NAME) 93 | fi 94 | 95 | # set up directories for the app 96 | if [ ! -d "$(echo $SERVER_APP_DIRECTORY)" ]; then 97 | mkdir $(echo $SERVER_APP_DIRECTORY) 98 | fi 99 | if [ ! -d "$(echo $SERVER_APP_DIRECTORY)/data" ]; then 100 | mkdir $(echo $SERVER_APP_DIRECTORY)/data 101 | fi 102 | 103 | # app configuration 104 | echo "$PRODUCTION_DOT_ENV" > $(echo $SERVER_APP_DIRECTORY)/.env 105 | 106 | cd $(echo $SERVER_APP_DIRECTORY) 107 | docker compose down 108 | docker compose up -d 109 | - name: Check the deployed service URL 110 | uses: jtalk/url-health-check-action@v4 111 | with: 112 | url: ${{ secrets.HEALTH_CHECK_URL }} 113 | max-attempts: 3 114 | retry-delay: 5s 115 | -------------------------------------------------------------------------------- /droplet-ssl-with-ci/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | # sqlite 165 | data 166 | 167 | # session 168 | .sesskey -------------------------------------------------------------------------------- /droplet-ssl-with-ci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | WORKDIR /code 4 | COPY --link --chown=1000 . . 5 | 6 | RUN mkdir -p /tmp/cache/ 7 | RUN chmod a+rwx -R /tmp/cache/ 8 | RUN pip install --no-cache-dir -r requirements.txt 9 | 10 | ENV PYTHONUNBUFFERED=1 PORT=5001 11 | 12 | CMD ["python", "main.py"] 13 | -------------------------------------------------------------------------------- /droplet-ssl-with-ci/README.md: -------------------------------------------------------------------------------- 1 | # Run FastHTML on your Digital Ocean droplet 2 | 3 | > If you don't need SSL and extra bells and whistles or do not want to use Docker, check out [this guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/droplet) 4 | 5 | 6 | What's included: 7 | 8 | - SQLite database 9 | - SSL using [SWAG](https://github.com/linuxserver/docker-swag) 10 | - CI using GH actions 11 | 12 | # Getting started 13 | 14 | ## Droplet + SSL 15 | 16 | Let's get a droplet up and get SSL to work 17 | 18 | - create [a droplet](https://cloud.digitalocean.com/droplets?i=102a02) 19 | - create or use your existing [container registry](https://cloud.digitalocean.com/registry) 20 | - point your domain/subdomain to your droplet IP (using `A Record`) 21 | - ssh into your droplet and [install docker](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) 22 | - stay in your droplet and configure swag; replace `YOUR_DOMAIN_HERE` with your domain (e.g. `fasthtml.com`) 23 | 24 | ```bash 25 | touch docker-compose.yml 26 | echo "services: 27 | swag: 28 | image: lscr.io/linuxserver/swag:latest 29 | container_name: swag 30 | cap_add: 31 | - NET_ADMIN 32 | environment: 33 | - PUID=1000 34 | - PGID=1000 35 | - TZ=Etc/UTC 36 | - URL=YOUR_DOMAIN_HERE 37 | - VALIDATION=http 38 | volumes: 39 | - /etc/config/swag:/config 40 | ports: 41 | - 443:443 42 | - 80:80 43 | restart: unless-stopped 44 | " > docker-compose.yml 45 | 46 | docker compose up 47 | ``` 48 | - navigate to your domain (e.g. https://fasthtml.com) and make sure you see SWAG welcome message `Welcome to your SWAG instance` 49 | - stop the container (Ctrl+C) 50 | - clean up 51 | 52 | ```bash 53 | rm docker-compose.yml 54 | docker rm swag 55 | ``` 56 | 57 | ## Github 58 | 59 | Configure GH actions to build and deploy the app to your droplet. Start by generating SSH key-pair to access server from GH CI server 60 | 61 | ```bash 62 | ssh-keygen -f ./id_rsa -t rsa -b 4096 -C "YOUR EMAIL HERE" 63 | ``` 64 | 65 | Grab the public key 66 | 67 | ```bash 68 | cat ./id_rsa.pub 69 | ``` 70 | 71 | SSH into your droplet and add it to `~/.ssh/authorized_keys` 72 | 73 | ```bash 74 | echo "YOUR_PUBLIC_KEY" >> ~/.ssh/authorized_keys 75 | ``` 76 | 77 | Head to `Settings > Secrets and variables > Actions` for your GH repository and set the following secrets 78 | 79 | 80 | | Input Parameter | Description | 81 | |--------------------|------------------------| 82 | | SSHKEY | SSH private key that you generated above using ssh-keygen (`id_rsa`) | 83 | | PASSPHRASE | Passphrase used when running ssh-keygen | 84 | | HOST_IP | IP address of your droplet | 85 | | HOST_URL | Your app URL (e.g. `fasthtml.com` ) | 86 | | HEALTH_CHECK_URL | e.g. `https://fasthtml.com/healthcheck`| 87 | | SERVER_APP_DIRECTORY | smth like `/opt/your_app_name` | 88 | | DOCKER_REGISTRY | URL of your docker registry on Digital ocean (smth like `registry.digitalocean.com/YOUR_REGISTRY_NAME`) | 89 | | DOCKER_IMAGE | name for you application's docker image | 90 | | DIGITALOCEAN_ACCESS_TOKEN | Digital ocean access token - needs at least `registry` and `image` scopes, get it [here](https://cloud.digitalocean.com/account/api/tokens) looks smth like `dop_v1_XXXXXXXX`| 91 | | PRODUCTION_DOT_ENV | `.env` style file content for your application specific env vars (can start with `FOO=bar` for now) | 92 | 93 | ## Credits 94 | 95 | - [SWAG](https://github.com/linuxserver/docker-swag) 96 | - [Full CI/CD with Docker + GitHub Actions + DigitalOcean (Droplets + Container Registry)](https://faun.pub/full-ci-cd-with-docker-github-actions-digitalocean-droplets-container-registry-db2938db8246) 97 | -------------------------------------------------------------------------------- /droplet-ssl-with-ci/config/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | swag: 3 | image: lscr.io/linuxserver/swag:latest 4 | container_name: swag 5 | cap_add: 6 | - NET_ADMIN 7 | environment: 8 | - PUID=1000 9 | - PGID=1000 10 | - TZ=Etc/UTC 11 | - URL=${HOST_URL} 12 | - VALIDATION=http 13 | volumes: 14 | - /etc/config/swag:/config 15 | ports: 16 | - 443:443 17 | - 80:80 18 | restart: unless-stopped 19 | # if you change the name of the service/port, make sure to update nginx/site-confs/defaul.conf 20 | app: 21 | image: ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} 22 | env_file: ${SERVER_APP_DIRECTORY}/.env 23 | ports: 24 | - 5001:5001 25 | volumes: 26 | - ${SERVER_APP_DIRECTORY}/data:/code/data 27 | 28 | -------------------------------------------------------------------------------- /droplet-ssl-with-ci/config/nginx/site-confs/default.conf: -------------------------------------------------------------------------------- 1 | ## Version 2024/07/16 - Changelog: https://github.com/linuxserver/docker-swag/commits/master/root/defaults/nginx/site-confs/default.conf.sample 2 | 3 | # redirect all traffic to https 4 | server { 5 | listen 80 default_server; 6 | listen [::]:80 default_server; 7 | 8 | location / { 9 | return 301 https://$host$request_uri; 10 | } 11 | } 12 | 13 | # main server block 14 | server { 15 | listen 443 ssl default_server; 16 | listen [::]:443 ssl default_server; 17 | 18 | server_name _; 19 | 20 | include /config/nginx/ssl.conf; 21 | 22 | root /config/www; 23 | index index.html index.htm index.php; 24 | 25 | # enable subfolder method reverse proxy confs 26 | include /config/nginx/proxy-confs/*.subfolder.conf; 27 | 28 | # enable for ldap auth (requires ldap-location.conf in the location block) 29 | #include /config/nginx/ldap-server.conf; 30 | 31 | # enable for Authelia (requires authelia-location.conf in the location block) 32 | #include /config/nginx/authelia-server.conf; 33 | 34 | # enable for Authentik (requires authentik-location.conf in the location block) 35 | #include /config/nginx/authentik-server.conf; 36 | 37 | location / { 38 | # change this based on your service name + port in docker-compose.yml 39 | proxy_pass http://app:5001; 40 | } 41 | 42 | # deny access to .htaccess/.htpasswd files 43 | location ~ /\.ht { 44 | deny all; 45 | } 46 | } 47 | 48 | # enable subdomain method reverse proxy confs 49 | include /config/nginx/proxy-confs/*.subdomain.conf; 50 | # enable proxy cache for auth 51 | proxy_cache_path cache/ keys_zone=auth_cache:10m; -------------------------------------------------------------------------------- /droplet-ssl-with-ci/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app,rt = fast_app() 4 | db = database('data/yourdatabase.db') 5 | 6 | @rt('/') 7 | def get(): return Div(P('Hello World!'), hx_get="/change") 8 | 9 | @rt("/healthcheck") 10 | def get(): return JSONResponse({"status": "ok"}) 11 | 12 | serve() 13 | -------------------------------------------------------------------------------- /droplet-ssl-with-ci/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.4.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 11 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 12 | ] 13 | 14 | [package.dependencies] 15 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 19 | 20 | [package.extras] 21 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 22 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 23 | trio = ["trio (>=0.23)"] 24 | 25 | [[package]] 26 | name = "beautifulsoup4" 27 | version = "4.12.3" 28 | description = "Screen-scraping library" 29 | optional = false 30 | python-versions = ">=3.6.0" 31 | files = [ 32 | {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, 33 | {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, 34 | ] 35 | 36 | [package.dependencies] 37 | soupsieve = ">1.2" 38 | 39 | [package.extras] 40 | cchardet = ["cchardet"] 41 | chardet = ["chardet"] 42 | charset-normalizer = ["charset-normalizer"] 43 | html5lib = ["html5lib"] 44 | lxml = ["lxml"] 45 | 46 | [[package]] 47 | name = "certifi" 48 | version = "2024.8.30" 49 | description = "Python package for providing Mozilla's CA Bundle." 50 | optional = false 51 | python-versions = ">=3.6" 52 | files = [ 53 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 54 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 55 | ] 56 | 57 | [[package]] 58 | name = "click" 59 | version = "8.1.7" 60 | description = "Composable command line interface toolkit" 61 | optional = false 62 | python-versions = ">=3.7" 63 | files = [ 64 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 65 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 66 | ] 67 | 68 | [package.dependencies] 69 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 70 | 71 | [[package]] 72 | name = "colorama" 73 | version = "0.4.6" 74 | description = "Cross-platform colored terminal text." 75 | optional = false 76 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 77 | files = [ 78 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 79 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 80 | ] 81 | 82 | [[package]] 83 | name = "exceptiongroup" 84 | version = "1.2.2" 85 | description = "Backport of PEP 654 (exception groups)" 86 | optional = false 87 | python-versions = ">=3.7" 88 | files = [ 89 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 90 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 91 | ] 92 | 93 | [package.extras] 94 | test = ["pytest (>=6)"] 95 | 96 | [[package]] 97 | name = "fastcore" 98 | version = "1.7.4" 99 | description = "Python supercharged for fastai development" 100 | optional = false 101 | python-versions = ">=3.8" 102 | files = [ 103 | {file = "fastcore-1.7.4-py3-none-any.whl", hash = "sha256:0523d15a89e7242db3a71126877550e7783f5a81d3c3881d27cf80e2ee37153e"}, 104 | {file = "fastcore-1.7.4.tar.gz", hash = "sha256:4195e3f14882aea02c7768b6bc7b686125f6164206ab81a200c83baf4230e1e8"}, 105 | ] 106 | 107 | [package.dependencies] 108 | packaging = "*" 109 | 110 | [package.extras] 111 | dev = ["matplotlib", "nbclassic", "nbdev (>=0.2.39)", "numpy", "pandas", "pillow", "torch"] 112 | 113 | [[package]] 114 | name = "fastlite" 115 | version = "0.0.10" 116 | description = "A bit of extra usability for sqlite" 117 | optional = false 118 | python-versions = ">=3.10" 119 | files = [ 120 | {file = "fastlite-0.0.10-py3-none-any.whl", hash = "sha256:3a4a510fb411e39cda98111d32421838b02c3a0ccaa1c543393c22ddd9f4553d"}, 121 | {file = "fastlite-0.0.10.tar.gz", hash = "sha256:90f111dc30338b1bbc7461b767b23e207356227d968eac60b0e06d5242f7b593"}, 122 | ] 123 | 124 | [package.dependencies] 125 | fastcore = ">=1.7.1" 126 | sqlite-minutils = ">=3.37" 127 | 128 | [[package]] 129 | name = "h11" 130 | version = "0.14.0" 131 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 132 | optional = false 133 | python-versions = ">=3.7" 134 | files = [ 135 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 136 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 137 | ] 138 | 139 | [[package]] 140 | name = "httpcore" 141 | version = "1.0.5" 142 | description = "A minimal low-level HTTP client." 143 | optional = false 144 | python-versions = ">=3.8" 145 | files = [ 146 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 147 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 148 | ] 149 | 150 | [package.dependencies] 151 | certifi = "*" 152 | h11 = ">=0.13,<0.15" 153 | 154 | [package.extras] 155 | asyncio = ["anyio (>=4.0,<5.0)"] 156 | http2 = ["h2 (>=3,<5)"] 157 | socks = ["socksio (==1.*)"] 158 | trio = ["trio (>=0.22.0,<0.26.0)"] 159 | 160 | [[package]] 161 | name = "httptools" 162 | version = "0.6.1" 163 | description = "A collection of framework independent HTTP protocol utils." 164 | optional = false 165 | python-versions = ">=3.8.0" 166 | files = [ 167 | {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, 168 | {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, 169 | {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, 170 | {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, 171 | {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, 172 | {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, 173 | {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, 174 | {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, 175 | {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, 176 | {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, 177 | {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, 178 | {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, 179 | {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, 180 | {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, 181 | {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, 182 | {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, 183 | {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, 184 | {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, 185 | {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, 186 | {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, 187 | {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, 188 | {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, 189 | {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, 190 | {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, 191 | {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, 192 | {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, 193 | {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, 194 | {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, 195 | {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, 196 | {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, 197 | {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, 198 | {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, 199 | {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, 200 | {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, 201 | {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, 202 | {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, 203 | ] 204 | 205 | [package.extras] 206 | test = ["Cython (>=0.29.24,<0.30.0)"] 207 | 208 | [[package]] 209 | name = "httpx" 210 | version = "0.27.2" 211 | description = "The next generation HTTP client." 212 | optional = false 213 | python-versions = ">=3.8" 214 | files = [ 215 | {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, 216 | {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, 217 | ] 218 | 219 | [package.dependencies] 220 | anyio = "*" 221 | certifi = "*" 222 | httpcore = "==1.*" 223 | idna = "*" 224 | sniffio = "*" 225 | 226 | [package.extras] 227 | brotli = ["brotli", "brotlicffi"] 228 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 229 | http2 = ["h2 (>=3,<5)"] 230 | socks = ["socksio (==1.*)"] 231 | zstd = ["zstandard (>=0.18.0)"] 232 | 233 | [[package]] 234 | name = "idna" 235 | version = "3.8" 236 | description = "Internationalized Domain Names in Applications (IDNA)" 237 | optional = false 238 | python-versions = ">=3.6" 239 | files = [ 240 | {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, 241 | {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, 242 | ] 243 | 244 | [[package]] 245 | name = "itsdangerous" 246 | version = "2.2.0" 247 | description = "Safely pass data to untrusted environments and back." 248 | optional = false 249 | python-versions = ">=3.8" 250 | files = [ 251 | {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, 252 | {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, 253 | ] 254 | 255 | [[package]] 256 | name = "oauthlib" 257 | version = "3.2.2" 258 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 259 | optional = false 260 | python-versions = ">=3.6" 261 | files = [ 262 | {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, 263 | {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, 264 | ] 265 | 266 | [package.extras] 267 | rsa = ["cryptography (>=3.0.0)"] 268 | signals = ["blinker (>=1.4.0)"] 269 | signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] 270 | 271 | [[package]] 272 | name = "packaging" 273 | version = "24.1" 274 | description = "Core utilities for Python packages" 275 | optional = false 276 | python-versions = ">=3.8" 277 | files = [ 278 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 279 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 280 | ] 281 | 282 | [[package]] 283 | name = "python-dateutil" 284 | version = "2.9.0.post0" 285 | description = "Extensions to the standard Python datetime module" 286 | optional = false 287 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 288 | files = [ 289 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 290 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 291 | ] 292 | 293 | [package.dependencies] 294 | six = ">=1.5" 295 | 296 | [[package]] 297 | name = "python-dotenv" 298 | version = "1.0.1" 299 | description = "Read key-value pairs from a .env file and set them as environment variables" 300 | optional = false 301 | python-versions = ">=3.8" 302 | files = [ 303 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 304 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 305 | ] 306 | 307 | [package.extras] 308 | cli = ["click (>=5.0)"] 309 | 310 | [[package]] 311 | name = "python-fasthtml" 312 | version = "0.5.1" 313 | description = "The fastest way to create an HTML app" 314 | optional = false 315 | python-versions = ">=3.10" 316 | files = [ 317 | {file = "python-fasthtml-0.5.1.tar.gz", hash = "sha256:4ea293dbb0037a172dc1ac457d1ad9a288f8394705061519451cf6421828cdf2"}, 318 | {file = "python_fasthtml-0.5.1-py3-none-any.whl", hash = "sha256:ba4d912b9f58dfdd8af04a7e44295a7d2a892a7193a863905712ffc265129de2"}, 319 | ] 320 | 321 | [package.dependencies] 322 | beautifulsoup4 = "*" 323 | fastcore = ">=1.7.2" 324 | fastlite = ">=0.0.9" 325 | httpx = "*" 326 | itsdangerous = "*" 327 | oauthlib = "*" 328 | python-dateutil = "*" 329 | python-multipart = "*" 330 | starlette = ">0.33" 331 | uvicorn = {version = ">=0.30", extras = ["standard"]} 332 | 333 | [package.extras] 334 | dev = ["ipython", "lxml"] 335 | 336 | [[package]] 337 | name = "python-multipart" 338 | version = "0.0.9" 339 | description = "A streaming multipart parser for Python" 340 | optional = false 341 | python-versions = ">=3.8" 342 | files = [ 343 | {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, 344 | {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, 345 | ] 346 | 347 | [package.extras] 348 | dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] 349 | 350 | [[package]] 351 | name = "pyyaml" 352 | version = "6.0.2" 353 | description = "YAML parser and emitter for Python" 354 | optional = false 355 | python-versions = ">=3.8" 356 | files = [ 357 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 358 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 359 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 360 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 361 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 362 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 363 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 364 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 365 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 366 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 367 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 368 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 369 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 370 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 371 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 372 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 373 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 374 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 375 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 376 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 377 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 378 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 379 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 380 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 381 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 382 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 383 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 384 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 385 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 386 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 387 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 388 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 389 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 390 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 391 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 392 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 393 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 394 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 395 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 396 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 397 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 398 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 399 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 400 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 401 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 402 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 403 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 404 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 405 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 406 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 407 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 408 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 409 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 410 | ] 411 | 412 | [[package]] 413 | name = "six" 414 | version = "1.16.0" 415 | description = "Python 2 and 3 compatibility utilities" 416 | optional = false 417 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 418 | files = [ 419 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 420 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 421 | ] 422 | 423 | [[package]] 424 | name = "sniffio" 425 | version = "1.3.1" 426 | description = "Sniff out which async library your code is running under" 427 | optional = false 428 | python-versions = ">=3.7" 429 | files = [ 430 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 431 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 432 | ] 433 | 434 | [[package]] 435 | name = "soupsieve" 436 | version = "2.6" 437 | description = "A modern CSS selector implementation for Beautiful Soup." 438 | optional = false 439 | python-versions = ">=3.8" 440 | files = [ 441 | {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, 442 | {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, 443 | ] 444 | 445 | [[package]] 446 | name = "sqlite-minutils" 447 | version = "3.37.0.post3" 448 | description = "A fork of sqlite-utils with CLI etc removed" 449 | optional = false 450 | python-versions = ">=3.8" 451 | files = [ 452 | {file = "sqlite-minutils-3.37.0.post3.tar.gz", hash = "sha256:d7f8db1581ef4e8e6d507ad8998f3c0ad574c507e36715818017108c0814a1e6"}, 453 | {file = "sqlite_minutils-3.37.0.post3-py3-none-any.whl", hash = "sha256:1990e1d44263914abcb23d8c87163925739b43110923d2263e383cabbb271cc3"}, 454 | ] 455 | 456 | [package.dependencies] 457 | fastcore = "*" 458 | 459 | [[package]] 460 | name = "starlette" 461 | version = "0.38.4" 462 | description = "The little ASGI library that shines." 463 | optional = false 464 | python-versions = ">=3.8" 465 | files = [ 466 | {file = "starlette-0.38.4-py3-none-any.whl", hash = "sha256:526f53a77f0e43b85f583438aee1a940fd84f8fd610353e8b0c1a77ad8a87e76"}, 467 | {file = "starlette-0.38.4.tar.gz", hash = "sha256:53a7439060304a208fea17ed407e998f46da5e5d9b1addfea3040094512a6379"}, 468 | ] 469 | 470 | [package.dependencies] 471 | anyio = ">=3.4.0,<5" 472 | 473 | [package.extras] 474 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 475 | 476 | [[package]] 477 | name = "typing-extensions" 478 | version = "4.12.2" 479 | description = "Backported and Experimental Type Hints for Python 3.8+" 480 | optional = false 481 | python-versions = ">=3.8" 482 | files = [ 483 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 484 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 485 | ] 486 | 487 | [[package]] 488 | name = "uvicorn" 489 | version = "0.30.6" 490 | description = "The lightning-fast ASGI server." 491 | optional = false 492 | python-versions = ">=3.8" 493 | files = [ 494 | {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, 495 | {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, 496 | ] 497 | 498 | [package.dependencies] 499 | click = ">=7.0" 500 | colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} 501 | h11 = ">=0.8" 502 | httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} 503 | python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} 504 | pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} 505 | typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} 506 | uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} 507 | watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} 508 | websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} 509 | 510 | [package.extras] 511 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 512 | 513 | [[package]] 514 | name = "uvloop" 515 | version = "0.20.0" 516 | description = "Fast implementation of asyncio event loop on top of libuv" 517 | optional = false 518 | python-versions = ">=3.8.0" 519 | files = [ 520 | {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996"}, 521 | {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b"}, 522 | {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10"}, 523 | {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae"}, 524 | {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006"}, 525 | {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73"}, 526 | {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037"}, 527 | {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9"}, 528 | {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e"}, 529 | {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756"}, 530 | {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0"}, 531 | {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf"}, 532 | {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d"}, 533 | {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e"}, 534 | {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9"}, 535 | {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab"}, 536 | {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5"}, 537 | {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00"}, 538 | {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba"}, 539 | {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf"}, 540 | {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847"}, 541 | {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b"}, 542 | {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6"}, 543 | {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2"}, 544 | {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95"}, 545 | {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7"}, 546 | {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a"}, 547 | {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541"}, 548 | {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315"}, 549 | {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66"}, 550 | {file = "uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469"}, 551 | ] 552 | 553 | [package.extras] 554 | docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] 555 | test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] 556 | 557 | [[package]] 558 | name = "watchfiles" 559 | version = "0.24.0" 560 | description = "Simple, modern and high performance file watching and code reload in python." 561 | optional = false 562 | python-versions = ">=3.8" 563 | files = [ 564 | {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"}, 565 | {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"}, 566 | {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361"}, 567 | {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3"}, 568 | {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571"}, 569 | {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd"}, 570 | {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a"}, 571 | {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e"}, 572 | {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c"}, 573 | {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188"}, 574 | {file = "watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735"}, 575 | {file = "watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04"}, 576 | {file = "watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428"}, 577 | {file = "watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c"}, 578 | {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43"}, 579 | {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327"}, 580 | {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5"}, 581 | {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61"}, 582 | {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15"}, 583 | {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"}, 584 | {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab"}, 585 | {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec"}, 586 | {file = "watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d"}, 587 | {file = "watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c"}, 588 | {file = "watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633"}, 589 | {file = "watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a"}, 590 | {file = "watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370"}, 591 | {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6"}, 592 | {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b"}, 593 | {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e"}, 594 | {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea"}, 595 | {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f"}, 596 | {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234"}, 597 | {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef"}, 598 | {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968"}, 599 | {file = "watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444"}, 600 | {file = "watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896"}, 601 | {file = "watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418"}, 602 | {file = "watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48"}, 603 | {file = "watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90"}, 604 | {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94"}, 605 | {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e"}, 606 | {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827"}, 607 | {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df"}, 608 | {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab"}, 609 | {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f"}, 610 | {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b"}, 611 | {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18"}, 612 | {file = "watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07"}, 613 | {file = "watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366"}, 614 | {file = "watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318"}, 615 | {file = "watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05"}, 616 | {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c"}, 617 | {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83"}, 618 | {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c"}, 619 | {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b"}, 620 | {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b"}, 621 | {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91"}, 622 | {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b"}, 623 | {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22"}, 624 | {file = "watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1"}, 625 | {file = "watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1"}, 626 | {file = "watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886"}, 627 | {file = "watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f"}, 628 | {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855"}, 629 | {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b"}, 630 | {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430"}, 631 | {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3"}, 632 | {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a"}, 633 | {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9"}, 634 | {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca"}, 635 | {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e"}, 636 | {file = "watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da"}, 637 | {file = "watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f"}, 638 | {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f"}, 639 | {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b"}, 640 | {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4"}, 641 | {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a"}, 642 | {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be"}, 643 | {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5"}, 644 | {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777"}, 645 | {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e"}, 646 | {file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"}, 647 | ] 648 | 649 | [package.dependencies] 650 | anyio = ">=3.0.0" 651 | 652 | [[package]] 653 | name = "websockets" 654 | version = "13.0.1" 655 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 656 | optional = false 657 | python-versions = ">=3.8" 658 | files = [ 659 | {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f"}, 660 | {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c"}, 661 | {file = "websockets-13.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f"}, 662 | {file = "websockets-13.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543"}, 663 | {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d"}, 664 | {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f"}, 665 | {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8"}, 666 | {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b"}, 667 | {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448"}, 668 | {file = "websockets-13.0.1-cp310-cp310-win32.whl", hash = "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3"}, 669 | {file = "websockets-13.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0"}, 670 | {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7"}, 671 | {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4"}, 672 | {file = "websockets-13.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2"}, 673 | {file = "websockets-13.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0"}, 674 | {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e"}, 675 | {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462"}, 676 | {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501"}, 677 | {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418"}, 678 | {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df"}, 679 | {file = "websockets-13.0.1-cp311-cp311-win32.whl", hash = "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f"}, 680 | {file = "websockets-13.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075"}, 681 | {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a"}, 682 | {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956"}, 683 | {file = "websockets-13.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af"}, 684 | {file = "websockets-13.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf"}, 685 | {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c"}, 686 | {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4"}, 687 | {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab"}, 688 | {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d"}, 689 | {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237"}, 690 | {file = "websockets-13.0.1-cp312-cp312-win32.whl", hash = "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185"}, 691 | {file = "websockets-13.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99"}, 692 | {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa"}, 693 | {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231"}, 694 | {file = "websockets-13.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9"}, 695 | {file = "websockets-13.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75"}, 696 | {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553"}, 697 | {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920"}, 698 | {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329"}, 699 | {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7"}, 700 | {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2"}, 701 | {file = "websockets-13.0.1-cp313-cp313-win32.whl", hash = "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb"}, 702 | {file = "websockets-13.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b"}, 703 | {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e"}, 704 | {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2"}, 705 | {file = "websockets-13.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b"}, 706 | {file = "websockets-13.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae"}, 707 | {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f"}, 708 | {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603"}, 709 | {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36"}, 710 | {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a"}, 711 | {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb"}, 712 | {file = "websockets-13.0.1-cp38-cp38-win32.whl", hash = "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4"}, 713 | {file = "websockets-13.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9"}, 714 | {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc"}, 715 | {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63"}, 716 | {file = "websockets-13.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37"}, 717 | {file = "websockets-13.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9"}, 718 | {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97"}, 719 | {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f"}, 720 | {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4"}, 721 | {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0"}, 722 | {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d"}, 723 | {file = "websockets-13.0.1-cp39-cp39-win32.whl", hash = "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58"}, 724 | {file = "websockets-13.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097"}, 725 | {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89"}, 726 | {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad"}, 727 | {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e"}, 728 | {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc"}, 729 | {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333"}, 730 | {file = "websockets-13.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32"}, 731 | {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491"}, 732 | {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f"}, 733 | {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060"}, 734 | {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491"}, 735 | {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026"}, 736 | {file = "websockets-13.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096"}, 737 | {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376"}, 738 | {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83"}, 739 | {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c"}, 740 | {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870"}, 741 | {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5"}, 742 | {file = "websockets-13.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980"}, 743 | {file = "websockets-13.0.1-py3-none-any.whl", hash = "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817"}, 744 | {file = "websockets-13.0.1.tar.gz", hash = "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e"}, 745 | ] 746 | 747 | [metadata] 748 | lock-version = "2.0" 749 | python-versions = "^3.10" 750 | content-hash = "a7fc3965985d8b3d031397b8e060f7f2be9ea2113e39cdb3ead555fc31fbafa5" 751 | -------------------------------------------------------------------------------- /droplet-ssl-with-ci/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fasthtml-digital-ocean" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["John Smith "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | python-fasthtml = "^0.5.1" 11 | 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /droplet/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sesskey 3 | nobs.txt -------------------------------------------------------------------------------- /droplet/README.md: -------------------------------------------------------------------------------- 1 | # Deploying a fastHTML Web App on a DigitalOcean Droplet 2 | 3 | > If you need SSL, automated CI setup and Docker support, check out [this guide](https://github.com/AnswerDotAI/fh-deploy/tree/main/droplet-ssl-with-ci) 4 | 5 | This guide details the process of setting up a $4/month Ubuntu Virtual Machine (Droplet) on DigitalOcean to host a fastHTML web application. It leverages the DigitalOcean API to streamline the creation and configuration of SSH keys and the Droplet itself. 6 | 7 | ### References 8 | - [How to Create a Droplet](https://docs.digitalocean.com/products/droplets/how-to/create/) 9 | - [How to Create a Personal Access Token](https://docs.digitalocean.com/reference/api/create-personal-access-token/) 10 | - [How To Install Python 3 and Set Up a Programming Environment on an Ubuntu 20.04 Server](https://www.digitalocean.com/community/tutorials/how-to-install-python-3-and-set-up-a-programming-environment-on-an-ubuntu-20-04-server) 11 | - [Cómo subir una app con FastAPI a DigitalOcean](https://www.youtube.com/watch?v=ZZNZbnTbodI) 12 | - [DEPLOY a DJANGO app with SQLite database on DIGITALOCEAN (Ubuntu 22.04) -- NO DOCKER](https://www.youtube.com/watch?v=pUG-uNzWAf4) 13 | 14 | ### Setup 15 | Run the commands below in your local machine. 16 | 17 | ```commandline 18 | git clone https://github.com/AnswerDotAI/fh-deploy.git 19 | 20 | cd fh-deploy/droplet 21 | pip install -r requirements.txt 22 | ``` 23 | 24 | ### Run the App Locally 25 | ```commandline 26 | uvicorn main:app --reload 27 | ``` 28 | 29 | ### Deploying to DigitalOcean 30 | 31 | #### Setting Up DigitalOcean 32 | 33 | 1. Create a DigitalOcean [account](https://www.digitalocean.com/). 34 | 2. Create a new Personal Access Token [here](https://cloud.digitalocean.com/account/api/tokens). Select **Custom Scopes**, and only `ssh_key` and `droplet` scopes are needed. 35 | 3. Create a `DIGITALOCEAN_TOKEN` environment variable (e.g. run `export DIGITALOCEAN_TOKEN=YOUR_API_TOKEN`). 36 | 37 | #### Create a SSH Key 38 | 39 | [API Docs](https://docs.digitalocean.com/reference/api/api-reference/#operation/sshKeys_create) 40 | 41 | 1. Generate a new SSH key pair, save them in the specified path and file, adding a passphrase. 42 | 43 | ```commandline 44 | ssh-keygen -f ~/.ssh/YOUR_KEY_FILE -N YOUR_PASSPHRASE 45 | ``` 46 | 47 | 2. Store the public key in an environment variable `PUBLIC_KEY`. 48 | 49 | ```commandline 50 | export PUBLIC_KEY=$(cat ~/.ssh/YOUR_KEY_FILE.pub) 51 | ``` 52 | 53 | 3. Generate the SSH key using this API endpoint and passing the public key that was just created. 54 | 55 | ```curl 56 | curl -X POST \ 57 | -H "Content-Type: application/json" \ 58 | -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ 59 | -d '{"name":"fastHTML SSH Key","public_key":"'"$PUBLIC_KEY"'"}' \ 60 | "https://api.digitalocean.com/v2/account/keys" 61 | ``` 62 | 63 | 4. After the API call, copy the SSH key ID from the response and store it as an environment variable. 64 | 65 | ```commandline 66 | export SSH_KEY_ID=RETURNED_SSH_KEY_ID 67 | ``` 68 | 69 | #### Creating a New Droplet 70 | 71 | [API Docs](https://docs.digitalocean.com/reference/api/api-reference/#operation/droplets_create) 72 | 73 | 1. While the DigitalOcean interface offers Droplet creation, this guide demonstrates the process using the API. Note that the `SSH_KEY_ID` is passed. 74 | 75 | The $4/month Droplet will have the following specs: 76 | 77 | - name: `fastHTML-Droplet` 78 | - region: `nyc1` 79 | - size: `s-1vcpu-512mb-10gb` (1 CPU, 512 MB, 10 GB SSD Disk) 80 | - OS: `Ubuntu 22.04 (LTS) x64` 81 | 82 | ```curl 83 | curl -X POST \ 84 | -H "Content-Type: application/json" \ 85 | -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ 86 | -d '{"name":"fastHTML-Droplet","region":"nyc1","size":"s-1vcpu-512mb-10gb","image":"ubuntu-22-04-x64","ssh_keys":['"$SSH_KEY_ID"']}' \ 87 | "https://api.digitalocean.com/v2/droplets" 88 | ``` 89 | Go to [droplets](https://cloud.digitalocean.com/droplets) and see that the droplet was just created. 90 | 91 | ![](images/01_droplet.PNG) 92 | 93 | 2. Copy and save the IP address as an environment variable `export IP_ADDRESS=DROPLET_IP_ADDRESS` 94 | 95 | 3. Before attempting to SSH into the Droplet, ensure the security of your public SSH key file by setting its permissions appropriately. 96 | 97 | ```commandline 98 | chmod 600 ~/.ssh/YOUR_KEY_FILE.pub 99 | ssh -i ~/.ssh/YOUR_KEY_FILE root@$IP_ADDRESS 100 | ``` 101 | 102 | 4. If everything has been configured correctly, you should now be connected to the Droplet. 103 | 104 | ![](images/02_droplet.PNG) 105 | 106 | #### Adding a User (Recommended) 107 | 108 | This step is recommended but not strictly required. If you choose not to create a dedicated user and continue using the root account, please remember to adjust the file paths mentioned in the following steps. The paths provided assume you're working as a regular user, not as root. 109 | 110 | Creating the new user: 111 | 112 | 1. Create a new user account with a given username. 113 | 2. Add the new user to the sudo group, granting them the ability to run commands with administrative privileges. 114 | 3. Copy the entire contents of the root user's SSH directory (/root/.ssh) to the new user's home directory. 115 | 4. Change the ownership of the copied SSH directory and its contents to the new user. 116 | 5. Disconnect from the remote server. 117 | 118 | ```commandline 119 | adduser YOUR_USERNAME 120 | adduser YOUR_USERNAME sudo 121 | cp -Rfv /root/.ssh /home/YOUR_USERNAME/ 122 | chown -Rfv YOUR_USERNAME:YOUR_USERNAME /home/YOUR_USERNAME/.ssh 123 | exit 124 | ``` 125 | 126 | #### SSH into the Server as the New User 127 | 128 | ```commandline 129 | ssh -i ~/.ssh/YOUR_KEY_FILE YOUR_USERNAME@$IP_ADDRESS 130 | ``` 131 | You should now be connected into the Droplet, with your new user. 132 | 133 | ![](images/05_newuser.PNG) 134 | 135 | #### Configuring python in the Droplet 136 | 137 | 1. After the SSH connection is established, start configuring the remote server. 138 | 139 | These commands prepare your Ubuntu system for Python development and web application deployment by updating packages, installing necessary tools and libraries, and setting up a web server (Nginx). 140 | 141 | ```commandline 142 | sudo apt-get update 143 | sudo apt update 144 | sudo apt -y upgrade 145 | sudo apt install -y python3-pip 146 | sudo apt install -y build-essential libssl-dev libffi-dev python3-dev python3-setuptools python3-venv 147 | sudo apt install nginx 148 | ``` 149 | 150 | - If asked, reboot the server with `sudo reboot` 151 | - Now, if you navigate to `http://IP_ADDRESS`, the "Welcome to nginx!" page must be shown. 152 | 153 | ![](images/04_nginx.PNG) 154 | 155 | #### Setup the Web App 156 | 157 | 1. Clone the repository 158 | 159 | ```commandline 160 | mkdir project 161 | cd project 162 | python3 -m venv env 163 | source env/bin/activate 164 | git clone https://github.com/AnswerDotAI/fh-deploy.git 165 | cd fh-deploy/droplet 166 | pip install -r requirements.txt 167 | ``` 168 | 169 | You should see something like this in your terminal: 170 | ``` 171 | (env) YOUR_USERNAME@fastHTML-Droplet:~/project/fh-deploy/droplet$ 172 | ``` 173 | 174 | #### Configuring Nginx 175 | 176 | 1. Create a Nginx server block configuration file named `fasthtml`. 177 | 178 | ```commandline 179 | sudo nano /etc/nginx/sites-available/fasthtml 180 | ``` 181 | 182 | 2. Add the following text to the file. 183 | ``` 184 | server { 185 | server_name DROPLET_IP_ADDRESS; 186 | location / { 187 | include proxy_params; 188 | proxy_pass http://127.0.0.1:8000; 189 | } 190 | } 191 | ``` 192 | 193 | This configuration will tell Nginx to listen for requests to your Droplet's IP address and proxy those requests to a local server running on port 8000. 194 | 195 | 3. Nginx needs a symbolic link to the configuration files that are currently active. 196 | 197 | ```commandline 198 | sudo ln -s /etc/nginx/sites-available/fasthtml /etc/nginx/sites-enabled/ 199 | ``` 200 | 201 | 4. Restart Nginx and the check its status. 202 | ```commandline 203 | sudo systemctl restart nginx.service 204 | systemctl status nginx.service 205 | ``` 206 | 207 | #### Install Gunicorn 208 | ```commandline 209 | pip install gunicorn 210 | gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app 211 | ``` 212 | You should now see: 213 | 214 | ![](images/03_web.PNG) 215 | 216 | #### Run Gunicorn as a service 217 | 218 | 1. Create a file called `fasthtml.service` in the folder `/etc/systemd/system`. 219 | 220 | ```commandline 221 | sudo nano /etc/systemd/system/fasthtml.service 222 | ``` 223 | 224 | 2. Add the following text to the file: 225 | ``` 226 | [Unit] 227 | Description=Your Description 228 | 229 | [Service] 230 | WorkingDirectory=/home/YOUR_USERNAME/project/fh-deploy/droplet 231 | Environment="PATH=/home/YOUR_USERNAME/project/env/bin" 232 | ExecStart=/root/project/env/bin/gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app 233 | 234 | [Install] 235 | WantedBy=multi-user.target 236 | ``` 237 | 238 | 3. Start the service. 239 | ```commandline 240 | sudo systemctl start fasthtml.service 241 | ``` 242 | 243 | And that's it, you should see your web app. 244 | 245 | To see the status of the service, run `sudo systemctl status fasthtml.service` 246 | 247 | #### Destroying the Droplet 248 | 249 | Exit from the remote server by executing `exit` in the terminal. 250 | 251 | 1. List droplets and copy the id. 252 | 253 | ```curl 254 | curl -X GET \ 255 | -H "Content-Type: application/json" \ 256 | -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ 257 | "https://api.digitalocean.com/v2/droplets" 258 | ``` 259 | 260 | ``` 261 | export DROPLET_ID=YOUR_DROPLET_ID 262 | ``` 263 | 264 | 2. Delete droplet. 265 | 266 | ```curl 267 | curl -X DELETE \ 268 | -H "Content-Type: application/json" \ 269 | -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ 270 | "https://api.digitalocean.com/v2/droplets/$DROPLET_ID" 271 | ``` -------------------------------------------------------------------------------- /droplet/images/00_droplet.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fh-deploy/773d353199c9348afaec11293b46f3b7242012dc/droplet/images/00_droplet.PNG -------------------------------------------------------------------------------- /droplet/images/01_droplet.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fh-deploy/773d353199c9348afaec11293b46f3b7242012dc/droplet/images/01_droplet.PNG -------------------------------------------------------------------------------- /droplet/images/02_droplet.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fh-deploy/773d353199c9348afaec11293b46f3b7242012dc/droplet/images/02_droplet.PNG -------------------------------------------------------------------------------- /droplet/images/03_web.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fh-deploy/773d353199c9348afaec11293b46f3b7242012dc/droplet/images/03_web.PNG -------------------------------------------------------------------------------- /droplet/images/04_nginx.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fh-deploy/773d353199c9348afaec11293b46f3b7242012dc/droplet/images/04_nginx.PNG -------------------------------------------------------------------------------- /droplet/images/05_newuser.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fh-deploy/773d353199c9348afaec11293b46f3b7242012dc/droplet/images/05_newuser.PNG -------------------------------------------------------------------------------- /droplet/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app, rt = fast_app() 4 | 5 | @rt("/") 6 | def get(): 7 | return Titled("FastHTML", P("FastHTML on a Digital Ocean's Droplet!")) 8 | 9 | #serve() 10 | -------------------------------------------------------------------------------- /droplet/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml -------------------------------------------------------------------------------- /fly/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim-bullseye 2 | 3 | ENV PYTHONUNBUFFERED True 4 | ENV APP_HOME /app 5 | WORKDIR $APP_HOME 6 | COPY . ./ 7 | 8 | RUN pip install --no-cache-dir -r requirements.txt 9 | 10 | CMD ["python", "main.py", "--host", "0.0.0.0", "--port", "8080"] -------------------------------------------------------------------------------- /fly/README.md: -------------------------------------------------------------------------------- 1 | ### Setup 2 | 3 | Run the commands below on your local machine. 4 | 5 | ```commandline 6 | git clone https://github.com/AnswerDotAI/fh-deploy.git 7 | cd fly 8 | pip install -r requirements.txt 9 | ``` 10 | 11 | ### Run the app locally 12 | 13 | ```commandline 14 | python main.py 15 | ``` 16 | 17 | ### Deploying to Fly.io 18 | 19 | 1. Create a Fly.io [account](https://fly.io/) 20 | 2. install the [CLI](https://fly.io/docs/flyctl/install/) 21 | 3. run the command `fly launch` 22 | 4. answer "n" when asked: `Do you want to tweak these settings before proceeding? (y/N)` 23 | 24 | Once your app is deployed, the CLI will display the endpoint. 25 | -------------------------------------------------------------------------------- /fly/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml import fastapp as fh 2 | 3 | app, rt = fh.fast_app() 4 | 5 | 6 | @rt("/") 7 | def get(): 8 | return fh.Titled("FastHTML", fh.P("FastHTML on Fly.io!")) 9 | 10 | 11 | if __name__ == "__main__": 12 | fh.serve(port=8080) 13 | -------------------------------------------------------------------------------- /fly/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml -------------------------------------------------------------------------------- /heroku/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sesskey -------------------------------------------------------------------------------- /heroku/Procfile: -------------------------------------------------------------------------------- 1 | web: python main.py -------------------------------------------------------------------------------- /heroku/README.md: -------------------------------------------------------------------------------- 1 | ### Setup 2 | 3 | Run the commands below on your local machine. 4 | 5 | ```commandline 6 | git clone https://github.com/AnswerDotAI/fh-deploy.git 7 | cd heroku 8 | pip install -r requirements.txt 9 | ``` 10 | 11 | ### Run the app locally 12 | 13 | ```commandline 14 | python main.py 15 | ``` 16 | ### Deploying to Heroku 17 | Create a Heroku [account](https://signup.heroku.com/), install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) and then run the commands below on your local machine: 18 | 19 | ```commandline 20 | heroku login 21 | heroku create 22 | git subtree push --prefix heroku heroku main 23 | ``` 24 | 25 | > [!NOTE] 26 | > `git subtree` is only used to deploy the `heroku` subfolder, on projects where your FastHTML is the main app you can just use `git push heroku main` 27 | 28 | For more information on how to deploy Python applications to Heroku refer to the [Getting Started on Heroku with Python](https://devcenter.heroku.com/articles/getting-started-with-python) docs. 29 | -------------------------------------------------------------------------------- /heroku/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.fastapp import * 2 | 3 | app, rt = fast_app() 4 | 5 | @rt("/") 6 | def get(): 7 | return Titled("FastHTML", P("FastHTML on Heroku!")) 8 | 9 | serve() 10 | -------------------------------------------------------------------------------- /heroku/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml -------------------------------------------------------------------------------- /huggingface/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sesskey -------------------------------------------------------------------------------- /huggingface/README.md: -------------------------------------------------------------------------------- 1 | ### Setup 2 | Run the commands below on your local machine. 3 | ```commandline 4 | git clone https://github.com/AnswerDotAI/fh-deploy.git 5 | cd huggingface 6 | pip install -r requirements.txt 7 | ``` 8 | 9 | ### Run the app locally 10 | ```commandline 11 | python main.py 12 | ``` 13 | ### Deploying to HuggingFace 14 | 1. Create a Huggingface [account](https://huggingface.co/). 15 | 2. Go to your account settings and create an access token with write access. Keep this token safe and don't share it. 16 | 3. Create a `HF_TOKEN` environment variable (e.g. run `export HF_TOKEN=YOUR_API_TOKEN`). 17 | 4. HuggingFace Spaces are [configured](https://huggingface.co/docs/hub/spaces-config-reference#spaces-configuration-reference) using a YAML block at the top of the app's README.md. To configure the HuggingFace Space for your app, add the YAML below to the top of **THIS** README.md file. 18 | ```yaml 19 | --- 20 | title: FastHTML on HuggingFace! 21 | emoji: 🤗 22 | colorFrom: indigo 23 | colorTo: green 24 | sdk: docker 25 | pinned: false 26 | --- 27 | ``` 28 | 29 | 5. Run `fh_hf_deploy NAME_OF_YOUR_APP` 30 | 31 | Note: 32 | - By default, your app will be public. To create a private app use `--private true` when running `fh_hf_deploy`. 33 | - For more information on how your app is being deployed on HuggingFace visit the fasthtml-hf [repo](https://github.com/AnswerDotAI/fasthtml-hf) -------------------------------------------------------------------------------- /huggingface/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml_hf import setup_hf_backup 2 | from fasthtml.fastapp import * 3 | 4 | app, rt = fast_app() 5 | 6 | @rt("/") 7 | def get(): 8 | return Titled("FastHTML", P("FastHTML on HuggingFace 🤗!")) 9 | 10 | setup_hf_backup(app) 11 | serve() 12 | -------------------------------------------------------------------------------- /huggingface/requirements.txt: -------------------------------------------------------------------------------- 1 | fasthtml-hf 2 | python-fasthtml -------------------------------------------------------------------------------- /modal/README.md: -------------------------------------------------------------------------------- 1 | ## What is this? 2 | 3 | [Modal](https://modal.com) is a serverless computing platform for Python. 4 | 5 | This folder demonstrates a simple pattern for developing a FastHTML app locally and deploying it to Modal, 6 | including tips for debugging the deployed app. 7 | Thanks to the power of open standards like ASGI, the integration between the two is seamless! 8 | 9 | The code in `app.py` is carefully documented to help you re-use this template for serving your own apps. 10 | 11 | 12 | ## Setup 13 | 14 | Set up a Python environment and then run the commands below on your local machine. 15 | 16 | ```bash 17 | git clone https://github.com/AnswerDotAI/fh-deploy.git # clone the repo 18 | cd modal # enter this directory 19 | pip install -r requirements-dev.txt # install the development dependencies 20 | ``` 21 | 22 | ## Develop the app locally 23 | 24 | FastHTML includes a local development server with automatic fast reloads for quick iteration. 25 | 26 | Run it with 27 | 28 | ```bash 29 | python app.py 30 | ``` 31 | 32 | and navigate to the URL shown in the terminal, which should begin with `localhost` or `127.0.0.1`. 33 | 34 | Changes to the code will be reflected in the browser after you save the file. 35 | 36 | This is great for working in a tight loop on core functionality. 37 | 38 | You can stop the server with Ctrl-C. 39 | 40 | ## Develop the app on Modal 41 | 42 | Modal can also run a web server in development mode. 43 | 44 | If you haven't used Modal on the machine before, you'll need to obtain a token. 45 | 46 | You can get one (and create an account if needed), by running 47 | 48 | ```bash 49 | modal setup 50 | ``` 51 | 52 | and following the instructions on the screen and in your browser. 53 | 54 | Once that's set up, you can run the development server with 55 | 56 | ```bash 57 | modal serve app.py 58 | ``` 59 | 60 | and navigate to the URL in the terminal, which should end in `-dev.modal.run`. 61 | 62 | Changes to the code (or requirements!) will trigger a new development deployment, 63 | which should finish in seconds. This server can also be stopped with Ctrl-C. 64 | 65 | This is useful for debugging issues that appear only in the production environment, 66 | like issues with accessing secrets or issues with GPU acceleration. 67 | 68 | You can also spin up a temporary shell to inspect the production environment: 69 | 70 | ```bash 71 | modal shell app.py 72 | cd root # enter the root directory, where `app.py` is located 73 | ``` 74 | 75 | ## Deploy the app on Modal 76 | 77 | Deployment works much the same way, but creates a permanent server. 78 | 79 | Deploy this server with 80 | 81 | ```bash 82 | modal deploy app.py 83 | ``` 84 | 85 | and navigate to the URL that appears in the terminal, which should end in `.modal.run`. 86 | It will be the same URL as your development server, but without the `-dev`. 87 | 88 | Instead of shutting down when you close the terminal, it will continue to run 89 | until you explicitly stop the app in the [Modal dashboard](https://modal.com/apps) 90 | or via the [command line](https://modal.com/docs/reference/cli/app). 91 | 92 | Unlike deployment to a traditional cloud server, this doesn't allocate a machine running your server. 93 | Instead, your server is dynamically run on demand in Modal's infrastructure. 94 | This can reduce costs for apps with highly variable traffic and enables easy scaling up and down of instances. 95 | 96 | The catch is that there will be a second of extra latency when a user interacts with an app with no running instances. 97 | You can read more about that [here](https://modal.com/docs/guide/cold-start). 98 | 99 | Modal includes $30/month in free credits, 100 | which is more than enough to host this application indefinitely. 101 | Pricing information is [here](https://modal.com/pricing). 102 | -------------------------------------------------------------------------------- /modal/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import fasthtml.common as fh 4 | import modal 5 | 6 | # create the fasthtml_app object and its rt decorator 7 | fasthtml_app, rt = fh.fast_app() 8 | 9 | 10 | # define the app first, e.g. adding routes 11 | # FastHTML uses decorators to add routes: 12 | @rt("/") 13 | def get(): 14 | return fh.Titled("FastHTML", fh.P("FastHTML on️ Modal!")) 15 | 16 | 17 | if __name__ == "__main__": # if invoked with `python`, run locally 18 | fh.serve(app="fasthtml-app") 19 | else: # create a modal app, which can be imported in another file or used with modal commands as in README 20 | app = modal.App(name="fasthtml-app") 21 | 22 | # modal uses decorators to define infrastructure and deployments 23 | @app.function( # here's where you can attach GPUs, define concurrency limits, etc. 24 | image=modal.Image.debian_slim().pip_install_from_requirements( # see https://modal.com/docs/guide/custom-container for more ways to install dependencies 25 | Path(__file__).parent / "requirements.txt" 26 | ), 27 | allow_concurrent_inputs=1000, # async functions can handle multiple inputs 28 | ) 29 | @modal.asgi_app() # add this decorator to a function that returns your fastHTML app to make it deployable on Modal 30 | def serve(): 31 | return fasthtml_app 32 | -------------------------------------------------------------------------------- /modal/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # requirements to develop locally and deploy to Modal 2 | -r requirements.txt 3 | modal 4 | -------------------------------------------------------------------------------- /modal/requirements.txt: -------------------------------------------------------------------------------- 1 | # requirements for the app itself 2 | python-fasthtml 3 | -------------------------------------------------------------------------------- /railway/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sesskey 3 | -------------------------------------------------------------------------------- /railway/README.md: -------------------------------------------------------------------------------- 1 | ### Setup 2 | Run the commands below on your local machine. 3 | ```commandline 4 | git clone https://github.com/AnswerDotAI/fh-deploy.git 5 | cd railway 6 | pip install -r requirements.txt 7 | ``` 8 | 9 | ### Run the app locally 10 | ```commandline 11 | python main.py 12 | ``` 13 | ### Deploying to Railway 14 | - create a Railway [account](https://railway.app/) and signup to the Hobby plan. 15 | - install the Railway [CLI](https://docs.railway.app/guides/cli#installing-the-cli). 16 | - run `railway login` to log in to your Railway account. 17 | - run `fh_railway_deploy YOUR_APP_NAME`. 18 | 19 | ⚠️ Your app's entry point must be located in a `main.py` file for this to work. 20 | 21 | ### Supplementary Info. 22 | `fh_railway_deploy` runs the following commands behind the scenes for you: 23 | 24 | ```bash 25 | railway init -n 26 | railway up -c 27 | railway domain 28 | railway link ... 29 | railway volume add -m /app/data 30 | ``` 31 | 32 | It handles automatically linking your current app to a railway project, setting up all the environment variables such as the port to listen on and setting up a `requirements.txt` if you haven't one already. 33 | 34 | ### Customizing your Domain Name 35 | 36 | Railway automatically assigns your website a unique domain name such as `quickdraw-production.up.railway.app`. However, if you want to use your own that you've purchased through services like [GoDaddy](https://www.godaddy.com/) or [Squarespace Domains](https://domains.squarespace.com/) and have users be able to navigate to your site using that domain, you'll need to configure it both in your domain registration service and in Railway. Railway has put together a nice tutorial for setting it up [here](https://docs.railway.app/guides/public-networking#custom-domains). 37 | 38 | Make sure to notice the difference between setting up a regular domain and a subdomain. Regular domains don't have any prefixes before the main site name such as `example.com` and is setup differently from a subdomain which might look like `subdomain.example.com`. Make sure to follow your domain registration service's documentation on how to set these types up. -------------------------------------------------------------------------------- /railway/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.fastapp import * 2 | 3 | app, rt = fast_app() 4 | 5 | @rt("/") 6 | def get(): 7 | return Titled("FastHTML", P("FastHTML on Railway!")) 8 | 9 | serve() 10 | -------------------------------------------------------------------------------- /railway/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml -------------------------------------------------------------------------------- /replit/.replit: -------------------------------------------------------------------------------- 1 | modules = ["python-3.11"] 2 | entrypoint = "main.py" 3 | run = "python main.py" 4 | 5 | [[ports]] 6 | localPort = 5001 7 | externalPort = 80 8 | -------------------------------------------------------------------------------- /replit/README.md: -------------------------------------------------------------------------------- 1 | # Building and deploying on Replit 2 | 3 |
4 | 5 |
6 | 7 | 8 | Replit is a cloud-based development and deployment platform that allows you to build and deploy in a single, integrated environment. 9 | It comes with tools like [secrets management](https://docs.replit.com/programming-ide/storing-sensitive-information-environment-variables) and [AI assisted coding](https://docs.replit.com/programming-ide/ai-code-completion), and has good Python, JS, HTML, and CSS support for FastHTML apps. Replit also has cloud services like [object storage](https://docs.replit.com/storage/replit-database), [embedded Postgres](https://docs.replit.com/hosting/databases/postgresql-database), and [key-value databases](https://docs.replit.com/hosting/databases/replit-database). 10 | 11 |
12 | 13 |
14 | 15 | ## Setup 16 | 17 | To get started on Replit, follow these steps: 18 | 19 | 1. Create a Replit [account](https://replit.com/), 20 | 2. Fork this Replit [template](https://replit.com/@jh151/FastHTML?v=1). 21 | 3. To run the app click `Run` in the nav bar. 22 | 4. Refresh the browser window (Replit will show errors in the file until this is done, since running the app installs the needed libraries, and Replit doesn't auto-refresh). 23 | 24 | A web view pane will open up with your application. Edit the code in the editor to see changes in real time. 25 | 26 | **An important note:** because Replit is a cloud-based editor, the [development URL](https://docs.replit.com/additional-resources/add-a-made-with-replit-badge-to-your-webview#what-is-the-webview) is accessible from any device (while your Repl is running). That means you can test your app out on desktop and mobile simultaneously. 27 | 28 |
29 | 30 |
31 | 32 | ## Deployment 33 | 34 | Deployment on Replit can be done in a few steps: 35 | 36 | 1. Click `Deploy` in the top navigation bar of your Repl. 37 | 2. Choose a deployment type: 38 | - For frontend apps, select "Autoscale" deployments. These automatically scale based on traffic. 39 | - For services requiring continuous execution (e.g., backends, APIs, bots), choose "Reserved VM" deployments. 40 | 3. Select a custom subdomain for your app or use the auto-generated one. 41 | 4. Configure your deployment settings, including environment variables if needed. 42 | 5. Review and confirm your deployment configuration. 43 | 44 | Important notes: 45 | - You'll need to add a payment method to deploy your app. 46 | - Additional options can be configured in the `.replit` file present in your project, note that you may have to reveal hidden files if you can't see it. For more information on configuring the `.replit` file, refer to the [docs](https://docs.replit.com/programming-ide/configuring-repl). 47 | - For detailed instructions and advanced configuration options, refer to the [official Replit deployment documentation](https://docs.replit.com/hosting/deployments/about-deployments). 48 | 49 | For a sample Replit app that makes use of the key-value database, go [here](https://replit.com/@matt/FastHTML-guestbook?v=1#README.md) or check out the live version [here](https://guestbook.mattpalmer.io) and follow the same instructions. 50 | 51 | ### Custom Domains 52 | 53 | Replit automatically assigns your website a unique domain name such as `your-app-name-your-username.replit.app`. However, if you want to use your own domain that you've purchased through services like [CloudFlare](https://www.cloudflare.com/) or [Name-cheap](https://www.namecheap.com/), you can configure it in your Replit deployment settings. Here's how: 54 | 55 | 1. Go to your Repl's "Deployment" tab. 56 | 2. Click on "Configure custom domain". 57 | 3. Enter your custom domain name and follow the instructions to set up DNS records with your domain provider. 58 | 59 | Make sure to notice the difference between setting up a root domain and a subdomain. Root domains don't have any prefixes before the main site name (e.g., `example.com`), while subdomains do (e.g., `subdomain.example.com`). The setup process might differ slightly for each. Always refer to your domain registrar's documentation for specific instructions on how to set up DNS records for your custom domain. 60 | 61 | For more detailed information on setting up custom domains in Replit, you can refer to their official documentation [here](https://docs.replit.com/hosting/deployments/custom-domains). 62 | -------------------------------------------------------------------------------- /replit/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.fastapp import * 2 | 3 | app, rt = fast_app() 4 | 5 | @rt("/") 6 | def get(): 7 | return Titled("FastHTML", P("FastHTML on Replit!")) 8 | 9 | serve() 10 | -------------------------------------------------------------------------------- /replit/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml -------------------------------------------------------------------------------- /vercel/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sesskey 3 | .vercel 4 | -------------------------------------------------------------------------------- /vercel/README.md: -------------------------------------------------------------------------------- 1 | ### Setup 2 | Run the commands below on your local machine. 3 | ```commandline 4 | git clone https://github.com/AnswerDotAI/fh-deploy.git 5 | cd vercel 6 | pip install -r requirements.txt 7 | ``` 8 | 9 | ### Run the app locally 10 | ```commandline 11 | python main.py 12 | ``` 13 | ### Deploying to Vercel 14 | Create a Vercel [account](https://vercel.com/) and then run the commands below on your local machine. 15 | 16 | ```commandline 17 | npm install -g vercel 18 | vercel login 19 | vercel --prod 20 | ``` 21 | 22 | If you would prefer to deploy to Vercel using a git integration, see this guide for more [info](https://vercel.com/docs/deployments/git). -------------------------------------------------------------------------------- /vercel/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.fastapp import * 2 | 3 | app, rt = fast_app() 4 | 5 | @rt("/") 6 | def get(): 7 | return Titled("FastHTML", P("FastHTML on Vercel!")) 8 | 9 | serve() 10 | -------------------------------------------------------------------------------- /vercel/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml --------------------------------------------------------------------------------