Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI
on:
push:
branches:
- "*"
pull_request:
branches:
- "*"
workflow_dispatch:

jobs:
test:
strategy:
fail-fast: true
matrix:
python-version: ['3.8', '3.10']
os: ['20.04']
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{matrix.python-version}}
- name: Install Poetry
run: pip install --upgrade pip && pip install poetry

- name: Install Project
run: poetry install

- name: Look for style errors
run: poetry run flake8 beerlog

- name: Look for auto format errors
run: poetry run black -l 79 --check --diff beerlog tests

- name: Run tests
run: poetry run pytest -v --junitxml=test-result.xml

- name: publish junit results
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
with:
files: test-result.xml
check_name: Test Result (Python ${{matrix.python-version}})
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extensions.showBuiltin": true,
"redhat.telemetry.enabled": false,
"workbench.colorTheme": "Gruvbox Dark Hard",
"workbench.colorTheme": "★★★★★ NORD DARK ★★★★★",
"editor.fontSize": 18,
"git.autofetch": true,
"python.insidersChannel": "off",
Expand Down
Binary file added beerlog.db
Binary file not shown.
27 changes: 27 additions & 0 deletions beerlog/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Optional, List
from fastapi import FastAPI, Response, status
from beerlog.core import get_beers_from_database
from beerlog.serializers import BeerIn, BeerOut
from beerlog.models import Beer
from beerlog.database import get_session

api = FastAPI(title="beerlog")


@api.get("/beers", response_model=List[BeerOut])
async def list_beers(style: Optional[str] = None):
"""Lists beers from the database"""
beers = get_beers_from_database(style)
return beers


@api.post("/beers", response_model=BeerOut)
async def add_beer(beer_in: BeerIn, response: Response):
beer = Beer(**beer_in.dict())
with get_session() as session:
session.add(beer)
session.commit()
session.refresh(beer)

response.status_code = status.HTTP_201_CREATED
return beer
53 changes: 50 additions & 3 deletions beerlog/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
from .config import settings
import typer
from typing import Optional
from rich.console import Console # NEW
from rich.table import Table # NEW
from rich import print # NEW

from beerlog.core import add_beer_to_database, get_beers_from_database

def main():
print("Hello from", settings.NAME)

main = typer.Typer(help="Beer Management Application")
console = Console()


@main.command()
def add(
name: str,
style: str,
flavor: int = typer.Option(...),
image: int = typer.Option(...),
cost: int = typer.Option(...),
):
"""Adds a new beer to the database"""
if add_beer_to_database(name, style, flavor, image, cost):
print(":beer_mug: Beer added!!!") # NEW
else:
print(":no_entry: - Cannot add beer.") # NEW


@main.command("list")
def list_beers(style: Optional[str] = None):
"""Lists beers from the database"""
beers = get_beers_from_database(style)
table = Table(
title="Beerlog Database" if not style else f"Beerlog {style}"
)
headers = [
"id",
"name",
"style",
"flavor",
"image",
"cost",
"rate",
"date",
]
for header in headers:
table.add_column(header, style="magenta")
for beer in beers:
beer.date = beer.date.strftime("%Y-%m-%d")
values = [str(getattr(beer, header)) for header in headers]
table.add_row(*values)
console.print(table)
35 changes: 35 additions & 0 deletions beerlog/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Optional, List
from sqlmodel import select
from beerlog.database import get_session
from beerlog.models import Beer


def add_beer_to_database(
name: str,
style: str,
flavor: int,
image: int,
cost: int,
) -> bool:
with get_session() as session:
beer = Beer(
name=name,
style=style,
flavor=flavor,
image=image,
cost=cost,
)
beer = Beer(name="Lagunitas", style="IPA", flavor=10, image=10, cost=5)
session.add(beer)
session.commit()
for beer in session.exec(select(Beer).where(Beer.style == "IPA")):
print(beer.name, beer.style, beer.rate)
return True


def get_beers_from_database(style: Optional[str] = None) -> List[Beer]:
with get_session() as session:
sql = select(Beer)
if style:
sql = sql.where(Beer.style == style)
return list(session.exec(sql))
18 changes: 18 additions & 0 deletions beerlog/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import warnings
from sqlalchemy.exc import SAWarning
from sqlmodel.sql.expression import Select, SelectOfScalar
from sqlmodel import create_engine, Session

from beerlog import models
from beerlog.config import settings

warnings.filterwarnings("ignore", category=SAWarning)
SelectOfScalar.inherit_cache = True
Select.inherit_cache = True

engine = create_engine(settings.database.url, echo=False)
models.SQLModel.metadata.create_all(engine)


def get_session():
return Session(engine)
27 changes: 27 additions & 0 deletions beerlog/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from sqlmodel import SQLModel, Field
from datetime import datetime
from typing import Optional
from pydantic import validator
from statistics import mean


class Beer(SQLModel, table=True):
id: Optional[int] = Field(primary_key=True, default=None, index=True)
name: str
style: str
flavor: int
image: int
cost: int
rate: int = 0
date: datetime = Field(default_factory=datetime.now)

@validator("image", "flavor", "cost")
def validate_ratings(cls, v, field):
if v < 1 or v > 10:
raise RuntimeError(f"{field.name} must be between 1 and 10")
return v

@validator("rate", always=True)
def calculate_rate(cls, v, values):
rate = mean([values["flavor"], values["image"], values["cost"]])
return int(rate)
31 changes: 31 additions & 0 deletions beerlog/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pydantic import BaseModel, validator
from datetime import datetime
from fastapi import HTTPException, status


class BeerOut(BaseModel):
id: int
name: str
style: str
flavor: int
image: int
cost: int
rate: int
date: datetime


class BeerIn(BaseModel):
name: str
style: str
flavor: int
image: int
cost: int

@validator("image", "flavor", "cost")
def validate_ratings(cls, v, field):
if v < 1 or v > 10:
raise HTTPException(
detail=f"{field.name} must be between 1 and 10",
status_code=status.HTTP_400_BAD_REQUEST,
)
return v
3 changes: 3 additions & 0 deletions beerlog/settings.toml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
name = "beerlog"

[database]
url = "sqlite:///beerlog.db"
14 changes: 14 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pytest
from unittest.mock import patch
from sqlmodel import create_engine
from beerlog import models


@pytest.fixture(autouse=True, scope="function")
def each_test_uses_separate_database(request):
tmpdir = request.getfixturevalue("tmpdir")
test_db = tmpdir.join("beerlog.test.db")
engine = create_engine(f"sqlite:///{test_db}")
models.SQLModel.metadata.create_all(bind=engine)
with patch("beerlog.database.engine", engine):
yield
Loading