Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CI improvements and Docker #7

Merged
merged 3 commits into from
Jun 30, 2024
Merged
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
20 changes: 0 additions & 20 deletions .github/workflows/lint.yaml

This file was deleted.

15 changes: 0 additions & 15 deletions .github/workflows/test.yaml

This file was deleted.

123 changes: 123 additions & 0 deletions .github/workflows/validation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Validation

on:
workflow_dispatch:
push:
branches:
- 'master'
paths:
- '**.py'
- '.github/workflows/validation.yml'
pull_request:
types: [ opened, synchronize, reopened ]
branches:
- 'master'
paths:
- '**.py'
- '.github/workflows/validation.yml'

env:
POETRY_NO_INTERACTION: 1
POETRY_VIRTUALENVS_IN_PROJECT: 1

concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Cache Poetry install
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: poetry

- name: Install Poetry
run: |
pipx install poetry

- name: Setup Python
id: setup_python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'poetry'
cache-dependency-path: 'poetry.lock'

- name: Cache venv
uses: actions/cache@v4
id: cache-venv
with:
path: ./.venv/
key: ${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-app-${{ hashFiles('./poetry.lock') }}

- name: Install App dependencies
run: |
poetry install --no-interaction --no-root
if: steps.cache-venv.outputs.cache-hit != 'true'

- name: Install App
run: |
poetry install --no-interaction

- name: Ruff format
run: |
poetry run ruff format --check .

- name: Ruff lint
if: success() || failure()
run: |
poetry run ruff check . --output-format github

test:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get install -y poppler-utils tesseract-ocr-ces

- name: Cache Poetry install
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: poetry

- name: Install Poetry
run: |
pipx install poetry

- name: Setup Python
id: setup_python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'poetry'
cache-dependency-path: 'poetry.lock'

- name: Cache venv
uses: actions/cache@v4
id: cache-venv
with:
path: ./.venv/
key: ${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-app-${{ hashFiles('./poetry.lock') }}

- name: Install App dependencies
run: |
poetry install --no-interaction --no-root
if: steps.cache-venv.outputs.cache-hit != 'true'

- name: Install App
run: |
poetry install --no-interaction

- name: Run tests
run: |
poetry run ./lunches.py
44 changes: 30 additions & 14 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.291
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix, --ignore, E501]
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
hooks:
- id: codespell
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-case-conflict
name: Check for files with names that would conflict on a case-insensitive filesystem
entry: check-case-conflict
- id: end-of-file-fixer
name: Makes sure files end in a newline and only a newline.
entry: end-of-file-fixer
types: [text]
- id: trailing-whitespace
name: Trims trailing whitespace.
entry: trailing-whitespace-fixer
types: [text]
- id: check-docstring-first
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff-format
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [ --fix ]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
34 changes: 34 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM python:3.12-slim-bookworm as build-stage

RUN pip install poetry

ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache

WORKDIR /app

COPY pyproject.toml poetry.lock* ./
RUN touch README.md

RUN poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR

FROM python:3.12-slim-bookworm as runtime

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install poppler-utils tesseract-ocr-ces

WORKDIR /app

COPY --from=build-stage /app .

ENV PATH="/app/.venv/bin:$PATH"

COPY templates ./templates

COPY *.py .

EXPOSE 443

CMD ["fastapi", "run", "--port", "443"]
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
## Local setup
Install and start redis server for caching.


```sh
$ pip install pre-commit
$ pre-commit install
Expand All @@ -22,3 +23,9 @@ $ cd frontend
$ yarn install
$ yarn run dev
```

## Production setup

```sh
docker build --target=runtime --tag="lunchmenu" .
```
57 changes: 28 additions & 29 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
#!/usr/bin/env python3
import datetime
import pickle
import ipaddress
import pickle

import redis.asyncio as redis
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
#from werkzeug.middleware.proxy_fix import ProxyFix
#from flask_redis import FlaskRedis

# from werkzeug.middleware.proxy_fix import ProxyFix
# from flask_redis import FlaskRedis
from lunches import gather_restaurants
from public_transport import public_transport_connections

app = FastAPI(debug=True)
templates = Jinja2Templates(directory="templates")
#app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
# app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
redis_client = redis.Redis()


@app.get("/public_transport")
async def public_transport(request: Request):
srcs = ["Václava Jiřikovského"]
Expand All @@ -23,57 +26,53 @@ async def public_transport(request: Request):
srcs, dsts = dsts, srcs

return templates.TemplateResponse(
request=request,
name='public_transport.html',
context={
'connections': await public_transport_connections(srcs, dsts)
}
request=request,
name="public_transport.html",
context={"connections": await public_transport_connections(srcs, dsts)},
)


@app.get("/lunch.json")
@app.post("/lunch.json")
async def lunch(request: Request):
now = int(datetime.datetime.now().timestamp())
key = f'restaurants.{datetime.date.today().strftime("%d-%m-%Y")}'
result_str = await redis_client.get(key)
if not result_str or request.method == 'POST':
throttle_key = f'{key}.throttle'
if not result_str or request.method == "POST":
throttle_key = f"{key}.throttle"
if await redis_client.incr(throttle_key) != 1:
return {'error': 'Fetch limit reached. Try again later.'}
return {"error": "Fetch limit reached. Try again later."}
await redis_client.expire(throttle_key, 60 * 3)

result = {
'last_fetch': now,
'fetch_count': await redis_client.incr(f'{key}.fetch_count'),
'restaurants': list(await gather_restaurants()),
"last_fetch": now,
"fetch_count": await redis_client.incr(f"{key}.fetch_count"),
"restaurants": list(await gather_restaurants()),
}
await redis_client.set(key, pickle.dumps(result))
else:
result = pickle.loads(result_str)

disallow_nets = [ipaddress.ip_network(net) for net in [
'127.0.0.0/8',
'::1/128',
'192.168.1.0/24',
'89.103.137.232/32',
'2001:470:5816::/48'
]]
disallow_nets = [
ipaddress.ip_network(net)
for net in ["127.0.0.0/8", "::1/128", "192.168.1.0/24", "89.103.137.232/32", "2001:470:5816::/48"]
]
for net in disallow_nets:
if net.version == 4:
disallow_nets.append(ipaddress.ip_network(f'::ffff:{net.network_address}/{96 + net.prefixlen}'))
disallow_nets.append(ipaddress.ip_network(f"::ffff:{net.network_address}/{96 + net.prefixlen}"))

visitor_addr = ipaddress.ip_address(request.client.host)
if not any([net for net in disallow_nets if visitor_addr in net]):
await redis_client.incr(f'{key}.access_count')
await redis_client.setnx(f'{key}.first_access', now)
if not any([net for net in disallow_nets if visitor_addr in net]): # noqa: C419
await redis_client.incr(f"{key}.access_count")
await redis_client.setnx(f"{key}.first_access", now)

async def get(k):
val = await redis_client.get(f'{key}.{k}')
val = await redis_client.get(f"{key}.{k}")
if val:
result[k] = int(val)
else:
result[k] = 0

await get('access_count')
await get('first_access')
await get("access_count")
await get("first_access")
return result
20 changes: 20 additions & 0 deletions docker-compose-local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# For local development, only database is running
#
# docker compose up -d
# fastapi dev
#
services:
redis:
container_name: lunchmenu-redis
image: redis:alpine
restart: unless-stopped
ports:
- "6379:6379"
command: "redis-server --save 20 1 --loglevel warning"
volumes:
- "redis_data:/data"
extra_hosts:
- host.docker.internal:host-gateway

volumes:
redis_data:
Loading