├── .gitignore ├── LICENSE ├── README.md ├── fastapi_quickstart ├── conf │ ├── __init__.py │ ├── constants │ │ ├── __init__.py │ │ ├── docker.py │ │ ├── fastapi.py │ │ ├── filepaths.py │ │ └── poetry.py │ ├── file_handler.py │ └── helper.py ├── config.py ├── main.py ├── setup │ ├── __init__.py │ ├── base.py │ ├── clean.py │ ├── docker.py │ ├── fastapi.py │ ├── libraries.py │ ├── static.py │ └── venv.py ├── setup_assets │ ├── .gitignore │ ├── __init__.py │ ├── backend │ │ └── database │ │ │ ├── __init__.py │ │ │ ├── crud.py │ │ │ ├── models.py │ │ │ └── schemas.py │ ├── frontend │ │ ├── static │ │ │ ├── css │ │ │ │ └── input.css │ │ │ ├── imgs │ │ │ │ └── avatar.svg │ │ │ └── js │ │ │ │ └── theme-toggle.js │ │ └── templates │ │ │ ├── _base.html │ │ │ ├── components │ │ │ └── navbar.html │ │ │ └── index.html │ ├── main.py │ └── tailwind.config.js ├── tests │ ├── __init__.py │ └── test_filepaths.py └── utils │ ├── __init__.py │ ├── helper.py │ └── printables.py ├── poetry.lock └── pyproject.toml /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ryan Partridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Project Quickstart Tool 2 | 3 | Welcome to the quickstart tool for creating a `FastAPI` project with the following stack: 4 | 5 | - [FastAPI](https://github.com/tiangolo/fastapi) 6 | - [Jinja2 Templates](https://jinja.palletsprojects.com/) 7 | - [TailwindCSS](https://tailwindcss.com/) and [Flowbite](https://flowbite.com/) 8 | - [AlpineJS](https://alpinejs.dev/) 9 | - [HTMX](https://htmx.org/) 10 | 11 | The default pip packages installed include: 12 | 13 | - `fastapi` 14 | - `uvicorn[standard]` 15 | - `sqlalchemy` 16 | - `jinja2` 17 | - `python-dotenv` 18 | - `poetry` 19 | 20 | _Note: all libraries and packages are automatically installed to their latest versions when running the tool._ 21 | 22 | ## Why This Tool? 23 | 24 | Creating a project from scratch can be a tedious process. So, I wanted to simplify it! This tool is ideal for those that are looking to quickly prototype a project without worrying about `JavaScript` frameworks, such as `Backend Developers` and `ML Engineers`. 25 | 26 | The tool does the following: 27 | 28 | - Creates a virtual environment in the project folder 29 | - Accesses it, updates `PIP` and installs the required packages 30 | - Creates a `.env` file 31 | - Creates a `backend` directory with a basic application template for `FastAPI` 32 | - Creates a `frontend` directory with some basic `Jinja2` template files 33 | - Such as a `_base.html` and `index.html` 34 | - Creates a `frontend/public` files directory for storing `css`, `js`, and `img` files locally 35 | - Adds `TailwindCSS`, `Flowbite`, `HTMX`, and `AlpineJS` static files 36 | - Performs some file cleanup such as removing `node_modules` (if your OS supports TailwindCSS standalone CLI), `venv` and `package.json` files 37 | 38 | ## Dependencies 39 | 40 | The tool is intended to be dynamic and aims to install the most recent packages where possible. To do this, we require [NodeJS](https://nodejs.org/en), `NPM` and [Python](https://www.python.org/downloads/) to be installed on your local machine, with the latest stable versions. 41 | 42 | We use `node_modules` and `PIP` to maintain the latest versions of the core stack, and remove the `node_modules` after creation to simplify the project folder. 43 | 44 | Fortunately, `Tailwind` has a [Standalone CLI](https://tailwindcss.com/blog/standalone-cli) that allows us to watch and minify files without needing `NodeJS`! 45 | 46 | 47 | ### Customisation and Configuration 48 | 49 | All files added to the project are stored in `setup_assets`. If you want add files, feel free but it is recommended not to mess with the file structure. Here a few things to note: 50 | - All the files are added to the `project` root directory 51 | - Static files **MUST** be stored in a `setup_assets/frontend/static` folder 52 | - The static folder name is changed dynamically during project creation from `frontend/static` -> `frontend/public` 53 | 54 | For configuration customisation go to `config.py` in the root directory. Here you have three options: 55 | - Changing the database URL -> `DATABASE_URL`, defaults to a SQLite local database. 56 | - Adding additional PIP packages to the project -> `ADDITIONAL_PIP_PACKAGES` 57 | - Adding additional `.env` file variables -> `ENV_FILE_ADDITIONAL_PARAMS` 58 | 59 | Note: the last two options are treated as python `list` objects that accept `strings` only. 60 | 61 | 62 | ### Creation 63 | 1. To get started, clone the repository, enter the folder and run `create` with a `name` (e.g., `my_project`) argument inside a `poetry shell`. This creates a new project inside the `parent` directory of the `fastapi-quickstart` directory: 64 | 65 | ```bash 66 | git clone https://github.com/Achronus/fastapi-quickstart.git 67 | cd fastapi-quickstart 68 | poetry shell 69 | create my_project # Replace me with custom name! 70 | ``` 71 | 72 | For example, if you have a parent folder called `projects` and are making a project called `todo_app` the project is created in `projects/todo_app` instead of `projects/fastapi-quickstart/todo_app`. 73 | 74 | 75 | ### And That's It! 76 | 77 | Everything is setup with a blank template ready to start building a project from scratch. Run the following commands to run the docker `development` server and watch `TailwindCSS` locally! 78 | 79 | Not got Docker? Follow these instructions from the [Docker website](https://docs.docker.com/get-docker/). 80 | 81 | 82 | ```bash 83 | cd ../my_project # Replace me with custom name! 84 | docker-compose up -d --build dev 85 | 86 | poetry shell 87 | poetry install 88 | 89 | watch 90 | ``` 91 | 92 | Then access the site at [localhost:8080](http://localhost:8080). 93 | 94 | 95 | ### Production 96 | 97 | Docker also comes configured with a production variant. Inside the `my_project` folder run: 98 | ```bash 99 | docker-compose up -d --build prod 100 | ``` 101 | 102 | Then you are good to go! 103 | 104 | 105 | ## Folder Structure 106 | 107 | The newly created project should look similar to the following: 108 | 109 | ```bash 110 | project_name 111 | └── config 112 | | └── docker 113 | | | └── Dockerfile.backend 114 | └── project_name 115 | | └── backend 116 | | | └── database 117 | | | └── __init__.py 118 | | | └── crud.py 119 | | | └── models.py 120 | | | └── schemas.py 121 | | └── frontend 122 | | | └── public 123 | | | | └── css 124 | | | | | └── flowbite.min.css 125 | | | | | └── input.css 126 | | | | | └── style.min.css 127 | | | | └── imgs 128 | | | | | └── avatar.svg 129 | | | | └── js 130 | | | | └── alpine.min.js 131 | | | | └── flowbite.min.js 132 | | | | └── htmx.min.js 133 | | | | └── theme-toggle.js 134 | | | └── templates 135 | | | └── components 136 | | | | └── navbar.html 137 | | | └── _base.html 138 | | | └── index.html 139 | | └── tests 140 | | | └── __init__.py 141 | | └── .env 142 | | └── .gitignore 143 | | └── build.py 144 | | └── main.py 145 | | └── tailwind.config.js 146 | | └── tailwindcss OR tailwindcss.exe 147 | └── .dockerignore 148 | └── database.db 149 | └── docker-compose.base.yml 150 | └── docker-compose.yml 151 | └── poetry.lock 152 | └── pyproject.toml 153 | └── README.md 154 | ``` 155 | -------------------------------------------------------------------------------- /fastapi_quickstart/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Achronus/fastapi-quickstart/38fb44bbf8cb2f2c282c40f4534b2cf4e03d2005/fastapi_quickstart/conf/__init__.py -------------------------------------------------------------------------------- /fastapi_quickstart/conf/constants/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from ..helper import dirname_check 5 | from fastapi_quickstart.config import DATABASE_URL 6 | 7 | 8 | # Change venv activation depending on OS 9 | VENV_NAME = 'env' 10 | VENV_LOCATION = os.path.join(os.getcwd(), VENV_NAME) 11 | 12 | if sys.platform.startswith("win"): 13 | VENV = f"{VENV_LOCATION}\\Scripts" 14 | else: 15 | VENV = f"source {VENV_LOCATION}/bin/" 16 | 17 | # Define core PIP packages 18 | CORE_PIP_PACKAGES = [ 19 | "fastapi", 20 | "uvicorn[standard]", 21 | "sqlalchemy", 22 | "jinja2", 23 | "python-dotenv", 24 | "poetry" 25 | ] 26 | 27 | # Define core .env file parameters 28 | CORE_ENV_PARAMS = [ 29 | f'DATABASE_URL={DATABASE_URL}' 30 | ] 31 | 32 | # Define core NPM packages to install 33 | NPM_PACKAGES = [ 34 | "tailwindcss", 35 | "flowbite", 36 | "alpinejs" 37 | ] 38 | 39 | # Custom print emoji's 40 | PASS = '[green]\u2713[/green]' 41 | FAIL = '[red]\u274c[/red]' 42 | PARTY = ':party_popper:' 43 | 44 | # Set default static directory name 45 | VALID_STATIC_DIR_NAMES = ['static', 'public', 'assets'] 46 | 47 | STATIC_FILES_DIR = 'public' 48 | STATIC_DIR_NAME = dirname_check( 49 | VALID_STATIC_DIR_NAMES, 50 | STATIC_FILES_DIR, 51 | err_msg_start="[blue]STATIC_FILES_DIR[/blue] in [yellow]config.py[/yellow]" 52 | ) 53 | -------------------------------------------------------------------------------- /fastapi_quickstart/conf/constants/docker.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from .filepaths import get_project_name, DockerPaths 4 | 5 | 6 | class DockerEnvConfig: 7 | def __init__(self) -> None: 8 | import importlib.metadata 9 | self.poetry_version = importlib.metadata.version('poetry') 10 | self.project_name = get_project_name() 11 | 12 | # Docker-compose 13 | self.ports = "8080:80" 14 | 15 | # Uvicorn 16 | self.port = self.ports.split(':')[-1] 17 | self.host = "0.0.0.0" 18 | 19 | self.python_version = '3.12.1' 20 | 21 | 22 | class DockerContent: 23 | def __init__(self) -> None: 24 | self.dotenv_config = DockerEnvConfig() 25 | 26 | @staticmethod 27 | def __format(content: str) -> str: 28 | """Helper function for formatting file content.""" 29 | return textwrap.dedent(content)[1:] 30 | 31 | def env_config(self) -> str: 32 | return self.__format(f""" 33 | #-------------------------- 34 | # DOCKER CONFIG SETTINGS 35 | # ------------------------- 36 | # Docker config 37 | POETRY_VERSION={self.dotenv_config.poetry_version} 38 | PROJECT_NAME={self.dotenv_config.project_name} 39 | PORTS={self.dotenv_config.ports} 40 | 41 | # Uvicorn 42 | PORT={self.dotenv_config.port} 43 | HOST={self.dotenv_config.host} 44 | 45 | #-------------------------- 46 | # CUSTOM CONFIG SETTINGS 47 | #-------------------------- 48 | """) 49 | 50 | def backend_df(self) -> str: 51 | """The content for the backend Dockerfile.""" 52 | start = f""" 53 | # Dockerfile for FastAPI 54 | FROM python:{self.dotenv_config.python_version}-slim 55 | """ 56 | 57 | return self.__format(start + """ 58 | ARG ENV_TYPE 59 | ARG POETRY_VERSION 60 | ARG PROJECT_NAME 61 | ARG PORT 62 | 63 | ENV YOUR_ENV=${ENV_TYPE} \\ 64 | \tPYTHONFAULTHANDLER=1 \\ 65 | \tPYTHONUNBUFFERED=1 \\ 66 | \tPYTHONHASHSEED=random \\ 67 | \tPIP_NO_CACHE_DIR=off \\ 68 | \tPIP_DISABLE_PIP_VERSION_CHECK=on \\ 69 | \tPIP_DEFAULT_TIMEOUT=100 70 | 71 | # System deps: 72 | RUN pip install "poetry==${POETRY_VERSION}" 73 | 74 | # Copy only requirements to cache them in docker layer 75 | WORKDIR /${PROJECT_NAME} 76 | 77 | COPY poetry.lock pyproject.toml /${PROJECT_NAME}/ 78 | 79 | # Project initialization: 80 | RUN poetry config virtualenvs.create false \\ 81 | \t&& poetry install $(test "$YOUR_ENV" == prod && echo "--no-dev") --no-interaction --no-ansi 82 | 83 | # Creating folders, and files for a project: 84 | COPY . /${PROJECT_NAME} 85 | 86 | # Expose the port for FastAPI 87 | EXPOSE ${PORT} 88 | 89 | # Command to run the FastAPI application 90 | RUN poetry install 91 | CMD ["run"] 92 | """) 93 | 94 | def compose_base(self) -> str: 95 | """The content for the Docker Compose base file.""" 96 | return self.__format(""" 97 | services: 98 | base: 99 | build: 100 | context: . 101 | dockerfile: ./config/docker/Dockerfile.backend 102 | args: 103 | POETRY_VERSION: ${POETRY_VERSION} 104 | PROJECT_NAME: ${PROJECT_NAME} 105 | PORT: ${PORT} 106 | ports: 107 | - "${PORTS}" 108 | volumes: 109 | - .:/${PROJECT_NAME} 110 | env_file: 111 | - .env 112 | """) 113 | 114 | def compose_main(self) -> str: 115 | """The content for the main (entry point) Docker Compose file.""" 116 | return self.__format(""" 117 | version: '1' 118 | 119 | services: 120 | dev: 121 | container_name: backend 122 | extends: 123 | file: docker-compose.base.yml 124 | service: base 125 | build: 126 | args: 127 | ENV_TYPE: dev 128 | command: uvicorn ${PROJECT_NAME}.main:app --host ${HOST} --port ${PORT} --reload 129 | 130 | prod: 131 | container_name: backend 132 | extends: 133 | file: docker-compose.base.yml 134 | service: base 135 | build: 136 | args: 137 | ENV_TYPE: prod 138 | command: uvicorn ${PROJECT_NAME}.main:app --host ${HOST} --port ${PORT} 139 | """) 140 | 141 | 142 | class DockerFileMapper: 143 | def __init__(self) -> None: 144 | self.content = DockerContent() 145 | self.paths = DockerPaths() 146 | 147 | def dockerfiles(self) -> list[tuple[str, str]]: 148 | """Maps the pairs of filepaths and content for Dockerfiles.""" 149 | return [ 150 | (self.paths.BACKEND_DF, self.content.backend_df()) 151 | ] 152 | 153 | def compose_files(self) -> list[tuple[str, str]]: 154 | """Maps the pairs of filepaths and content for Docker compose files.""" 155 | return [ 156 | (self.paths.COMPOSE_BASE, self.content.compose_base()), 157 | (self.paths.COMPOSE_MAIN, self.content.compose_main()) 158 | ] 159 | -------------------------------------------------------------------------------- /fastapi_quickstart/conf/constants/fastapi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .filepaths import ProjectPaths 4 | 5 | 6 | # FastAPI directory and filenames names 7 | class FastAPIDirnames: 8 | DATABASE = 'database' 9 | 10 | 11 | class FastAPIFilenames: 12 | BASE = '__init__.py' 13 | 14 | 15 | # FastAPI directory filepaths 16 | class FastAPIDirPaths: 17 | def __init__(self) -> None: 18 | project_paths = ProjectPaths() 19 | 20 | self.DATABASE_DIR = os.path.join(project_paths.BACKEND, FastAPIDirnames.DATABASE) 21 | self.DATABASE_INIT_FILE = os.path.join(self.DATABASE_DIR, FastAPIFilenames.BASE) 22 | 23 | 24 | # Define extra content 25 | class FastAPIContent: 26 | SQLITE_DB_POSITION = "os.getenv('DATABASE_URL')" 27 | SQLITE_DB_CONTENT = ', connect_args={"check_same_thread": False}' 28 | -------------------------------------------------------------------------------- /fastapi_quickstart/conf/constants/filepaths.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from . import STATIC_DIR_NAME 4 | from ..helper import set_tw_standalone_filename 5 | 6 | 7 | # Setup assets directory names 8 | class SetupAssetsDirNames: 9 | ROOT = 'setup_assets' 10 | FRONTEND = 'frontend' 11 | BACKEND = 'backend' 12 | 13 | CSS = 'css' 14 | JS = 'js' 15 | IMGS = 'imgs' 16 | 17 | CONFIG = 'config' 18 | DOCKERFILES = 'docker' 19 | 20 | 21 | # Asset filenames 22 | class AssetFilenames: 23 | _js_ext = '.min.js' 24 | _css_ext = '.min.css' 25 | 26 | TW_STANDALONE = set_tw_standalone_filename() 27 | ALPINE = 'alpine' + _js_ext 28 | HTMX = 'htmx' + _js_ext 29 | FLOWBITE_CSS = 'flowbite' + _css_ext 30 | FLOWBITE_JS = 'flowbite' + _js_ext 31 | 32 | REQUIREMENTS = 'requirements.txt' 33 | POETRY_CONF = 'pyproject.toml' 34 | README = 'README.md' 35 | ENV = '.env' 36 | MAIN = 'main.py' 37 | BUILD = 'build.py' 38 | 39 | 40 | # Asset URLs 41 | class AssetUrls: 42 | TW_STANDALONE = 'https://github.com/tailwindlabs/tailwindcss/releases/latest/download/' 43 | ALPINE = 'node_modules/alpinejs/dist/cdn.min.js' 44 | HTMX = f'https://unpkg.com/htmx.org/dist/{AssetFilenames.HTMX}' 45 | FLOWBITE_CSS = f'node_modules/flowbite/dist/{AssetFilenames.FLOWBITE_CSS}' 46 | FLOWBITE_JS = f'node_modules/flowbite/dist/{AssetFilenames.FLOWBITE_JS}' 47 | 48 | 49 | # Static folder directory names 50 | class StaticDirNames: 51 | ROOT = os.path.join(os.getcwd(), STATIC_DIR_NAME, SetupAssetsDirNames.FRONTEND) 52 | CSS = os.path.join(ROOT, SetupAssetsDirNames.CSS) 53 | JS = os.path.join(ROOT, SetupAssetsDirNames.JS) 54 | IMGS = os.path.join(ROOT, SetupAssetsDirNames.IMGS) 55 | 56 | 57 | # Setup assets filepaths 58 | class SetupDirPaths: 59 | ROOT = os.path.dirname(os.path.join(os.getcwd(), SetupAssetsDirNames.ROOT)) 60 | SETUP_ROOT = os.path.join(ROOT, 'fastapi_quickstart') 61 | ASSETS = os.path.join(SETUP_ROOT, SetupAssetsDirNames.ROOT) 62 | PROJECT_NAME = os.path.join(SETUP_ROOT, 'conf', 'name') 63 | 64 | 65 | def set_project_name(name: str) -> str: 66 | os.environ['PROJECT_NAME'] = name 67 | 68 | 69 | def get_project_name() -> str: 70 | return os.environ.get('PROJECT_NAME') 71 | 72 | 73 | # Project directory and filename filepaths 74 | class ProjectPaths: 75 | def __init__(self) -> None: 76 | self.PROJECT_NAME = get_project_name() 77 | self.ROOT = os.path.join(os.path.dirname(os.getcwd()), self.PROJECT_NAME) 78 | self.BACKEND = os.path.join(self.ROOT, SetupAssetsDirNames.BACKEND) 79 | self.FRONTEND = os.path.join(self.ROOT, SetupAssetsDirNames.FRONTEND) 80 | 81 | self.PROJECT = os.path.join(self.ROOT, self.PROJECT_NAME) 82 | self.INIT_POETRY_CONF = os.path.join(self.PROJECT, AssetFilenames.POETRY_CONF) 83 | self.INIT_README = os.path.join(self.PROJECT, AssetFilenames.README) 84 | 85 | self.POETRY_CONF = os.path.join(self.ROOT, AssetFilenames.POETRY_CONF) 86 | self.PROJECT_MAIN = os.path.join(self.ROOT, AssetFilenames.MAIN) 87 | self.PROJECT_BUILD = os.path.join(self.ROOT, AssetFilenames.BUILD) 88 | 89 | self.STATIC = os.path.join(self.ROOT, SetupAssetsDirNames.FRONTEND, STATIC_DIR_NAME) 90 | self.CSS = os.path.join(self.STATIC, SetupAssetsDirNames.CSS) 91 | self.JS = os.path.join(self.STATIC, SetupAssetsDirNames.JS) 92 | self.IMGS = os.path.join(self.STATIC, SetupAssetsDirNames.IMGS) 93 | 94 | 95 | # Dockerfile specific directory and filename filepaths 96 | class DockerPaths: 97 | def __init__(self) -> None: 98 | self.df = 'Dockerfile' 99 | self.compose = 'docker-compose' 100 | 101 | self._yml_ext = '.yml' 102 | self._project_root = os.path.dirname(os.getcwd()) 103 | 104 | self.ROOT_DIR = os.path.join(self._project_root, SetupAssetsDirNames.CONFIG, SetupAssetsDirNames.DOCKERFILES) 105 | 106 | self.BACKEND_DF = os.path.join(self.ROOT_DIR, f'{self.df}.backend') 107 | self.IGNORE = os.path.join(self._project_root, '.dockerignore') 108 | 109 | self.COMPOSE_BASE = os.path.join(self._project_root, f"{self.compose}.base{self._yml_ext}") 110 | self.COMPOSE_MAIN = os.path.join(self._project_root, f"{self.compose}{self._yml_ext}") 111 | -------------------------------------------------------------------------------- /fastapi_quickstart/conf/constants/poetry.py: -------------------------------------------------------------------------------- 1 | from . import STATIC_DIR_NAME 2 | from ..helper import set_tw_standalone_filename 3 | from .filepaths import get_project_name, AssetFilenames 4 | 5 | 6 | # Define Poetry script commands 7 | class PoetryCommands: 8 | def __init__(self) -> None: 9 | self.project_name = get_project_name() 10 | 11 | self.TW_CMD = 'tailwindcss -i {INPUT_PATH} -o {OUTPUT_PATH}' 12 | self.WATCH_TW_CMD = f"{self.TW_CMD} --watch --minify" 13 | 14 | self.START_SERVER_CMD = f"{self.project_name}.main:start" 15 | self.WATCH_POETRY_CMD = f"{self.project_name}.{AssetFilenames.BUILD.split('.')[0]}:tw_build" 16 | 17 | # Define Poetry script content 18 | # Specific to setup/venv.py -> init_project() 19 | class PoetryContent: 20 | def __init__(self) -> None: 21 | self.tw_type = set_tw_standalone_filename() 22 | self.project_name = get_project_name() 23 | self.commands = PoetryCommands() 24 | 25 | self.SCRIPT_INSERT_LOC = 'readme = "README.md"' 26 | self.SCRIPT_CONTENT = '\n'.join([ 27 | "[tool.poetry.scripts]", 28 | f'run = "{self.commands.START_SERVER_CMD}"', 29 | f'watch = "{self.commands.WATCH_POETRY_CMD}"' 30 | ]) 31 | 32 | self.BUILD_FILE_CONTENT = f""" 33 | from pathlib import Path 34 | import os 35 | import subprocess 36 | 37 | PROJECT_DIR = os.path.basename(Path(__file__).resolve().parent) 38 | CSS_DIR = os.path.join('frontend', '{STATIC_DIR_NAME}', 'css') 39 | INPUT_PATH = os.path.join(CSS_DIR, 'input.css') 40 | OUTPUT_PATH = os.path.join(CSS_DIR, 'styles.min.css') 41 | 42 | def tw_build() -> None: 43 | cmd = f"{'npx ' if self.tw_type == 'unsupported' else ''}{self.commands.WATCH_TW_CMD}" 44 | os.chdir(os.path.join(os.getcwd(), '{self.project_name}')) 45 | subprocess.run(cmd.split(' '), check=True) 46 | """ 47 | -------------------------------------------------------------------------------- /fastapi_quickstart/conf/file_handler.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def write_to_file(item: str, path: str) -> None: 4 | """Stores an item in a file.""" 5 | with open(path, "w") as file: 6 | file.write(item) 7 | 8 | 9 | def add_content_to_file(content: str, path: str) -> None: 10 | """Writes multiple lines (`content`) to a file at `path`.""" 11 | with open(path, "w") as file: 12 | file.writelines(content) 13 | 14 | 15 | def read_all_file_content(path: str) -> str: 16 | """Retrieves all content from a basic file.""" 17 | with open(path, 'r') as file: 18 | content = file.read() 19 | 20 | return content 21 | 22 | 23 | def insert_into_file(position: str, new_content: str, path: str) -> None: 24 | """Adds `new_content` to a file (`path`) at a specific `position`. 25 | 26 | Note: `new = (position + new_content).strip()`.""" 27 | content = read_all_file_content(path) 28 | 29 | content = content.replace( 30 | position, 31 | (position + new_content).strip(), 32 | 1 33 | ) 34 | 35 | add_content_to_file(content, path) 36 | 37 | 38 | def replace_content(old: str, new: str, path: str) -> None: 39 | """Replaces `old` content with `new` ones in a file at `path`.""" 40 | content = read_all_file_content(path) 41 | 42 | content = content.replace( 43 | old, 44 | new.strip(), 45 | 1 46 | ) 47 | 48 | add_content_to_file(content, path) 49 | -------------------------------------------------------------------------------- /fastapi_quickstart/conf/helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | 5 | def tw_executable_exists(project_path: str) -> bool: 6 | """Checks if the `tailwindcss` executable exists in the project root directory.""" 7 | windows_tw = os.path.join(project_path, 'tailwindcss.exe') 8 | other_tw = os.path.join(project_path, 'tailwindcss') 9 | 10 | # If executable exists, return True 11 | if os.path.exists(other_tw) or os.path.exists(windows_tw): 12 | return True 13 | return False 14 | 15 | 16 | def dirname_check(valid_names: list[str], dir_name: str, err_msg_start: str) -> str: 17 | """Checks if a directory name is within a valid list of names. If it is, we return it. Otherwise, we raise an error.""" 18 | if dir_name not in valid_names: 19 | return ValueError(f"{err_msg_start} must be one of: '{valid_names}'!") 20 | 21 | return dir_name 22 | 23 | 24 | def set_tw_standalone_filename() -> str: 25 | """Checks the type of operating system the user is on and assigns the appropriate TailwindCSS Standalone CLI installation. Returns the filename. If Unsupported, returns `unsupported`.""" 26 | # Determine os 27 | system_platform = platform.system().lower() 28 | machine = platform.machine() 29 | 30 | # Define the filename based on the platform 31 | if system_platform == 'darwin' and machine == 'arm64': 32 | filename = 'tailwindcss-macos-arm64' 33 | elif system_platform == 'darwin' and machine == 'x86_64': 34 | filename = 'tailwindcss-macos-x64' 35 | elif system_platform == 'linux' and machine == 'arm64': 36 | filename = 'tailwindcss-linux-arm64' 37 | elif system_platform == 'linux' and machine == 'armv7l': 38 | filename = 'tailwindcss-linux-armv7' 39 | elif system_platform == 'linux' and machine == 'x86_64': 40 | filename = 'tailwindcss-linux-x64' 41 | elif system_platform == 'windows' and machine == 'AMD64': 42 | filename = 'tailwindcss-windows-x64.exe' 43 | elif system_platform == 'windows' and machine == 'arm64': 44 | filename = 'tailwindcss-windows-arm64.exe' 45 | else: 46 | filename = 'unsupported' 47 | 48 | return filename 49 | -------------------------------------------------------------------------------- /fastapi_quickstart/config.py: -------------------------------------------------------------------------------- 1 | # Define your database URL 2 | DATABASE_URL = 'sqlite:///./database.db' 3 | # DATABASE_URL = "postgresql://user:password@postgresserver/db" 4 | 5 | # Pip packages to install 6 | ADDITIONAL_PIP_PACKAGES = [ 7 | "pytest" 8 | ] 9 | 10 | # .env file additional parameters 11 | ENV_FILE_ADDITIONAL_PARAMS = [ 12 | # f'example={example}' # example 13 | ] 14 | -------------------------------------------------------------------------------- /fastapi_quickstart/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | 5 | from .conf.constants.filepaths import set_project_name 6 | from .conf.helper import set_tw_standalone_filename 7 | from .setup import run_tasks 8 | from .utils.helper import strip_whitespace_and_dashes 9 | from .utils.printables import project_table, project_complete_panel 10 | 11 | import typer 12 | from typing_extensions import Annotated 13 | from rich.console import Console 14 | 15 | 16 | app = typer.Typer(rich_markup_mode="rich") 17 | 18 | console = Console() 19 | 20 | 21 | @app.command() 22 | def main(name: Annotated[str, typer.Argument(help="The name of the project", show_default=False)]) -> None: 23 | """Create a FastAPI project with NAME.""" 24 | name = strip_whitespace_and_dashes(name) 25 | path = os.path.join(os.path.dirname(os.getcwd()), name) 26 | 27 | # Store name as temporary env variable 28 | set_project_name(name) 29 | 30 | # Provide pretty print formats 31 | name_print = f'[cyan]{name}[/cyan]' 32 | path_print = f'[dark_goldenrod]{path}[/dark_goldenrod]' 33 | 34 | console.print(project_table(name, path)) 35 | 36 | # Replace project if exists 37 | if os.path.exists(path): 38 | typer.confirm("Replace project?", abort=True) 39 | 40 | console.print(f"\nRemoving {name_print} and creating a new one...\n") 41 | shutil.rmtree(path) 42 | else: 43 | console.print(f"\nCreating project {name_print}...\n") 44 | 45 | # Create and move into directory 46 | os.makedirs(path) 47 | os.chdir(path) 48 | 49 | # Run task handler 50 | run_tasks() 51 | 52 | # End of script 53 | console.print(project_complete_panel()) 54 | console.print(f"Access {name_print} at {path_print}") 55 | 56 | # Provide information for unsupported TailwindCSS standalone CLI 57 | if set_tw_standalone_filename() == 'unsupported': 58 | console.print('\nOS not supported for standalone TailwindCSS. [magenta]node_modules[/magenta] kept.') 59 | 60 | if __name__ == '__main__': 61 | app() 62 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/__init__.py: -------------------------------------------------------------------------------- 1 | from .venv import VEnvController 2 | from .static import StaticAssetsController 3 | from .libraries import LibraryController 4 | from .fastapi import FastAPIFileController 5 | from .docker import DockerFileController 6 | from .clean import CleanupController 7 | from ..conf.constants import PASS 8 | 9 | from rich.console import Console 10 | from rich.progress import Progress, SpinnerColumn, TextColumn 11 | 12 | 13 | TASKS = [ 14 | (VEnvController, "Creating virtual environment..."), 15 | (StaticAssetsController, "Creating static assets..."), 16 | (LibraryController, "Installing libraries..."), 17 | (FastAPIFileController, "Checking FastAPI assets..."), 18 | (DockerFileController, "Creating Dockerfiles..."), 19 | (CleanupController, "Cleaning project...") 20 | ] 21 | 22 | console = Console() 23 | 24 | 25 | def run_tasks() -> None: 26 | """The task handler for performing each operation in the CLI.""" 27 | with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress: 28 | for idx, (task, desc) in enumerate(TASKS, 1): 29 | new_desc = f"{idx}. {desc}" 30 | task_id = progress.add_task(description=new_desc, total=None) 31 | task().run(progress) 32 | progress.update(task_id, completed=1, description=f"{new_desc} {PASS}") 33 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/base.py: -------------------------------------------------------------------------------- 1 | from ..conf.constants import PASS 2 | from ..conf.constants.filepaths import ProjectPaths 3 | 4 | from rich.progress import Progress 5 | 6 | 7 | class ControllerBase: 8 | """A parent class for all controllers. 9 | 10 | :param tasks: (list[tuple]) - a list of tuples in the format of (task, desc), where `task` is a class method and `desc` is a descriptive string highlighting what the task does. For example: 11 | ```python 12 | sub_tasks = [ 13 | (self.create, "Building venv"), 14 | (self.update_pip, "Updating PIP") 15 | ] 16 | ``` 17 | """ 18 | def __init__(self, tasks: list[tuple]) -> None: 19 | self.tasks = tasks 20 | self.project_paths = ProjectPaths() 21 | 22 | @staticmethod 23 | def update_desc(desc: str) -> str: 24 | """Updates task description format.""" 25 | return f" {desc}..." 26 | 27 | def format_tasks(self) -> None: 28 | """Formats controller tasks into a standardised format.""" 29 | updated_tasks = [] 30 | for task, desc in self.tasks: 31 | updated_tasks.append((task, self.update_desc(desc))) 32 | 33 | self.tasks = updated_tasks 34 | 35 | def run(self, progress: Progress) -> None: 36 | self.format_tasks() 37 | 38 | for task, desc in self.tasks: 39 | task_id = progress.add_task(description=desc, total=None) 40 | task() 41 | progress.update(task_id, completed=1, description=f"{desc} {PASS}") 42 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/clean.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | 5 | from ..conf.helper import tw_executable_exists 6 | from ..conf.constants import VENV_NAME 7 | from .base import ControllerBase 8 | 9 | 10 | class CleanupController(ControllerBase): 11 | """A controller for handling project cleanup.""" 12 | def __init__(self) -> None: 13 | tasks = [ 14 | (self.node_modules, "Removing [magenta]node_modules[/magenta]"), 15 | (self.delete_venv, "Removing [yellow]venv[/yellow]"), 16 | (self.remove_files, "Removing redundant files"), 17 | (self.poetry_install, "Finalising project") 18 | ] 19 | 20 | super().__init__(tasks) 21 | 22 | def node_modules(self) -> None: 23 | """Removes the `node_modules` folder if `tailwindcss` does not exist.""" 24 | # If exists, remove node_modules 25 | if tw_executable_exists(self.project_paths.ROOT): 26 | shutil.rmtree(os.path.join(self.project_paths.ROOT, 'node_modules')) 27 | 28 | def poetry_install(self) -> None: 29 | """Finalise the application with a poetry install.""" 30 | subprocess.run(["poetry", "shell"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 31 | subprocess.run(["poetry", "install"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 32 | 33 | def delete_venv(self) -> None: 34 | """Deletes the virtual environment folder and assets.""" 35 | shutil.rmtree(os.path.join(os.path.dirname(self.project_paths.ROOT), VENV_NAME)) 36 | 37 | def remove_files(self) -> None: 38 | """Removes redundant files.""" 39 | os.remove(os.path.join(self.project_paths.ROOT, '__init__.py')) 40 | os.remove(os.path.join(self.project_paths.ROOT, 'package.json')) 41 | os.remove(os.path.join(self.project_paths.ROOT, 'package-lock.json')) 42 | 43 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/docker.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .base import ControllerBase 4 | from ..conf.constants.docker import DockerFileMapper 5 | from ..conf.constants.filepaths import DockerPaths 6 | from ..conf.file_handler import write_to_file 7 | 8 | 9 | class DockerFileController(ControllerBase): 10 | """A FastAPI file creation controller.""" 11 | def __init__(self) -> None: 12 | tasks = [ 13 | (self.create_dockerfiles, "Generating [bright_blue]Docker[/bright_blue] files"), 14 | (self.create_compose_files, "Generating [bright_blue]Compose[/bright_blue] files") 15 | ] 16 | 17 | super().__init__(tasks) 18 | 19 | self.mapper = DockerFileMapper() 20 | self.paths = DockerPaths() 21 | 22 | def __create_files(self, path_content_pair: list[tuple[str, str]]) -> None: 23 | """Helper function for creating files.""" 24 | for (path, content) in path_content_pair: 25 | write_to_file(content, path) 26 | 27 | def create_dockerfiles(self) -> None: 28 | """Creates the required Dockerfiles in the appropriate folder.""" 29 | os.makedirs(self.paths.ROOT_DIR, exist_ok=True) 30 | self.__create_files(self.mapper.dockerfiles()) 31 | 32 | def create_compose_files(self) -> None: 33 | """Creates the required docker-compose files in the root directory.""" 34 | open(self.paths.IGNORE, 'w') 35 | 36 | self.__create_files(self.mapper.compose_files()) 37 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/fastapi.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from .base import ControllerBase 4 | from ..conf.constants.fastapi import FastAPIDirPaths, FastAPIContent 5 | from ..conf.constants.poetry import PoetryCommands, PoetryContent 6 | from ..conf.file_handler import insert_into_file 7 | from ..config import DATABASE_URL 8 | 9 | 10 | class FastAPIFileController(ControllerBase): 11 | """A FastAPI file creation controller.""" 12 | def __init__(self) -> None: 13 | tasks = [ 14 | (self.check_db, "Checking [red]database[/red] files"), 15 | (self.create_build, "Creating [yellow]build[/yellow] file") 16 | ] 17 | 18 | super().__init__(tasks) 19 | 20 | self.poetry_commands = PoetryCommands() 21 | self.poetry_content = PoetryContent() 22 | 23 | self.dir_paths = FastAPIDirPaths() 24 | 25 | def check_db(self) -> None: 26 | """Checks that the correct database config is setup.""" 27 | sqlite_db = DATABASE_URL.split(':')[0] == 'sqlite' 28 | 29 | if sqlite_db: 30 | insert_into_file( 31 | FastAPIContent.SQLITE_DB_POSITION, 32 | FastAPIContent.SQLITE_DB_CONTENT, 33 | self.dir_paths.DATABASE_INIT_FILE 34 | ) 35 | 36 | def create_build(self) -> None: 37 | """Creates a build file in the root directory for watching TailwindCSS.""" 38 | content = textwrap.dedent(self.poetry_content.BUILD_FILE_CONTENT)[1:] 39 | 40 | with open(self.project_paths.PROJECT_BUILD, 'w') as file: 41 | file.write(content) 42 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/libraries.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import urllib.request 5 | 6 | from ..conf.constants import NPM_PACKAGES 7 | from ..conf.constants.filepaths import AssetFilenames, AssetUrls 8 | from ..conf.constants.poetry import PoetryCommands 9 | from .base import ControllerBase 10 | 11 | 12 | class LibraryController(ControllerBase): 13 | """A controller for handling CSS and JS libraries.""" 14 | def __init__(self) -> None: 15 | tasks = [ 16 | (self.npm_installs, "Installing [red]NPM[/red] packages"), 17 | (self.get_tw_standalone, "Retrieving [bright_blue]TailwindCSS[/bright_blue] standalone CLI"), 18 | (self.get_flowbite, "Storing [bright_blue]Flowbite[/bright_blue] assets"), 19 | (self.get_htmx, "Storing [bright_cyan]HTMX[/bright_cyan] assets"), 20 | (self.get_alpine, "Storing [bright_cyan]AlpineJS[/bright_cyan] assets") 21 | ] 22 | 23 | super().__init__(tasks) 24 | 25 | self.poetry_commands = PoetryCommands() 26 | 27 | def npm_installs(self) -> None: 28 | """Installed required Node packages (TailwindCSS, Flowbite, and AlpineJS) and creates a TailwindCSS output file.""" 29 | subprocess.run(["npm", "install", "-D", *NPM_PACKAGES], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 30 | 31 | subprocess.run(["npx", *self.poetry_commands.TW_CMD.split(' ')], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 32 | 33 | @staticmethod 34 | def get_tw_standalone() -> None: 35 | filename = AssetFilenames.TW_STANDALONE 36 | 37 | if filename != 'unsupported': 38 | try: 39 | with urllib.request.urlopen(AssetUrls.TW_STANDALONE + filename) as response, open(filename, 'wb') as file: 40 | file.write(response.read()) 41 | 42 | # Make executable if not windows exe 43 | if not filename.endswith('.exe'): 44 | os.chmod(filename, 0o755) 45 | 46 | _, extension = os.path.splitext(filename) 47 | os.rename(filename, f'tailwindcss{extension}') 48 | 49 | except Exception as e: 50 | print(f"Error: {e}") 51 | 52 | def get_flowbite(self) -> None: 53 | """Copies Flowbite CSS and JS from `node_modules`.""" 54 | shutil.copy(AssetUrls.FLOWBITE_CSS, os.path.join(self.project_paths.CSS, AssetFilenames.FLOWBITE_CSS)) 55 | shutil.copy(AssetUrls.FLOWBITE_JS, os.path.join(self.project_paths.JS, AssetFilenames.FLOWBITE_JS)) 56 | 57 | def get_htmx(self) -> None: 58 | """Retrieves `HTMX` from the official downloads page.""" 59 | with urllib.request.urlopen(AssetUrls.HTMX) as response: 60 | htmx_content = response.read().decode('utf-8') 61 | 62 | with open(os.path.join(self.project_paths.JS, AssetFilenames.HTMX), 'w') as file: 63 | file.write(htmx_content) 64 | 65 | def get_alpine(self) -> None: 66 | """Retrieves `AlpineJS` from `node_modules`.""" 67 | shutil.copy(AssetUrls.ALPINE, os.path.join(self.project_paths.JS, AssetFilenames.ALPINE)) 68 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/static.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from ..conf.constants import STATIC_DIR_NAME, VALID_STATIC_DIR_NAMES, CORE_ENV_PARAMS 5 | from ..conf.constants.docker import DockerContent 6 | from ..conf.constants.filepaths import AssetFilenames, SetupDirPaths, SetupAssetsDirNames 7 | from ..config import ENV_FILE_ADDITIONAL_PARAMS 8 | from .base import ControllerBase 9 | 10 | 11 | class StaticAssetsController(ControllerBase): 12 | """A controller for handling the static assets.""" 13 | def __init__(self) -> None: 14 | tasks = [ 15 | (self.create_dotenv, "Building [magenta].env[/magenta]"), 16 | (self.move_setup_assets, "Creating [green]static files[/green] and [green]templates[/green]") 17 | ] 18 | 19 | super().__init__(tasks) 20 | 21 | @staticmethod 22 | def create_dotenv() -> None: 23 | """Creates a `.env` file and adds items to it.""" 24 | docker_content = DockerContent() 25 | path = os.path.join(os.path.dirname(os.getcwd()), AssetFilenames.ENV) 26 | 27 | with open(path, "w") as file: 28 | file.write(docker_content.env_config()) 29 | 30 | with open(path, "a") as file: 31 | for item in CORE_ENV_PARAMS + ENV_FILE_ADDITIONAL_PARAMS: 32 | file.write(item) 33 | 34 | def move_setup_assets(self) -> None: 35 | """Moves the items in the `setup_assets` folder into the project directory.""" 36 | static_exists = False 37 | 38 | # Move assets into root project dir 39 | shutil.copytree(SetupDirPaths.ASSETS, os.getcwd(), dirs_exist_ok=True) 40 | 41 | for dir_name in VALID_STATIC_DIR_NAMES: 42 | dir_path = os.path.join(os.getcwd(), SetupAssetsDirNames.FRONTEND, dir_name) 43 | 44 | # Check if static folder exists and matches desired name 45 | if os.path.exists(dir_path): 46 | if dir_name != STATIC_DIR_NAME: 47 | os.rename(dir_path, self.project_paths.STATIC) 48 | static_exists = True 49 | break 50 | 51 | # If static folder doesn't exist, make one 52 | if not static_exists: 53 | static_dirs = [ 54 | self.project_paths.STATIC, 55 | self.project_paths.CSS, 56 | self.project_paths.JS, 57 | self.project_paths.IMGS 58 | ] 59 | for item in static_dirs: 60 | os.mkdir(item) 61 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup/venv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | 5 | from ..conf.constants import VENV, VENV_NAME, CORE_PIP_PACKAGES 6 | from ..conf.constants.filepaths import get_project_name 7 | from ..conf.constants.poetry import PoetryContent 8 | from ..conf.file_handler import insert_into_file 9 | from ..config import ADDITIONAL_PIP_PACKAGES 10 | from .base import ControllerBase 11 | 12 | 13 | class VEnvController(ControllerBase): 14 | """A controller for creating a Python virtual environment.""" 15 | def __init__(self) -> None: 16 | tasks = [ 17 | (self.create, "Building [yellow]venv[/yellow]"), 18 | (self.update_pip, "Updating [yellow]PIP[/yellow]"), 19 | (self.install, "Installing [yellow]PIP[/yellow] packages"), 20 | (self.init_project, f"Initalising [cyan]{get_project_name()}[/cyan] as [green]Poetry[/green] project"), 21 | (self.add_dependencies, "Adding [yellow]PIP[/yellow] packages to [green]Poetry[/green]") 22 | ] 23 | 24 | super().__init__(tasks) 25 | 26 | self.poetry_content = PoetryContent() 27 | 28 | @staticmethod 29 | def create() -> None: 30 | """Creates a new virtual environment.""" 31 | subprocess.run(["python", "-m", "venv", VENV_NAME], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 32 | 33 | @staticmethod 34 | def update_pip() -> None: 35 | """Updates `PIP` to the latest version.""" 36 | subprocess.run([os.path.join(VENV, "pip"), "install", "--upgrade", "pip"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 37 | 38 | @staticmethod 39 | def install() -> None: 40 | """Installs a set of `PIP` packages.""" 41 | subprocess.run([os.path.join(VENV, "pip"), "install", *CORE_PIP_PACKAGES, *ADDITIONAL_PIP_PACKAGES], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 42 | 43 | def init_project(self) -> None: 44 | """Creates a poetry project.""" 45 | # Create Poetry project 46 | subprocess.run(["poetry", "new", self.project_paths.PROJECT_NAME], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 47 | 48 | # Organise new project directory 49 | shutil.rmtree(os.path.join(self.project_paths.PROJECT, self.project_paths.PROJECT_NAME)) 50 | shutil.move(self.project_paths.INIT_POETRY_CONF, self.project_paths.ROOT) 51 | shutil.move(self.project_paths.INIT_README, self.project_paths.ROOT) 52 | 53 | # Add scripts to pyproject.toml 54 | insert_into_file( 55 | self.poetry_content.SCRIPT_INSERT_LOC, 56 | f'\n\n{self.poetry_content.SCRIPT_CONTENT}', 57 | self.project_paths.POETRY_CONF 58 | ) 59 | 60 | def add_dependencies(self) -> None: 61 | """Adds PIP packages to the poetry project.""" 62 | subprocess.run(["poetry", "shell"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 63 | 64 | subprocess.run(["poetry", "add", *CORE_PIP_PACKAGES, *ADDITIONAL_PIP_PACKAGES], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 65 | 66 | # Move into project directory 67 | os.chdir(self.project_paths.PROJECT) 68 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/.gitignore: -------------------------------------------------------------------------------- 1 | # Database # 2 | *.log 3 | *.pot 4 | *.pyc 5 | __pycache__ 6 | db.sqlite3 7 | media 8 | *.db 9 | 10 | # Backup files # 11 | *.bak 12 | 13 | # If you are using PyCharm # 14 | # User-specific stuff 15 | .idea/**/workspace.xml 16 | .idea/**/tasks.xml 17 | .idea/**/usage.statistics.xml 18 | .idea/**/dictionaries 19 | .idea/**/shelf 20 | 21 | # AWS User-specific 22 | .idea/**/aws.xml 23 | 24 | # Generated files 25 | .idea/**/contentModel.xml 26 | 27 | # Sensitive or high-churn files 28 | .idea/**/dataSources/ 29 | .idea/**/dataSources.ids 30 | .idea/**/dataSources.local.xml 31 | .idea/**/sqlDataSources.xml 32 | .idea/**/dynamic.xml 33 | .idea/**/uiDesigner.xml 34 | .idea/**/dbnavigator.xml 35 | 36 | # Gradle 37 | .idea/**/gradle.xml 38 | .idea/**/libraries 39 | 40 | # File-based project format 41 | *.iws 42 | 43 | # IntelliJ 44 | out/ 45 | 46 | # JIRA plugin 47 | atlassian-ide-plugin.xml 48 | 49 | # Python # 50 | *.py[cod] 51 | *$py.class 52 | 53 | # Distribution / packaging 54 | .Python build/ 55 | develop-eggs/ 56 | dist/ 57 | downloads/ 58 | eggs/ 59 | .eggs/ 60 | lib/ 61 | lib64/ 62 | parts/ 63 | sdist/ 64 | var/ 65 | wheels/ 66 | *.whl 67 | *.egg-info/ 68 | .installed.cfg 69 | *.egg 70 | *.manifest 71 | *.spec 72 | 73 | # Installer logs 74 | pip-log.txt 75 | pip-delete-this-directory.txt 76 | 77 | # Unit test / coverage reports 78 | htmlcov/ 79 | .tox/ 80 | .coverage 81 | .coverage.* 82 | .cache 83 | .pytest_cache/ 84 | nosetests.xml 85 | coverage.xml 86 | *.cover 87 | .hypothesis/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery 96 | celerybeat-schedule.* 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | 113 | # Sublime Text # 114 | *.tmlanguage.cache 115 | *.tmPreferences.cache 116 | *.stTheme.cache 117 | *.sublime-workspace 118 | *.sublime-project 119 | 120 | # sftp configuration file 121 | sftp-config.json 122 | 123 | # Package control specific files Package 124 | Control.last-run 125 | Control.ca-list 126 | Control.ca-bundle 127 | Control.system-ca-bundle 128 | GitHub.sublime-settings 129 | 130 | # Visual Studio Code # 131 | .vscode/* 132 | !.vscode/settings.json 133 | !.vscode/tasks.json 134 | !.vscode/launch.json 135 | !.vscode/extensions.json 136 | .history 137 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Achronus/fastapi-quickstart/38fb44bbf8cb2f2c282c40f4534b2cf4e03d2005/fastapi_quickstart/setup_assets/__init__.py -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/backend/database/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import sessionmaker 7 | 8 | load_dotenv() 9 | 10 | engine = create_engine( 11 | os.getenv('DATABASE_URL') 12 | ) 13 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 14 | 15 | Base = declarative_base() 16 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/backend/database/crud.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | 6 | def get_user(db: Session, user_id: int): 7 | return db.query(models.User).filter(models.User.id == user_id).first() 8 | 9 | 10 | def get_user_by_email(db: Session, email: str): 11 | return db.query(models.User).filter(models.User.email == email).first() 12 | 13 | 14 | def get_users(db: Session, skip: int = 0, limit: int = 100): 15 | return db.query(models.User).offset(skip).limit(limit).all() 16 | 17 | 18 | def create_user(db: Session, user: schemas.UserCreate): 19 | fake_hashed_password = user.password + "notreallyhashed" 20 | db_user = models.User(email=user.email, hashed_password=fake_hashed_password) 21 | db.add(db_user) 22 | db.commit() 23 | db.refresh(db_user) 24 | return db_user 25 | 26 | 27 | def get_items(db: Session, skip: int = 0, limit: int = 100): 28 | return db.query(models.Item).offset(skip).limit(limit).all() 29 | 30 | 31 | def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): 32 | db_item = models.Item(**item.model_dump(), owner_id=user_id) 33 | db.add(db_item) 34 | db.commit() 35 | db.refresh(db_item) 36 | return db_item 37 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/backend/database/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, ForeignKey, Integer, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from . import Base 5 | 6 | 7 | class User(Base): 8 | __tablename__ = "users" 9 | 10 | id = Column(Integer, primary_key=True, index=True) 11 | email = Column(String, unique=True, index=True) 12 | hashed_password = Column(String) 13 | is_active = Column(Boolean, default=True) 14 | 15 | items = relationship("Item", back_populates="owner") 16 | 17 | 18 | class Item(Base): 19 | __tablename__ = "items" 20 | 21 | id = Column(Integer, primary_key=True, index=True) 22 | title = Column(String, index=True) 23 | description = Column(String, index=True) 24 | owner_id = Column(Integer, ForeignKey("users.id")) 25 | 26 | owner = relationship("User", back_populates="items") 27 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/backend/database/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ItemBase(BaseModel): 5 | title: str 6 | description: str | None = None 7 | 8 | 9 | class ItemCreate(ItemBase): 10 | pass 11 | 12 | 13 | class Item(ItemBase): 14 | id: int 15 | owner_id: int 16 | 17 | class Config: 18 | from_attributes = True 19 | 20 | 21 | class UserBase(BaseModel): 22 | email: str 23 | 24 | 25 | class UserCreate(UserBase): 26 | password: str 27 | 28 | 29 | class User(UserBase): 30 | id: int 31 | is_active: bool 32 | items: list[Item] = [] 33 | 34 | class Config: 35 | from_attributes = True 36 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/frontend/static/css/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/frontend/static/imgs/avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/frontend/static/js/theme-toggle.js: -------------------------------------------------------------------------------- 1 | const sunIcon = document.getElementById("sun"); 2 | const moonIcon = document.getElementById("moon"); 3 | 4 | const userTheme = localStorage.getItem('theme'); 5 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; 6 | 7 | // Icon Toggling 8 | const iconToggle = () => { 9 | moonIcon.classList.toggle("hidden"); 10 | sunIcon.classList.toggle("hidden"); 11 | } 12 | 13 | // Theme check 14 | const themeCheck = () => { 15 | if (userTheme === 'dark' || (!userTheme && systemTheme)) { 16 | document.documentElement.classList.add('dark'); 17 | moonIcon.classList.add('hidden'); 18 | return; 19 | } 20 | sunIcon.classList.add('hidden'); 21 | } 22 | 23 | // Manual theme switching 24 | const themeSwitch = () => { 25 | if (document.documentElement.classList.contains("dark")) { 26 | document.documentElement.classList.remove("dark"); 27 | localStorage.setItem("theme", "light"); 28 | iconToggle(); 29 | return; 30 | } 31 | 32 | document.documentElement.classList.add("dark"); 33 | localStorage.setItem("theme", "dark"); 34 | iconToggle(); 35 | } 36 | 37 | // Call theme switch on button clicks 38 | sunIcon.addEventListener("click", () => { 39 | themeSwitch(); 40 | }); 41 | 42 | moonIcon.addEventListener("click", () => { 43 | themeSwitch(); 44 | }); 45 | 46 | // Invoke theme check on initial load 47 | themeCheck(); -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/frontend/templates/_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Homepage{% endblock title %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% include './components/navbar.html' %} 21 |
22 | {% block content %} 23 | {% endblock content %} 24 |
25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/frontend/templates/components/navbar.html: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/frontend/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends './_base.html' %} 2 | 3 | {% block content %} 4 | 5 |

6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import uvicorn 5 | from fastapi import Depends, FastAPI, HTTPException, Request 6 | from fastapi.responses import HTMLResponse 7 | from fastapi.staticfiles import StaticFiles 8 | from fastapi.templating import Jinja2Templates 9 | from sqlalchemy.orm import Session 10 | 11 | from .backend.database import SessionLocal, engine, crud, schemas 12 | from .backend.database.models import Base 13 | 14 | 15 | PROJECT_DIR = os.path.basename(Path(__file__).resolve().parent) 16 | FRONTEND_DIR = os.path.join(PROJECT_DIR, 'frontend') 17 | 18 | # Create DB tables 19 | Base.metadata.create_all(bind=engine) 20 | 21 | app = FastAPI() 22 | app.mount("/static", StaticFiles(directory=os.path.join(FRONTEND_DIR, 'public')), name="static") 23 | templates = Jinja2Templates(directory=os.path.join(FRONTEND_DIR, "templates")) 24 | 25 | 26 | # Dependency 27 | def get_db(): 28 | db = SessionLocal() 29 | try: 30 | yield db 31 | finally: 32 | db.close() 33 | 34 | 35 | @app.get("/", response_class=HTMLResponse) 36 | async def home(request: Request): 37 | return templates.TemplateResponse(request=request, name='index.html', context={}) 38 | 39 | 40 | @app.post("/users/", response_model=schemas.User) 41 | def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): 42 | db_user = crud.get_user_by_email(db, email=user.email) 43 | if db_user: 44 | raise HTTPException(status_code=400, detail="Email already registered") 45 | return crud.create_user(db=db, user=user) 46 | 47 | 48 | @app.get("/users/", response_model=list[schemas.User]) 49 | def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 50 | users = crud.get_users(db, skip=skip, limit=limit) 51 | return users 52 | 53 | 54 | @app.get("/users/{user_id}", response_model=schemas.User) 55 | def read_user(user_id: int, db: Session = Depends(get_db)): 56 | db_user = crud.get_user(db, user_id=user_id) 57 | if db_user is None: 58 | raise HTTPException(status_code=404, detail="User not found") 59 | return db_user 60 | 61 | 62 | @app.post("/users/{user_id}/items/", response_model=schemas.Item) 63 | def create_item_for_user( 64 | user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) 65 | ): 66 | return crud.create_user_item(db=db, item=item, user_id=user_id) 67 | 68 | 69 | @app.get("/items/", response_model=list[schemas.Item]) 70 | def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 71 | items = crud.get_items(db, skip=skip, limit=limit) 72 | return items 73 | 74 | 75 | def start() -> None: 76 | """Start the server.""" 77 | uvicorn.run(f"{PROJECT_DIR}.main:app", host="0.0.0.0", port=8080, reload=True) 78 | -------------------------------------------------------------------------------- /fastapi_quickstart/setup_assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: 'class', 3 | content: [ 4 | './**/*.html', 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /fastapi_quickstart/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Achronus/fastapi-quickstart/38fb44bbf8cb2f2c282c40f4534b2cf4e03d2005/fastapi_quickstart/tests/__init__.py -------------------------------------------------------------------------------- /fastapi_quickstart/tests/test_filepaths.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from conf.constants.filepaths import ( 5 | ProjectPaths, 6 | SetupDirPaths, 7 | get_project_name, 8 | set_project_name, 9 | AssetFilenames, 10 | SetupAssetsDirNames 11 | ) 12 | from conf.constants import STATIC_DIR_NAME 13 | 14 | PROJECT_NAME = 'test_project' 15 | 16 | set_project_name(PROJECT_NAME) 17 | CUSTOM_PROJECT_PARENT = get_project_name() 18 | CUSTOM_PROJECT_ROOT = os.path.join(CUSTOM_PROJECT_PARENT, get_project_name()) 19 | CUSTOM_PROJECT_STATIC = os.path.join(CUSTOM_PROJECT_ROOT, STATIC_DIR_NAME) 20 | 21 | SETUP_ROOT = 'fastapi-quickstart' 22 | SETUP_PROJECT_ROOT = os.path.join(SETUP_ROOT, 'fastapi_quickstart') 23 | STATIC = os.path.join(SETUP_PROJECT_ROOT, STATIC_DIR_NAME) 24 | 25 | 26 | @pytest.fixture 27 | def project_paths() -> None: 28 | set_project_name(PROJECT_NAME) 29 | return ProjectPaths() 30 | 31 | 32 | def test_project_name_valid(project_paths: ProjectPaths) -> None: 33 | set_project_name(ProjectPaths().PROJECT_NAME) 34 | assert get_project_name() is not None 35 | assert get_project_name() != '' 36 | assert get_project_name != project_paths.PROJECT_NAME 37 | 38 | 39 | class TestProjectPaths: 40 | @pytest.fixture(autouse=True) 41 | def initialize_project_paths(self) -> None: 42 | set_project_name(PROJECT_NAME) 43 | self.project_paths = ProjectPaths() 44 | 45 | def __validate_path(self, directory_path: str, ending: str) -> None: 46 | assert directory_path.endswith(ending) 47 | 48 | def test_root_valid(self) -> None: 49 | self.__validate_path( 50 | self.project_paths.ROOT, 51 | CUSTOM_PROJECT_PARENT 52 | ) 53 | 54 | def test_project_valid(self) -> None: 55 | self.__validate_path( 56 | self.project_paths.PROJECT, 57 | CUSTOM_PROJECT_ROOT 58 | ) 59 | 60 | def test_init_poetry_conf_valid(self) -> None: 61 | self.__validate_path( 62 | self.project_paths.INIT_POETRY_CONF, 63 | os.path.join(CUSTOM_PROJECT_ROOT, AssetFilenames.POETRY_CONF) 64 | ) 65 | 66 | def test_init_readme_valid(self) -> None: 67 | self.__validate_path( 68 | self.project_paths.INIT_README, 69 | os.path.join(CUSTOM_PROJECT_ROOT, AssetFilenames.README) 70 | ) 71 | 72 | def test_poetry_conf_valid(self) -> None: 73 | self.__validate_path( 74 | self.project_paths.POETRY_CONF, 75 | os.path.join(CUSTOM_PROJECT_PARENT, AssetFilenames.POETRY_CONF) 76 | ) 77 | 78 | def test_project_main_valid(self) -> None: 79 | self.__validate_path( 80 | self.project_paths.PROJECT_MAIN, 81 | os.path.join(CUSTOM_PROJECT_ROOT, AssetFilenames.MAIN) 82 | ) 83 | 84 | def test_project_build_valid(self) -> None: 85 | self.__validate_path( 86 | self.project_paths.PROJECT_BUILD, 87 | os.path.join(CUSTOM_PROJECT_ROOT, AssetFilenames.BUILD) 88 | ) 89 | 90 | def test_static_valid(self) -> None: 91 | self.__validate_path( 92 | self.project_paths.STATIC, 93 | CUSTOM_PROJECT_STATIC 94 | ) 95 | 96 | def test_static_css_valid(self) -> None: 97 | self.__validate_path( 98 | self.project_paths.CSS, 99 | os.path.join(CUSTOM_PROJECT_STATIC, SetupAssetsDirNames.CSS) 100 | ) 101 | 102 | def test_static_js_valid(self) -> None: 103 | self.__validate_path( 104 | self.project_paths.JS, 105 | os.path.join(CUSTOM_PROJECT_STATIC, SetupAssetsDirNames.JS) 106 | ) 107 | 108 | def test_static_imgs_valid(self) -> None: 109 | self.__validate_path( 110 | self.project_paths.IMGS, 111 | os.path.join(CUSTOM_PROJECT_STATIC, SetupAssetsDirNames.IMGS) 112 | ) 113 | 114 | 115 | class TestSetupDirPaths: 116 | def __validate_path(self, directory_path: str, ending: str) -> None: 117 | assert directory_path.endswith(ending) 118 | 119 | def test_root_valid(self) -> None: 120 | self.__validate_path( 121 | SetupDirPaths.ROOT, 122 | SETUP_ROOT 123 | ) 124 | 125 | def test_setup_root_valid(self) -> None: 126 | self.__validate_path( 127 | SetupDirPaths.SETUP_ROOT, 128 | SETUP_PROJECT_ROOT 129 | ) 130 | 131 | def test_assets_valid(self) -> None: 132 | self.__validate_path( 133 | SetupDirPaths.ASSETS, 134 | os.path.join(SETUP_PROJECT_ROOT, SetupAssetsDirNames.ROOT) 135 | ) 136 | 137 | def test_project_name_valid(self) -> None: 138 | self.__validate_path( 139 | SetupDirPaths.PROJECT_NAME, 140 | os.path.join(SETUP_PROJECT_ROOT, 'conf', 'name') 141 | ) 142 | -------------------------------------------------------------------------------- /fastapi_quickstart/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Achronus/fastapi-quickstart/38fb44bbf8cb2f2c282c40f4534b2cf4e03d2005/fastapi_quickstart/utils/__init__.py -------------------------------------------------------------------------------- /fastapi_quickstart/utils/helper.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def strip_whitespace_and_dashes(name: str) -> str: 4 | """Replaces whitespace and dashes with '_' for a given `name` and returns the updated version.""" 5 | name_split = [] 6 | 7 | if '-' in name: 8 | name_split = name.split('-') 9 | elif ' ' in name: 10 | name_split = name.split(' ') 11 | 12 | if len(name_split) != 0: 13 | name = '_'.join(name_split) 14 | 15 | return name.strip() 16 | -------------------------------------------------------------------------------- /fastapi_quickstart/utils/printables.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ..conf.constants import PASS, FAIL, PARTY 4 | 5 | from rich.table import Table 6 | from rich.panel import Panel 7 | 8 | 9 | def project_table(name: str, path: str) -> Table: 10 | """Creates a printable project table showing whether it exists based on a `name` and `path`.""" 11 | colour, icon = ('green', PASS) if os.path.exists(path) else ('red', FAIL) 12 | 13 | table = Table() 14 | table.add_column("Project", style="cyan", justify="center") 15 | table.add_column("Exists", style=colour, justify="center") 16 | table.add_row(name, icon) 17 | return table 18 | 19 | 20 | def project_complete_panel() -> Panel: 21 | """Creates a printable project complete panel.""" 22 | panel = Panel.fit(f"\n{PARTY} Project created successfully! {PARTY}", height=5, border_style="bright_green", style="bright_green") 23 | return panel 24 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "click" 5 | version = "8.1.7" 6 | description = "Composable command line interface toolkit" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 11 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 12 | ] 13 | 14 | [package.dependencies] 15 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 16 | 17 | [[package]] 18 | name = "colorama" 19 | version = "0.4.6" 20 | description = "Cross-platform colored terminal text." 21 | optional = false 22 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 23 | files = [ 24 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 25 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 26 | ] 27 | 28 | [[package]] 29 | name = "iniconfig" 30 | version = "2.0.0" 31 | description = "brain-dead simple config-ini parsing" 32 | optional = false 33 | python-versions = ">=3.7" 34 | files = [ 35 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 36 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 37 | ] 38 | 39 | [[package]] 40 | name = "markdown-it-py" 41 | version = "3.0.0" 42 | description = "Python port of markdown-it. Markdown parsing, done right!" 43 | optional = false 44 | python-versions = ">=3.8" 45 | files = [ 46 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 47 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 48 | ] 49 | 50 | [package.dependencies] 51 | mdurl = ">=0.1,<1.0" 52 | 53 | [package.extras] 54 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 55 | code-style = ["pre-commit (>=3.0,<4.0)"] 56 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 57 | linkify = ["linkify-it-py (>=1,<3)"] 58 | plugins = ["mdit-py-plugins"] 59 | profiling = ["gprof2dot"] 60 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 61 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 62 | 63 | [[package]] 64 | name = "mdurl" 65 | version = "0.1.2" 66 | description = "Markdown URL utilities" 67 | optional = false 68 | python-versions = ">=3.7" 69 | files = [ 70 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 71 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 72 | ] 73 | 74 | [[package]] 75 | name = "packaging" 76 | version = "23.2" 77 | description = "Core utilities for Python packages" 78 | optional = false 79 | python-versions = ">=3.7" 80 | files = [ 81 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 82 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 83 | ] 84 | 85 | [[package]] 86 | name = "pluggy" 87 | version = "1.3.0" 88 | description = "plugin and hook calling mechanisms for python" 89 | optional = false 90 | python-versions = ">=3.8" 91 | files = [ 92 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, 93 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, 94 | ] 95 | 96 | [package.extras] 97 | dev = ["pre-commit", "tox"] 98 | testing = ["pytest", "pytest-benchmark"] 99 | 100 | [[package]] 101 | name = "pygments" 102 | version = "2.17.2" 103 | description = "Pygments is a syntax highlighting package written in Python." 104 | optional = false 105 | python-versions = ">=3.7" 106 | files = [ 107 | {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, 108 | {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, 109 | ] 110 | 111 | [package.extras] 112 | plugins = ["importlib-metadata"] 113 | windows-terminal = ["colorama (>=0.4.6)"] 114 | 115 | [[package]] 116 | name = "pytest" 117 | version = "7.4.4" 118 | description = "pytest: simple powerful testing with Python" 119 | optional = false 120 | python-versions = ">=3.7" 121 | files = [ 122 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 123 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 124 | ] 125 | 126 | [package.dependencies] 127 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 128 | iniconfig = "*" 129 | packaging = "*" 130 | pluggy = ">=0.12,<2.0" 131 | 132 | [package.extras] 133 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 134 | 135 | [[package]] 136 | name = "rich" 137 | version = "13.7.0" 138 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 139 | optional = false 140 | python-versions = ">=3.7.0" 141 | files = [ 142 | {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, 143 | {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, 144 | ] 145 | 146 | [package.dependencies] 147 | markdown-it-py = ">=2.2.0" 148 | pygments = ">=2.13.0,<3.0.0" 149 | 150 | [package.extras] 151 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 152 | 153 | [[package]] 154 | name = "shellingham" 155 | version = "1.5.4" 156 | description = "Tool to Detect Surrounding Shell" 157 | optional = false 158 | python-versions = ">=3.7" 159 | files = [ 160 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 161 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 162 | ] 163 | 164 | [[package]] 165 | name = "typer" 166 | version = "0.9.0" 167 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 168 | optional = false 169 | python-versions = ">=3.6" 170 | files = [ 171 | {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, 172 | {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, 173 | ] 174 | 175 | [package.dependencies] 176 | click = ">=7.1.1,<9.0.0" 177 | colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} 178 | rich = {version = ">=10.11.0,<14.0.0", optional = true, markers = "extra == \"all\""} 179 | shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} 180 | typing-extensions = ">=3.7.4.3" 181 | 182 | [package.extras] 183 | all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 184 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] 185 | doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] 186 | test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 187 | 188 | [[package]] 189 | name = "typing-extensions" 190 | version = "4.9.0" 191 | description = "Backported and Experimental Type Hints for Python 3.8+" 192 | optional = false 193 | python-versions = ">=3.8" 194 | files = [ 195 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 196 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 197 | ] 198 | 199 | [metadata] 200 | lock-version = "2.0" 201 | python-versions = "^3.12" 202 | content-hash = "5d3883161d7e78dbe7b8f3722838c2388914f7ad588f09d6c17c5530ebbf34a4" 203 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-quickstart" 3 | version = "1.0.0" 4 | description = "A quickstart tool for creating a FastAPI project with Jinja2, TailwindCSS, Flowbite, HTMX, and AlpineJS." 5 | authors = ["Ryan Partridge "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.scripts] 9 | create = "fastapi_quickstart.main:app" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.12" 13 | typer = {extras = ["all"], version = "^0.9.0"} 14 | pytest = "^7.4.4" 15 | 16 | 17 | [build-system] 18 | requires = ["poetry-core"] 19 | build-backend = "poetry.core.masonry.api" 20 | --------------------------------------------------------------------------------