├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples └── fastapi │ └── task-management │ ├── generated │ ├── gemini-pro │ │ ├── .qwikcrud.json.lock │ │ ├── README.md │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── crud.py │ │ │ ├── db.py │ │ │ ├── deps.py │ │ │ ├── endpoints │ │ │ │ ├── __init__.py │ │ │ │ ├── project.py │ │ │ │ ├── task.py │ │ │ │ └── user.py │ │ │ ├── enums.py │ │ │ ├── main.py │ │ │ ├── models.py │ │ │ ├── pre_start.py │ │ │ ├── schemas.py │ │ │ ├── settings.py │ │ │ └── storage.py │ │ ├── requirements.txt │ │ ├── static │ │ │ └── css │ │ │ │ └── style.css │ │ └── templates │ │ │ └── index.html │ └── openai │ │ ├── .qwikcrud.json.lock │ │ ├── README.md │ │ ├── app │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── crud.py │ │ ├── db.py │ │ ├── deps.py │ │ ├── endpoints │ │ │ ├── __init__.py │ │ │ ├── project.py │ │ │ ├── task.py │ │ │ └── user.py │ │ ├── enums.py │ │ ├── main.py │ │ ├── models.py │ │ ├── pre_start.py │ │ ├── schemas.py │ │ ├── settings.py │ │ └── storage.py │ │ ├── requirements.txt │ │ ├── static │ │ └── css │ │ │ └── style.css │ │ └── templates │ │ └── index.html │ └── prompt ├── pyproject.toml ├── qwikcrud ├── __init__.py ├── cli.py ├── generator.py ├── helpers.py ├── logger.py ├── prompts │ └── system ├── provider │ ├── __init__.py │ ├── base.py │ ├── dummy.py │ ├── google.py │ └── openai.py ├── schemas.py ├── settings.py └── templates │ └── fastapi │ ├── README.md.j2 │ ├── app │ ├── __init__.py.j2 │ ├── admin.py.j2 │ ├── crud.py.j2 │ ├── db.py.j2 │ ├── deps.py.j2 │ ├── endpoints │ │ ├── __init__.py.j2 │ │ └── template.py.j2 │ ├── enums.py.j2 │ ├── main.py.j2 │ ├── models.py.j2 │ ├── pre_start.py.j2 │ ├── schemas.py.j2 │ ├── settings.py.j2 │ └── storage.py.j2 │ ├── requirements.txt.j2 │ ├── static │ └── css │ │ └── style.css │ └── templates │ └── index.html └── tests ├── __init__.py ├── dummy.json ├── test_app.py └── test_helpers.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [ published ] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | publish: 12 | name: "Publish release" 13 | runs-on: "ubuntu-latest" 14 | permissions: 15 | # this permission is mandatory for trusted publishing 16 | id-token: write 17 | steps: 18 | - uses: "actions/checkout@v4" 19 | - uses: "actions/setup-python@v4" 20 | with: 21 | python-version: "3.10" 22 | cache: "pip" 23 | - name: Install Dependencies 24 | run: pip install hatch 25 | - name: Build 26 | run: hatch build 27 | - name: Publish 28 | uses: pypa/gh-action-pypi-publish@release/v1 -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test suite 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | tests: 12 | name: "Python ${{ matrix.python-version }}" 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: [ "3.9", "3.10", "3.11", "3.12" ] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | cache: "pip" 25 | - name: Install Dependencies 26 | run: pip install hatch 27 | - name: Lint 28 | run: hatch run lint 29 | - name: Test suite 30 | run: hatch run test -------------------------------------------------------------------------------- /.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/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | /gen 162 | *.sqlite 163 | assets/ -------------------------------------------------------------------------------- /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 | # qwikcrud 2 | 3 | [![PyPI - Version](https://img.shields.io/pypi/v/qwikcrud.svg)](https://pypi.org/project/qwikcrud) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qwikcrud.svg)](https://pypi.org/project/qwikcrud) 5 | 6 | ----- 7 | 8 | `qwikcrud` is a powerful command-line tool designed to enhance your backend development experience by automating the 9 | generation of comprehensive REST APIs and admin interfaces. Say goodbye to the tedious task of 10 | writing repetitive CRUD (Create, Read, Update, Delete) endpoints when starting a new project, allowing developers to 11 | concentrate on the core business logic and functionality. 12 | 13 | > [!WARNING] 14 | > The generated application is not ready for production use. Additional steps are required to 15 | > set up a secure and production-ready environment. 16 | 17 | 18 | [![qwikcrud demo](https://github.com/jowilf/qwikcrud/assets/31705179/fc010d41-597c-4ab7-a0ad-22570ba3b182)](https://youtu.be/XYuLDk0bjQA "qwikcrud demo - A restaurant management app") 19 | 20 | ## Table of Contents 21 | 22 | * [Installation](#installation) 23 | * [Quickstart](#quickstart) 24 | * [Environment variables](#environment-variables) 25 | * [Usage](#usage) 26 | * [Generated Application stack](#generated-application-stack) 27 | * [Roadmap](#roadmap) 28 | * [Contributing](#contributing) 29 | * [Acknowledgments](#acknowledgments) 30 | * [License](#license) 31 | 32 | ## Installation 33 | 34 | ```shell 35 | pip install qwikcrud 36 | ``` 37 | 38 | ## Quickstart 39 | 40 | ### Environment variables 41 | 42 | Before running the command-line tool, ensure the following environment variables are configured: 43 | 44 | #### Google 45 | 46 | ```shell 47 | export GOOGLE_API_KEY="your_google_api_key" 48 | ``` 49 | 50 | #### OpenAI 51 | 52 | ```shell 53 | export OPENAI_API_KEY="your_openai_api_key" 54 | export OPENAI_MODEL="your_openai_model" # Defaults to "gpt-3.5-turbo-1106" 55 | ``` 56 | 57 | ### Usage 58 | 59 | To generate your application, open your terminal, run the following command and follow the instructions: 60 | 61 | #### Google 62 | 63 | ```shell 64 | qwikcrud -o output_dir 65 | ``` 66 | 67 | #### OpenAI 68 | 69 | ```shell 70 | qwikcrud -o output_dir --ai openai 71 | ``` 72 | 73 | ### Generated Application stack 74 | 75 | - [FastAPI](https://fastapi.tiangolo.com/) 76 | - [SQLAlchemy v2](https://www.sqlalchemy.org/) 77 | - [Pydantic v2](https://docs.pydantic.dev/latest/) 78 | - [Starlette-admin](https://github.com/jowilf/starlette-admin) 79 | - [SQLAlchemy-file](https://github.com/jowilf/sqlalchemy-file) 80 | 81 | ### Examples 82 | 83 | - Task 84 | Management ([prompt](./examples/fastapi/task-management/prompt), [generated app](./examples/fastapi/task-management/generated)) 85 | 86 | ## Roadmap 87 | 88 | `qwikcrud` is designed to support various frameworks and AI providers. Here's an overview of what has been accomplished 89 | and 90 | what is planned for the future: 91 | 92 | ### Frameworks 93 | 94 | - [x] FastAPI + SQLAlchemy 95 | - [x] Restful APIs 96 | - [x] Admin interfaces 97 | - [ ] Authentication 98 | - [ ] FastAPI + Beanie 99 | - [ ] Spring Boot 100 | 101 | ### AI providers 102 | 103 | - [x] Google (_default_) 104 | - [x] OpenAI 105 | - [ ] Anthropic 106 | - [ ] Ollama (_self-hosted LLMs_) 107 | 108 | ## Pricing 109 | 110 | `qwikcrud` makes one API call per prompt and add a system prompt of around 900 tokens to 111 | your prompt. 112 | 113 | - **Google**: Currently free. 114 | - **OpenAI**: With the default gpt-3.5-turbo model, each app generation costs approximately $0.003. The exact cost can 115 | vary slightly based on the model selected and the output length 116 | 117 | ## Contributing 118 | 119 | Contributions are welcome and greatly appreciated! If you have ideas for improvements or encounter issues, please feel 120 | free to submit a pull request or open an issue. 121 | 122 | ## Acknowledgments 123 | 124 | - The FastAPI + SQLAlchemy template is inspired by the excellent work 125 | in [full-stack-fastapi-postgresql](https://github.com/tiangolo/full-stack-fastapi-postgresql) 126 | by [Sebastian Ramirez (tiangolo)]. 127 | 128 | ## License 129 | 130 | ``qwikcrud`` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license. 131 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/.qwikcrud.json.lock: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Task Manager", 3 | "description": "An application for managing tasks and collaboration.", 4 | "entities": [ 5 | { 6 | "name": "User", 7 | "fields": [ 8 | { 9 | "name": "id", 10 | "type": "ID" 11 | }, 12 | { 13 | "name": "username", 14 | "type": "String", 15 | "constraints": { 16 | "unique": true, 17 | "max_length": 50 18 | } 19 | }, 20 | { 21 | "name": "email", 22 | "type": "Email", 23 | "constraints": { 24 | "unique": true, 25 | "max_length": 100 26 | } 27 | }, 28 | { 29 | "name": "password", 30 | "type": "String", 31 | "constraints": { 32 | "min_length": 8, 33 | "max_length": 100 34 | } 35 | }, 36 | { 37 | "name": "first_name", 38 | "type": "String", 39 | "constraints": { 40 | "max_length": 50 41 | } 42 | }, 43 | { 44 | "name": "last_name", 45 | "type": "String", 46 | "constraints": { 47 | "max_length": 50 48 | } 49 | }, 50 | { 51 | "name": "avatar", 52 | "type": "Image" 53 | } 54 | ] 55 | }, 56 | { 57 | "name": "Project", 58 | "fields": [ 59 | { 60 | "name": "id", 61 | "type": "ID" 62 | }, 63 | { 64 | "name": "name", 65 | "type": "String", 66 | "constraints": { 67 | "unique": true, 68 | "max_length": 100 69 | } 70 | }, 71 | { 72 | "name": "description", 73 | "type": "Text" 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "Task", 79 | "fields": [ 80 | { 81 | "name": "id", 82 | "type": "ID" 83 | }, 84 | { 85 | "name": "task_name", 86 | "type": "String", 87 | "constraints": { 88 | "max_length": 100 89 | } 90 | }, 91 | { 92 | "name": "description", 93 | "type": "Text" 94 | }, 95 | { 96 | "name": "due_date", 97 | "type": "Date" 98 | }, 99 | { 100 | "name": "start_date", 101 | "type": "Date" 102 | }, 103 | { 104 | "name": "status", 105 | "type": "Enum", 106 | "constraints": { 107 | "allowed_values": [ 108 | "Open", 109 | "In Progress", 110 | "Completed" 111 | ] 112 | } 113 | }, 114 | { 115 | "name": "instruction_file", 116 | "type": "File", 117 | "constraints": { 118 | "mime_types": [ 119 | "application/pdf", 120 | "application/msword", 121 | "text/plain" 122 | ] 123 | } 124 | } 125 | ] 126 | } 127 | ], 128 | "relations": [ 129 | { 130 | "name": "User_Project", 131 | "type": "MANY_TO_MANY", 132 | "from": "User", 133 | "to": "Project", 134 | "field_name": "projects", 135 | "backref_field_name": "users" 136 | }, 137 | { 138 | "name": "Project_Task", 139 | "type": "ONE_TO_MANY", 140 | "from": "Project", 141 | "to": "Task", 142 | "field_name": "tasks", 143 | "backref_field_name": "project" 144 | }, 145 | { 146 | "name": "Task_User", 147 | "type": "MANY_TO_MANY", 148 | "from": "Task", 149 | "to": "User", 150 | "field_name": "users", 151 | "backref_field_name": "tasks" 152 | } 153 | ] 154 | } -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/README.md: -------------------------------------------------------------------------------- 1 | # Task Manager 2 | 3 | An application for managing tasks and collaboration. 4 | 5 | Follow these steps to run the application: 6 | 7 | ## Prerequisites 8 | 9 | Before you begin, make sure you have the following prerequisites installed: 10 | 11 | - [Python 3](https://www.python.org/downloads/) 12 | 13 | ## Installation and Setup 14 | 15 | 1. Create and activate a virtual environment: 16 | 17 | ```shell 18 | python3 -m venv env 19 | source env/bin/activate 20 | ``` 21 | 22 | 2. Install the required Python packages: 23 | 24 | ```shell 25 | pip install -r 'requirements.txt' 26 | ``` 27 | 28 | 3. Start the FastAPI application: 29 | 30 | ```shell 31 | uvicorn app.main:app --reload 32 | ``` 33 | 34 | 4. Open your web browser and navigate to [http://127.0.0.1:8000](http://127.0.0.1:8000) -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowilf/qwikcrud/560dcabb1dfcf6ea5885d7077e63dd3496a89908/examples/fastapi/task-management/generated/gemini-pro/app/__init__.py -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/admin.py: -------------------------------------------------------------------------------- 1 | from app.db import engine 2 | from app.models import Project, Task, User 3 | from starlette_admin.contrib.sqla import Admin 4 | from starlette_admin.contrib.sqla import ModelView as BaseModelView 5 | 6 | 7 | class ModelView(BaseModelView): 8 | exclude_fields_from_create = ["created_at", "updated_at"] 9 | exclude_fields_from_edit = ["created_at", "updated_at"] 10 | 11 | 12 | def init_admin(app): 13 | admin = Admin(engine, templates_dir="templates/admin") 14 | admin.add_view(ModelView(User)) 15 | admin.add_view(ModelView(Project)) 16 | admin.add_view(ModelView(Task)) 17 | admin.mount_to(app) 18 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/crud.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, Optional, Sequence, Type, TypeVar, Union 2 | 3 | from app.models import Project, Task, User 4 | from app.schemas import (ProjectCreate, ProjectUpdate, TaskCreate, TaskUpdate, 5 | UserCreate, UserUpdate) 6 | from fastapi import HTTPException 7 | from pydantic import BaseModel 8 | from sqlalchemy import select 9 | from sqlalchemy.orm import Session 10 | 11 | ModelType = TypeVar("ModelType", bound=Any) 12 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 13 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 14 | 15 | 16 | class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 17 | def __init__(self, model: Type[ModelType]): 18 | self.model = model 19 | 20 | async def get(self, db: Session, id: Any) -> Optional[ModelType]: 21 | return db.get(self.model, id) 22 | 23 | async def get_or_404(self, db: Session, id: Any) -> Optional[ModelType]: 24 | obj = await self.get(db, id) 25 | if obj is None: 26 | raise HTTPException( 27 | status_code=404, detail=f"{self.model.__name__} with id: {id} not found" 28 | ) 29 | return obj 30 | 31 | async def get_all( 32 | self, db: Session, *, skip: int = 0, limit: int = 100 33 | ) -> Sequence[ModelType]: 34 | stmt = select(self.model).offset(skip).limit(limit) 35 | return db.execute(stmt).scalars().all() 36 | 37 | async def save(self, db: Session, db_obj: ModelType) -> ModelType: 38 | db.add(db_obj) 39 | db.commit() 40 | db.refresh(db_obj) 41 | return db_obj 42 | 43 | async def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: 44 | db_obj = self.model(**obj_in.model_dump()) # type: ignore 45 | return await self.save(db, db_obj) 46 | 47 | async def update( 48 | self, 49 | db: Session, 50 | *, 51 | db_obj: ModelType, 52 | obj_in: Union[UpdateSchemaType, Dict[str, Any]], 53 | ) -> ModelType: 54 | update_data = obj_in.model_dump(exclude_unset=True) 55 | for key, value in update_data.items(): 56 | setattr(db_obj, key, value) 57 | return await self.save(db, db_obj) 58 | 59 | async def delete(self, db: Session, *, db_obj: ModelType) -> None: 60 | db.delete(db_obj) 61 | db.commit() 62 | 63 | 64 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 65 | pass 66 | 67 | 68 | class CRUDProject(CRUDBase[Project, ProjectCreate, ProjectUpdate]): 69 | pass 70 | 71 | 72 | class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]): 73 | pass 74 | 75 | 76 | user = CRUDUser(User) 77 | project = CRUDProject(Project) 78 | task = CRUDTask(Task) 79 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/db.py: -------------------------------------------------------------------------------- 1 | from app.models import Base 2 | from app.settings import settings 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | 6 | engine = create_engine(settings.SQLALCHEMY_DATABASE_URI) 7 | 8 | Session = sessionmaker(engine) 9 | 10 | 11 | async def init_db(): 12 | Base.metadata.create_all(engine) 13 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Generator 2 | 3 | from app.db import Session 4 | from fastapi import Depends 5 | 6 | 7 | def get_db() -> Generator: 8 | with Session() as session: 9 | yield session 10 | 11 | 12 | SessionDep = Annotated[Session, Depends(get_db)] 13 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowilf/qwikcrud/560dcabb1dfcf6ea5885d7077e63dd3496a89908/examples/fastapi/task-management/generated/gemini-pro/app/endpoints/__init__.py -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/endpoints/project.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from app import crud 4 | from app.deps import SessionDep 5 | from app.schemas import (ProjectCreate, ProjectOut, ProjectPatch, 6 | ProjectUpdate, TaskOut, UserOut) 7 | from fastapi import APIRouter 8 | 9 | router = APIRouter(tags=["projects"]) 10 | 11 | 12 | @router.get("/") 13 | async def read_all(db: SessionDep, skip: int = 0, limit: int = 100) -> list[ProjectOut]: 14 | return await crud.project.get_all(db, skip=skip, limit=limit) 15 | 16 | 17 | @router.get("/{id}") 18 | async def read_one(db: SessionDep, id: int) -> ProjectOut: 19 | project = await crud.project.get_or_404(db, id) 20 | return project 21 | 22 | 23 | @router.post("/", status_code=201) 24 | async def create(*, db: SessionDep, project_in: ProjectCreate) -> ProjectOut: 25 | return await crud.project.create(db, obj_in=project_in) 26 | 27 | 28 | @router.put("/{id}") 29 | async def update(*, db: SessionDep, id: int, project_in: ProjectUpdate) -> ProjectOut: 30 | project = await crud.project.get_or_404(db, id) 31 | return await crud.project.update(db, db_obj=project, obj_in=project_in) 32 | 33 | 34 | @router.patch("/{id}") 35 | async def patch(*, db: SessionDep, id: int, project_in: ProjectPatch) -> ProjectOut: 36 | project = await crud.project.get_or_404(db, id) 37 | return await crud.project.update(db, db_obj=project, obj_in=project_in) 38 | 39 | 40 | @router.delete("/{id}", status_code=204) 41 | async def delete(*, db: SessionDep, id: int) -> None: 42 | project = await crud.project.get_or_404(db, id) 43 | return await crud.project.delete(db, db_obj=project) 44 | 45 | 46 | # Handle relationships 47 | 48 | 49 | @router.get("/{id}/users") 50 | async def get_associated_users(db: SessionDep, id: int) -> List[UserOut]: 51 | project = await crud.project.get_or_404(db, id) 52 | return project.users 53 | 54 | 55 | @router.put("/{id}/users") 56 | async def add_users_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[UserOut]: 57 | project = await crud.project.get_or_404(db, id) 58 | for _id in ids: 59 | project.users.append(await crud.user.get_or_404(db, _id)) 60 | project = await crud.project.save(db, project) 61 | return project.users 62 | 63 | 64 | @router.get("/{id}/tasks") 65 | async def get_associated_tasks(db: SessionDep, id: int) -> List[TaskOut]: 66 | project = await crud.project.get_or_404(db, id) 67 | return project.tasks 68 | 69 | 70 | @router.put("/{id}/tasks") 71 | async def add_tasks_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[TaskOut]: 72 | project = await crud.project.get_or_404(db, id) 73 | for _id in ids: 74 | project.tasks.append(await crud.task.get_or_404(db, _id)) 75 | project = await crud.project.save(db, project) 76 | return project.tasks 77 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/endpoints/task.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from app import crud 4 | from app.deps import SessionDep 5 | from app.schemas import (ProjectOut, TaskCreate, TaskOut, TaskPatch, 6 | TaskUpdate, UserOut) 7 | from fastapi import APIRouter, UploadFile 8 | 9 | router = APIRouter(tags=["tasks"]) 10 | 11 | 12 | @router.get("/") 13 | async def read_all(db: SessionDep, skip: int = 0, limit: int = 100) -> list[TaskOut]: 14 | return await crud.task.get_all(db, skip=skip, limit=limit) 15 | 16 | 17 | @router.get("/{id}") 18 | async def read_one(db: SessionDep, id: int) -> TaskOut: 19 | task = await crud.task.get_or_404(db, id) 20 | return task 21 | 22 | 23 | @router.post("/", status_code=201) 24 | async def create(*, db: SessionDep, task_in: TaskCreate) -> TaskOut: 25 | return await crud.task.create(db, obj_in=task_in) 26 | 27 | 28 | @router.put("/{id}") 29 | async def update(*, db: SessionDep, id: int, task_in: TaskUpdate) -> TaskOut: 30 | task = await crud.task.get_or_404(db, id) 31 | return await crud.task.update(db, db_obj=task, obj_in=task_in) 32 | 33 | 34 | @router.patch("/{id}") 35 | async def patch(*, db: SessionDep, id: int, task_in: TaskPatch) -> TaskOut: 36 | task = await crud.task.get_or_404(db, id) 37 | return await crud.task.update(db, db_obj=task, obj_in=task_in) 38 | 39 | 40 | @router.delete("/{id}", status_code=204) 41 | async def delete(*, db: SessionDep, id: int) -> None: 42 | task = await crud.task.get_or_404(db, id) 43 | return await crud.task.delete(db, db_obj=task) 44 | 45 | 46 | # Handle files 47 | 48 | 49 | @router.put("/{id}/instruction_file") 50 | async def set_instruction_file(*, db: SessionDep, id: int, file: UploadFile) -> TaskOut: 51 | task = await crud.task.get_or_404(db, id) 52 | task.instruction_file = file 53 | return await crud.task.save(db, db_obj=task) 54 | 55 | 56 | @router.delete("/{id}/instruction_file", status_code=204) 57 | async def remove_instruction_file(*, db: SessionDep, id: int) -> None: 58 | task = await crud.task.get_or_404(db, id) 59 | task.instruction_file = None 60 | await crud.task.save(db, db_obj=task) 61 | 62 | 63 | # Handle relationships 64 | 65 | 66 | @router.get("/{id}/project") 67 | async def get_associated_project(db: SessionDep, id: int) -> Optional[ProjectOut]: 68 | task = await crud.task.get_or_404(db, id) 69 | return task.project 70 | 71 | 72 | @router.put("/{id}/project/{project_id}") 73 | async def set_project_by_id( 74 | db: SessionDep, id: int, project_id: int 75 | ) -> Optional[ProjectOut]: 76 | task = await crud.task.get_or_404(db, id) 77 | task.project = await crud.project.get_or_404(db, project_id) 78 | task = await crud.task.save(db, task) 79 | return task.project 80 | 81 | 82 | @router.get("/{id}/users") 83 | async def get_associated_users(db: SessionDep, id: int) -> List[UserOut]: 84 | task = await crud.task.get_or_404(db, id) 85 | return task.users 86 | 87 | 88 | @router.put("/{id}/users") 89 | async def add_users_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[UserOut]: 90 | task = await crud.task.get_or_404(db, id) 91 | for _id in ids: 92 | task.users.append(await crud.user.get_or_404(db, _id)) 93 | task = await crud.task.save(db, task) 94 | return task.users 95 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/endpoints/user.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from app import crud 4 | from app.deps import SessionDep 5 | from app.schemas import (ProjectOut, TaskOut, UserCreate, UserOut, UserPatch, 6 | UserUpdate) 7 | from fastapi import APIRouter, UploadFile 8 | 9 | router = APIRouter(tags=["users"]) 10 | 11 | 12 | @router.get("/") 13 | async def read_all(db: SessionDep, skip: int = 0, limit: int = 100) -> list[UserOut]: 14 | return await crud.user.get_all(db, skip=skip, limit=limit) 15 | 16 | 17 | @router.get("/{id}") 18 | async def read_one(db: SessionDep, id: int) -> UserOut: 19 | user = await crud.user.get_or_404(db, id) 20 | return user 21 | 22 | 23 | @router.post("/", status_code=201) 24 | async def create(*, db: SessionDep, user_in: UserCreate) -> UserOut: 25 | return await crud.user.create(db, obj_in=user_in) 26 | 27 | 28 | @router.put("/{id}") 29 | async def update(*, db: SessionDep, id: int, user_in: UserUpdate) -> UserOut: 30 | user = await crud.user.get_or_404(db, id) 31 | return await crud.user.update(db, db_obj=user, obj_in=user_in) 32 | 33 | 34 | @router.patch("/{id}") 35 | async def patch(*, db: SessionDep, id: int, user_in: UserPatch) -> UserOut: 36 | user = await crud.user.get_or_404(db, id) 37 | return await crud.user.update(db, db_obj=user, obj_in=user_in) 38 | 39 | 40 | @router.delete("/{id}", status_code=204) 41 | async def delete(*, db: SessionDep, id: int) -> None: 42 | user = await crud.user.get_or_404(db, id) 43 | return await crud.user.delete(db, db_obj=user) 44 | 45 | 46 | # Handle files 47 | 48 | 49 | @router.put("/{id}/avatar") 50 | async def set_avatar(*, db: SessionDep, id: int, file: UploadFile) -> UserOut: 51 | user = await crud.user.get_or_404(db, id) 52 | user.avatar = file 53 | return await crud.user.save(db, db_obj=user) 54 | 55 | 56 | @router.delete("/{id}/avatar", status_code=204) 57 | async def remove_avatar(*, db: SessionDep, id: int) -> None: 58 | user = await crud.user.get_or_404(db, id) 59 | user.avatar = None 60 | await crud.user.save(db, db_obj=user) 61 | 62 | 63 | # Handle relationships 64 | 65 | 66 | @router.get("/{id}/projects") 67 | async def get_associated_projects(db: SessionDep, id: int) -> List[ProjectOut]: 68 | user = await crud.user.get_or_404(db, id) 69 | return user.projects 70 | 71 | 72 | @router.put("/{id}/projects") 73 | async def add_projects_by_ids( 74 | db: SessionDep, id: int, ids: List[int] 75 | ) -> List[ProjectOut]: 76 | user = await crud.user.get_or_404(db, id) 77 | for _id in ids: 78 | user.projects.append(await crud.project.get_or_404(db, _id)) 79 | user = await crud.user.save(db, user) 80 | return user.projects 81 | 82 | 83 | @router.get("/{id}/tasks") 84 | async def get_associated_tasks(db: SessionDep, id: int) -> List[TaskOut]: 85 | user = await crud.user.get_or_404(db, id) 86 | return user.tasks 87 | 88 | 89 | @router.put("/{id}/tasks") 90 | async def add_tasks_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[TaskOut]: 91 | user = await crud.user.get_or_404(db, id) 92 | for _id in ids: 93 | user.tasks.append(await crud.task.get_or_404(db, _id)) 94 | user = await crud.user.save(db, user) 95 | return user.tasks 96 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class Status(str, enum.Enum): 5 | OPEN = "Open" 6 | IN_PROGRESS = "In Progress" 7 | COMPLETED = "Completed" 8 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/main.py: -------------------------------------------------------------------------------- 1 | from app.admin import init_admin 2 | from app.endpoints import project, task, user 3 | from app.pre_start import init 4 | from app.settings import settings 5 | from fastapi import FastAPI, Request 6 | from fastapi.responses import FileResponse, JSONResponse 7 | from fastapi.staticfiles import StaticFiles 8 | from fastapi.templating import Jinja2Templates 9 | from libcloud.storage.types import ObjectDoesNotExistError 10 | from sqlalchemy_file.exceptions import ValidationError as FileValidationError 11 | from sqlalchemy_file.storage import StorageManager 12 | 13 | 14 | def create_app(): 15 | _app = FastAPI( 16 | title=settings.PROJECT_NAME, 17 | description=settings.PROJECT_DESCRIPTION, 18 | on_startup=[init], 19 | ) 20 | 21 | _app.include_router(user.router, prefix="/api/v1/users") 22 | _app.include_router(project.router, prefix="/api/v1/projects") 23 | _app.include_router(task.router, prefix="/api/v1/tasks") 24 | _app.mount("/static", StaticFiles(directory="static"), name="static") 25 | init_admin(_app) 26 | 27 | return _app 28 | 29 | 30 | templates = Jinja2Templates(directory="templates") 31 | 32 | app = create_app() 33 | 34 | 35 | @app.get("/", include_in_schema=False) 36 | async def home(request: Request): 37 | return templates.TemplateResponse( 38 | "index.html", {"request": request, "settings": settings} 39 | ) 40 | 41 | 42 | @app.get("/medias", response_class=FileResponse, tags=["medias"]) 43 | async def serve_files(path: str): 44 | try: 45 | file = StorageManager.get_file(path) 46 | return FileResponse( 47 | file.get_cdn_url(), media_type=file.content_type, filename=file.filename 48 | ) 49 | except ObjectDoesNotExistError: 50 | return JSONResponse({"detail": "Not found"}, status_code=404) 51 | 52 | 53 | @app.exception_handler(FileValidationError) 54 | async def sqla_file_validation_error(request: Request, exc: FileValidationError): 55 | return JSONResponse({"error": {"key": exc.key, "msg": exc.msg}}, status_code=422) 56 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List, Optional, Union 3 | 4 | from app.enums import Status 5 | from fastapi import UploadFile 6 | from pydantic import EmailStr 7 | from sqlalchemy import JSON, Column, Enum, ForeignKey, MetaData, String, Table 8 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship 9 | from sqlalchemy_file import File, FileField, ImageField 10 | from sqlalchemy_file.validators import ContentTypeValidator, SizeValidator 11 | 12 | 13 | class TimestampMixin: 14 | created_at: Mapped[datetime.datetime] = mapped_column( 15 | default=datetime.datetime.utcnow 16 | ) 17 | updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( 18 | onupdate=datetime.datetime.utcnow 19 | ) 20 | 21 | 22 | class Base(DeclarativeBase, TimestampMixin): 23 | metadata = MetaData( 24 | naming_convention={ 25 | "ix": "ix_%(column_0_label)s", 26 | "uq": "uq_%(table_name)s_%(column_0_name)s", 27 | "ck": "ck_%(table_name)s_%(constraint_name)s", 28 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 29 | "pk": "pk_%(table_name)s", 30 | } 31 | ) 32 | type_annotation_map = {EmailStr: String, dict: JSON} 33 | 34 | 35 | user__project = Table( 36 | "user__project", 37 | Base.metadata, 38 | Column("user_id", ForeignKey("user.id"), primary_key=True), 39 | Column("project_id", ForeignKey("project.id"), primary_key=True), 40 | ) 41 | task__user = Table( 42 | "task__user", 43 | Base.metadata, 44 | Column("task_id", ForeignKey("task.id"), primary_key=True), 45 | Column("user_id", ForeignKey("user.id"), primary_key=True), 46 | ) 47 | 48 | 49 | class User(Base): 50 | __tablename__ = "user" 51 | 52 | id: Mapped[int] = mapped_column(primary_key=True) 53 | username: Mapped[str] = mapped_column(unique=True) 54 | email: Mapped[EmailStr] = mapped_column(unique=True) 55 | password: Mapped[str] 56 | first_name: Mapped[str] 57 | last_name: Mapped[str] 58 | avatar: Mapped[Union[File, UploadFile, None]] = mapped_column( 59 | ImageField( 60 | thumbnail_size=(150, 150), validators=[SizeValidator(max_size="20M")] 61 | ) 62 | ) 63 | 64 | projects: Mapped[List["Project"]] = relationship( 65 | secondary=user__project, back_populates="users" 66 | ) 67 | 68 | tasks: Mapped[List["Task"]] = relationship( 69 | secondary=task__user, back_populates="users" 70 | ) 71 | 72 | 73 | class Project(Base): 74 | __tablename__ = "project" 75 | 76 | id: Mapped[int] = mapped_column(primary_key=True) 77 | name: Mapped[str] = mapped_column(unique=True) 78 | description: Mapped[str] 79 | 80 | users: Mapped[List["User"]] = relationship( 81 | secondary=user__project, back_populates="projects" 82 | ) 83 | 84 | tasks: Mapped[List["Task"]] = relationship(back_populates="project") 85 | 86 | 87 | class Task(Base): 88 | __tablename__ = "task" 89 | 90 | id: Mapped[int] = mapped_column(primary_key=True) 91 | task_name: Mapped[str] 92 | description: Mapped[str] 93 | due_date: Mapped[datetime.date] 94 | start_date: Mapped[datetime.date] 95 | status: Mapped[str] = mapped_column(Enum(Status)) 96 | instruction_file: Mapped[Union[File, UploadFile, None]] = mapped_column( 97 | FileField( 98 | validators=[ 99 | SizeValidator(max_size="20M"), 100 | ContentTypeValidator( 101 | ["application/pdf", "application/msword", "text/plain"] 102 | ), 103 | ] 104 | ) 105 | ) 106 | 107 | project_id: Mapped[Optional[int]] = mapped_column(ForeignKey("project.id")) 108 | project: Mapped["Project"] = relationship(back_populates="tasks") 109 | 110 | users: Mapped[List["User"]] = relationship( 111 | secondary=task__user, back_populates="tasks" 112 | ) 113 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/pre_start.py: -------------------------------------------------------------------------------- 1 | 2 | from app.db import init_db 3 | from app.storage import init_storage 4 | 5 | 6 | async def init() -> None: 7 | await init_db() 8 | await init_storage() 9 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/schemas.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | 4 | from app.enums import Status 5 | from pydantic import BaseModel, EmailStr, Field 6 | 7 | 8 | class Thumbnail(BaseModel): 9 | path: str 10 | 11 | 12 | class FileInfo(BaseModel): 13 | filename: str 14 | content_type: str 15 | path: str 16 | thumbnail: Optional[Thumbnail] = None 17 | 18 | 19 | # -------------- User ------------------ 20 | 21 | 22 | class UserCreate(BaseModel): 23 | username: str = Field(max_length=50) 24 | email: EmailStr = Field(max_length=100) 25 | password: str = Field(min_length=8, max_length=100) 26 | first_name: str = Field(max_length=50) 27 | last_name: str = Field(max_length=50) 28 | 29 | 30 | class UserUpdate(UserCreate): 31 | pass 32 | 33 | 34 | class UserPatch(BaseModel): 35 | username: Optional[str] = Field(None, max_length=50) 36 | email: Optional[EmailStr] = Field(None, max_length=100) 37 | password: Optional[str] = Field(None, min_length=8, max_length=100) 38 | first_name: Optional[str] = Field(None, max_length=50) 39 | last_name: Optional[str] = Field(None, max_length=50) 40 | 41 | 42 | class UserOut(BaseModel): 43 | id: int 44 | username: str = Field(max_length=50) 45 | email: EmailStr = Field(max_length=100) 46 | password: str = Field(min_length=8, max_length=100) 47 | first_name: str = Field(max_length=50) 48 | last_name: str = Field(max_length=50) 49 | avatar: Optional[FileInfo] 50 | 51 | 52 | # -------------- Project ------------------ 53 | 54 | 55 | class ProjectCreate(BaseModel): 56 | name: str = Field(max_length=100) 57 | description: str 58 | 59 | 60 | class ProjectUpdate(ProjectCreate): 61 | pass 62 | 63 | 64 | class ProjectPatch(BaseModel): 65 | name: Optional[str] = Field(None, max_length=100) 66 | description: Optional[str] = Field(None) 67 | 68 | 69 | class ProjectOut(BaseModel): 70 | id: int 71 | name: str = Field(max_length=100) 72 | description: str 73 | 74 | 75 | # -------------- Task ------------------ 76 | 77 | 78 | class TaskCreate(BaseModel): 79 | task_name: str = Field(max_length=100) 80 | description: str 81 | due_date: datetime.date 82 | start_date: datetime.date 83 | status: Status 84 | 85 | 86 | class TaskUpdate(TaskCreate): 87 | pass 88 | 89 | 90 | class TaskPatch(BaseModel): 91 | task_name: Optional[str] = Field(None, max_length=100) 92 | description: Optional[str] = Field(None) 93 | due_date: Optional[datetime.date] = Field(None) 94 | start_date: Optional[datetime.date] = Field(None) 95 | status: Optional[Status] = Field(None) 96 | 97 | 98 | class TaskOut(BaseModel): 99 | id: int 100 | task_name: str = Field(max_length=100) 101 | description: str 102 | due_date: datetime.date 103 | start_date: datetime.date 104 | status: Status 105 | instruction_file: Optional[FileInfo] = Field( 106 | mime_types=["application/pdf", "application/msword", "text/plain"] 107 | ) 108 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | 3 | 4 | class Settings(BaseSettings): 5 | SQLALCHEMY_DATABASE_URI: str = "sqlite:///db.sqlite" 6 | PROJECT_NAME: str = "Task Manager" 7 | PROJECT_DESCRIPTION: str = "An application for managing tasks and collaboration." 8 | 9 | 10 | settings = Settings() 11 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/app/storage.py: -------------------------------------------------------------------------------- 1 | from libcloud.storage.base import Container, StorageDriver 2 | from libcloud.storage.drivers.local import LocalStorageDriver 3 | from libcloud.storage.types import ContainerDoesNotExistError 4 | from sqlalchemy_file.storage import StorageManager 5 | 6 | 7 | def get_or_create_container(driver: StorageDriver, name: str) -> Container: 8 | try: 9 | return driver.get_container(name) 10 | except ContainerDoesNotExistError: 11 | return driver.create_container(name) 12 | 13 | 14 | async def init_storage() -> None: 15 | StorageManager.add_storage( 16 | "default", get_or_create_container(LocalStorageDriver("."), "assets") 17 | ) 18 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.104.1 2 | starlette-admin>=0.12 3 | pydantic[email]>=2 4 | pydantic_settings 5 | sqlalchemy>=2 6 | sqlalchemy-file==0.6.0 7 | fasteners==0.19 8 | pillow>=10.1 9 | python-multipart==0.0.6 10 | uvicorn>=0.24.0.post1 -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/static/css/style.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.btn{display:inline-flex;height:2.25rem;align-items:center;justify-content:center;border-radius:.375rem;--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity:1;color:rgb(250 250 250/var(--tw-text-opacity));--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn:hover{background-color:#18181be6}.btn:focus-visible{outline:2px solid #0000;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(9 9 11/var(--tw-ring-opacity))}.mx-8{margin-left:2rem;margin-right:2rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-full{height:100%}.h-screen{height:100vh}.resize{resize:both}.flex-col{flex-direction:column}.justify-center{justify-content:center}.gap-8{gap:2rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-zinc-300{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.text-center{text-align:center}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.tracking-tighter{letter-spacing:-.05em}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (prefers-color-scheme:dark){.dark\:text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:text-5xl{font-size:3rem;line-height:1}}@media (min-width:768px){.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:text-6xl{font-size:3.75rem;line-height:1}} -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/gemini-pro/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ settings.PROJECT_NAME }} 8 | 9 | 10 | 11 |
12 |
13 |

14 | {{ settings.PROJECT_NAME }} 15 |

16 |

17 | {{ settings.PROJECT_DESCRIPTION }} 18 |

19 |
20 |
21 |

Swagger Documentation

22 |

Explore and test your endpoints using Swagger UI. 23 |

24 | 25 | Browse 26 | 27 |
28 |
29 |

Redoc Documentation

30 |

View and interact with your endpoints using Redoc. 31 |

32 | 33 | Browse 34 | 35 |
36 |
37 |

Admin Interface

38 |

Manage your entities with the admin interface. 39 |

40 | 41 | Browse 42 | 43 |
44 |
45 |
46 | 51 |
52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/.qwikcrud.json.lock: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TaskManager", 3 | "description": "An application for managing tasks that enables multiple users to collaborate on a single project with multiple tasks.", 4 | "entities": [ 5 | { 6 | "name": "User", 7 | "fields": [ 8 | { 9 | "name": "id", 10 | "type": "ID" 11 | }, 12 | { 13 | "name": "username", 14 | "type": "String", 15 | "constraints": { 16 | "unique": true, 17 | "max_length": 50 18 | } 19 | }, 20 | { 21 | "name": "email", 22 | "type": "Email", 23 | "constraints": { 24 | "unique": true, 25 | "max_length": 100 26 | } 27 | }, 28 | { 29 | "name": "password", 30 | "type": "String", 31 | "constraints": { 32 | "min_length": 8, 33 | "max_length": 100 34 | } 35 | }, 36 | { 37 | "name": "first_name", 38 | "type": "String", 39 | "constraints": { 40 | "max_length": 50 41 | } 42 | }, 43 | { 44 | "name": "last_name", 45 | "type": "String", 46 | "constraints": { 47 | "max_length": 50 48 | } 49 | }, 50 | { 51 | "name": "avatar", 52 | "type": "Image" 53 | }, 54 | { 55 | "name": "bio", 56 | "type": "Text" 57 | } 58 | ] 59 | }, 60 | { 61 | "name": "Project", 62 | "fields": [ 63 | { 64 | "name": "id", 65 | "type": "ID" 66 | }, 67 | { 68 | "name": "name", 69 | "type": "String", 70 | "constraints": { 71 | "max_length": 100 72 | } 73 | }, 74 | { 75 | "name": "description", 76 | "type": "Text" 77 | }, 78 | { 79 | "name": "start_date", 80 | "type": "Date" 81 | }, 82 | { 83 | "name": "due_date", 84 | "type": "Date" 85 | } 86 | ] 87 | }, 88 | { 89 | "name": "Task", 90 | "fields": [ 91 | { 92 | "name": "id", 93 | "type": "ID" 94 | }, 95 | { 96 | "name": "name", 97 | "type": "String", 98 | "constraints": { 99 | "max_length": 100 100 | } 101 | }, 102 | { 103 | "name": "description", 104 | "type": "Text" 105 | }, 106 | { 107 | "name": "start_date", 108 | "type": "Date" 109 | }, 110 | { 111 | "name": "due_date", 112 | "type": "Date" 113 | }, 114 | { 115 | "name": "status", 116 | "type": "Enum", 117 | "constraints": { 118 | "allowed_values": [ 119 | "open", 120 | "in progress", 121 | "completed" 122 | ] 123 | } 124 | }, 125 | { 126 | "name": "instruction_file", 127 | "type": "File", 128 | "constraints": { 129 | "mime_types": [ 130 | "application/pdf", 131 | "application/msword", 132 | "text/plain" 133 | ] 134 | } 135 | } 136 | ] 137 | } 138 | ], 139 | "relations": [ 140 | { 141 | "name": "User_Projects", 142 | "type": "ONE_TO_MANY", 143 | "from": "User", 144 | "to": "Project", 145 | "field_name": "projects", 146 | "backref_field_name": "user" 147 | }, 148 | { 149 | "name": "Project_Tasks", 150 | "type": "ONE_TO_MANY", 151 | "from": "Project", 152 | "to": "Task", 153 | "field_name": "tasks", 154 | "backref_field_name": "project" 155 | }, 156 | { 157 | "name": "Task_Users", 158 | "type": "MANY_TO_MANY", 159 | "from": "Task", 160 | "to": "User", 161 | "field_name": "assigned_users", 162 | "backref_field_name": "tasks" 163 | } 164 | ] 165 | } -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/README.md: -------------------------------------------------------------------------------- 1 | # TaskManager 2 | 3 | An application for managing tasks that enables multiple users to collaborate on a single project with multiple tasks. 4 | 5 | Follow these steps to run the application: 6 | 7 | ## Prerequisites 8 | 9 | Before you begin, make sure you have the following prerequisites installed: 10 | 11 | - [Python 3](https://www.python.org/downloads/) 12 | 13 | ## Installation and Setup 14 | 15 | 1. Create and activate a virtual environment: 16 | 17 | ```shell 18 | python3 -m venv env 19 | source env/bin/activate 20 | ``` 21 | 22 | 2. Install the required Python packages: 23 | 24 | ```shell 25 | pip install -r 'requirements.txt' 26 | ``` 27 | 28 | 3. Start the FastAPI application: 29 | 30 | ```shell 31 | uvicorn app.main:app --reload 32 | ``` 33 | 34 | 4. Open your web browser and navigate to [http://127.0.0.1:8000](http://127.0.0.1:8000) -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowilf/qwikcrud/560dcabb1dfcf6ea5885d7077e63dd3496a89908/examples/fastapi/task-management/generated/openai/app/__init__.py -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/admin.py: -------------------------------------------------------------------------------- 1 | from app.db import engine 2 | from app.models import Project, Task, User 3 | from starlette_admin.contrib.sqla import Admin 4 | from starlette_admin.contrib.sqla import ModelView as BaseModelView 5 | 6 | 7 | class ModelView(BaseModelView): 8 | exclude_fields_from_create = ["created_at", "updated_at"] 9 | exclude_fields_from_edit = ["created_at", "updated_at"] 10 | 11 | 12 | def init_admin(app): 13 | admin = Admin(engine, templates_dir="templates/admin") 14 | admin.add_view(ModelView(User)) 15 | admin.add_view(ModelView(Project)) 16 | admin.add_view(ModelView(Task)) 17 | admin.mount_to(app) 18 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/crud.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, Optional, Sequence, Type, TypeVar, Union 2 | 3 | from app.models import Project, Task, User 4 | from app.schemas import (ProjectCreate, ProjectUpdate, TaskCreate, TaskUpdate, 5 | UserCreate, UserUpdate) 6 | from fastapi import HTTPException 7 | from pydantic import BaseModel 8 | from sqlalchemy import select 9 | from sqlalchemy.orm import Session 10 | 11 | ModelType = TypeVar("ModelType", bound=Any) 12 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 13 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 14 | 15 | 16 | class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 17 | def __init__(self, model: Type[ModelType]): 18 | self.model = model 19 | 20 | async def get(self, db: Session, id: Any) -> Optional[ModelType]: 21 | return db.get(self.model, id) 22 | 23 | async def get_or_404(self, db: Session, id: Any) -> Optional[ModelType]: 24 | obj = await self.get(db, id) 25 | if obj is None: 26 | raise HTTPException( 27 | status_code=404, detail=f"{self.model.__name__} with id: {id} not found" 28 | ) 29 | return obj 30 | 31 | async def get_all( 32 | self, db: Session, *, skip: int = 0, limit: int = 100 33 | ) -> Sequence[ModelType]: 34 | stmt = select(self.model).offset(skip).limit(limit) 35 | return db.execute(stmt).scalars().all() 36 | 37 | async def save(self, db: Session, db_obj: ModelType) -> ModelType: 38 | db.add(db_obj) 39 | db.commit() 40 | db.refresh(db_obj) 41 | return db_obj 42 | 43 | async def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: 44 | db_obj = self.model(**obj_in.model_dump()) # type: ignore 45 | return await self.save(db, db_obj) 46 | 47 | async def update( 48 | self, 49 | db: Session, 50 | *, 51 | db_obj: ModelType, 52 | obj_in: Union[UpdateSchemaType, Dict[str, Any]], 53 | ) -> ModelType: 54 | update_data = obj_in.model_dump(exclude_unset=True) 55 | for key, value in update_data.items(): 56 | setattr(db_obj, key, value) 57 | return await self.save(db, db_obj) 58 | 59 | async def delete(self, db: Session, *, db_obj: ModelType) -> None: 60 | db.delete(db_obj) 61 | db.commit() 62 | 63 | 64 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 65 | pass 66 | 67 | 68 | class CRUDProject(CRUDBase[Project, ProjectCreate, ProjectUpdate]): 69 | pass 70 | 71 | 72 | class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]): 73 | pass 74 | 75 | 76 | user = CRUDUser(User) 77 | project = CRUDProject(Project) 78 | task = CRUDTask(Task) 79 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/db.py: -------------------------------------------------------------------------------- 1 | from app.models import Base 2 | from app.settings import settings 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | 6 | engine = create_engine(settings.SQLALCHEMY_DATABASE_URI) 7 | 8 | Session = sessionmaker(engine) 9 | 10 | 11 | async def init_db(): 12 | Base.metadata.create_all(engine) 13 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Generator 2 | 3 | from app.db import Session 4 | from fastapi import Depends 5 | 6 | 7 | def get_db() -> Generator: 8 | with Session() as session: 9 | yield session 10 | 11 | 12 | SessionDep = Annotated[Session, Depends(get_db)] 13 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowilf/qwikcrud/560dcabb1dfcf6ea5885d7077e63dd3496a89908/examples/fastapi/task-management/generated/openai/app/endpoints/__init__.py -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/endpoints/project.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from app import crud 4 | from app.deps import SessionDep 5 | from app.schemas import (ProjectCreate, ProjectOut, ProjectPatch, 6 | ProjectUpdate, TaskOut, UserOut) 7 | from fastapi import APIRouter 8 | 9 | router = APIRouter(tags=["projects"]) 10 | 11 | 12 | @router.get("/") 13 | async def read_all(db: SessionDep, skip: int = 0, limit: int = 100) -> list[ProjectOut]: 14 | return await crud.project.get_all(db, skip=skip, limit=limit) 15 | 16 | 17 | @router.get("/{id}") 18 | async def read_one(db: SessionDep, id: int) -> ProjectOut: 19 | project = await crud.project.get_or_404(db, id) 20 | return project 21 | 22 | 23 | @router.post("/", status_code=201) 24 | async def create(*, db: SessionDep, project_in: ProjectCreate) -> ProjectOut: 25 | return await crud.project.create(db, obj_in=project_in) 26 | 27 | 28 | @router.put("/{id}") 29 | async def update(*, db: SessionDep, id: int, project_in: ProjectUpdate) -> ProjectOut: 30 | project = await crud.project.get_or_404(db, id) 31 | return await crud.project.update(db, db_obj=project, obj_in=project_in) 32 | 33 | 34 | @router.patch("/{id}") 35 | async def patch(*, db: SessionDep, id: int, project_in: ProjectPatch) -> ProjectOut: 36 | project = await crud.project.get_or_404(db, id) 37 | return await crud.project.update(db, db_obj=project, obj_in=project_in) 38 | 39 | 40 | @router.delete("/{id}", status_code=204) 41 | async def delete(*, db: SessionDep, id: int) -> None: 42 | project = await crud.project.get_or_404(db, id) 43 | return await crud.project.delete(db, db_obj=project) 44 | 45 | 46 | # Handle relationships 47 | 48 | 49 | @router.get("/{id}/user") 50 | async def get_associated_user(db: SessionDep, id: int) -> Optional[UserOut]: 51 | project = await crud.project.get_or_404(db, id) 52 | return project.user 53 | 54 | 55 | @router.put("/{id}/user/{user_id}") 56 | async def set_user_by_id(db: SessionDep, id: int, user_id: int) -> Optional[UserOut]: 57 | project = await crud.project.get_or_404(db, id) 58 | project.user = await crud.user.get_or_404(db, user_id) 59 | project = await crud.project.save(db, project) 60 | return project.user 61 | 62 | 63 | @router.get("/{id}/tasks") 64 | async def get_associated_tasks(db: SessionDep, id: int) -> List[TaskOut]: 65 | project = await crud.project.get_or_404(db, id) 66 | return project.tasks 67 | 68 | 69 | @router.put("/{id}/tasks") 70 | async def add_tasks_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[TaskOut]: 71 | project = await crud.project.get_or_404(db, id) 72 | for _id in ids: 73 | project.tasks.append(await crud.task.get_or_404(db, _id)) 74 | project = await crud.project.save(db, project) 75 | return project.tasks 76 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/endpoints/task.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from app import crud 4 | from app.deps import SessionDep 5 | from app.schemas import (ProjectOut, TaskCreate, TaskOut, TaskPatch, 6 | TaskUpdate, UserOut) 7 | from fastapi import APIRouter, UploadFile 8 | 9 | router = APIRouter(tags=["tasks"]) 10 | 11 | 12 | @router.get("/") 13 | async def read_all(db: SessionDep, skip: int = 0, limit: int = 100) -> list[TaskOut]: 14 | return await crud.task.get_all(db, skip=skip, limit=limit) 15 | 16 | 17 | @router.get("/{id}") 18 | async def read_one(db: SessionDep, id: int) -> TaskOut: 19 | task = await crud.task.get_or_404(db, id) 20 | return task 21 | 22 | 23 | @router.post("/", status_code=201) 24 | async def create(*, db: SessionDep, task_in: TaskCreate) -> TaskOut: 25 | return await crud.task.create(db, obj_in=task_in) 26 | 27 | 28 | @router.put("/{id}") 29 | async def update(*, db: SessionDep, id: int, task_in: TaskUpdate) -> TaskOut: 30 | task = await crud.task.get_or_404(db, id) 31 | return await crud.task.update(db, db_obj=task, obj_in=task_in) 32 | 33 | 34 | @router.patch("/{id}") 35 | async def patch(*, db: SessionDep, id: int, task_in: TaskPatch) -> TaskOut: 36 | task = await crud.task.get_or_404(db, id) 37 | return await crud.task.update(db, db_obj=task, obj_in=task_in) 38 | 39 | 40 | @router.delete("/{id}", status_code=204) 41 | async def delete(*, db: SessionDep, id: int) -> None: 42 | task = await crud.task.get_or_404(db, id) 43 | return await crud.task.delete(db, db_obj=task) 44 | 45 | 46 | # Handle files 47 | 48 | 49 | @router.put("/{id}/instruction_file") 50 | async def set_instruction_file(*, db: SessionDep, id: int, file: UploadFile) -> TaskOut: 51 | task = await crud.task.get_or_404(db, id) 52 | task.instruction_file = file 53 | return await crud.task.save(db, db_obj=task) 54 | 55 | 56 | @router.delete("/{id}/instruction_file", status_code=204) 57 | async def remove_instruction_file(*, db: SessionDep, id: int) -> None: 58 | task = await crud.task.get_or_404(db, id) 59 | task.instruction_file = None 60 | await crud.task.save(db, db_obj=task) 61 | 62 | 63 | # Handle relationships 64 | 65 | 66 | @router.get("/{id}/project") 67 | async def get_associated_project(db: SessionDep, id: int) -> Optional[ProjectOut]: 68 | task = await crud.task.get_or_404(db, id) 69 | return task.project 70 | 71 | 72 | @router.put("/{id}/project/{project_id}") 73 | async def set_project_by_id( 74 | db: SessionDep, id: int, project_id: int 75 | ) -> Optional[ProjectOut]: 76 | task = await crud.task.get_or_404(db, id) 77 | task.project = await crud.project.get_or_404(db, project_id) 78 | task = await crud.task.save(db, task) 79 | return task.project 80 | 81 | 82 | @router.get("/{id}/assigned_users") 83 | async def get_associated_assigned_users(db: SessionDep, id: int) -> List[UserOut]: 84 | task = await crud.task.get_or_404(db, id) 85 | return task.assigned_users 86 | 87 | 88 | @router.put("/{id}/assigned_users") 89 | async def add_assigned_users_by_ids( 90 | db: SessionDep, id: int, ids: List[int] 91 | ) -> List[UserOut]: 92 | task = await crud.task.get_or_404(db, id) 93 | for _id in ids: 94 | task.assigned_users.append(await crud.user.get_or_404(db, _id)) 95 | task = await crud.task.save(db, task) 96 | return task.assigned_users 97 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/endpoints/user.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from app import crud 4 | from app.deps import SessionDep 5 | from app.schemas import (ProjectOut, TaskOut, UserCreate, UserOut, UserPatch, 6 | UserUpdate) 7 | from fastapi import APIRouter, UploadFile 8 | 9 | router = APIRouter(tags=["users"]) 10 | 11 | 12 | @router.get("/") 13 | async def read_all(db: SessionDep, skip: int = 0, limit: int = 100) -> list[UserOut]: 14 | return await crud.user.get_all(db, skip=skip, limit=limit) 15 | 16 | 17 | @router.get("/{id}") 18 | async def read_one(db: SessionDep, id: int) -> UserOut: 19 | user = await crud.user.get_or_404(db, id) 20 | return user 21 | 22 | 23 | @router.post("/", status_code=201) 24 | async def create(*, db: SessionDep, user_in: UserCreate) -> UserOut: 25 | return await crud.user.create(db, obj_in=user_in) 26 | 27 | 28 | @router.put("/{id}") 29 | async def update(*, db: SessionDep, id: int, user_in: UserUpdate) -> UserOut: 30 | user = await crud.user.get_or_404(db, id) 31 | return await crud.user.update(db, db_obj=user, obj_in=user_in) 32 | 33 | 34 | @router.patch("/{id}") 35 | async def patch(*, db: SessionDep, id: int, user_in: UserPatch) -> UserOut: 36 | user = await crud.user.get_or_404(db, id) 37 | return await crud.user.update(db, db_obj=user, obj_in=user_in) 38 | 39 | 40 | @router.delete("/{id}", status_code=204) 41 | async def delete(*, db: SessionDep, id: int) -> None: 42 | user = await crud.user.get_or_404(db, id) 43 | return await crud.user.delete(db, db_obj=user) 44 | 45 | 46 | # Handle files 47 | 48 | 49 | @router.put("/{id}/avatar") 50 | async def set_avatar(*, db: SessionDep, id: int, file: UploadFile) -> UserOut: 51 | user = await crud.user.get_or_404(db, id) 52 | user.avatar = file 53 | return await crud.user.save(db, db_obj=user) 54 | 55 | 56 | @router.delete("/{id}/avatar", status_code=204) 57 | async def remove_avatar(*, db: SessionDep, id: int) -> None: 58 | user = await crud.user.get_or_404(db, id) 59 | user.avatar = None 60 | await crud.user.save(db, db_obj=user) 61 | 62 | 63 | # Handle relationships 64 | 65 | 66 | @router.get("/{id}/projects") 67 | async def get_associated_projects(db: SessionDep, id: int) -> List[ProjectOut]: 68 | user = await crud.user.get_or_404(db, id) 69 | return user.projects 70 | 71 | 72 | @router.put("/{id}/projects") 73 | async def add_projects_by_ids( 74 | db: SessionDep, id: int, ids: List[int] 75 | ) -> List[ProjectOut]: 76 | user = await crud.user.get_or_404(db, id) 77 | for _id in ids: 78 | user.projects.append(await crud.project.get_or_404(db, _id)) 79 | user = await crud.user.save(db, user) 80 | return user.projects 81 | 82 | 83 | @router.get("/{id}/tasks") 84 | async def get_associated_tasks(db: SessionDep, id: int) -> List[TaskOut]: 85 | user = await crud.user.get_or_404(db, id) 86 | return user.tasks 87 | 88 | 89 | @router.put("/{id}/tasks") 90 | async def add_tasks_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[TaskOut]: 91 | user = await crud.user.get_or_404(db, id) 92 | for _id in ids: 93 | user.tasks.append(await crud.task.get_or_404(db, _id)) 94 | user = await crud.user.save(db, user) 95 | return user.tasks 96 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class Status(str, enum.Enum): 5 | OPEN = "open" 6 | IN_PROGRESS = "in progress" 7 | COMPLETED = "completed" 8 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/main.py: -------------------------------------------------------------------------------- 1 | from app.admin import init_admin 2 | from app.endpoints import project, task, user 3 | from app.pre_start import init 4 | from app.settings import settings 5 | from fastapi import FastAPI, Request 6 | from fastapi.responses import FileResponse, JSONResponse 7 | from fastapi.staticfiles import StaticFiles 8 | from fastapi.templating import Jinja2Templates 9 | from libcloud.storage.types import ObjectDoesNotExistError 10 | from sqlalchemy_file.exceptions import ValidationError as FileValidationError 11 | from sqlalchemy_file.storage import StorageManager 12 | 13 | 14 | def create_app(): 15 | _app = FastAPI( 16 | title=settings.PROJECT_NAME, 17 | description=settings.PROJECT_DESCRIPTION, 18 | on_startup=[init], 19 | ) 20 | 21 | _app.include_router(user.router, prefix="/api/v1/users") 22 | _app.include_router(project.router, prefix="/api/v1/projects") 23 | _app.include_router(task.router, prefix="/api/v1/tasks") 24 | _app.mount("/static", StaticFiles(directory="static"), name="static") 25 | init_admin(_app) 26 | 27 | return _app 28 | 29 | 30 | templates = Jinja2Templates(directory="templates") 31 | 32 | app = create_app() 33 | 34 | 35 | @app.get("/", include_in_schema=False) 36 | async def home(request: Request): 37 | return templates.TemplateResponse( 38 | "index.html", {"request": request, "settings": settings} 39 | ) 40 | 41 | 42 | @app.get("/medias", response_class=FileResponse, tags=["medias"]) 43 | async def serve_files(path: str): 44 | try: 45 | file = StorageManager.get_file(path) 46 | return FileResponse( 47 | file.get_cdn_url(), media_type=file.content_type, filename=file.filename 48 | ) 49 | except ObjectDoesNotExistError: 50 | return JSONResponse({"detail": "Not found"}, status_code=404) 51 | 52 | 53 | @app.exception_handler(FileValidationError) 54 | async def sqla_file_validation_error(request: Request, exc: FileValidationError): 55 | return JSONResponse({"error": {"key": exc.key, "msg": exc.msg}}, status_code=422) 56 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List, Optional, Union 3 | 4 | from app.enums import Status 5 | from fastapi import UploadFile 6 | from pydantic import EmailStr 7 | from sqlalchemy import JSON, Column, Enum, ForeignKey, MetaData, String, Table 8 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship 9 | from sqlalchemy_file import File, FileField, ImageField 10 | from sqlalchemy_file.validators import ContentTypeValidator, SizeValidator 11 | 12 | 13 | class TimestampMixin: 14 | created_at: Mapped[datetime.datetime] = mapped_column( 15 | default=datetime.datetime.utcnow 16 | ) 17 | updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( 18 | onupdate=datetime.datetime.utcnow 19 | ) 20 | 21 | 22 | class Base(DeclarativeBase, TimestampMixin): 23 | metadata = MetaData( 24 | naming_convention={ 25 | "ix": "ix_%(column_0_label)s", 26 | "uq": "uq_%(table_name)s_%(column_0_name)s", 27 | "ck": "ck_%(table_name)s_%(constraint_name)s", 28 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 29 | "pk": "pk_%(table_name)s", 30 | } 31 | ) 32 | type_annotation_map = {EmailStr: String, dict: JSON} 33 | 34 | 35 | task__users = Table( 36 | "task__users", 37 | Base.metadata, 38 | Column("task_id", ForeignKey("task.id"), primary_key=True), 39 | Column("user_id", ForeignKey("user.id"), primary_key=True), 40 | ) 41 | 42 | 43 | class User(Base): 44 | __tablename__ = "user" 45 | 46 | id: Mapped[int] = mapped_column(primary_key=True) 47 | username: Mapped[str] = mapped_column(unique=True) 48 | email: Mapped[EmailStr] = mapped_column(unique=True) 49 | password: Mapped[str] 50 | first_name: Mapped[str] 51 | last_name: Mapped[str] 52 | avatar: Mapped[Union[File, UploadFile, None]] = mapped_column( 53 | ImageField( 54 | thumbnail_size=(150, 150), validators=[SizeValidator(max_size="20M")] 55 | ) 56 | ) 57 | bio: Mapped[str] 58 | 59 | projects: Mapped[List["Project"]] = relationship(back_populates="user") 60 | 61 | tasks: Mapped[List["Task"]] = relationship( 62 | secondary=task__users, back_populates="assigned_users" 63 | ) 64 | 65 | 66 | class Project(Base): 67 | __tablename__ = "project" 68 | 69 | id: Mapped[int] = mapped_column(primary_key=True) 70 | name: Mapped[str] 71 | description: Mapped[str] 72 | start_date: Mapped[datetime.date] 73 | due_date: Mapped[datetime.date] 74 | 75 | user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("user.id")) 76 | user: Mapped["User"] = relationship(back_populates="projects") 77 | 78 | tasks: Mapped[List["Task"]] = relationship(back_populates="project") 79 | 80 | 81 | class Task(Base): 82 | __tablename__ = "task" 83 | 84 | id: Mapped[int] = mapped_column(primary_key=True) 85 | name: Mapped[str] 86 | description: Mapped[str] 87 | start_date: Mapped[datetime.date] 88 | due_date: Mapped[datetime.date] 89 | status: Mapped[str] = mapped_column(Enum(Status)) 90 | instruction_file: Mapped[Union[File, UploadFile, None]] = mapped_column( 91 | FileField( 92 | validators=[ 93 | SizeValidator(max_size="20M"), 94 | ContentTypeValidator( 95 | ["application/pdf", "application/msword", "text/plain"] 96 | ), 97 | ] 98 | ) 99 | ) 100 | 101 | project_id: Mapped[Optional[int]] = mapped_column(ForeignKey("project.id")) 102 | project: Mapped["Project"] = relationship(back_populates="tasks") 103 | 104 | assigned_users: Mapped[List["User"]] = relationship( 105 | secondary=task__users, back_populates="tasks" 106 | ) 107 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/pre_start.py: -------------------------------------------------------------------------------- 1 | 2 | from app.db import init_db 3 | from app.storage import init_storage 4 | 5 | 6 | async def init() -> None: 7 | await init_db() 8 | await init_storage() 9 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/schemas.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | 4 | from app.enums import Status 5 | from pydantic import BaseModel, EmailStr, Field 6 | 7 | 8 | class Thumbnail(BaseModel): 9 | path: str 10 | 11 | 12 | class FileInfo(BaseModel): 13 | filename: str 14 | content_type: str 15 | path: str 16 | thumbnail: Optional[Thumbnail] = None 17 | 18 | 19 | # -------------- User ------------------ 20 | 21 | 22 | class UserCreate(BaseModel): 23 | username: str = Field(max_length=50) 24 | email: EmailStr = Field(max_length=100) 25 | password: str = Field(min_length=8, max_length=100) 26 | first_name: str = Field(max_length=50) 27 | last_name: str = Field(max_length=50) 28 | bio: str 29 | 30 | 31 | class UserUpdate(UserCreate): 32 | pass 33 | 34 | 35 | class UserPatch(BaseModel): 36 | username: Optional[str] = Field(None, max_length=50) 37 | email: Optional[EmailStr] = Field(None, max_length=100) 38 | password: Optional[str] = Field(None, min_length=8, max_length=100) 39 | first_name: Optional[str] = Field(None, max_length=50) 40 | last_name: Optional[str] = Field(None, max_length=50) 41 | bio: Optional[str] = Field(None) 42 | 43 | 44 | class UserOut(BaseModel): 45 | id: int 46 | username: str = Field(max_length=50) 47 | email: EmailStr = Field(max_length=100) 48 | password: str = Field(min_length=8, max_length=100) 49 | first_name: str = Field(max_length=50) 50 | last_name: str = Field(max_length=50) 51 | avatar: Optional[FileInfo] 52 | bio: str 53 | 54 | 55 | # -------------- Project ------------------ 56 | 57 | 58 | class ProjectCreate(BaseModel): 59 | name: str = Field(max_length=100) 60 | description: str 61 | start_date: datetime.date 62 | due_date: datetime.date 63 | 64 | 65 | class ProjectUpdate(ProjectCreate): 66 | pass 67 | 68 | 69 | class ProjectPatch(BaseModel): 70 | name: Optional[str] = Field(None, max_length=100) 71 | description: Optional[str] = Field(None) 72 | start_date: Optional[datetime.date] = Field(None) 73 | due_date: Optional[datetime.date] = Field(None) 74 | 75 | 76 | class ProjectOut(BaseModel): 77 | id: int 78 | name: str = Field(max_length=100) 79 | description: str 80 | start_date: datetime.date 81 | due_date: datetime.date 82 | 83 | 84 | # -------------- Task ------------------ 85 | 86 | 87 | class TaskCreate(BaseModel): 88 | name: str = Field(max_length=100) 89 | description: str 90 | start_date: datetime.date 91 | due_date: datetime.date 92 | status: Status 93 | 94 | 95 | class TaskUpdate(TaskCreate): 96 | pass 97 | 98 | 99 | class TaskPatch(BaseModel): 100 | name: Optional[str] = Field(None, max_length=100) 101 | description: Optional[str] = Field(None) 102 | start_date: Optional[datetime.date] = Field(None) 103 | due_date: Optional[datetime.date] = Field(None) 104 | status: Optional[Status] = Field(None) 105 | 106 | 107 | class TaskOut(BaseModel): 108 | id: int 109 | name: str = Field(max_length=100) 110 | description: str 111 | start_date: datetime.date 112 | due_date: datetime.date 113 | status: Status 114 | instruction_file: Optional[FileInfo] = Field( 115 | mime_types=["application/pdf", "application/msword", "text/plain"] 116 | ) 117 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | 3 | 4 | class Settings(BaseSettings): 5 | SQLALCHEMY_DATABASE_URI: str = "sqlite:///db.sqlite" 6 | PROJECT_NAME: str = "TaskManager" 7 | PROJECT_DESCRIPTION: str = "An application for managing tasks that enables multiple users to collaborate on a single project with multiple tasks." 8 | 9 | 10 | settings = Settings() 11 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/app/storage.py: -------------------------------------------------------------------------------- 1 | from libcloud.storage.base import Container, StorageDriver 2 | from libcloud.storage.drivers.local import LocalStorageDriver 3 | from libcloud.storage.types import ContainerDoesNotExistError 4 | from sqlalchemy_file.storage import StorageManager 5 | 6 | 7 | def get_or_create_container(driver: StorageDriver, name: str) -> Container: 8 | try: 9 | return driver.get_container(name) 10 | except ContainerDoesNotExistError: 11 | return driver.create_container(name) 12 | 13 | 14 | async def init_storage() -> None: 15 | StorageManager.add_storage( 16 | "default", get_or_create_container(LocalStorageDriver("."), "assets") 17 | ) 18 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.104.1 2 | starlette-admin>=0.12 3 | pydantic[email]>=2 4 | pydantic_settings 5 | sqlalchemy>=2 6 | sqlalchemy-file==0.6.0 7 | fasteners==0.19 8 | pillow>=10.1 9 | python-multipart==0.0.6 10 | uvicorn>=0.24.0.post1 -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/static/css/style.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.btn{display:inline-flex;height:2.25rem;align-items:center;justify-content:center;border-radius:.375rem;--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity:1;color:rgb(250 250 250/var(--tw-text-opacity));--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn:hover{background-color:#18181be6}.btn:focus-visible{outline:2px solid #0000;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(9 9 11/var(--tw-ring-opacity))}.mx-8{margin-left:2rem;margin-right:2rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-full{height:100%}.h-screen{height:100vh}.resize{resize:both}.flex-col{flex-direction:column}.justify-center{justify-content:center}.gap-8{gap:2rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-zinc-300{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.text-center{text-align:center}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.tracking-tighter{letter-spacing:-.05em}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (prefers-color-scheme:dark){.dark\:text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:text-5xl{font-size:3rem;line-height:1}}@media (min-width:768px){.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:text-6xl{font-size:3.75rem;line-height:1}} -------------------------------------------------------------------------------- /examples/fastapi/task-management/generated/openai/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ settings.PROJECT_NAME }} 8 | 9 | 10 | 11 |
12 |
13 |

14 | {{ settings.PROJECT_NAME }} 15 |

16 |

17 | {{ settings.PROJECT_DESCRIPTION }} 18 |

19 |
20 |
21 |

Swagger Documentation

22 |

Explore and test your endpoints using Swagger UI. 23 |

24 | 25 | Browse 26 | 27 |
28 |
29 |

Redoc Documentation

30 |

View and interact with your endpoints using Redoc. 31 |

32 | 33 | Browse 34 | 35 |
36 |
37 |

Admin Interface

38 |

Manage your entities with the admin interface. 39 |

40 | 41 | Browse 42 | 43 |
44 |
45 |
46 | 51 |
52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/fastapi/task-management/prompt: -------------------------------------------------------------------------------- 1 | An application for managing tasks that enables multiple users to collaborate on a single project with multiple tasks. 2 | The user creates a project, and each task can be assigned to multiple users, while a user can also work on multiple tasks. 3 | Each task has a start date and a due date, with a status that can be open, in progress, or completed. 4 | Additionally, each task can have an instruction file attached to it, which can be a PDF, a Word document, or a text file. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "qwikcrud" 7 | dynamic = ["version"] 8 | description = "An AI-powered command-line tool that generates RESTful APIs and admin interfaces based on user prompts" 9 | readme = "README.md" 10 | requires-python = ">=3.9" 11 | license = "Apache-2.0" 12 | keywords = ["ai", "chatgpt", "fastapi", "sqlalchemy", "starlette-admin", "crud"] 13 | authors = [ 14 | { name = "Jocelin Hounon", email = "hounonj@gmail.com" }, 15 | ] 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: Implementation :: CPython", 24 | "Programming Language :: Python :: Implementation :: PyPy", 25 | ] 26 | dependencies = [ 27 | "pydantic>=2.5,<2.6", 28 | "pydantic-settings", 29 | "aiofile>=3.8,<3.9", 30 | "httpx>=0.25,<0.26", 31 | "openai>=1.3,<1.4", 32 | "google-generativeai>=0.3.1,<0.4", 33 | "rich>=13", 34 | "prompt_toolkit>=3.0.41,<3.1", 35 | "jinja2>=3,<4", 36 | "black>=23.11.0,<23.12", 37 | "autoflake>=2.2.1,<2.3", 38 | "isort>=5.12.0,<5.13", 39 | ] 40 | 41 | [project.urls] 42 | Documentation = "https://github.com/jowilf/qwikcrud/#readme" 43 | Issues = "https://github.com/jowilf/qwikcrud/issues" 44 | Source = "https://github.com/jowilf/qwikcrud" 45 | 46 | [tool.hatch.version] 47 | path = "qwikcrud/__init__.py" 48 | 49 | [tool.hatch.envs.default] 50 | dependencies = [ 51 | "ruff==0.1.1", 52 | "pytest>=7.4.3,<7.5" 53 | ] 54 | [tool.hatch.envs.default.scripts] 55 | cli = "python -m qwikcrud.cli {args}" 56 | format = [ 57 | "black {args:qwikcrud tests}", 58 | "ruff --fix {args:qwikcrud tests}", 59 | ] 60 | test = "pytest {args:tests}" 61 | lint = [ 62 | "ruff {args:qwikcrud tests}", 63 | "black --check {args:qwikcrud tests}" 64 | ] 65 | 66 | [tool.ruff] 67 | target-version = "py39" 68 | line-length = 120 69 | select = [ 70 | "A", 71 | "ARG", 72 | "B", 73 | "C", 74 | "DTZ", 75 | "E", 76 | "EM", 77 | "F", 78 | "I", 79 | "ICN", 80 | "ISC", 81 | "N", 82 | "PLC", 83 | "PLE", 84 | "PLW", 85 | "Q", 86 | "RUF", 87 | "S", 88 | "T", 89 | "TID", 90 | "UP", 91 | "W", 92 | "YTT", 93 | ] 94 | 95 | [tool.ruff.per-file-ignores] 96 | "tests/**" = ["S101"] 97 | 98 | [tool.ruff.isort] 99 | known-first-party = ["qwikcrud"] 100 | 101 | [project.scripts] 102 | qwikcrud = "qwikcrud.cli:main" 103 | 104 | 105 | [tool.hatch.build.targets.sdist] 106 | exclude = ["/.github"] -------------------------------------------------------------------------------- /qwikcrud/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.4" 2 | -------------------------------------------------------------------------------- /qwikcrud/cli.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from pathlib import Path 4 | 5 | import click 6 | import prompt_toolkit as pt 7 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 8 | from prompt_toolkit.history import FileHistory 9 | from rich.console import Console 10 | from rich.status import Status 11 | 12 | from qwikcrud import __version__ 13 | from qwikcrud.generator import FastAPIAppGenerator 14 | from qwikcrud.logger import setup_logging 15 | from qwikcrud.provider.base import AIProvider 16 | from qwikcrud.provider.google import GoogleProvider 17 | from qwikcrud.provider.openai import OpenAIProvider 18 | 19 | 20 | @click.command() 21 | @click.option( 22 | "-o", "--output-dir", default=".", help="Output directory for the generated app." 23 | ) 24 | @click.option( 25 | "--ai", 26 | "ai_provider", 27 | type=click.Choice(["google", "openai"]), 28 | default="google", 29 | help="Choose the AI provider to use for generation. Default is Google.", 30 | ) 31 | def main(output_dir: str, ai_provider: AIProvider) -> None: 32 | setup_logging() 33 | 34 | history = Path().home() / ".qwikcrud-prompt-history.txt" 35 | session = pt.PromptSession(history=FileHistory(str(history))) 36 | 37 | ai_provider_map: dict[str, type[AIProvider]] = { 38 | "google": GoogleProvider, 39 | "openai": OpenAIProvider, 40 | } 41 | ai = ai_provider_map[ai_provider]() 42 | code_generator = FastAPIAppGenerator(Path(output_dir).resolve()) 43 | 44 | console = Console() 45 | console.print( 46 | f"[bold green]Qwikcrud v{__version__}[/bold green] - {ai.get_name()}\n\n" 47 | "Type [blink]/exit[/blink] at any time to exit the generator.\n" 48 | ) 49 | 50 | is_first_prompt = True 51 | while True: 52 | prompt = session.prompt( 53 | "qwikcrud" 54 | f" ({'Describe your app' if is_first_prompt else 'Specify any modifications or enhancements'}) ➤ ", 55 | auto_suggest=AutoSuggestFromHistory(), 56 | ) 57 | if prompt == "/exit": 58 | return 59 | try: 60 | with Status( 61 | f"[dim]Asking {ai.get_name()} …[/dim]", console=console 62 | ) as status: 63 | app = ai.query(prompt) 64 | status.update("[dim]Generating the app[/dim]") 65 | code_generator.clean() 66 | code_generator.generate(app) 67 | console.print(f"App successfully generated in {Path(output_dir).resolve()}") 68 | console.print("\nHere is the summary of the generated app:\n") 69 | app.summary() 70 | console.print( 71 | "Follow the instructions in the README.md file to run your application." 72 | ) 73 | console.print( 74 | "\nYou can request changes if the generated app doesn't meet your" 75 | " expectations." 76 | ) 77 | is_first_prompt = False 78 | except Exception as e: 79 | logging.exception(e) 80 | console.print("[red]Something went wrong, Try again![/red]") 81 | 82 | 83 | if __name__ == "__main__": 84 | sys.exit(main()) 85 | -------------------------------------------------------------------------------- /qwikcrud/generator.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from pathlib import Path 3 | 4 | import autoflake 5 | import black 6 | import isort 7 | from jinja2 import Environment, FileSystemLoader 8 | 9 | from qwikcrud import helpers as h 10 | from qwikcrud.schemas import App 11 | 12 | 13 | class BaseAppGenerator: 14 | def __init__(self, output_directory: Path) -> None: 15 | self.env = Environment( # noqa: S701 16 | loader=FileSystemLoader(h.path_to("templates")), 17 | trim_blocks=True, 18 | lstrip_blocks=True, 19 | ) 20 | self.env.filters["snake_case"] = h.snake_case 21 | self.output_directory = output_directory 22 | 23 | def _write_code_into_file(self, path, code_text: str, format_code: bool = True): 24 | with open(self.output_directory / f"{path}", "w") as file: 25 | if format_code: 26 | code_text = black.format_str(code_text, mode=black.Mode()) 27 | code_text = autoflake.fix_code( 28 | code_text, remove_all_unused_imports=True 29 | ) 30 | code_text = isort.code(code_text) 31 | file.write(code_text) 32 | 33 | def _generate_from_template( 34 | self, 35 | app: App, 36 | relative_template_path, 37 | destination_path=None, 38 | template_data=None, 39 | skip_render=False, 40 | extension="py", 41 | ): 42 | if template_data is None: 43 | template_data = {"app": app} 44 | if destination_path is None: 45 | destination_path = relative_template_path 46 | if skip_render: 47 | template_text = open( 48 | f"{h.path_to('templates')}/{self._absolute_template_path(f'{relative_template_path}.{extension}')}" 49 | ).read() 50 | self._write_code_into_file( 51 | f"{destination_path}.{extension}", template_text, extension == "py" 52 | ) 53 | else: 54 | template = self.env.get_template( 55 | self._absolute_template_path(f"{relative_template_path}.{extension}.j2") 56 | ) 57 | rendered_code = template.render(template_data) 58 | self._write_code_into_file( 59 | f"{destination_path}.{extension}", rendered_code, extension == "py" 60 | ) 61 | 62 | @abstractmethod 63 | def _absolute_template_path(self, relative_path: str) -> str: 64 | raise NotImplementedError 65 | 66 | @abstractmethod 67 | def generate(self, app: App) -> None: 68 | raise NotImplementedError 69 | 70 | @abstractmethod 71 | def clean(self): 72 | raise NotImplementedError 73 | 74 | 75 | class FastAPIAppGenerator(BaseAppGenerator): 76 | def _absolute_template_path(self, relative_path: str) -> str: 77 | return f"fastapi/{relative_path}" 78 | 79 | def generate(self, app: App) -> None: 80 | h.apply_python_naming_convention(app) 81 | h.make_dirs(self.output_directory / "app") 82 | h.make_dirs(self.output_directory / "templates") 83 | h.make_dirs(self.output_directory / "static/css") 84 | self._generate_from_template(app, "app/__init__") 85 | self._generate_from_template(app, "app/models") 86 | self._generate_from_template(app, "app/schemas") 87 | self._generate_from_template(app, "app/crud") 88 | self._generate_from_template(app, "app/deps") 89 | self._generate_from_template(app, "app/settings") 90 | self._generate_from_template(app, "app/db") 91 | self._generate_from_template(app, "app/pre_start") 92 | self._generate_from_template(app, "app/main") 93 | self._generate_from_template(app, "app/admin") 94 | self._generate_from_template(app, "requirements", extension="txt") 95 | self._generate_from_template(app, "README", extension="md") 96 | self._generate_from_template( 97 | app, "templates/index", extension="html", skip_render=True 98 | ) 99 | self._generate_from_template( 100 | app, "static/css/style", extension="css", skip_render=True 101 | ) 102 | self.__generate_endpoints(app) 103 | if app.has_file(): 104 | self._generate_from_template(app, "app/storage") 105 | if app.has_enum(): 106 | self._generate_from_template(app, "app/enums") 107 | self._write_code_into_file( 108 | ".qwikcrud.json.lock", 109 | app.model_dump_json(indent=4, exclude_unset=True, by_alias=True), 110 | format_code=False, 111 | ) 112 | 113 | def __generate_endpoints(self, app: App) -> None: 114 | h.make_dirs(self.output_directory / "app/endpoints") 115 | self._generate_from_template(app, "app/endpoints/__init__") 116 | for entity in app.entities: 117 | template_path = "app/endpoints/template" 118 | destination_path = f"app/endpoints/{entity.name.lower()}" 119 | self._generate_from_template( 120 | app, template_path, destination_path, {"entity": entity, "app": app} 121 | ) 122 | 123 | def clean(self): 124 | h.delete_dir(self.output_directory / "app") 125 | h.delete_dir(self.output_directory / "templates") 126 | h.delete_dir(self.output_directory / "static") 127 | h.delete_file(self.output_directory / "requirements.txt") 128 | h.delete_file(self.output_directory / "README.md") 129 | -------------------------------------------------------------------------------- /qwikcrud/helpers.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import re 4 | import shutil 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from qwikcrud.schemas import App 10 | 11 | 12 | def path_to(relative: str): 13 | return Path(__file__).parent / relative 14 | 15 | 16 | def make_dirs(_dir: Path): 17 | os.makedirs(_dir, 0o777, exist_ok=True) 18 | 19 | 20 | def delete_dir(_dir: Path): 21 | shutil.rmtree(_dir, ignore_errors=True) 22 | 23 | 24 | def delete_file(file: Path): 25 | with contextlib.suppress(FileNotFoundError): 26 | os.remove(file) 27 | 28 | 29 | def lower_first_character(text: str) -> str: 30 | return text[0].lower() + text[1:] 31 | 32 | 33 | def upper_first_character(text: str) -> str: 34 | return text[0].upper() + text[1:] 35 | 36 | 37 | def snake_case(text: str) -> str: 38 | return "".join(["_" + c.lower() if c.isupper() else c for c in text]).lstrip("_") 39 | 40 | 41 | def extract_json_from_markdown(markdown_text): 42 | # Regular expression to find a JSON object in the text 43 | json_pattern = re.compile(r"```(json)?(.*)```", re.DOTALL) 44 | 45 | # Attempt to find a JSON object in the text 46 | json_match = json_pattern.search(markdown_text) 47 | 48 | if json_match: 49 | # If match found, extract the JSON string from the code block 50 | json_str = json_match.group(2) 51 | else: 52 | # If no JSON object is found, consider the entire text as JSON 53 | json_str = markdown_text 54 | return json_str 55 | 56 | 57 | def apply_python_naming_convention(app: "App"): 58 | for entity in app.entities: 59 | for field in entity.fields: 60 | field.name = snake_case(field.name) 61 | for relation in app.relations: 62 | relation.field_name = snake_case(relation.field_name) 63 | relation.backref_field_name = snake_case(relation.backref_field_name) 64 | -------------------------------------------------------------------------------- /qwikcrud/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from qwikcrud.settings import settings 4 | 5 | 6 | def setup_logging(): 7 | logging.basicConfig( 8 | format="%(levelname)s [%(asctime)s] %(name)s - %(message)s", 9 | datefmt="%Y-%m-%d %H:%M:%S", 10 | level=settings.logging_level, 11 | ) 12 | -------------------------------------------------------------------------------- /qwikcrud/prompts/system: -------------------------------------------------------------------------------- 1 | You are an assistant that can analyze an application idea given by a user, 2 | identifying the entities and the relationships between them as well as the fields 3 | of each entity and their constraints necessary to build a RestfulAPIs for the given idea. 4 | 5 | Format the output as a JSON instance, following RFC8259 compliance. Do not include any explanations; 6 | provide the JSON response strictly adhering to the instructions below without deviation: 7 | 8 | 1. Create a JSON object for the root App schema. This will be the base JSON. 9 | 2. Include the required "name" (lowercase) key with a string value for the app name (maximum 100 characters). 10 | 3. Include the required "description" key with a string value describing the app (maximum 256 characters). 11 | 4. Include the required "entities" key containing an array of entity objects. 12 | 4.1 For each entity, include the required "name" (lowercase) and "fields" key. 13 | 4.1.1 The "fields" value should be an array of field objects with "name," "type," and "constraints" keys. 14 | 4.1.1.1 The possible values for "type" (field types) are: 'ID','Integer','Float','Boolean','Date','Time','DateTime','String','Text','Enum','Email','JSON','Image','File' 15 | 4.1.1.1 Use the 'Image' type for image fields and 'File' type for file fields. For 'File' type, add the "mime_types" constraints to specify allowed mime types. 16 | 4.1.2 The "constraints" key is an object with possible keys such as "unique"(boolean), "not_null"(boolean), "gt"(number), "ge"(number),"lt"(number), "le"(number), "multiple_of"(number), and "allowed_values" (for Enum fields). Only include necessary constraints; omit unnecessary ones. 17 | 4.1.3 Include all necessary fields, including images and files. Exclude foreign key and relationship fields. 18 | 4.1.4 Each field should have a unique field of type ID named "id" 19 | 4.2 Exclude "created_at" and "updated_at" fields from any entity. 20 | 5. Include the required "relations" key containing an array of relation objects. Each relation object has the following keys: 21 | - "name": string (capitalize and use maximum 100 characters) 22 | - "type": the possible values are: "ONE_TO_ONE" "ONE_TO_MANY" "MANY_TO_MANY" 23 | - "from": entity name (capitalize) 24 | - "to": entity name (capitalize) 25 | - "field_name": name of the relation field in the 'from' Entity 26 | - "backref_field_name": name of the relation field in the 'to' Entity 27 | Here is an example of the response you should produce: 28 | {"name":"The app name","description":"A description of the app","entities":[{"name":"User","fields":[{"name":"id","type":"ID"},{"name":"username","type":"String","constraints":{"unique":true,"max_length":50}},{"name":"email","type":"Email","constraints":{"unique":true,"max_length":100}},{"name":"password","type":"String","constraints":{"min_length":8,"max_length":100}},{"name":"first_name","type":"String","constraints":{"max_length":50}},{"name":"last_name","type":"String","constraints":{"max_length":50}},{"name":"avatar","type":"Image"},{"name":"bio","type":"Text"}]},{"name":"Address","fields":[{"name":"id","type":"ID"},{"name":"street","type":"String","constraints":{"max_length":100}},{"name":"city","type":"String","constraints":{"max_length":50}},{"name":"zip_code","type":"String","constraints":{"max_length":20}}]},{"name":"Product","fields":[{"name":"id","type":"ID"},{"name":"name","type":"String","constraints":{"max_length":100}},{"name":"description","type":"Text"},{"name":"price","type":"Float","constraints":{"ge":0}},{"name":"in_stock","type":"Boolean"},{"name":"image","type":"Image"}]},{"name":"Order","fields":[{"name":"id","type":"ID"},{"name":"order_date","type":"Date"},{"name":"total_amount","type":"Float","constraints":{"ge":0}},{"name":"is_paid","type":"Boolean"}]}],"relations":[{"name":"User_Address","type":"ONE_TO_ONE","from":"User","to":"Address","field_name":"address","backref_field_name":"user"},{"name":"User_Products","type":"ONE_TO_MANY","from":"User","to":"Product","field_name":"products","backref_field_name":"user"},{"name":"Product_Orders","type":"MANY_TO_MANY","from":"Product","to":"Order","field_name":"orders","backref_field_name":"products"}]} -------------------------------------------------------------------------------- /qwikcrud/provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowilf/qwikcrud/560dcabb1dfcf6ea5885d7077e63dd3496a89908/qwikcrud/provider/__init__.py -------------------------------------------------------------------------------- /qwikcrud/provider/base.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from qwikcrud.schemas import App 4 | 5 | 6 | class AIProvider: 7 | def __init__(self) -> None: 8 | pass 9 | 10 | @abstractmethod 11 | def get_name(self) -> str: 12 | raise NotImplementedError 13 | 14 | @abstractmethod 15 | def query(self, prompt: str) -> App: 16 | raise NotImplementedError 17 | -------------------------------------------------------------------------------- /qwikcrud/provider/dummy.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from qwikcrud.helpers import path_to 4 | from qwikcrud.provider.base import AIProvider 5 | from qwikcrud.schemas import App 6 | 7 | 8 | class DummyAIProvider(AIProvider): 9 | def get_name(self) -> str: 10 | return "DummyAI" 11 | 12 | def query(self, prompt: str) -> App: # noqa ARG002 13 | time.sleep(0.1) 14 | return App.model_validate_json(open(path_to("../tests/dummy.json")).read()) 15 | -------------------------------------------------------------------------------- /qwikcrud/provider/google.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | import google.generativeai as genai 5 | 6 | import qwikcrud.helpers as h 7 | from qwikcrud.provider.base import AIProvider 8 | from qwikcrud.schemas import App 9 | from qwikcrud.settings import settings 10 | 11 | 12 | class GoogleProvider(AIProvider): 13 | def __init__(self): 14 | super().__init__() 15 | genai.configure(api_key=settings.google_api_key) 16 | self.model = genai.GenerativeModel(settings.google_model) 17 | with open(h.path_to("prompts/system")) as f: 18 | system_message = f.read() 19 | self.messages: list[dict[str, Any]] = [ 20 | # Workaround for system message 21 | { 22 | "role": "user", 23 | "parts": [system_message], 24 | }, 25 | { 26 | "role": "model", 27 | "parts": [ 28 | "Please provide a brief description of your app and any specific" 29 | " features or functionalities you have in mind." 30 | ], 31 | }, 32 | ] 33 | 34 | def get_name(self) -> str: 35 | return f"Google ({settings.google_model})" 36 | 37 | def query(self, prompt: str) -> App: 38 | self.messages.append({"role": "user", "parts": [prompt]}) 39 | completion = self.model.generate_content(self.messages) 40 | self.messages.append( 41 | { 42 | "role": "model", 43 | "parts": [completion.text], 44 | } 45 | ) 46 | logging.debug(f"Result from {self.get_name()}: {completion.text}") 47 | return App.model_validate_json( 48 | h.extract_json_from_markdown(completion.text), strict=False 49 | ) 50 | -------------------------------------------------------------------------------- /qwikcrud/provider/openai.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | from openai import OpenAI 5 | 6 | from qwikcrud.helpers import path_to 7 | from qwikcrud.provider.base import AIProvider 8 | from qwikcrud.schemas import App 9 | from qwikcrud.settings import settings 10 | 11 | 12 | class OpenAIProvider(AIProvider): 13 | def __init__(self): 14 | super().__init__() 15 | self.client = OpenAI(api_key=settings.openai_api_key) 16 | with open(path_to("prompts/system")) as f: 17 | system_message = f.read() 18 | self.messages: list[dict[str, Any]] = [ 19 | { 20 | "role": "system", 21 | "content": system_message, 22 | }, 23 | ] 24 | 25 | def get_name(self) -> str: 26 | return f"ChatGPT ({settings.openai_model})" 27 | 28 | def query(self, prompt: str) -> App: 29 | self.messages.append({"role": "user", "content": prompt}) 30 | completion = self.client.chat.completions.create( 31 | model=settings.openai_model, 32 | response_format={"type": "json_object"}, 33 | messages=self.messages, 34 | temperature=0.4, 35 | ) 36 | self.messages.append( 37 | { 38 | "role": "assistant", 39 | "content": completion.choices[0].message.content, 40 | } 41 | ) 42 | logging.debug( 43 | f"Result from {self.get_name()}: {completion.choices[0].message.content}" 44 | ) 45 | return App.model_validate_json( 46 | completion.choices[0].message.content, strict=False 47 | ) 48 | -------------------------------------------------------------------------------- /qwikcrud/schemas.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, Optional 3 | 4 | from pydantic import BaseModel, Field, model_validator 5 | from rich.console import Console 6 | from rich.tree import Tree 7 | 8 | from qwikcrud import helpers as h 9 | 10 | 11 | class FieldType(str, Enum): 12 | ID = "ID" 13 | Integer = "Integer" 14 | Float = "Float" 15 | Boolean = "Boolean" 16 | Date = "Date" 17 | Time = "Time" 18 | DateTime = "DateTime" 19 | String = "String" 20 | Text = "Text" 21 | Enum = "Enum" 22 | Email = "Email" 23 | JSON = "JSON" 24 | Image = "Image" 25 | File = "File" 26 | 27 | @classmethod 28 | def _missing_(cls: "FieldType", value: str) -> Optional["str"]: 29 | """When an invalid or case-mismatched string value is provided, this method attempts to find a 30 | case-insensitive match among the enum members.""" 31 | value = value.lower() 32 | for member in cls: 33 | if member.lower() == value: 34 | return member 35 | return None 36 | 37 | 38 | field_python_type_mapping = { 39 | FieldType.ID: "int", 40 | FieldType.Integer: "int", 41 | FieldType.Float: "float", 42 | FieldType.Boolean: "bool", 43 | FieldType.Date: "datetime.date", 44 | FieldType.Time: "datetime.time", 45 | FieldType.DateTime: "datetime.datetime", 46 | FieldType.String: "str", 47 | FieldType.Text: "str", 48 | FieldType.Enum: "str", 49 | FieldType.Email: "EmailStr", 50 | FieldType.JSON: "dict", 51 | FieldType.Image: "Union[File, UploadFile, None]", 52 | FieldType.File: "Union[File, UploadFile, None]", 53 | } 54 | 55 | 56 | class RelationType(str, Enum): 57 | ONE_TO_ONE = "ONE_TO_ONE" 58 | ONE_TO_MANY = "ONE_TO_MANY" 59 | MANY_TO_MANY = "MANY_TO_MANY" 60 | 61 | @classmethod 62 | def _missing_(cls: "FieldType", value: str) -> Optional["str"]: 63 | """When an invalid or case-mismatched string value is provided, this method attempts to find a 64 | case-insensitive match among the enum members.""" 65 | value = value.lower() 66 | for member in cls: 67 | if member.lower() == value: 68 | return member 69 | return None 70 | 71 | 72 | class Constraints(BaseModel): 73 | unique: Optional[bool] = Field(None) 74 | not_null: Optional[bool] = Field(None) 75 | gt: Optional[float] = Field(None) 76 | ge: Optional[float] = Field(None) 77 | lt: Optional[float] = Field(None) 78 | le: Optional[float] = Field(None) 79 | multiple_of: Optional[float] = Field(None) 80 | min_length: Optional[int] = Field(None) 81 | max_length: Optional[int] = Field(None) 82 | mime_types: Optional[list[str]] = Field(None) 83 | allowed_values: Optional[list[str]] = Field(None) 84 | 85 | def get_allowed_values(self): 86 | return self.allowed_values or [] 87 | 88 | 89 | class FieldModel(BaseModel): 90 | name: str = Field(...) 91 | type_: FieldType = Field(..., alias="type") 92 | constraints: Optional[Constraints] = Field(Constraints()) 93 | 94 | @model_validator(mode="after") 95 | def root_validator(self) -> "FieldModel": 96 | self.name = h.lower_first_character(self.name) 97 | return self 98 | 99 | def is_id(self): 100 | return self.type_ == FieldType.ID 101 | 102 | def is_file(self): 103 | return self.type_ in (FieldType.Image, FieldType.File) 104 | 105 | def sqla_column_def(self) -> str: 106 | """Generate the sqlalchemy column definition 107 | 108 | Example: 109 | id: Mapped[int] = mapped_column(primary_key=True) 110 | """ 111 | type_mapping = field_python_type_mapping.get(self.type_) 112 | if self.constraints.not_null: 113 | type_mapping = f"Optional[{type_mapping}]" 114 | mapped_column_kwargs = [] 115 | if self.type_ in [FieldType.Image, FieldType.File]: 116 | file_field_mapping = ( 117 | "ImageField" if self.type_ == FieldType.Image else "FileField" 118 | ) 119 | file_field_mapping_kwargs = [] 120 | if self.type_ == FieldType.Image: 121 | file_field_mapping_kwargs.append("thumbnail_size=(150,150)") 122 | file_field_validators = ['SizeValidator(max_size="20M")'] 123 | if self.constraints.mime_types: 124 | mimetypes_list = ",".join( 125 | ('"' + v + '"') for v in self.constraints.mime_types 126 | ) 127 | file_field_validators.append( 128 | f"ContentTypeValidator([{mimetypes_list}])" 129 | ) 130 | file_field_mapping_kwargs.append( 131 | f'validators=[{",".join(file_field_validators)}]' 132 | ) 133 | file_field_mapping += f'({",".join(file_field_mapping_kwargs)})' 134 | mapped_column_kwargs.append(file_field_mapping) 135 | if self.type_ == FieldType.Enum: 136 | mapped_column_kwargs.append(f"Enum({self.name.capitalize()})") 137 | if self.constraints.unique: 138 | mapped_column_kwargs.append("unique=True") 139 | if self.type_ == FieldType.ID: 140 | mapped_column_kwargs.append("primary_key=True") 141 | return f"{h.snake_case(self.name)}: Mapped[{type_mapping}]" + ( 142 | f'=mapped_column({",".join(mapped_column_kwargs)})' 143 | if len(mapped_column_kwargs) > 0 144 | else "" 145 | ) 146 | 147 | def pydantic_def(self, all_optional: bool = False): 148 | type_mapping = field_python_type_mapping.get(self.type_) 149 | if self.type_ == FieldType.Enum: 150 | type_mapping = self.name.capitalize() 151 | if all_optional or self.constraints.not_null: 152 | type_mapping = f"Optional[{type_mapping}]" 153 | if self.is_file(): 154 | type_mapping = "Optional[FileInfo]" 155 | pydantic_field_kwargs = [ 156 | f"{k}={v}" 157 | for (k, v) in self.constraints.model_dump( 158 | exclude={"unique", "not_null", "allowed_values"}, exclude_none=True 159 | ).items() 160 | ] 161 | if all_optional: 162 | pydantic_field_kwargs = ["None", *pydantic_field_kwargs] 163 | 164 | return f"{h.snake_case(self.name)}: {type_mapping}" + ( 165 | f'=Field({",".join(pydantic_field_kwargs)})' 166 | if len(pydantic_field_kwargs) > 0 167 | else "" 168 | ) 169 | 170 | 171 | class Entity(BaseModel): 172 | name: str = Field(...) 173 | fields: list[FieldModel] = Field(...) 174 | 175 | @model_validator(mode="after") 176 | def root_validator(self) -> "Entity": 177 | self.name = h.upper_first_character(self.name) 178 | return self 179 | 180 | 181 | class Relation(BaseModel): 182 | name: str 183 | type_: RelationType = Field(..., alias="type") 184 | from_: str = Field(..., alias="from") 185 | to: str 186 | field_name: str 187 | backref_field_name: str 188 | 189 | @model_validator(mode="after") 190 | def root_validator_mode_after(self) -> "Relation": 191 | self.name = h.upper_first_character(self.name) 192 | self.from_ = h.upper_first_character(self.from_) 193 | self.to = h.upper_first_character(self.to) 194 | self.field_name = h.lower_first_character(self.field_name) 195 | self.backref_field_name = h.lower_first_character(self.backref_field_name) 196 | return self 197 | 198 | @model_validator(mode="before") 199 | @classmethod 200 | def root_validator_mode_before(cls, data: Any) -> Any: 201 | if isinstance(data, dict): 202 | # Transform MANY_TO_ONE to ONE_TO_MANY 203 | if data.get("type", "ONE_TO_ONE").upper() == "MANY_TO_ONE": 204 | data["type"] = "ONE_TO_MANY" 205 | data["from"], data["to"] = data["to"], data["from"] 206 | data["field_name"], data["backref_field_name"] = ( 207 | data["backref_field_name"], 208 | data["field_name"], 209 | ) 210 | return data 211 | 212 | 213 | class App(BaseModel): 214 | name: str = Field(...) 215 | description: str = Field(...) 216 | entities: list[Entity] = Field(...) 217 | relations: list[Relation] = Field(...) 218 | 219 | def has_file(self) -> bool: 220 | for entity in self.entities: 221 | for field in entity.fields: 222 | if field.is_file(): 223 | return True 224 | return False 225 | 226 | def has_enum(self) -> bool: 227 | for entity in self.entities: 228 | for field in entity.fields: 229 | if field.type_ == FieldType.Enum: 230 | return True 231 | return False 232 | 233 | def summary(self): 234 | console = Console() 235 | console.print(f"[bold magenta]Name[/bold magenta]: {self.name}") 236 | console.print(f"[bold magenta]Description[/bold magenta]: {self.description}") 237 | console.print("\n[bold underline]Entities:[/bold underline]\n") 238 | 239 | for entity in self.entities: 240 | entity_tree = Tree(f"[bold cyan]{entity.name}[/bold cyan]") 241 | for field in entity.fields: 242 | field_str = f"[cyan]{field.name}[/cyan]: [yellow]{field.type_}[/yellow]" 243 | constraints_str = ", ".join( 244 | f"{key}={value}" 245 | for key, value in field.constraints.model_dump().items() 246 | if value is not None 247 | ) 248 | if constraints_str: 249 | field_str += f" ({constraints_str})" 250 | entity_tree.add(field_str) 251 | 252 | console.print(entity_tree) 253 | console.print("\n") 254 | 255 | if self.relations: 256 | console.print("[bold underline]Relationships:[/bold underline]\n") 257 | 258 | for relation in self.relations: 259 | console.print( 260 | f"[bold cyan]{relation.from_}[/bold cyan]" 261 | f" ([magenta]{relation.field_name}[/magenta])" 262 | f" --[{relation.type_}]--> [bold cyan]{relation.to}[/bold cyan]" 263 | f" ([magenta]{relation.backref_field_name}[/magenta])" 264 | ) 265 | console.print("\n") 266 | -------------------------------------------------------------------------------- /qwikcrud/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import Field 4 | from pydantic_settings import BaseSettings 5 | 6 | 7 | class Settings(BaseSettings): 8 | openai_api_key: Optional[str] = Field(None) 9 | openai_model: str = "gpt-3.5-turbo-1106" 10 | 11 | google_api_key: Optional[str] = Field(None) 12 | google_model: str = "gemini-pro" 13 | 14 | logging_level: str = "ERROR" 15 | 16 | 17 | settings = Settings() 18 | -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/README.md.j2: -------------------------------------------------------------------------------- 1 | # {{app.name}} 2 | 3 | {{ app.description }} 4 | 5 | Follow these steps to run the application: 6 | 7 | ## Prerequisites 8 | 9 | Before you begin, make sure you have the following prerequisites installed: 10 | 11 | - [Python 3](https://www.python.org/downloads/) 12 | 13 | ## Installation and Setup 14 | 15 | 1. Create and activate a virtual environment: 16 | 17 | ```shell 18 | python3 -m venv env 19 | source env/bin/activate 20 | ``` 21 | 22 | 2. Install the required Python packages: 23 | 24 | ```shell 25 | pip install -r 'requirements.txt' 26 | ``` 27 | 28 | 3. Start the FastAPI application: 29 | 30 | ```shell 31 | uvicorn app.main:app --reload 32 | ``` 33 | 34 | 4. Open your web browser and navigate to [http://127.0.0.1:8000](http://127.0.0.1:8000) -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/__init__.py.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowilf/qwikcrud/560dcabb1dfcf6ea5885d7077e63dd3496a89908/qwikcrud/templates/fastapi/app/__init__.py.j2 -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/admin.py.j2: -------------------------------------------------------------------------------- 1 | from app.db import engine 2 | {% for entity in app.entities %} 3 | from app.models import {{ entity.name }} 4 | {% endfor %} 5 | from starlette_admin.contrib.sqla import Admin, ModelView as BaseModelView 6 | 7 | class ModelView(BaseModelView): 8 | exclude_fields_from_create = ["created_at", "updated_at"] 9 | exclude_fields_from_edit = ["created_at", "updated_at"] 10 | 11 | def init_admin(app): 12 | admin = Admin(engine, templates_dir="templates/admin") 13 | {% for entity in app.entities %} 14 | admin.add_view(ModelView({{ entity.name }})) 15 | {% endfor %} 16 | admin.mount_to(app) -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/crud.py.j2: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, Optional, Type, TypeVar, Union, Sequence 2 | 3 | from fastapi import HTTPException 4 | from pydantic import BaseModel 5 | from sqlalchemy import select 6 | from sqlalchemy.orm import Session 7 | 8 | 9 | {% for entity in app.entities %} 10 | from app.models import {{ entity.name }} 11 | from app.schemas import {{ entity.name }}Create,{{ entity.name }}Update 12 | {% endfor %} 13 | 14 | ModelType = TypeVar("ModelType", bound=Any) 15 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 16 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 17 | 18 | class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 19 | def __init__(self, model: Type[ModelType]): 20 | self.model = model 21 | 22 | async def get(self, db: Session, id: Any) -> Optional[ModelType]: 23 | return db.get(self.model, id) 24 | 25 | async def get_or_404(self, db: Session, id: Any) -> Optional[ModelType]: 26 | obj = await self.get(db, id) 27 | if obj is None: 28 | raise HTTPException(status_code=404, detail=f"{self.model.__name__} with id: {id} not found") 29 | return obj 30 | 31 | async def get_all( 32 | self, db: Session, *, skip: int = 0, limit: int = 100 33 | ) -> Sequence[ModelType]: 34 | stmt = select(self.model).offset(skip).limit(limit) 35 | return db.execute(stmt).scalars().all() 36 | 37 | async def save(self, db: Session, db_obj: ModelType)->ModelType: 38 | db.add(db_obj) 39 | db.commit() 40 | db.refresh(db_obj) 41 | return db_obj 42 | 43 | async def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: 44 | db_obj = self.model(**obj_in.model_dump()) # type: ignore 45 | return await self.save(db, db_obj) 46 | 47 | async def update( 48 | self, 49 | db: Session, 50 | *, 51 | db_obj: ModelType, 52 | obj_in: Union[UpdateSchemaType, Dict[str, Any]] 53 | ) -> ModelType: 54 | update_data = obj_in.model_dump(exclude_unset=True) 55 | for key, value in update_data.items(): 56 | setattr(db_obj, key, value) 57 | return await self.save(db, db_obj) 58 | 59 | async def delete(self, db: Session, *, db_obj: ModelType) -> None: 60 | db.delete(db_obj) 61 | db.commit() 62 | 63 | {% for entity in app.entities %} 64 | {% with e = entity.name%} 65 | class CRUD{{ e }}(CRUDBase[{{ e }}, {{ e }}Create, {{ e }}Update]): 66 | pass 67 | {% endwith %} 68 | {% endfor %} 69 | 70 | {% for entity in app.entities %} 71 | {{entity.name | snake_case }} = CRUD{{entity.name }}({{entity.name}}) 72 | {% endfor %} 73 | 74 | -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/db.py.j2: -------------------------------------------------------------------------------- 1 | from app.settings import settings 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.orm import sessionmaker 4 | from app.models import Base 5 | engine = create_engine(settings.SQLALCHEMY_DATABASE_URI) 6 | 7 | Session = sessionmaker(engine) 8 | 9 | async def init_db(): 10 | Base.metadata.create_all(engine) -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/deps.py.j2: -------------------------------------------------------------------------------- 1 | from typing import Generator, Annotated 2 | 3 | from fastapi import Depends 4 | from app.db import Session 5 | 6 | 7 | 8 | def get_db() -> Generator: 9 | with Session() as session: 10 | yield session 11 | 12 | 13 | SessionDep = Annotated[Session, Depends(get_db)] 14 | -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/endpoints/__init__.py.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowilf/qwikcrud/560dcabb1dfcf6ea5885d7077e63dd3496a89908/qwikcrud/templates/fastapi/app/endpoints/__init__.py.j2 -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/endpoints/template.py.j2: -------------------------------------------------------------------------------- 1 | {# This template generates FastAPI router code for CRUD operations on entities and their relationships. #} 2 | {% set e = entity %} 3 | {% set name_lower = e.name | snake_case %} 4 | from app import crud 5 | from fastapi import APIRouter, HTTPException, UploadFile 6 | 7 | from app.deps import SessionDep 8 | from app.schemas import {{ e.name }}Create,{{ e.name }}Update,{{ e.name }}Out,{{ e.name }}Patch 9 | from typing import List, Optional 10 | {% for entity in app.entities %} 11 | from app.models import {{ entity.name }} 12 | from app.schemas import {{ entity.name }}Out 13 | {% endfor %} 14 | 15 | router = APIRouter(tags=["{{ name_lower }}s"]) 16 | 17 | 18 | @router.get("/") 19 | async def read_all( 20 | db: SessionDep, skip: int = 0, limit: int = 100 21 | ) -> list[{{ e.name }}Out]: 22 | return await crud.{{ name_lower }}.get_all(db,skip=skip,limit=limit) 23 | 24 | 25 | @router.get("/{id}") 26 | async def read_one(db: SessionDep, id: int) -> {{ e.name }}Out: 27 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 28 | return {{name_lower}} 29 | 30 | @router.post("/", status_code=201) 31 | async def create( 32 | *, db: SessionDep, {{ name_lower }}_in: {{ e.name }}Create 33 | ) -> {{ e.name }}Out: 34 | return await crud.{{ name_lower }}.create(db, obj_in={{ name_lower }}_in) 35 | 36 | 37 | @router.put("/{id}") 38 | async def update( 39 | *, db: SessionDep, id: int, {{ name_lower }}_in: {{ e.name }}Update 40 | ) -> {{ e.name }}Out: 41 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 42 | return await crud.{{ name_lower }}.update(db, db_obj={{name_lower}}, obj_in={{ name_lower }}_in) 43 | 44 | @router.patch("/{id}") 45 | async def patch( 46 | *, db: SessionDep, id: int, {{ name_lower }}_in: {{ e.name }}Patch 47 | ) -> {{ e.name }}Out: 48 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 49 | return await crud.{{ name_lower }}.update(db, db_obj={{name_lower}}, obj_in={{ name_lower }}_in) 50 | 51 | @router.delete("/{id}", status_code=204) 52 | async def delete( 53 | *, db: SessionDep, id: int 54 | ) -> None: 55 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 56 | return await crud.{{ name_lower }}.delete(db, db_obj={{name_lower}}) 57 | 58 | 59 | {% for field in e.fields if field.is_file()%} 60 | 61 | {% if loop.first %} 62 | # Handle files 63 | {% endif %} 64 | 65 | @router.put("/{id}/{{ field.name }}") 66 | async def set_{{ field.name }}( 67 | *, db: SessionDep, id: int, file: UploadFile 68 | ) -> {{ e.name }}Out: 69 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 70 | {{name_lower}}.{{ field.name }} = file 71 | return await crud.{{ name_lower }}.save(db, db_obj={{name_lower}}) 72 | 73 | 74 | 75 | @router.delete("/{id}/{{ field.name }}", status_code=204) 76 | async def remove_{{ field.name }}( 77 | *, db: SessionDep, id: int 78 | ) -> None: 79 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 80 | {{name_lower}}.{{ field.name }} = None 81 | await crud.{{ name_lower }}.save(db, db_obj={{name_lower}}) 82 | 83 | 84 | {% endfor %} 85 | 86 | # Handle relationships 87 | 88 | {% for r in app.relations if entity.name in [r.to, r.from_] %} 89 | {% if r.type_ == 'ONE_TO_MANY' %} 90 | 91 | {% if r.from_ == entity.name %} 92 | 93 | @router.get("/{id}/{{r.field_name}}") 94 | async def get_associated_{{r.field_name}}(db: SessionDep, id: int) -> List[{{ r.to }}Out]: 95 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 96 | return {{name_lower}}.{{r.field_name}} 97 | 98 | @router.put("/{id}/{{r.field_name}}") 99 | async def add_{{r.field_name}}_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[{{ r.to }}Out]: 100 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 101 | for _id in ids: 102 | {{name_lower}}.{{r.field_name}}.append(await crud.{{ r.to | lower }}.get_or_404(db, _id)) 103 | {{name_lower}} = await crud.{{name_lower}}.save(db, {{name_lower}}) 104 | return {{name_lower}}.{{r.field_name}} 105 | 106 | {% else %} 107 | 108 | @router.get("/{id}/{{r.backref_field_name}}") 109 | async def get_associated_{{r.backref_field_name}}(db: SessionDep, id: int) -> Optional[{{ r.from_ }}Out]: 110 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 111 | return {{name_lower}}.{{r.backref_field_name}} 112 | 113 | @router.put("/{id}/{{r.backref_field_name}}/{{ '{' }}{{r.backref_field_name}}_id}") 114 | async def set_{{r.backref_field_name}}_by_id(db: SessionDep, id: int, {{r.backref_field_name}}_id: int) -> Optional[{{ r.from_ }}Out]: 115 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 116 | {{name_lower}}.{{r.backref_field_name}} = await crud.{{ r.from_ | lower }}.get_or_404(db, {{r.backref_field_name}}_id) 117 | {{name_lower}} = await crud.{{name_lower}}.save(db, {{name_lower}}) 118 | return {{name_lower}}.{{r.backref_field_name}} 119 | 120 | {% endif %} 121 | {% endif %} 122 | 123 | {% if r.type_ == 'ONE_TO_ONE' %} 124 | 125 | {% if r.from_ == entity.name %} 126 | 127 | @router.get("/{id}/{{r.field_name}}") 128 | async def get_associated_{{r.field_name}}(db: SessionDep, id: int) -> Optional[{{ r.to }}Out]: 129 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 130 | return {{name_lower}}.{{r.field_name}} 131 | 132 | @router.put("/{id}/{{r.field_name}}/{{ '{' }}{{r.field_name}}_id}") 133 | async def set_{{r.field_name}}_by_id(db: SessionDep, id: int, {{r.field_name}}_id: int) -> Optional[{{ r.to }}Out]: 134 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 135 | {{name_lower}}.{{r.field_name}} = await crud.{{ r.to | lower }}.get_or_404(db, {{r.field_name}}_id) 136 | {{name_lower}} = await crud.{{name_lower}}.save(db, {{name_lower}}) 137 | return {{name_lower}}.{{r.field_name}} 138 | 139 | {% else %} 140 | 141 | @router.get("/{id}/{{r.backref_field_name}}") 142 | async def get_associated_{{r.backref_field_name}}(db: SessionDep, id: int) -> Optional[{{ r.from_ }}Out]: 143 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 144 | return {{name_lower}}.{{r.backref_field_name}} 145 | 146 | @router.put("/{id}/{{r.backref_field_name}}/{{ '{' }}{{r.backref_field_name}}_id}") 147 | async def set_{{r.backref_field_name}}_by_id(db: SessionDep, id: int, {{r.backref_field_name}}_id: int) -> Optional[{{ r.from_ }}Out]: 148 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 149 | {{name_lower}}.{{r.backref_field_name}} = await crud.{{ r.from_ | lower }}.get_or_404(db, {{r.backref_field_name}}_id) 150 | {{name_lower}} = await crud.{{name_lower}}.save(db, {{name_lower}}) 151 | return {{name_lower}}.{{r.backref_field_name}} 152 | 153 | {% endif %} 154 | {% endif %} 155 | 156 | {% if r.type_ == 'MANY_TO_MANY' %} 157 | 158 | {% if r.from_ == entity.name %} 159 | 160 | @router.get("/{id}/{{r.field_name}}") 161 | async def get_associated_{{r.field_name}}(db: SessionDep, id: int) -> List[{{ r.to }}Out]: 162 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 163 | return {{name_lower}}.{{r.field_name}} 164 | 165 | @router.put("/{id}/{{r.field_name}}") 166 | async def add_{{r.field_name}}_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[{{ r.to }}Out]: 167 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 168 | for _id in ids: 169 | {{name_lower}}.{{r.field_name}}.append(await crud.{{ r.to | lower }}.get_or_404(db, _id)) 170 | {{name_lower}} = await crud.{{name_lower}}.save(db, {{name_lower}}) 171 | return {{name_lower}}.{{r.field_name}} 172 | 173 | {% else %} 174 | 175 | @router.get("/{id}/{{r.backref_field_name}}") 176 | async def get_associated_{{r.backref_field_name}}(db: SessionDep, id: int) -> List[{{ r.from_ }}Out]: 177 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 178 | return {{name_lower}}.{{r.backref_field_name}} 179 | 180 | @router.put("/{id}/{{r.backref_field_name}}") 181 | async def add_{{r.backref_field_name}}_by_ids(db: SessionDep, id: int, ids: List[int]) -> List[{{ r.from_ }}Out]: 182 | {{name_lower}} = await crud.{{ name_lower }}.get_or_404(db, id) 183 | for _id in ids: 184 | {{name_lower}}.{{r.backref_field_name}}.append(await crud.{{ r.from_ | lower }}.get_or_404(db, _id)) 185 | {{name_lower}} = await crud.{{name_lower}}.save(db, {{name_lower}}) 186 | return {{name_lower}}.{{r.backref_field_name}} 187 | {% endif %} 188 | {% endif %} 189 | {% endfor %} -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/enums.py.j2: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | {%- for entity in app.entities %} 5 | {%- for field in entity.fields if field.type_ == "Enum" %} 6 | 7 | class {{ field.name | title }}(str, enum.Enum): 8 | {% for v in field.constraints.get_allowed_values() %} 9 | {{ v | upper | replace(' ', '_') }} = "{{ v }}" 10 | {% endfor %} 11 | {% endfor %} 12 | {% endfor %} -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/main.py.j2: -------------------------------------------------------------------------------- 1 | {% for entity in app.entities %} 2 | from app.endpoints import {{ entity.name | lower }} 3 | {% endfor %} 4 | from fastapi.staticfiles import StaticFiles 5 | from fastapi.responses import FileResponse, JSONResponse 6 | from app.pre_start import init 7 | from app.settings import settings 8 | from fastapi import FastAPI, Request 9 | from sqlalchemy_file.exceptions import ValidationError as FileValidationError 10 | from app.admin import init_admin 11 | from fastapi.templating import Jinja2Templates 12 | from libcloud.storage.types import ObjectDoesNotExistError 13 | from sqlalchemy_file.storage import StorageManager 14 | 15 | def create_app(): 16 | _app = FastAPI( 17 | title=settings.PROJECT_NAME, 18 | description=settings.PROJECT_DESCRIPTION, 19 | on_startup=[init], 20 | ) 21 | 22 | {% for entity in app.entities %} 23 | _app.include_router({{ entity.name | lower}}.router, prefix="/api/v1/{{ entity.name | lower}}s") 24 | {% endfor %} 25 | _app.mount("/static", StaticFiles(directory="static"), name="static") 26 | init_admin(_app) 27 | 28 | return _app 29 | 30 | templates = Jinja2Templates(directory="templates") 31 | 32 | app = create_app() 33 | 34 | @app.get("/", include_in_schema=False) 35 | async def home(request: Request): 36 | return templates.TemplateResponse("index.html", {"request": request, "settings": settings}) 37 | 38 | {% if app.has_file() %} 39 | @app.get("/medias", response_class=FileResponse, tags=["medias"]) 40 | async def serve_files(path: str): 41 | try: 42 | file = StorageManager.get_file(path) 43 | return FileResponse( 44 | file.get_cdn_url(), media_type=file.content_type, filename=file.filename 45 | ) 46 | except ObjectDoesNotExistError: 47 | return JSONResponse({"detail": "Not found"}, status_code=404) 48 | 49 | @app.exception_handler(FileValidationError) 50 | async def sqla_file_validation_error(request: Request, exc: FileValidationError): 51 | return JSONResponse({"error": {"key": exc.key, "msg": exc.msg}}, status_code=422) 52 | 53 | {% endif %} 54 | -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/models.py.j2: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pydantic import EmailStr 3 | from sqlalchemy import MetaData, String, ForeignKey, Column, Table, Enum, JSON 4 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship 5 | from typing import Optional, List, Union 6 | from sqlalchemy_file import ImageField, FileField, File 7 | from sqlalchemy_file.validators import ContentTypeValidator, SizeValidator 8 | from fastapi import UploadFile 9 | {% for entity in app.entities %} 10 | {%- for field in entity.fields if field.type_ == "Enum" %} 11 | from app.enums import {{ field.name | title }} 12 | {% endfor %} 13 | {% endfor %} 14 | 15 | class TimestampMixin: 16 | created_at: Mapped[datetime.datetime] = mapped_column( 17 | default=datetime.datetime.utcnow 18 | ) 19 | updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( 20 | onupdate=datetime.datetime.utcnow 21 | ) 22 | 23 | 24 | class Base(DeclarativeBase, TimestampMixin): 25 | metadata = MetaData( 26 | naming_convention={ 27 | "ix": "ix_%(column_0_label)s", 28 | "uq": "uq_%(table_name)s_%(column_0_name)s", 29 | "ck": "ck_%(table_name)s_%(constraint_name)s", 30 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 31 | "pk": "pk_%(table_name)s", 32 | } 33 | ) 34 | type_annotation_map = {EmailStr: String, dict: JSON} 35 | 36 | {% for r in app.relations if r.type_ == 'MANY_TO_MANY' %} 37 | {{r.name | snake_case}} = Table( 38 | "{{r.name | snake_case}}", 39 | Base.metadata, 40 | Column("{{ r.from_ | snake_case }}_id", ForeignKey("{{ r.from_ | snake_case }}.id"), primary_key=True), 41 | Column("{{ r.to | snake_case }}_id", ForeignKey("{{ r.to | snake_case }}.id"), primary_key=True), 42 | ) 43 | {% endfor %} 44 | 45 | {% for entity in app.entities %} 46 | 47 | class {{ entity.name }}(Base): 48 | __tablename__ = '{{ entity.name | snake_case }}' 49 | 50 | {% for field in entity.fields %} 51 | {{ field.sqla_column_def() }} 52 | {% endfor %} 53 | 54 | {% for r in app.relations if entity.name in [r.to, r.from_] %} 55 | {% if r.type_ == 'ONE_TO_MANY' %} 56 | {% if r.from_ == entity.name %} 57 | {{r.field_name}}: Mapped[List["{{ r.to }}"]] = relationship(back_populates="{{ r.backref_field_name }}") 58 | {% else %} 59 | {{r.backref_field_name}}_id: Mapped[Optional[int]] = mapped_column(ForeignKey("{{ r.from_ | snake_case }}.id")) 60 | {{r.backref_field_name}}: Mapped["{{ r.from_ }}"] = relationship(back_populates="{{ r.field_name }}") 61 | {% endif %} 62 | {% endif %} 63 | 64 | {% if r.type_ == 'ONE_TO_ONE' %} 65 | {% if r.from_ == entity.name %} 66 | {{r.field_name}}: Mapped["{{ r.to }}"] = relationship(back_populates="{{ r.backref_field_name }}") 67 | {% else %} 68 | {{r.backref_field_name}}_id: Mapped[Optional[int]] = mapped_column(ForeignKey("{{ r.from_ | snake_case }}.id")) 69 | {{r.backref_field_name}}: Mapped["{{ r.from_ }}"] = relationship(back_populates="{{ r.field_name }}") 70 | {% endif %} 71 | {% endif %} 72 | 73 | {% if r.type_ == 'MANY_TO_MANY' %} 74 | {% if r.from_ == entity.name %} 75 | {{r.field_name}}: Mapped[List["{{ r.to }}"]] = relationship(secondary={{r.name | snake_case}},back_populates="{{ r.backref_field_name }}") 76 | {% else %} 77 | {{r.backref_field_name}}: Mapped[List["{{ r.from_ }}"]] = relationship(secondary={{r.name | snake_case}},back_populates="{{ r.field_name }}") 78 | {% endif %} 79 | {% endif %} 80 | 81 | {% endfor %} 82 | 83 | {% endfor %} 84 | -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/pre_start.py.j2: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from app.db import init_db 4 | from app.storage import init_storage 5 | 6 | async def init() -> None: 7 | await init_db() 8 | {% if app.has_file() %} 9 | await init_storage() 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/schemas.py.j2: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | from pydantic import BaseModel, Field, EmailStr 4 | {% for entity in app.entities %} 5 | {%- for field in entity.fields if field.type_ == "Enum" %} 6 | from app.enums import {{ field.name | title }} 7 | {% endfor %} 8 | {% endfor %} 9 | 10 | {% if app.has_file() %} 11 | 12 | class Thumbnail(BaseModel): 13 | path: str 14 | 15 | 16 | class FileInfo(BaseModel): 17 | filename: str 18 | content_type: str 19 | path: str 20 | thumbnail: Optional[Thumbnail] = None 21 | {% endif %} 22 | 23 | 24 | {%- for entity in app.entities %} 25 | 26 | 27 | #-------------- {{ entity.name }} ------------------ 28 | 29 | class {{ entity.name }}Create(BaseModel): 30 | {% for field in entity.fields %} 31 | {% if not (field.is_id() or field.is_file()) %} 32 | {{ field.pydantic_def() }} 33 | {% endif %} 34 | {% endfor %} 35 | 36 | class {{ entity.name }}Update({{ entity.name }}Create): 37 | pass 38 | 39 | class {{ entity.name }}Patch(BaseModel): 40 | {% for field in entity.fields %} 41 | {% if not (field.is_id() or field.is_file()) %} 42 | {{ field.pydantic_def(True) }} 43 | {% endif %} 44 | {% endfor %} 45 | 46 | class {{ entity.name }}Out(BaseModel): 47 | {% for field in entity.fields %} 48 | {{ field.pydantic_def() }} 49 | {% endfor %} 50 | 51 | {% endfor %} 52 | -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/settings.py.j2: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | 3 | 4 | class Settings(BaseSettings): 5 | SQLALCHEMY_DATABASE_URI: str = "sqlite:///db.sqlite" 6 | PROJECT_NAME: str = "{{ app.name }}" 7 | PROJECT_DESCRIPTION: str = "{{ app.description }}" 8 | 9 | 10 | settings = Settings() -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/app/storage.py.j2: -------------------------------------------------------------------------------- 1 | from sqlalchemy_file.storage import StorageManager 2 | from libcloud.storage.base import Container, StorageDriver 3 | from libcloud.storage.types import ContainerDoesNotExistError 4 | from libcloud.storage.drivers.local import LocalStorageDriver 5 | 6 | def get_or_create_container(driver: StorageDriver, name: str) -> Container: 7 | try: 8 | return driver.get_container(name) 9 | except ContainerDoesNotExistError: 10 | return driver.create_container(name) 11 | 12 | async def init_storage() -> None: 13 | StorageManager.add_storage("default", get_or_create_container(LocalStorageDriver("."), "assets")) -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/requirements.txt.j2: -------------------------------------------------------------------------------- 1 | fastapi>=0.104.1 2 | starlette-admin>=0.12 3 | pydantic[email]>=2 4 | pydantic_settings 5 | sqlalchemy>=2 6 | sqlalchemy-file==0.6.0 7 | fasteners==0.19 8 | pillow>=10.1 9 | python-multipart==0.0.6 10 | uvicorn>=0.24.0.post1 -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/static/css/style.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.btn{display:inline-flex;height:2.25rem;align-items:center;justify-content:center;border-radius:.375rem;--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity:1;color:rgb(250 250 250/var(--tw-text-opacity));--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn:hover{background-color:#18181be6}.btn:focus-visible{outline:2px solid #0000;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(9 9 11/var(--tw-ring-opacity))}.mx-8{margin-left:2rem;margin-right:2rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-full{height:100%}.h-screen{height:100vh}.resize{resize:both}.flex-col{flex-direction:column}.justify-center{justify-content:center}.gap-8{gap:2rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-zinc-300{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.text-center{text-align:center}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.tracking-tighter{letter-spacing:-.05em}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (prefers-color-scheme:dark){.dark\:text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:text-5xl{font-size:3rem;line-height:1}}@media (min-width:768px){.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:text-6xl{font-size:3.75rem;line-height:1}} -------------------------------------------------------------------------------- /qwikcrud/templates/fastapi/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ settings.PROJECT_NAME }} 8 | 9 | 10 | 11 |
12 |
13 |

14 | {{ settings.PROJECT_NAME }} 15 |

16 |

17 | {{ settings.PROJECT_DESCRIPTION }} 18 |

19 |
20 |
21 |

Swagger Documentation

22 |

Explore and test your endpoints using Swagger UI. 23 |

24 | 25 | Browse 26 | 27 |
28 |
29 |

Redoc Documentation

30 |

View and interact with your endpoints using Redoc. 31 |

32 | 33 | Browse 34 | 35 |
36 |
37 |

Admin Interface

38 |

Manage your entities with the admin interface. 39 |

40 | 41 | Browse 42 | 43 |
44 |
45 |
46 | 51 |
52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/dummy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The app name", 3 | "description": "A description of the app", 4 | "entities": [ 5 | { 6 | "name": "User", 7 | "fields": [ 8 | { 9 | "name": "id", 10 | "type": "ID" 11 | }, 12 | { 13 | "name": "username", 14 | "type": "String", 15 | "constraints": { 16 | "unique": true, 17 | "max_length": 50 18 | } 19 | }, 20 | { 21 | "name": "email", 22 | "type": "Email", 23 | "constraints": { 24 | "unique": true, 25 | "max_length": 100 26 | } 27 | }, 28 | { 29 | "name": "password", 30 | "type": "String", 31 | "constraints": { 32 | "min_length": 8, 33 | "max_length": 100 34 | } 35 | }, 36 | { 37 | "name": "first_name", 38 | "type": "String", 39 | "constraints": { 40 | "max_length": 50 41 | } 42 | }, 43 | { 44 | "name": "last_name", 45 | "type": "String", 46 | "constraints": { 47 | "max_length": 50 48 | } 49 | }, 50 | { 51 | "name": "avatar", 52 | "type": "Image" 53 | }, 54 | { 55 | "name": "bio", 56 | "type": "Text" 57 | } 58 | ] 59 | }, 60 | { 61 | "name": "Address", 62 | "fields": [ 63 | { 64 | "name": "id", 65 | "type": "ID" 66 | }, 67 | { 68 | "name": "street", 69 | "type": "String", 70 | "constraints": { 71 | "max_length": 100 72 | } 73 | }, 74 | { 75 | "name": "city", 76 | "type": "String", 77 | "constraints": { 78 | "max_length": 50 79 | } 80 | }, 81 | { 82 | "name": "zip_code", 83 | "type": "String", 84 | "constraints": { 85 | "max_length": 20 86 | } 87 | } 88 | ] 89 | }, 90 | { 91 | "name": "Product", 92 | "fields": [ 93 | { 94 | "name": "id", 95 | "type": "ID" 96 | }, 97 | { 98 | "name": "name", 99 | "type": "String", 100 | "constraints": { 101 | "max_length": 100 102 | } 103 | }, 104 | { 105 | "name": "description", 106 | "type": "Text" 107 | }, 108 | { 109 | "name": "price", 110 | "type": "Float", 111 | "constraints": { 112 | "ge": 0.0 113 | } 114 | }, 115 | { 116 | "name": "in_stock", 117 | "type": "Boolean" 118 | }, 119 | { 120 | "name": "image", 121 | "type": "Image" 122 | } 123 | ] 124 | }, 125 | { 126 | "name": "Order", 127 | "fields": [ 128 | { 129 | "name": "id", 130 | "type": "ID" 131 | }, 132 | { 133 | "name": "order_date", 134 | "type": "Date" 135 | }, 136 | { 137 | "name": "total_amount", 138 | "type": "Float", 139 | "constraints": { 140 | "ge": 0.0 141 | } 142 | }, 143 | { 144 | "name": "is_paid", 145 | "type": "Boolean" 146 | } 147 | ] 148 | } 149 | ], 150 | "relations": [ 151 | { 152 | "name": "User_Address", 153 | "type": "ONE_TO_ONE", 154 | "from": "User", 155 | "to": "Address", 156 | "field_name": "address", 157 | "backref_field_name": "user" 158 | }, 159 | { 160 | "name": "User_Products", 161 | "type": "ONE_TO_MANY", 162 | "from": "User", 163 | "to": "Product", 164 | "field_name": "products", 165 | "backref_field_name": "user" 166 | }, 167 | { 168 | "name": "Product_Orders", 169 | "type": "MANY_TO_MANY", 170 | "from": "Product", 171 | "to": "Order", 172 | "field_name": "orders", 173 | "backref_field_name": "products" 174 | } 175 | ] 176 | } -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from qwikcrud.schemas import App 4 | 5 | 6 | def test_validate(): 7 | with open(Path(__file__).parent / "dummy.json") as f: 8 | App.model_validate_json(f.read()) 9 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from qwikcrud import helpers as h 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "input_str, expected_output", 8 | [ 9 | ("camelCaseString", "camel_case_string"), 10 | ("AnotherExample", "another_example"), 11 | ("Single", "single"), 12 | ("mixedCase123", "mixed_case123"), 13 | ], 14 | ) 15 | def test_snake_case(input_str, expected_output) -> None: 16 | assert h.snake_case(input_str) == expected_output 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "markdown_text, expected_result", 21 | [ 22 | ( 23 | """ 24 | Here is a JSON object: 25 | 26 | ```json 27 | {"key":"value","number":42,"comment":"JSON within a code block"} 28 | ``` 29 | End of JSON. 30 | """, 31 | '{"key":"value","number":42,"comment":"JSON within a code block"}', 32 | ), 33 | ( 34 | '{"key":"value","number":42,"comment":"Entire text considered as JSON"}', 35 | '{"key":"value","number":42,"comment":"Entire text considered as JSON"}', 36 | ), 37 | ], 38 | ) 39 | def test_extract_json_from_markdown(markdown_text, expected_result): 40 | parse_result = h.extract_json_from_markdown(markdown_text) 41 | assert parse_result.strip() == expected_result.strip() 42 | --------------------------------------------------------------------------------