├── .dockerignore ├── .env.sample ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── Secrets Sync Action.yml ├── dependabot.yml ├── img │ └── header.svg └── workflows │ └── docker-image.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── api.py ├── deps.py └── routers │ ├── accounts │ ├── login.py │ └── users.py │ └── recipes │ ├── categories.py │ └── recipes.py ├── core ├── config.py └── security.py ├── crud ├── __init__.py ├── base.py ├── crud_categories.py ├── crud_recipes.py └── crud_users.py ├── database ├── Dockerfile ├── base.py ├── base_class.py ├── init_db.py └── session.py ├── docker-compose.yaml ├── main.py ├── models ├── __init__.py ├── recipes.py └── users.py ├── requirements.txt └── schema ├── __init__.py ├── msg.py ├── recipes.py ├── token.py └── users.py /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | .Python 6 | env 7 | pip-log.txt 8 | pip-delete-this-directory.txt 9 | .tox 10 | .coverage 11 | .coverage.* 12 | .cache 13 | nosetests.xml 14 | coverage.xml 15 | *,cover 16 | *.log 17 | .git 18 | .mypy_cache 19 | .pytest_cache 20 | .hypothesis 21 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | POSTGRES_SERVER = 2 | POSTGRES_USER = 3 | POSTGRES_PASSWORD = 4 | POSTGRES_DB = -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: paypal.me/yassertahiri 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. Ubuntu] 29 | - Browser [e.g. chrome, Firefox] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/Secrets Sync Action.yml: -------------------------------------------------------------------------------- 1 | - name: Secrets Sync Action 2 | uses: jpoehnelt/secrets-sync-action@v1.4.1 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/img/header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 52 | 53 | 54 | CHEFAPI 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Build the Docker image 18 | run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) 19 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt /app/requirements.txt 6 | COPY api /app/api 7 | COPY core /app/core 8 | COPY crud /app/crud 9 | COPY database /app/database 10 | COPY models /app/models 11 | COPY schema /app/schema 12 | COPY main.py /app/main.py 13 | 14 | RUN python3 -m pip install -r requirements.txt 15 | 16 | EXPOSE 80 17 | 18 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yasser Tahiri 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ChefAPI](.github/img/header.svg) 2 | 3 |

4 | 5 | Docker 6 | 7 | Docker Image CI 8 |

9 | 10 | # ChefAPI 11 | 12 | API using FastAPI and PostgreSQL to create and share or keeping track of awesome food recipes. Our API have aslo a Crud System Using JWT and Oauth2 to Create a Complete API that Can Be Used with a High Quality Frontend Project. ⛏ 13 | 14 | ## Getting Started 15 | 16 | - To start using ChefAPI You need some experience in Cuisine maybe how to create a Moroccan `CousCous` or `Tajine`. 17 | 18 | ### Prerequisites 19 | 20 | - Python 3.8.6 or higher 21 | - PostgreSQL 22 | - FastAPI 23 | - Docker 24 | 25 | ### Project setup 26 | 27 | ```sh 28 | # clone the repo 29 | $ git clone https://github.com/GDGSNF/ChefAPI 30 | 31 | # move to the project folder 32 | $ cd ChefAPI 33 | ``` 34 | 35 | ### Creating virtual environment 36 | 37 | - Install `pipenv` a global python project `pip install pipenv` 38 | - Create a `virtual environment` for this project 39 | 40 | ```shell 41 | # creating pipenv environment for python 3 42 | $ pipenv --three 43 | 44 | # activating the pipenv environment 45 | $ pipenv shell 46 | 47 | # if you have multiple python 3 versions installed then 48 | $ pipenv install -d --python 3.8 49 | 50 | # install all dependencies (include -d for installing dev dependencies) 51 | $ pipenv install -d 52 | ``` 53 | 54 | ### Configured Enviromment 55 | 56 | #### Database 57 | 58 | - Using SQLAlchemy to Connect to our PostgreSQL Database 59 | - Containerization The Database. 60 | - Drop your PostgreSQL Configuration at the `.env.sample` and Don't Forget to change the Name to `.env` 61 | 62 | ```conf 63 | # example of Configuration for the .env file 64 | 65 | POSTGRES_SERVER = localhost 66 | POSTGRES_USER = root 67 | POSTGRES_PASSWORD = password 68 | POSTGRES_DB = ChefAPI 69 | ``` 70 | 71 | ### Running the Application 72 | 73 | - To run the [Main](main.py) we need to use [uvicorn](https://www.uvicorn.org/) a lightning-fast ASGI server implementation, using uvloop and httptools. 74 | 75 | ```sh 76 | # Running the application using uvicorn 77 | $ uvicorn main:app 78 | 79 | # To run the Application under a reload enviromment use -- reload 80 | $ uvicorn main:app --reload 81 | ``` 82 | 83 | ## Running the Docker Container 84 | 85 | - We have the Dockerfile created in above section. Now, we will use the Dockerfile to create the image of the FastAPI app and then start the FastAPI app container. 86 | 87 | ```sh 88 | $ docker build 89 | ``` 90 | 91 | - list all the docker images and you can also see the image `chefapi:latest` in the list. 92 | 93 | ```sh 94 | $ docker images 95 | ``` 96 | 97 | - run the application at port 5000. The various options used are: 98 | 99 | > - `-p`: publish the container's port to the host port. 100 | > - `-d`: run the container in the background. 101 | > - `-i`: run the container in interactive mode. 102 | > - `-t`: to allocate pseudo-TTY. 103 | > - `--name`: name of the container 104 | 105 | ```sh 106 | $ docker container run -p 5000:5000 -dit --name ChefAPI chefapi:latest 107 | ``` 108 | 109 | - Check the status of the docker container 110 | 111 | ```sh 112 | $ docker container ps 113 | ``` 114 | 115 | ## Preconfigured Packages 116 | 117 | Includes preconfigured packages to kick start ChefAPI by just setting appropriate configuration. 118 | 119 | | Package | Usage | 120 | | ------------------------------------------------------------ | ---------------------------------------------------------------- | 121 | | [uvicorn](https://www.uvicorn.org/) | a lightning-fast ASGI server implementation, using uvloop and httptools. | 122 | | [Python-Jose](https://github.com/mpdavis/python-jose) | a JavaScript Object Signing and Encryption implementation in Python. | 123 | | [SQLAlchemy](https://www.sqlalchemy.org/) | is the Python SQL toolkit and Object Relational Mapper that gives application developers the full power and flexibility of SQL. | 124 | | [starlette](https://www.starlette.io/) | a lightweight ASGI framework/toolkit, which is ideal for building high performance asyncio services. | 125 | | [passlib](https://passlib.readthedocs.io/en/stable/) | a password hashing library for Python 2 & 3, which provides cross-platform implementations of over 30 password hashing algorithms | 126 | | [bcrypt](https://github.com/pyca/bcrypt/) | Good password hashing for your software and your servers. | 127 | | [python-multipart](https://github.com/andrew-d/python-multipart) | streaming multipart parser for Python. | 128 | 129 | `yapf` packages for `linting and formatting` 130 | 131 | ## Contributing 132 | 133 | - Join the ChefAPI Creator and Contribute to the Project if you have any enhancement or add-ons to create a good and Secure Project, Help any User to Use it in a good and simple way. 134 | 135 | ## License 136 | 137 | This project is licensed under the terms of the MIT license. -------------------------------------------------------------------------------- /api/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from api.routers.accounts import users, login 4 | from api.routers.recipes import categories, recipes 5 | 6 | api_router = APIRouter() 7 | api_router.include_router(login.router, tags=["login"]) 8 | api_router.include_router(users.router, prefix="/users", tags=["users"]) 9 | api_router.include_router( 10 | categories.router, prefix="/recipes", tags=["categories"]) 11 | api_router.include_router(recipes.router, prefix="/recipes", tags=["recipes"]) 12 | -------------------------------------------------------------------------------- /api/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from fastapi import Depends, HTTPException, status 4 | from fastapi.security import OAuth2PasswordBearer 5 | from jose import jwt 6 | from pydantic import ValidationError 7 | from sqlalchemy.orm import Session 8 | 9 | from . import models, crud, schemas 10 | from core import security 11 | from core.config import settings 12 | from database.session import SessionLocal 13 | 14 | reusable_oauth2 = OAuth2PasswordBearer( 15 | tokenUrl=f"{settings.API_V1_STR}/login/access-token" 16 | ) 17 | 18 | 19 | def get_db() -> Generator: 20 | try: 21 | db = SessionLocal() 22 | yield db 23 | finally: 24 | db.close() 25 | 26 | 27 | def get_current_user( 28 | db: Session = Depends(get_db), token: str = Depends(reusable_oauth2) 29 | ) -> models.User: 30 | try: 31 | payload = jwt.decode( 32 | token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] 33 | ) 34 | token_data = schemas.TokenPayload(**payload) 35 | except (jwt.JWTError, ValidationError): 36 | raise HTTPException( 37 | status_code=status.HTTP_403_FORBIDDEN, 38 | detail="Could not validate credentials", 39 | ) 40 | user = crud.user.get(db, id=token_data.sub) 41 | if not user: 42 | raise HTTPException(status_code=404, detail="User not found") 43 | return user 44 | 45 | 46 | def get_current_active_user( 47 | current_user: models.User = Depends(get_current_user), 48 | ) -> models.User: 49 | if not crud.user.is_active(current_user): 50 | raise HTTPException(status_code=400, detail="Inactive user") 51 | return current_user 52 | 53 | 54 | def get_current_active_superuser( 55 | current_user: models.User = Depends(get_current_user), 56 | ) -> models.User: 57 | if not crud.user.is_superuser(current_user): 58 | raise HTTPException( 59 | status_code=400, detail="User Without enough privileges" 60 | ) 61 | return current_user 62 | -------------------------------------------------------------------------------- /api/routers/accounts/login.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from typing import Any 3 | 4 | 5 | from fastapi import APIRouter, Body, Depends, HTTPException 6 | from fastapi.security import OAuth2PasswordRequestForm 7 | from sqlalchemy.orm import Session 8 | 9 | from . import crud, models, schemas 10 | from api import deps 11 | from core import security 12 | from core.config import settings 13 | from core.security import get_password_hash 14 | 15 | router = APIRouter() 16 | 17 | 18 | @router.post("/login/access-token", response_model=schemas.Token) 19 | def login_access_token( 20 | db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends() 21 | ) -> Any: 22 | """ 23 | OAuth2 compatible token login, get an access token for future requests 24 | """ 25 | user = crud.user.authenticate( 26 | db, email=form_data.username, password=form_data.password 27 | ) 28 | if not user: 29 | raise HTTPException( 30 | status_code=400, detail="Incorrect email or password") 31 | elif not crud.user.is_active(user): 32 | raise HTTPException(status_code=400, detail="Inactive user") 33 | access_token_expires = timedelta( 34 | minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) 35 | return { 36 | "access_token": security.create_access_token( 37 | user.id, expires_delta=access_token_expires 38 | ), 39 | "token_type": "bearer", 40 | } 41 | -------------------------------------------------------------------------------- /api/routers/accounts/users.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from fastapi import APIRouter, Body, Depends, HTTPException, status 4 | from fastapi.encoders import jsonable_encoder 5 | from pydantic.networks import EmailStr 6 | from sqlalchemy.orm import Session 7 | 8 | from . import crud, models, schemas 9 | from api import deps 10 | from core.config import settings 11 | 12 | router = APIRouter() 13 | 14 | 15 | @router.get("/", response_model=List[schemas.User]) 16 | def read_users( 17 | db: Session = Depends(deps.get_db), 18 | skip: int = 0, 19 | limit: int = 100, 20 | current_user: models.User = Depends(deps.get_current_active_superuser), 21 | ) -> Any: 22 | """ 23 | Retrieve users. 24 | """ 25 | users = crud.user.get_multi(db, skip=skip, limit=limit) 26 | return users 27 | 28 | 29 | @router.post("/", response_model=schemas.User, status_code=status.HTTP_201_CREATED) 30 | def create_user( 31 | *, 32 | db: Session = Depends(deps.get_db), 33 | user_in: schemas.UserCreate, 34 | ) -> Any: 35 | """ 36 | Create new user. 37 | """ 38 | user_by_email = crud.user.get_by_email(db, email=user_in.email) 39 | user_by_username = crud.user.get_by_username(db, username=user_in.username) 40 | 41 | if user_by_email or user_by_username: 42 | raise HTTPException( 43 | status_code=400, 44 | detail="The user with this username or email already exists in the system.", 45 | ) 46 | user = crud.user.create(db, obj_in=user_in) 47 | # TODO: Implement sending email when user is created 48 | return user 49 | -------------------------------------------------------------------------------- /api/routers/recipes/categories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | from sqlalchemy.orm import Session 5 | 6 | from . import crud, models, schemas 7 | from api import deps 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.post("/categories", response_model=schemas.Category) 13 | def create_category( 14 | *, 15 | db: Session = Depends(deps.get_db), 16 | category_in: schemas.CategoryCreate, 17 | current_user: models.User = Depends(deps.get_current_active_user) 18 | ) -> Any: 19 | """ 20 | Create new category. 21 | """ 22 | category = crud.category.create_with_owner( 23 | db=db, obj_in=category_in, owner_id=current_user.id) 24 | return category 25 | 26 | 27 | @router.get("/categories", response_model=List[schemas.Category]) 28 | def read_categories( 29 | db: Session = Depends(deps.get_db), 30 | skip: int = 0, 31 | limit: int = 100, 32 | current_user: models.User = Depends(deps.get_current_active_user), 33 | ) -> Any: 34 | """ 35 | Retrieve categories 36 | """ 37 | if crud.user.is_superuser(current_user): 38 | categories = crud.category.get_multi(db=db, skip=skip, limit=limit) 39 | else: 40 | categories = crud.category.get_multi_by_owner( 41 | db=db, owner_id=current_user.id, skip=skip, limit=limit 42 | ) 43 | return categories 44 | 45 | 46 | @router.get("/categories/{id}", response_model=schemas.Category) 47 | def read_category( 48 | *, 49 | db: Session = Depends(deps.get_db), 50 | id: int, 51 | current_user: models.User = Depends(deps.get_current_active_user) 52 | ) -> Any: 53 | """ 54 | Get Category by ID. 55 | """ 56 | category = crud.category.get(db=db, id=id) 57 | if not category: 58 | raise HTTPException(status_code=404, detail="Category not found") 59 | if not crud.user.is_superuser(current_user) and (category.owner_id != current_user.id): 60 | raise HTTPException(status_code=400, detail="Not enough permissions") 61 | return category 62 | 63 | 64 | @router.put("/categories/{id}", response_model=schemas.Category) 65 | def update_category( 66 | *, 67 | db: Session = Depends(deps.get_db), 68 | id: int, 69 | category_in: schemas.CategoryUpdate, 70 | current_user: models.User = Depends(deps.get_current_active_user) 71 | ) -> Any: 72 | """ 73 | Update category 74 | """ 75 | category = crud.category.get(db=db, id=id) 76 | if not category: 77 | raise HTTPException(status_code=404, detail="Category not found") 78 | if not crud.user.is_superuser(current_user) and (category.owner_id != current_user.id): 79 | raise HTTPException(status_code=400, detail="Not enough permissions") 80 | category = crud.category.update(db=db, db_obj=category, obj_in=category_in) 81 | return category 82 | 83 | 84 | @router.delete("/categories/{id}", response_model=schemas.Category) 85 | def delete_category( 86 | *, 87 | db: Session = Depends(deps.get_db), 88 | id: int, 89 | current_user: models.User = Depends(deps.get_current_active_user) 90 | ) -> Any: 91 | """ 92 | Delete a category 93 | """ 94 | category = crud.category.get(db=db, id=id) 95 | if not category: 96 | raise HTTPException(status_code=404, detail="Category not found") 97 | if not crud.user.is_superuser(current_user) and (category.owner_id != current_user.id): 98 | raise HTTPException(status_code=400, detail="Not enough permissions") 99 | category = crud.category.remove(db=db, id=id) 100 | return category 101 | -------------------------------------------------------------------------------- /api/routers/recipes/recipes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from fastapi import APIRouter, Body, Depends, HTTPException 4 | from sqlalchemy.orm import Session 5 | 6 | from . import crud, models, schemas 7 | from api import deps 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.post("/", response_model=schemas.Recipe) 13 | def create_recipe( 14 | *, 15 | db: Session = Depends(deps.get_db), 16 | recipe_in: schemas.RecipeCreate, 17 | current_user: models.User = Depends(deps.get_current_active_user) 18 | ) -> Any: 19 | """ 20 | Create a recipe 21 | """ 22 | ingredients = recipe_in.ingredients 23 | recipe = crud.recipe.create_with_owner( 24 | db=db, obj_in=recipe_in, owner_id=current_user.id 25 | ) 26 | for ing in ingredients: 27 | crud.recipe.add_ingredients(db=db, recipe_id=recipe.id, name=ing) 28 | db.refresh(recipe) 29 | return recipe 30 | -------------------------------------------------------------------------------- /core/config.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Any, Dict, List, Optional, Union 3 | 4 | from pydantic import AnyHttpUrl, BaseSettings, EmailStr, Field, HttpUrl, PostgresDsn, validator 5 | 6 | 7 | class Settings(BaseSettings): 8 | API_V1_STR = "/api/v1" 9 | SECRET_KEY: str = secrets.token_urlsafe(64) 10 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 11 | BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] 12 | PROJECT_NAME: str = "ChefAPI" 13 | 14 | POSTGRES_SERVER: str = Field(..., env="POSTGRES_SERVER") 15 | POSTGRES_USER: str = Field(..., env="POSTGRES_USER") 16 | POSTGRES_PASSWORD: str = Field(..., env="POSTGRES_PASSWORD") 17 | POSTGRES_DB: str = Field(..., env="POSTGRES_DB") 18 | SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None 19 | 20 | @validator("SQLALCHEMY_DATABASE_URI", pre=True) 21 | def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: 22 | if isinstance(v, str): 23 | return v 24 | return PostgresDsn.build( 25 | scheme="postgresql", 26 | user=values.get("POSTGRES_USER"), 27 | password=values.get("POSTGRES_PASSWORD"), 28 | host=values.get("POSTGRES_SERVER"), 29 | path=f"/{values.get('POSTGRES_DB') or ''}", 30 | ) 31 | 32 | class Config: 33 | case_sensitive = True 34 | env_file = '.env' 35 | env_file_encoding = 'utf-8' 36 | 37 | 38 | settings = Settings() 39 | -------------------------------------------------------------------------------- /core/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Any, Union 3 | 4 | from jose import jwt 5 | from passlib.context import CryptContext 6 | 7 | from core.config import settings 8 | 9 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 10 | 11 | ALGORITHM = "HS256" 12 | 13 | 14 | def create_access_token( 15 | subject: Union[str, Any], expires_delta: timedelta = None 16 | ) -> str: 17 | if expires_delta: 18 | expire = datetime.utcnow() + expires_delta 19 | else: 20 | expire = datetime.utcnow() + timedelta( 21 | minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES 22 | ) 23 | to_encode = {"exp": expire, "sub": str(subject)} 24 | encoded_jwt = jwt.encode( 25 | to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) 26 | return encoded_jwt 27 | 28 | 29 | def verify_password(plain_password: str, hashed_password: str) -> bool: 30 | return pwd_context.verify(plain_password, hashed_password) 31 | 32 | 33 | def get_password_hash(password: str) -> str: 34 | return pwd_context.hash(password) 35 | -------------------------------------------------------------------------------- /crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_users import user 2 | from .crud_categories import category 3 | from .crud_recipes import recipe -------------------------------------------------------------------------------- /crud/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union 2 | 3 | from fastapi.encoders import jsonable_encoder 4 | from pydantic import BaseModel 5 | from sqlalchemy.orm import Session 6 | 7 | from database.base_class import Base 8 | 9 | ModelType = TypeVar("ModelType", bound=Base) 10 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 11 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 12 | 13 | 14 | class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 15 | def __init__(self, model: Type[ModelType]): 16 | """ 17 | CRUD object with default methods to Create, Read, Update, Delete (CRUD). 18 | **Parameters** 19 | * `model`: A SQLAlchemy model class 20 | * `schema`: A Pydantic model (schema) class 21 | """ 22 | self.model = model 23 | 24 | def get(self, db: Session, id: Any) -> Optional[ModelType]: 25 | return db.query(self.model).filter(self.model.id == id).first() 26 | 27 | def get_multi( 28 | self, db: Session, *, skip: int = 0, limit: int = 100 29 | ) -> List[ModelType]: 30 | return db.query(self.model).offset(skip).limit(limit).all() 31 | 32 | def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: 33 | obj_in_data = jsonable_encoder(obj_in) 34 | db_obj = self.model(**obj_in_data) 35 | db.add(db_obj) 36 | db.commit() 37 | db.refresh(db_obj) 38 | return db_obj 39 | 40 | def update( 41 | self, 42 | db: Session, 43 | *, 44 | db_obj: ModelType, 45 | obj_in: Union[UpdateSchemaType, Dict[str, Any]], 46 | ) -> ModelType: 47 | obj_data = jsonable_encoder(db_obj) 48 | if isinstance(obj_in, dict): 49 | update_data = obj_in 50 | else: 51 | update_data = obj_in.dict(exclude_unset=True) 52 | 53 | for field in obj_data: 54 | if field in update_data: 55 | setattr(db_obj, field, update_data[field]) 56 | db.add(db_obj) 57 | db.commit() 58 | db.refresh(db_obj) 59 | return db_obj 60 | 61 | def remove(self, db: Session, *, id: int) -> ModelType: 62 | obj = db.query(self.model).get(id) 63 | db.delete(obj) 64 | db.commit() 65 | return obj 66 | -------------------------------------------------------------------------------- /crud/crud_categories.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi.encoders import jsonable_encoder 4 | from sqlalchemy.orm import Session 5 | 6 | from crud.base import CRUDBase 7 | from models.recipes import Category 8 | from schema.recipes import CategoryCreate, CategoryUpdate 9 | 10 | 11 | class CRUDCategory(CRUDBase[Category, CategoryCreate, CategoryUpdate]): 12 | def create_with_owner( 13 | self, db: Session, *, obj_in: CategoryCreate, owner_id: int 14 | ) -> Category: 15 | obj_in_data = jsonable_encoder(obj_in) 16 | db_obj = self.model(**obj_in_data, owner_id=owner_id) 17 | db.add(db_obj) 18 | db.commit() 19 | db.refresh(db_obj) 20 | return db_obj 21 | 22 | def get_multi_by_owner( 23 | self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100 24 | ) -> List[Category]: 25 | return ( 26 | db.query(self.model) 27 | .filter(Category.owner_id == owner_id) 28 | .offset(skip) 29 | .limit(limit) 30 | .all() 31 | ) 32 | 33 | 34 | category = CRUDCategory(Category) 35 | -------------------------------------------------------------------------------- /crud/crud_recipes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from fastapi.encoders import jsonable_encoder 4 | from sqlalchemy.orm import Session 5 | 6 | from crud.base import CRUDBase 7 | from models import Category, Recipe, Ingredient 8 | from schema import RecipeCreate, RecipeUpdate 9 | 10 | 11 | class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): 12 | def create_with_owner( 13 | self, db: Session, *, obj_in: RecipeCreate, owner_id: int 14 | ) -> Recipe: 15 | """ 16 | Add a recipe item by user 17 | """ 18 | obj_in_data = jsonable_encoder(obj_in) 19 | obj_in_data.pop("ingredients") 20 | db_obj = self.model( 21 | **obj_in_data, 22 | owner_id=owner_id 23 | ) 24 | db.add(db_obj) 25 | db.commit() 26 | db.refresh(db_obj) 27 | return db_obj 28 | 29 | def add_ingredients( 30 | self, db: Session, *, recipe_id: int, name: str 31 | ) -> Any: 32 | """ 33 | Add recipe ingredients 34 | """ 35 | ing_obj = Ingredient( 36 | recipe_id=recipe_id, 37 | name=name 38 | ) 39 | db.add(ing_obj) 40 | db.commit() 41 | db.refresh(ing_obj) 42 | return ing_obj 43 | 44 | def get_multi_with_owner( 45 | self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100 46 | ) -> List[Recipe]: 47 | """ 48 | Retrieve owner created recipes 49 | """ 50 | return ( 51 | db.query(self.model) 52 | .filter(Recipe.owner_id == owner_id) 53 | .offset(skip) 54 | .limit(limit) 55 | .all() 56 | ) 57 | 58 | 59 | recipe = CRUDRecipe(Recipe) 60 | -------------------------------------------------------------------------------- /crud/crud_users.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from core.security import get_password_hash, verify_password 6 | from crud.base import CRUDBase 7 | from models.users import User 8 | from schema.users import UserCreate, UserUpdate 9 | 10 | 11 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 12 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 13 | return db.query(User).filter(User.email == email).first() 14 | 15 | def get_by_username(self, db: Session, *, username: str) -> Optional[User]: 16 | return db.query(User).filter(User.username == username).first() 17 | 18 | def create(self, db: Session, *, obj_in: UserCreate) -> User: 19 | db_obj = User( 20 | email=obj_in.email, 21 | username=obj_in.username, 22 | hashed_password=get_password_hash(obj_in.password), 23 | full_name=obj_in.full_name, 24 | is_superuser=obj_in.is_superuser 25 | ) 26 | db.add(db_obj) 27 | db.commit() 28 | db.refresh(db_obj) 29 | return db_obj 30 | 31 | def update( 32 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 33 | ) -> User: 34 | if (obj_in, dict): 35 | update_data = obj_in 36 | else: 37 | update_data = obj_in.dict(exclude_unset=True) 38 | if update_data["password"]: 39 | hashed_password = get_password_hash(update_data["password"]) 40 | del update_data["password"] 41 | update_data["hashed_password"] = hashed_password 42 | return super().update(db, db_obj=db_obj, obj_in=update_data) 43 | 44 | def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]: 45 | user = self.get_by_email(db, email=email) 46 | if not user: 47 | return None 48 | if not verify_password(password, user.hashed_password): 49 | return None 50 | return user 51 | 52 | def is_active(self, user: User) -> bool: 53 | return user.is_active 54 | 55 | def is_superuser(self, user: User) -> bool: 56 | return user.is_superuser 57 | 58 | 59 | user = CRUDUser(User) 60 | -------------------------------------------------------------------------------- /database/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM ubuntu:16.04 3 | 4 | RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 5 | 6 | RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" >/etc/apt/sources.list.d/pgdg.list 7 | 8 | RUN apt-get update && apt-get install -y python-software-properties software-properties-common postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3 9 | 10 | USER postgres 11 | 12 | RUN /etc/init.d/postgresql start && \ 13 | psql --command "CREATE USER docker WITH SUPERUSER PASSWORD 'docker';" && \ 14 | createdb -O docker docker 15 | 16 | RUN echo "host all all 0.0.0.0/0 md5" >>/etc/postgresql/9.3/main/pg_hba.conf 17 | 18 | RUN echo "listen_addresses='*'" >>/etc/postgresql/9.3/main/postgresql.conf 19 | 20 | EXPOSE 5432 21 | 22 | VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"] 23 | 24 | CMD ["/usr/lib/postgresql/9.3/bin/postgres", "-D", "/var/lib/postgresql/9.3/main", "-c", "config_file=/etc/postgresql/9.3/main/postgresql.conf"] 25 | -------------------------------------------------------------------------------- /database/base.py: -------------------------------------------------------------------------------- 1 | from .base_class import Base 2 | from models.recipes import Category, Recipe 3 | from models.users import User 4 | from models.recipes import Category, Recipe, Ingredient 5 | -------------------------------------------------------------------------------- /database/base_class.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | @as_declarative() 7 | class Base: 8 | id: Any 9 | __name__: str 10 | 11 | @declared_attr 12 | def __tablename__(cls) -> str: 13 | return cls.__name__.lower() 14 | -------------------------------------------------------------------------------- /database/init_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from core.config import settings 4 | from database import base 5 | 6 | def init_db(db: Session) -> None: 7 | # Tables should be created with Alembic migrations 8 | # But if you don't want to use migrations, create 9 | # the tables un-commenting the next line 10 | # Base.metadata.create_all(bind=engine) 11 | print("****Initializing Database****") 12 | -------------------------------------------------------------------------------- /database/session.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | from core.config import settings 6 | 7 | engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) 8 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 9 | Mapping.capitalize(id=2) = create_engine() 10 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | db: 5 | image: postgres 6 | volumes: 7 | - ./data/db:/var/lib/postgresql/data 8 | environment: 9 | - POSTGRES_DB=postgres 10 | - POSTGRES_USER=postgres 11 | - POSTGRES_PASSWORD=postgres 12 | web: 13 | build: . 14 | command: python main.py runserver 0.0.0.0:8000 15 | volumes: 16 | - .:/code 17 | ports: 18 | - "8000:8000" 19 | depends_on: 20 | - db 21 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.middleware.cors import CORSMiddleware 3 | 4 | from api.api import api_router 5 | from core.config import settings 6 | 7 | app = FastAPI( 8 | title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json" 9 | ) 10 | 11 | # Set all CORS enabled origins 12 | # TODO: Add origins in settings.BACKEND_CORS_ORIGINS 13 | if settings.BACKEND_CORS_ORIGINS: 14 | app.add_middleware( 15 | CORSMiddleware, 16 | allow_origins=[str(origin) 17 | for origin in settings.BACKEND_CORS_ORIGINS], 18 | allow_credentials=True, 19 | allow_methods=["*"], 20 | allow_headers=["*"], 21 | ) 22 | 23 | 24 | # Include and register api routers/endpoints 25 | app.include_router(api_router, prefix=settings.API_V1_STR) 26 | 27 | # Health url 28 | 29 | 30 | @app.get("/ping", description="ChefAPI recipes,Yummy") 31 | def pong(): 32 | return {"msg": "pong"} 33 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | from .users import User 2 | from .recipes import Category, Recipe, Ingredient -------------------------------------------------------------------------------- /models/recipes.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import TYPE_CHECKING 3 | 4 | from sqlalchemy import (Boolean, 5 | Column, 6 | DateTime, 7 | ForeignKey, 8 | Integer, 9 | String, 10 | Text) 11 | from sqlalchemy.sql import func 12 | from sqlalchemy.orm import relationship 13 | 14 | from database.base_class import Base 15 | 16 | if TYPE_CHECKING: 17 | from .users import User 18 | 19 | 20 | class Category(Base): 21 | id = Column(Integer, primary_key=True, index=True) 22 | name = Column(String, index=True) 23 | description = Column(String, index=True) 24 | owner_id = Column(Integer, ForeignKey("user.id")) 25 | created_at = Column(DateTime, default=datetime.datetime.utcnow) 26 | updated_at = Column(DateTime, onupdate=func.now()) 27 | owner = relationship("User", back_populates="categories") 28 | recipes = relationship("Recipe", back_populates="category") 29 | 30 | 31 | class Recipe(Base): 32 | id = Column(Integer, primary_key=True, index=True) 33 | name = Column(String, index=True) 34 | description = Column(Text, nullable=False) 35 | is_public = Column(Boolean, default=False) 36 | owner_id = Column(Integer, ForeignKey("user.id")) 37 | category_id = Column(Integer, ForeignKey("category.id")) 38 | created_at = Column(DateTime, default=datetime.datetime.utcnow) 39 | updated_at = Column(DateTime, onupdate=func.now()) 40 | ingredients = relationship("Ingredient", back_populates="recipe") 41 | category = relationship("Category", back_populates="recipes") 42 | owner = relationship("User", back_populates="recipes") 43 | 44 | 45 | class Ingredient(Base): 46 | id = Column(Integer, primary_key=True, index=True) 47 | name = Column(String, index=True) 48 | recipe_id = Column(Integer, ForeignKey("recipe.id")) 49 | recipe = relationship("Recipe", back_populates="ingredients") 50 | -------------------------------------------------------------------------------- /models/users.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from sqlalchemy import Boolean, Column, Integer, String 4 | from sqlalchemy.orm import relationship 5 | 6 | from database.base_class import Base 7 | 8 | if TYPE_CHECKING: 9 | from models.recipes import Catgory, Recipe 10 | 11 | 12 | class User(Base): 13 | id = Column(Integer, primary_key=True, index=True) 14 | full_name = Column(String, index=True) 15 | username = Column(String, unique=True, index=True, nullable=False) 16 | email = Column(String, unique=True, index=True, nullable=False) 17 | hashed_password = Column(String, nullable=False) 18 | is_active = Column(Boolean(), default=True) 19 | is_superuser = Column(Boolean(), default=False) 20 | categories = relationship("Category", back_populates="owner") 21 | recipes = relationship("Recipe", back_populates="owner") 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles 2 | aniso8601 3 | async-exit-stack 4 | async-generator 5 | asyncpg 6 | autopep8 7 | bcrypt 8 | certifi 9 | cffi 10 | chardet 11 | click 12 | databases 13 | dnspython 14 | ecdsa 15 | email-validator 16 | fastapi 17 | graphene 18 | graphql-core 19 | graphql-relay 20 | h11 21 | httptools 22 | idna 23 | itsdangerous 24 | Jinja2 25 | MarkupSafe 26 | orjson 27 | passlib 28 | promise 29 | pyasn1 30 | pycodestyle 31 | pycparser 32 | pydantic 33 | python-dotenv 34 | python-jose 35 | python-multipart 36 | PyYAML 37 | requests 38 | rsa 39 | Rx 40 | six 41 | SQLAlchemy 42 | starlette 43 | toml 44 | typing-extensions 45 | ujson 46 | urllib3 47 | uvicorn 48 | uvloop 49 | watchgod 50 | websockets 51 | -------------------------------------------------------------------------------- /schema/__init__.py: -------------------------------------------------------------------------------- 1 | from .users import User, UserCreate, UserInDB, UserUpdate 2 | from .recipes import (Category, 3 | CategoryCreate, 4 | CategoryInDB, 5 | CategoryUpdate, 6 | Recipe, 7 | RecipeCreate, 8 | RecipeInDB, 9 | RecipeUpdate) 10 | from .token import Token, TokenPayload 11 | -------------------------------------------------------------------------------- /schema/msg.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Msg(BaseModel): 5 | msg: str 6 | -------------------------------------------------------------------------------- /schema/recipes.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | # Shared properties 7 | class CategoryBase(BaseModel): 8 | name: Optional[str] = None 9 | description: Optional[str] = None 10 | 11 | 12 | # Properties to receive on category creation 13 | class CategoryCreate(CategoryBase): 14 | name: str 15 | 16 | 17 | # Properties to receive on category update 18 | class CategoryUpdate(CategoryBase): 19 | pass 20 | 21 | 22 | # Properties shared by models stored in DB 23 | class CategoryInDBBase(CategoryBase): 24 | id: int 25 | name: str 26 | owner_id: int 27 | 28 | class Config: 29 | orm_mode = True 30 | 31 | 32 | # Properties to return to client 33 | class Category(CategoryInDBBase): 34 | pass 35 | 36 | 37 | # Properties stored in DB 38 | class CategoryInDB(CategoryInDBBase): 39 | pass 40 | 41 | 42 | # Shared properties 43 | class RecipeBase(BaseModel): 44 | category_id: Optional[int] = None 45 | name: Optional[str] = None 46 | description: Optional[str] = None 47 | ingredients: Optional[list] = None 48 | is_public: Optional[bool] = False 49 | 50 | 51 | # Properties to receive on recipe creation 52 | class RecipeCreate(RecipeBase): 53 | category_id: int 54 | name: str 55 | description: str 56 | ingredients: List[str] 57 | 58 | class Config: 59 | schema_extra = { 60 | "example": { 61 | "category_id": 1, 62 | "name": "Example Ingredient", 63 | "description": "Sample summarized description instruction", 64 | "ingredients": ["150ml ingredient 1", "2 tsp mixed ingredient"], 65 | "is_public": False, 66 | } 67 | } 68 | 69 | 70 | # Properties to receive on recipe update 71 | class RecipeUpdate(RecipeBase): 72 | pass 73 | 74 | 75 | # Properties shared by models in DB 76 | class RecipeInDBBase(RecipeBase): 77 | id: int 78 | name: str 79 | owner_id: int 80 | category_id: int 81 | 82 | class Config: 83 | orm_mode = True 84 | 85 | 86 | # Properties to return to client 87 | class Recipe(RecipeInDBBase): 88 | pass 89 | 90 | 91 | # Properties stored in DB 92 | class RecipeInDB(RecipeInDBBase): 93 | pass 94 | -------------------------------------------------------------------------------- /schema/token.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Token(BaseModel): 7 | access_token: str 8 | token_type: str 9 | 10 | 11 | class TokenPayload(BaseModel): 12 | sub: Optional[int] = None 13 | -------------------------------------------------------------------------------- /schema/users.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | # Shared properties 7 | class UserBase(BaseModel): 8 | email: Optional[EmailStr] = None 9 | is_active: Optional[bool] = None 10 | is_superuser: bool = False 11 | full_name: Optional[str] = None 12 | username: Optional[str] = None 13 | 14 | # Properties to receive via API on creation 15 | 16 | 17 | class UserCreate(UserBase): 18 | email: EmailStr 19 | username: str 20 | password: str 21 | 22 | 23 | # Properties to receive vaia API on update 24 | class UserUpdate(UserBase): 25 | password: Optional[str] = None 26 | 27 | 28 | class UserInDBBase(UserBase): 29 | id: Optional[int] = None 30 | 31 | class Config: 32 | orm_mode = True 33 | 34 | 35 | # Additional properties to return via API 36 | class User(UserInDBBase): 37 | pass 38 | 39 | 40 | # Additional properties stored in DB 41 | class UserInDB(UserInDBBase): 42 | hashed_password: str 43 | --------------------------------------------------------------------------------