Skip to content

Commit

Permalink
Add Docker (DHBW-FN-TIT20#30)
Browse files Browse the repository at this point in the history
* changed main folder

* added gunicorn

* added Dockerfile

* added .env

* now use env for config

* updated from poetry

* updated

* refactored the config for docker

* Updated for Docker

* added

* updated

* delete folder

* some fixes

* added for easy

* added docker-compose

* updated test path

* updated working directory

* updated working directories

* added poetry
  • Loading branch information
Floskinner authored Feb 3, 2022
1 parent 926049d commit 6c5b50c
Show file tree
Hide file tree
Showing 79 changed files with 339 additions and 81 deletions.
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
*.md
app/tests
__pycache__
.env
*example*
.git
.vscode
.venv
*cache*
.github
app/logs
app/essensfindung.db
6 changes: 6 additions & 0 deletions .github/workflows/python-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.9

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -30,12 +31,17 @@ jobs:
- uses: psf/black@stable
with:
options: "--line-length 120"

- name: Lint with flake8
working-directory: ./app
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
working-directory: ./app
run: |
pytest ./tests
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ dmypy.json
# Ignore all Configuration Files
*.conf
!example*.conf
tests/test_db.db
app/tests/test_db.db

# Ignore Logs
*log*
*log*

# Ignore local data
app/data
39 changes: 39 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Base Source https://fastapi.tiangolo.com/deployment/docker
# This is the first stage, to create the requirements.txt
FROM python:3.9 as requirements-stage

WORKDIR /tmp
RUN apt-get update && apt-get install -y \
build-essential \
libssl-dev \
libffi-dev \
python3-dev \
cargo \
&& rm -rf /var/lib/apt/lists/*
RUN pip install poetry
COPY ["./pyproject.toml", "./poetry.lock", "/tmp/"]
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

# Start final stage
FROM python:3.9

# Install Dependencies
COPY --from=requirements-stage /tmp/requirements.txt /tmp/requirements.txt
RUN /usr/local/bin/python -m pip install --upgrade pip && \
pip install --no-cache-dir --upgrade -r /tmp/requirements.txt

# Expose port 80
EXPOSE 80

# Copy the app
RUN ["mkdir", "-p", "/essensfindung/"]
COPY ./app /essensfindung/app
WORKDIR /essensfindung/app


# Setup local DB if needed
RUN ["mkdir", "data"]
VOLUME [ "/essensfindung/app/data" ]

# Start gunicorn server with 2 workers, uvicorn worker type and use the 0.0.0.0 host with port 80
ENTRYPOINT ["gunicorn", "-w", "2", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:80", "main:app"]
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
build: clean_python
docker build -t essensfindung .

clean_python:
find . -type d -name __pycache__ -exec rm -r {} \+
rm -r -f app/logs
rm -f app/essensfindung.db

clean_docker:
docker rm --force essensfindung
docker container prune --force
docker volume prune --force

clean_all: clean_python clean_python clean_docker
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Student project for DHBW-Friedrichshafen to search for restaurant or recipe suggestions.

# Usage
If you want to run it make sure you habe a GoogleAPI KEY<br>
The Application use [place-details](https://developers.google.com/maps/billing-and-pricing/pricing#places-details) and [nearby-search](https://developers.google.com/maps/billing-and-pricing/pricing#nearby-search)<br>
**!!! The GoogleAPI Requests are not for free, so pay attention on the pricing !!!**
## Direct with Python
### Requirements
To run the application install the requirements:
```console
pip install requirements.txt
Expand All @@ -10,8 +15,55 @@ OR
```console
poetry install --no-dev
```
You also need the configuration files in the `configuration` Folder.<br>
Just copy the examples in the Folder and fill in the Data.

### Configuration
If you **dont** have a PostgreSQL Database use `.env-example1` with only the GOOGLE_API. With this Configuration it will create a SQL-Lite DB in the app folder. Not Recommended for Production!
```console
# Copy the Example
cp .env-example1 .env
# Edit the File
nano .env
```

If you have a PostgreSQL Database use `.env-example2`
```console
# Copy the Example
cp .env-example2 .env
# Edit the File
nano .env
```

## Docker
You also can build a docker container
```console
git clone https://github.com/DHBW-FN-TIT20/essensfindung.git
cd essensfindung
docker build essensfindung .
```
If you **dont** have a PostgreSQL Database start the container:
```console
docker run -p 8080:80 \
-v /essensfindung/app/data \
-e GOOGLE_API_KEY=KEY \
--name essensfindung \
essensfindung
```

If you have a PostgreSQL Database:
```console
docker run -p 8080:80 \
-e GOOGLE_API_KEY=KEY \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=PASSWORD \
-e POSTGRES_SERVER=localhost \
-e POSTGRES_DATABASE=essensfindung \
-e POSTGRES_PORT=5432 \
--name essensfindung \
essensfindung
```

## Docker-Compose
See the `docker-compose.yml` File
# Developing
Python-Verison 3.9 or higher<br>
You can use [pyenv](https://github.com/pyenv/pyenv) to manage you virtual envioremnts.
Expand All @@ -31,8 +83,10 @@ pre-commit install
```
If you want to manuall start the pre-commit check run:
```console
pre-commit run --all-files
pre-commit run
```


## Dependencies
To develop on this project we recommend to use [venv](https://docs.python.org/3/library/venv.html).<br>
You can use some extra tools like:
Expand Down
2 changes: 0 additions & 2 deletions api/README.md

This file was deleted.

1 change: 1 addition & 0 deletions app/.env-example1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GOOGLE_API_KEY=KEY
6 changes: 6 additions & 0 deletions app/.env-example2
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
GOOGLE_API_KEY=KEY
POSTGRES_USER=USER!
POSTGRES_PASSWORD=PASSWORD!
POSTGRES_SERVER=DB_SERVER!
POSTGRES_DATABASE=DATABASE!
POSTGRES_PORT=5432
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
27 changes: 27 additions & 0 deletions app/db/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Create the connection to the Database"""
from pathlib import Path
from typing import Generator

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from tools.config import settings

if settings.SQL_LITE:
Path("./data").mkdir(exist_ok=True)
SQLALCHEMY_DATABASE_URL = "sqlite:///./data/essensfindung.db"
# Use connect_args parameter only with sqlite
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
else:
SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}@{settings.POSTGRES_SERVER}:{settings.POSTGRES_PORT}/{settings.POSTGRES_DATABASE}"
engine = create_engine(SQLALCHEMY_DATABASE_URL)


SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_db() -> Generator:
try:
database = SessionLocal()
yield database
finally:
database.close()
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 7 additions & 3 deletions main.py → app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

import fastapi
import uvicorn
from starlette.staticfiles import StaticFiles

from db.base import Base
from db.database import engine
from sqlalchemy import exc
from starlette.staticfiles import StaticFiles
from views import index
from views import restaurant
from views import signin
Expand Down Expand Up @@ -62,7 +62,11 @@ def configure_routing():

def configure_database():
"""Configure connection to the Database"""
Base.metadata.create_all(bind=engine)
try:
Base.metadata.create_all(bind=engine)
except exc.OperationalError:
# Ignore if Tables already exist
pass


if __name__ == "__main__":
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 2 additions & 3 deletions tests/test_gapi.py → app/tests/test_gapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import pytest
from pytest_httpx import HTTPXMock
from pytest_mock import MockerFixture

from schemes import Cuisine
from schemes.scheme_rest import Restaurant
from tools import gapi
Expand Down Expand Up @@ -56,7 +55,7 @@ def test_nearby_search(
httpx_mock.add_response(status_code=status_code, json=fake_nearby_search)

# Mock other functions
mocker.patch("configuration.config.Setting.GOOGLE_API_KEY", "42")
mocker.patch("tools.config.Setting.GOOGLE_API_KEY", "42")

if status_code != 200:
with pytest.raises(httpx.HTTPStatusError):
Expand All @@ -78,7 +77,7 @@ def test_place_details(
httpx_mock.add_response(status_code=200, json=fake_place_details[0], url=url)

# Mock other functions
mocker.patch("configuration.config.Setting.GOOGLE_API_KEY", "42")
mocker.patch("tools.config.Setting.GOOGLE_API_KEY", "42")

restaurant = gapi.place_details(fake_nearby_search_restaurants[0])
assert fake_restaurants[0] == restaurant
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@
from typing import List

import pytest
from pytest_httpx import HTTPXMock
from pytest_mock import MockerFixture
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker

from db.base_class import Base
from db.crud.user import create_user
from pytest_httpx import HTTPXMock
from pytest_mock import MockerFixture
from schemes import Allergies
from schemes import Cuisine
from schemes.scheme_filter import FilterRest
from schemes.scheme_rest import LocationBase
from schemes.scheme_rest import Restaurant
from schemes.scheme_user import UserCreate
from services import service_res
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./tests/test_db.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
Expand Down Expand Up @@ -68,7 +67,7 @@ def test_search_for_restaurant(

# ...googleapi
mocker.patch("tools.gapi.search_restaurant", return_value=google_api_restaurants)
mocker.patch("configuration.config.Setting.GOOGLE_API_KEY", "42")
mocker.patch("tools.config.Setting.GOOGLE_API_KEY", "42")

url = f"https://maps.googleapis.com/maps/api/place/details/json?key=42&place_id={random_res.place_id}"
httpx_mock.add_response(status_code=200, json={"result": random_res.dict()}, url=url)
Expand Down
File renamed without changes.
26 changes: 26 additions & 0 deletions app/tools/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os
from pathlib import Path

from dotenv import load_dotenv

ENV_PATH = Path("./.env")
load_dotenv(dotenv_path=ENV_PATH)


class Setting:
"""Contains all Settings - Loads from the os env"""

GOOGLE_API_KEY: str = os.getenv("GOOGLE_API_KEY")

if os.getenv("POSTGRES_SERVER"):
SQL_LITE: bool = False
POSTGRES_USER: str = os.getenv("POSTGRES_USER")
POSTGRES_PASSWORD: str = os.getenv("POSTGRES_PASSWORD")
POSTGRES_SERVER: str = os.getenv("POSTGRES_SERVER")
POSTGRES_DATABASE: str = os.getenv("POSTGRES_DATABASE")
POSTGRES_PORT: str = os.getenv("POSTGRES_PORT")
else:
SQL_LITE: bool = True


settings = Setting()
3 changes: 1 addition & 2 deletions tools/gapi.py → app/tools/gapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
from typing import List

import httpx

from configuration.config import settings
from schemes.exceptions import GoogleApiException
from schemes.scheme_filter import FilterRest
from schemes.scheme_rest import Restaurant
from tools.config import settings

# TODO: Asynchrone Funktionen
# TODO: Asynchrone API Anfragen
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 0 additions & 10 deletions configuration/__init__.py

This file was deleted.

18 changes: 0 additions & 18 deletions configuration/config.py

This file was deleted.

Loading

0 comments on commit 6c5b50c

Please sign in to comment.