# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['fastapi_plan',
 'fastapi_plan.template.hooks',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.api',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.api.routers',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.core',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.crud',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.models',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.schemas',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.tests',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.tests.api',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.tests.crud',
 'fastapi_plan.template.{{cookiecutter.project_name}}.app.tests.utils']

package_data = \
{'': ['*'],
 'fastapi_plan': ['template/*',
                  'template/{{cookiecutter.project_name}}/*',
                  'template/{{cookiecutter.project_name}}/config/*'],
 'fastapi_plan.template.{{cookiecutter.project_name}}.app': ['migrations/models/*']}

install_requires = \
['cookiecutter>=1.7.2,<2.0.0']

entry_points = \
{'console_scripts': ['fastapi-plan = fastapi_plan:main']}

setup_kwargs = {
    'name': 'fastapi-plan',
    'version': '0.3.1',
    'description': 'Dead simple template manager for FastAPI applications',
    'long_description': '[![GitHub license](https://img.shields.io/github/license/rafsaf/fastapi-plan)](https://github.com/rafsaf/fastapi-plan/blob/master/LICENSE)\n![PyPI](https://img.shields.io/pypi/v/fastapi-plan)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-plan)\n![tests](https://github.com/rafsaf/fastapi-plan/actions/workflows/tests.yml/badge.svg)\n\ndead simple but powerful template manager for FastAPI applications.\n\n- [About](#about)\n- [Quickstart](#quickstart)\n  - [Initialization](#0-initialization)\n  - [Development](#1-development)\n  - [Debug (http)](#2-debug-http)\n  - [Production (https)](#3-production-https-own-domain)\n- [Project structure](#project-structure)\n- [High level overview](#high-level-overview)\n- [How to add new endpoint (step by step)](#how-to-add-new-endpoint)\n\n## About\n\nfeatures:\n- postgresql database with [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/index.html) as ORM\n- well organised, rock solid project structure (see section [Project structure](#project-structure))\n- ready-to-use user model, authentiaction system (JWT), hashing with Bcrypt\n- easy to undarstand config.py with settings (there is only one file for changes: `.env`)\n- out-of-the-box well-tested routes for login and user (register, read, read_me, update etc.)\n- aerich for migrations\n- well-designed tests folder filled with tests for existing user model/user endpoints\n- auto-generated strong passwords for database, secret_key and superuser password\n- poetry or pip\n- deployment ready docker-compose.prod.yml file with poetry, you will only need own domain\n\nfurthermore:\n- full [project structure schema](#project-structure)\n- [high level overview](#high-level-overview) how this project is organised and why, questions like where do the settings live or what every variable in `.env` file \n- step by step explanation [how to add new endpoint](#how-to-add-new-endpoint), from creating new model, adding schemas and routes to migrating database and writting tests (it\'s always better to have it and optionally adopt it, than wasting time trying to figure out the best dev path)\n\n\n## Quickstart\n\nNOTE: you will need [docker](https://www.docker.com/get-started) and optional but recommended [poetry](https://python-poetry.org/docs/) installed!\n\ninstall via pip (or poetry) **globally**:\n\n```bash\npip install fastapi-plan\n```\n\nthere are 3 docker-compose files available, one for development, one for running project via http, and the last one is for production with enabled https using traefic as a proxy (and letsencrypt for ssl), steps to initialize new project are the same for every approach, you can then choose from 1-3.\n\n### 0. INITIALIZATION\n\ninitialize new FastAPI project:\n\n```bash\nfastapi-plan\n```\n\nenter project_name and other information and after project is ready, `cd project_name` and continue installing dependencies:\n\n```bash\npoetry install\n\n# optional if you selected "requirements.txt" (with venv installed)\npip install -r requirements.txt\n```\n\n### 1. DEVELOPMENT\n\n\nsince we wanna use uvicorn in development, create only postgres container using docker-compose.yml file like that:\n\n```bash\ndocker-compose up -d\n```\n\nnow run aerich migrations and configure tortoise (and add first superuser)\n\n```bash\naerich upgrade\npython app/initial_data.py\n```\n\nfinally you can run this command to start uvicorn server\n\n```bash\nuvicorn app.main:app --reload\n```\n\n### 2. DEBUG (http)\n\nTo make it available from http://localhost on your local machine or http://your-host-name on VM just run\n\n```bash\ndocker-compose -f docker-compose.debug.yml up -d\n```\n\nThe diffrence between development approach is that web server automatically runs aerich and initial_data.py using shell script (`app/initial.sh`), so you don\'t have to do anything except changing some lines in `.env` file:\n\n1. `PROJECT_NAME` - it will show up in docs view as a name of project.\n2. `FIRST_SUPER_USER_EMAIL` - first account email\n3. `DEBUG` - when it\'s false, the `POSTGRES_SERVER` is set to `localhost` for development, so change it to `DEBUG=true` to use `db` postgres server.\n\n  \n### 3. PRODUCTION (https, own domain)\n\nTo make it available from https://your_domain.com on VM run\n\n```bash\ndocker-compose -f docker-compose.prod.yml up -d\n```\n\nThe diffrence between development approach is that web server automatically runs aerich and initial_data.py using shell script (`app/initial.sh`), so you don\'t have to do anything except changing some lines in `.env` file:\n\n1. `PROJECT_NAME` - it will show up in docs view as a name of project.\n2. `FIRST_SUPER_USER_EMAIL` - first account email\n3. `DEBUG` - when it\'s false, the `POSTGRES_SERVER` is set to `localhost` for development, so change it to `DEBUG=true` to use `db` postgres server.\n4. `DEFAULT_FROM_EMAIL` - your private email for ssl purposes, e.g. they will inform you shortly after some problems with you certificate.\n5. `MAIN_DOMAIN` - your own domain e.g. `example.com`\n\nPlesae also note that to get no-test certificate, you should comment line `"--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"` in `docker-compose.prod.yml` file, by default you will use test certifactes (to be sure that everything works, there are some hard limits on number of certifiactes you can ask per week!). You should comment line `"--log.level=DEBUG"` also (but it can be useful when debugging traefik). There would probably be problems anyway, just be sure that everything works via http using **2. DEBUG** apropach. If it then doesn\'t with the https, you should refer to traefik docs.\n\n## Project structure\n\n```\n|── app\n|    ├── api                              # endpoints/dependecies\n|    |\n|    ├── core                             # settings and security algorithms\n|    |\n|    ├── crud                             # CRUD operations\n|    |\n|    ├── migrations                       # for aerich migrations\n|    |\n|    ├── models                           # tortoise models\n|    |\n|    ├── schemas                          # pandatic schemas\n|    |\n|    ├── tests                            # tests\n|    |\n|    ├── initial.sh                       # initial shell script used by docker\n|    ├── initial_data.py                  # init database and add first superuser\n|    ├── main.py                          # main fastapi application file\n|\n├── config                                # nginx server config file\n|\n├── .env                                  # .env file with settings\n|\n├── Dockerfile                            # dockerfile for web app\n|\n├── aerich.ini                            # aerich (migrations) configuration\n|\n├── docker-compose.prod.yml               # puts it all together in prod (https)\n|\n├── docker-compose.debug.yml              # puts it all together in debug (http)\n|\n├── docker-compose.yml                    # puts it all together (development)\n|\n├── (optional) pyproject.toml             # python dependencies (poetry)\n|\n├── (optional) poetry.lock                # python dependencies (poetry)\n|\n├── requirements.txt                      # python dependencies (pip)\n```\n\n## High level overview\n\nThis project strucutre is mostly based on the [official template](#https://github.com/tiangolo/full-stack-fastapi-postgresql) (but not only) which is really great but unfortunatly does not support Tortoise ORM and is... (too?) complicated. All the security or problematic stuff (`app/core/security.py` with `verify_password` function, login and token routes, JWT token schemas) are just copied from there, so you can be preety sure it will work as expected.\n\nThe main thougts are:\n\n- There two sorts of settings, first one located in `.env` file for the ENTIRE project, and python-specific settings which lives in `app/core/config.py`, the file is based on pydantic solution (using dotenv lib). Why? Well, that\'s simple, this is due to [12factor methodology](https://12factor.net/), python-specific settings inherit from `.env` file, so this is the only place where you actually change something. If you have any problems understanding mentioned `config.py` file, just refer to [pydantic - settings management](https://pydantic-docs.helpmanual.io/usage/settings/), it\'s preety clear.\n\n- Models, crud, schemas, api routes, tests... it might be confusing how to actually ADD SOMETHING NEW here, but after following next section (learn by doing, step by step), it should be pretty easy\n\n- Database-related stuff is very convinient, taken mostly from [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/index.html) docs and just *working*. There is `register_tortoise` function in `main.py`, `TORTOISE_ORM` variable in `app/core/config.py`. Please, be aware that if you don\'t run `initial_data.py` SOMEHOW (in development- you have to do it yourself, in debug/production it is handled by shell script `initial.sh`, which also runs tests and migrations), you won\'t be able to connect to database. `initial_data.py` is hearbly based on the same named file in **official template** mentioned earlier. It has two responsibilities, first is running `init` function from Tortoise to initialize connection, and the second - creating first superuser (defined in `.env`) if one doesn\'t yet exists.\n\n- Migrations are also provided by Tortiose (the tool is aerich), docs can be found [here in aerich repo](https://github.com/tortoise/aerich). The default migration (default user model) file is already included. After changes in models (e.g. new model `Cars`), just run `aerich migrate`, `aerich upgrade` and you are good to go.\n\n- All tests lives in `tests` folder, with some pytest-specific content included. If you feel unconfortable with pytest, feel free to read articles about using it, and if you just want to see how to test new enpoints/models, just read next section.\n\n## How to add new endpoint\n\nLet\'s imagine we need to create API for a website where users brag about their dogs... or whatever, they just can crud dogs in user panel for some reason. We will add dummy model `Dog` to our API, with relation to the default table `User` and crud auth endpoints, then test it shortly.\n\n1. Create file `dog.py` in `app/models` folder:\n\n```python\nfrom tortoise import fields\nfrom tortoise.models import Model\n\n\nclass Dog(Model):\n    name = fields.CharField(max_length=100)\n    age = fields.IntField(null=True, default=None)\n    breed = fields.CharField(max_length=100, null=True, default=None)\n    owner = fields.ForeignKeyField("models.User", related_name="dogs")\n```\n\n2. Add import in `app/models.__init__.py`:\n\n```python\nfrom .dog import Dog # type: ignore\n```\n\n3. Migrate changes\n\n```bash\naerich migrate\naerich upgrade\n```\n\n4. Create file `dog.py` in `app/schemas` folder (pydantic schemas with typing support):\n\n```python\nfrom typing import Optional\nfrom tortoise import Tortoise\nfrom tortoise.contrib.pydantic.creator import (\n    pydantic_model_creator,\n    pydantic_queryset_creator,\n)\nfrom pydantic import BaseModel\nfrom app.models import Dog\n\n\n# Pydantic models from Tortoise models, pls refer\n# https://tortoise-orm.readthedocs.io/en/latest/examples/pydantic.html#basic-pydantic\n\nTortoise.init_models(["app.models"], "models")\nDogPydantic = pydantic_model_creator(Dog, exclude=("owner",))\nDogPydanticList = pydantic_queryset_creator(Dog, exclude=("owner",))\n\n# Unfortunately, it doesn\'t work the other way around\n\n\nclass DogCreate(BaseModel):\n    name: str\n    age: Optional[int]\n    breed: Optional[str]\n\n\nclass DogUpdate(BaseModel):\n    name: Optional[str]\n    age: Optional[int]\n    breed: Optional[str]\n\n```\n\n5. Add import in `app/schemas.__init__.py`:\n\n```python\nfrom .dog import DogUpdate, DogCreate, DogPydantic, DogPydanticList # type: ignore\n```\n\n6. Create `crud_dog.py` in `app/crud` folder\n\n```python\nfrom app.schemas import DogCreate, DogUpdate\nfrom app.crud.base import CRUDBase\nfrom app.models import Dog, User\n\n\nclass CRUDDog(CRUDBase[Dog, DogCreate, DogUpdate]):\n    def get_dogs_by_user(self, user: User, skip: int = 0, limit: int = 100):\n        return Dog.filter(owner=user).offset(offset=skip).limit(limit=limit)\n\n    async def create_dog_me(self, dog_in: DogCreate, user: User):\n        new_dog = await Dog.create(\n            name=dog_in.name, age=dog_in.age, breed=dog_in.breed, owner=user\n        )\n        return new_dog\n\n    async def get_by_id_and_user(self, dog_id: int, user: User):\n        return await Dog.get(id=dog_id, owner=user)\n\n    async def remove_all_user_dogs(self, user: User):\n        await Dog.filter(owner=user).delete()\n        return\n\n\ndog = CRUDDog(Dog)\n\n```\n\n7. Add import in `app/crud.__init__.py`:\n\n```python\nfrom .crud_dog import dog # type: ignore\n```\n\n\n8. Create `dogs.py` with endpoints in `app/api/routers` folder\n\n```python\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom app import crud, models, schemas\nfrom app.api import deps\nfrom app.models import Dog\n\nrouter = APIRouter()\n\n\n@router.get("/{dog_id}", response_model=schemas.DogPydantic)\nasync def read_dog(\n    dog_id: int,\n):\n    dog = await crud.dog.get(dog_id)\n    if not dog:\n        raise HTTPException(\n            status_code=status.HTTP_404_NOT_FOUND,\n            detail="The dog does not exist in the system",\n        )\n    return await schemas.DogPydantic.from_tortoise_orm(dog)\n\n\n@router.post(\n    "/", response_model=schemas.DogPydantic, status_code=status.HTTP_201_CREATED\n)\nasync def create_dog_me(\n    dog_in: schemas.DogCreate,\n    current_user: models.User = Depends(deps.get_current_active_user),\n):\n    dog: Dog = await crud.dog.create_dog_me(dog_in, current_user)\n    return await schemas.DogPydantic.from_tortoise_orm(dog)\n\n\n@router.put("/", response_model=schemas.DogPydantic)\nasync def update_dog_me(\n    dog_id: int,\n    dog_in: schemas.DogUpdate,\n    current_user: models.User = Depends(deps.get_current_active_user),\n):\n    dog = await crud.dog.get_by_id_and_user(dog_id, current_user)\n    if not dog:\n        raise HTTPException(\n            status_code=status.HTTP_404_NOT_FOUND,\n            detail="The dog does not exist in the system",\n        )\n    new_dog = await crud.dog.update(dog, dog_in)\n    return await schemas.DogPydantic.from_tortoise_orm(new_dog)\n\n\n@router.delete("/", response_model=None, status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dog_me(\n    dog_id: int,\n    current_user: models.User = Depends(deps.get_current_active_user),\n):\n    dog = await crud.dog.get_by_id_and_user(dog_id, current_user)\n    if not dog:\n        raise HTTPException(\n            status_code=status.HTTP_404_NOT_FOUND,\n            detail="The dog does not exist in the system",\n        )\n    await crud.dog.remove(dog_id)\n    return None\n\n\n@router.delete("/all", response_model=None, status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dogs_me(\n    current_user: models.User = Depends(deps.get_current_active_user),\n):\n    await crud.dog.remove_all_user_dogs(current_user)\n    return None\n\n\n@router.get("/all", response_model=schemas.DogPydanticList)\nasync def read_all_dogs_me(\n    skip: int = 0,\n    limit: int = 100,\n    current_user: models.User = Depends(deps.get_current_active_user),\n):\n    dogs = crud.dog.get_dogs_by_user(current_user, skip, limit)\n    return await schemas.DogPydanticList.from_queryset(dogs)\n\n\n@router.get("/all/{user_id}", response_model=schemas.DogPydanticList)\nasync def read_all_dogs(\n    user_id: int,\n    skip: int = 0,\n    limit: int = 100,\n):\n    user = await crud.user.get(user_id)\n    if not user:\n        raise HTTPException(\n            status_code=status.HTTP_404_NOT_FOUND,\n            detail="The user does not exist",\n        )\n    dogs = crud.dog.get_dogs_by_user(user, skip, limit)\n\n    return await schemas.DogPydanticList.from_queryset(dogs)\n\n```\n\n9. Finally add those enpoints to the app with label "dogs", add this line in `app/api/api.py` file:\n\n```python\napi_router.include_router(dogs.router, prefix="/dogs", tags=["dogs"])\n```\n\n10. API endpoints are ready to go, you can play with them at `localhost:8000` by default\n\n11. Now we gonna create tests for crud and endpoints, let\'s first create utils for dog model (we can use it in multiple places in tests then), add `dog.py` in `app/tests/utils` folder:\n\n```python\nfrom asyncio import AbstractEventLoop as EventLoop\nfrom app import models\nimport app.tests.utils.utils as utils\n\n\ndef create_random_dog(user: models.User, event_loop: EventLoop) -> models.Dog:\n    name = utils.random_lower_string()\n    breed = utils.random_lower_string()\n    age = utils.random_integer_below_100()\n    dog: models.Dog = event_loop.run_until_complete(\n        models.Dog.create(name=name, breed=breed, age=age, owner=user)\n    )\n    return dog\n```\n\n12. Then add `test_dog.py` in `app/tests/crud` folder:\n\n```python\nimport pytest\nfrom asyncio import AbstractEventLoop as EventLoop\nfrom typing import List\nfrom app import crud, models, schemas\nfrom app.tests.utils.utils import (\n    random_lower_string,\n    random_integer_below_100,\n)\nfrom app.tests.utils.dog import create_random_dog\n\n\n@pytest.fixture(autouse=True)\ndef drop_dogs(event_loop: EventLoop) -> None:\n    yield\n    event_loop.run_until_complete(models.Dog.all().delete())\n\n\ndef test_get_dogs_by_user(event_loop: EventLoop, normal_user: models.User):\n\n    dog0 = create_random_dog(normal_user, event_loop)\n    dog1 = create_random_dog(normal_user, event_loop)\n    dog_lst: List[models.Dog] = list(\n        event_loop.run_until_complete(crud.dog.get_dogs_by_user(normal_user))\n    )\n\n    assert len(dog_lst) == 2\n    assert dog_lst[0].name == dog0.name\n    assert dog_lst[1].name == dog1.name\n    assert dog_lst[0].age == dog0.age\n    assert dog_lst[1].age == dog1.age\n    assert dog_lst[0].breed == dog0.breed\n    assert dog_lst[1].breed == dog1.breed\n\n\ndef test_create_dog_me(event_loop: EventLoop, normal_user: models.User):\n    name = random_lower_string()\n    breed = random_lower_string()\n    age = random_integer_below_100()\n    dog_in = schemas.DogCreate(name=name, breed=breed, age=age)\n    dog: models.Dog = event_loop.run_until_complete(\n        crud.dog.create_dog_me(dog_in, normal_user)\n    )\n\n    assert dog.name == name\n    assert dog.breed == breed\n    assert dog.age == age\n    assert dog.owner == normal_user\n\n\ndef test_get_dog_by_user(event_loop: EventLoop, normal_user: models.User):\n\n    dog0 = create_random_dog(normal_user, event_loop)\n\n    dog: models.Dog = event_loop.run_until_complete(\n        crud.dog.get_by_id_and_user(dog0.pk, normal_user)\n    )\n    assert dog == dog0\n\n\ndef test_remove_all_user_dogs(event_loop: EventLoop, normal_user: models.User):\n\n    create_random_dog(normal_user, event_loop)\n    create_random_dog(normal_user, event_loop)\n    dog_number0: int = event_loop.run_until_complete(\n        models.Dog.filter(owner=normal_user).count()\n    )\n    assert dog_number0 == 2\n    event_loop.run_until_complete(crud.dog.remove_all_user_dogs(normal_user))\n    dog_number1: int = event_loop.run_until_complete(\n        models.Dog.filter(owner=normal_user).count()\n    )\n    assert dog_number1 == 0\n```\n\n13. And then `test_dogs.py` for endpoints in `app/tests/api` folder\n\n',
    'author': 'rafsaf',
    'author_email': 'rafal.safin12@gmail.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/rafsaf/fastapi-template',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'entry_points': entry_points,
    'python_requires': '>=3.8,<4.0',
}


setup(**setup_kwargs)
