diff --git a/ROADMAP.md b/ROADMAP.md index 8247558..1c7e89a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,18 +7,23 @@ The package plans are here. If you want to contribute with new ideas, or develop ## Checklist +### Must - [X] License support on `startproject`. - [X] Docker/Docker-compose support on `startproject`. -- [ ] VSCode debugger support on `startproject` (available via docker). - [X] Add basic linter tools on `startproject` (flake8, mypy and isort). - [X] Add `.pre-commit-config.yaml` on `startproject`. +- [ ] Integrate databases on `startproject`. +- [ ] Different Authentication support on `startproject`. +- [ ] Add tests. + +### Nice to have +- [ ] VSCode debugger support on `startproject` (available via docker). - [ ] Support different CI on `startproject`. - [ ] Add support for `hypercorn` on `run`. -- [ ] Add tests. -- [ ] Integrate databases on `startproject`. - [ ] Create `migrations`/`migrate` command. -- [ ] Different Authentication support on `startproject`. - [ ] Configuration file support: being able to run `fastapi startproject --config-file myconfig`. +- [ ] Add `logger` to `startproject` structure. +- [ ] Base CRUD class to `startproject`. ## Questions diff --git a/haha/.gitignore b/haha/.gitignore deleted file mode 100644 index bad5d49..0000000 --- a/haha/.gitignore +++ /dev/null @@ -1,141 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# Text Editor -.vscode diff --git a/haha/.pre-commit-config.yaml b/haha/.pre-commit-config.yaml deleted file mode 100644 index d4e5154..0000000 --- a/haha/.pre-commit-config.yaml +++ /dev/null @@ -1,36 +0,0 @@ -repos: - - repo: https://github.com/myint/autoflake - rev: v1.4 - hooks: - - id: autoflake - exclude: .*/__init__.py - args: - - --in-place - - --remove-all-unused-imports - - --expand-star-imports - - --remove-duplicate-keys - - --remove-unused-variables - - repo: local - hooks: - - id: flake8 - name: flake8 - entry: flake8 - language: system - types: [python] - - repo: https://github.com/pre-commit/mirrors-isort - rev: v5.4.2 - hooks: - - id: isort - args: ["--profile", "black"] - - repo: local - hooks: - - id: mypy - name: mypy - entry: mypy - language: system - types: [python] - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer diff --git a/haha/Dockerfile b/haha/Dockerfile deleted file mode 100644 index 74ee421..0000000 --- a/haha/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 - -ENV PYTHONPATH "${PYTHONPATH}:/" -ENV PORT=8000 - -RUN pip install --upgrade pip - -COPY ./requirements.txt /app/ - -RUN pip install -r requirements.txt - -COPY ./app /app diff --git a/haha/LICENSE b/haha/LICENSE deleted file mode 100644 index cb4e883..0000000 --- a/haha/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Marcelo Trylesinski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/haha/README.md b/haha/README.md deleted file mode 100644 index 06d7295..0000000 --- a/haha/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# haha - -This project was generated via [manage-fastapi](https://ycd.github.io/manage-fastapi/)! :tada: - -## License - -This project is licensed under the terms of the MIT license. diff --git a/haha/app/__init__.py b/haha/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/haha/app/core/__init__.py b/haha/app/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/haha/app/core/config.py b/haha/app/core/config.py deleted file mode 100644 index f3c4df5..0000000 --- a/haha/app/core/config.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import List, Union - -from pydantic import AnyHttpUrl, BaseSettings, validator - - -class Settings(BaseSettings): - PROJECT_NAME: str - BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] - - @validator("BACKEND_CORS_ORIGINS", pre=True) - def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: - if isinstance(v, str) and not v.startswith("["): - return [i.strip() for i in v.split(",")] - elif isinstance(v, (list, str)): - return v - raise ValueError(v) - - class Config: - case_sensitive = True - env_file = ".env" - - -settings = Settings() diff --git a/haha/app/main.py b/haha/app/main.py deleted file mode 100644 index 361e75d..0000000 --- a/haha/app/main.py +++ /dev/null @@ -1,21 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -from app.core.config import settings - - -def get_application(): - _app = FastAPI(title=settings.PROJECT_NAME) - - _app.add_middleware( - CORSMiddleware, - allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - - return _app - - -app = get_application() diff --git a/haha/docker-compose.yaml b/haha/docker-compose.yaml deleted file mode 100644 index cb32fdc..0000000 --- a/haha/docker-compose.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "3.8" - -services: - app: - build: . - env_file: ".env" - ports: - - "8000:8000" diff --git a/haha/requirements.txt b/haha/requirements.txt deleted file mode 100644 index 59db9ea..0000000 --- a/haha/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi==0.61.2 -uvicorn==0.12.2 diff --git a/haha/setup.cfg b/haha/setup.cfg deleted file mode 100644 index a732dcf..0000000 --- a/haha/setup.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[isort] -profile = black -known_first_party = app - -[flake8] -max-complexity = 7 -statistics = True -max-line-length = 88 -ignore = W503,E203 -per-file-ignores = - __init__.py: F401 - -[mypy] -plugins = pydantic.mypy -ignore_missing_imports = True -follow_imports = skip -strict_optional = True diff --git a/haha/tests/__init__.py b/haha/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/manage_fastapi/constants.py b/manage_fastapi/constants.py index 0fb1877..a3b9634 100644 --- a/manage_fastapi/constants.py +++ b/manage_fastapi/constants.py @@ -34,3 +34,8 @@ class License(BaseEnum): BSD = "BSD-3" GNU = "GNU GPL v3.0" APACHE = "Apache Software License 2.0" + + +class Database(BaseEnum): + POSTGRES = "Postgres" + NONE = "None" diff --git a/manage_fastapi/main.py b/manage_fastapi/main.py index a20b84c..30338b5 100644 --- a/manage_fastapi/main.py +++ b/manage_fastapi/main.py @@ -3,7 +3,7 @@ import subprocess import typer -from manage_fastapi.constants import License, PackageManager, PythonVersion +from manage_fastapi.constants import Database, License, PackageManager, PythonVersion from manage_fastapi.generator import generate_project from manage_fastapi.helpers import bullet, launch_cli, yes_no from manage_fastapi.schemas import Context @@ -21,6 +21,7 @@ def startproject(name: str, default: bool = typer.Option(False)): license=License.MIT, pre_commit=True, docker=True, + database=Database.NONE, ) else: result = launch_cli( @@ -29,6 +30,7 @@ def startproject(name: str, default: bool = typer.Option(False)): ("license", bullet(License)), ("pre_commit", yes_no("pre commit")), ("docker", yes_no("docker")), + ("database", bullet(Database)), ) context = Context(name=name, **result) generate_project(context) diff --git a/manage_fastapi/schemas.py b/manage_fastapi/schemas.py index 9e5e8c0..1c3514e 100644 --- a/manage_fastapi/schemas.py +++ b/manage_fastapi/schemas.py @@ -5,7 +5,7 @@ from typing import Optional from pydantic import BaseModel, EmailStr, root_validator from manage_fastapi.config import FASTAPI_VERSION -from manage_fastapi.constants import License, PackageManager, PythonVersion +from manage_fastapi.constants import Database, License, PackageManager, PythonVersion class Context(BaseModel): @@ -25,6 +25,8 @@ class Context(BaseModel): pre_commit: bool docker: bool + database: Database + @root_validator(pre=True) def git_info(cls, values: dict): try: diff --git a/manage_fastapi/templates/project/cookiecutter.json b/manage_fastapi/templates/project/cookiecutter.json index ed51721..2911aad 100644 --- a/manage_fastapi/templates/project/cookiecutter.json +++ b/manage_fastapi/templates/project/cookiecutter.json @@ -1,4 +1,5 @@ { + "database": "{{ cookiecutter.database }}", "docker": "{{ cookiecutter.docker }}", "email": "{{ cookiecutter.email }}", "env": ".env", diff --git a/manage_fastapi/templates/project/hooks/post_gen_project.py b/manage_fastapi/templates/project/hooks/post_gen_project.py index 7e05f03..253b3b6 100644 --- a/manage_fastapi/templates/project/hooks/post_gen_project.py +++ b/manage_fastapi/templates/project/hooks/post_gen_project.py @@ -1,6 +1,6 @@ import os -from manage_fastapi.constants import PackageManager +from manage_fastapi.constants import Database, PackageManager def remove_paths(paths: list): @@ -39,6 +39,16 @@ def set_docker(): remove_paths(["Dockerfile", "docker-compose.yaml"]) +def set_database(): + database = "{{ cookiecutter.database }}" + paths = [] + + if database == Database.NONE: + paths = ["app/database.py"] + + remove_paths(paths) + + def main(): set_packaging() set_pre_commit() diff --git a/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/app/core/config.py b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/app/core/config.py index f3c4df5..6c2f4b8 100644 --- a/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/app/core/config.py +++ b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/app/core/config.py @@ -1,6 +1,12 @@ +{% if cookiecutter.database == "Postgres" %} +from typing import Any, Dict, List, Optional, Union + +from pydantic import AnyHttpUrl, BaseSettings, validator, PostgresDsn +{% else %} from typing import List, Union from pydantic import AnyHttpUrl, BaseSettings, validator +{% endif %} class Settings(BaseSettings): @@ -15,9 +21,30 @@ class Settings(BaseSettings): return v raise ValueError(v) + {% if cookiecutter.database == "Postgres" %} + POSTGRES_SERVER: str + POSTGRES_USER: str + POSTGRES_PASSWORD: str + POSTGRES_DB: str + DATABASE_URI: Optional[PostgresDsn] = None + + @validator("DATABASE_URI", pre=True) + def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: + if isinstance(v, str): + return v + return PostgresDsn.build( + scheme="postgresql", + user=values.get("POSTGRES_USER"), + password=values.get("POSTGRES_PASSWORD"), + host=values.get("POSTGRES_SERVER"), + path=f"/{values.get('POSTGRES_DB') or ''}", + ) + {% endif %} + class Config: case_sensitive = True env_file = ".env" + settings = Settings() diff --git a/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/app/database.py b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/app/database.py new file mode 100644 index 0000000..27fa327 --- /dev/null +++ b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/app/database.py @@ -0,0 +1,16 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import as_declarative, declared_attr +from sqlalchemy.orm import sessionmaker + +from app.core.config import settings + +engine = create_engine(settings.DATABASE_URI, pool_pre_ping=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +@as_declarative() +class Base: + + @declared_attr + def __tablename__(cls) -> str: + return cls.__name__.lower() diff --git a/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/docker-compose.yaml b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/docker-compose.yaml index cb32fdc..1a0f250 100644 --- a/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/docker-compose.yaml +++ b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/docker-compose.yaml @@ -3,6 +3,16 @@ version: "3.8" services: app: build: . - env_file: ".env" + env_file: + - .env ports: - "8000:8000" + +{% if cookiecutter.database == "Postgres" %} + database: + image: postgres:12 + env_file: + - .env + ports: + - "5432:5432" +{% endif %} diff --git a/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/{{ cookiecutter.env }} b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/{{ cookiecutter.env }} index 040534d..062dcfc 100644 --- a/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/{{ cookiecutter.env }} +++ b/manage_fastapi/templates/project/{{ cookiecutter.folder_name }}/{{ cookiecutter.env }} @@ -1,2 +1,9 @@ PROJECT_NAME={{ cookiecutter.name }} BACKEND_CORS_ORIGINS=["http://localhost:8000", "https://localhost:8000", "http://localhost", "https://localhost"] + +{% if cookiecutter.database == "Postgres" %} +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_SERVER=database +POSTGRES_DB=app +{% endif %} diff --git a/tests/test_startproject.py b/tests/test_startproject.py index 8d515be..c8fe14c 100644 --- a/tests/test_startproject.py +++ b/tests/test_startproject.py @@ -18,6 +18,7 @@ ALREADY_EXISTS = "Folder 'potato' already exists. 😞\n" ) @pytest.mark.parametrize("pre_commit", [True, False]) @pytest.mark.parametrize("docker", [True, False]) +@pytest.mark.parametrize("database", ["Postgres", "None"]) def test_startproject( project_name: str, packaging: str, @@ -25,6 +26,7 @@ def test_startproject( license_: str, pre_commit: bool, docker: bool, + database: str, ): package = "manage_fastapi.main.launch_cli" with patch(package) as mock_obj: @@ -36,6 +38,7 @@ def test_startproject( "license": license_, "pre_commit": pre_commit, "docker": docker, + "database": database, } mock_obj.side_effect = side_effect