Skip to content

Commit

Permalink
CI improvements and Docker
Browse files Browse the repository at this point in the history
  • Loading branch information
JersyJ committed Jun 30, 2024
1 parent 5fc21d4 commit 8906bba
Show file tree
Hide file tree
Showing 12 changed files with 705 additions and 308 deletions.
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.

116 changes: 116 additions & 0 deletions .github/workflows/validation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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'

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
if: success() || failure()
run: |
poetry run ruff format --check . --output-format github
- 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
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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

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" .
```
59 changes: 29 additions & 30 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
#!/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)
redis_client = redis.Redis()
# app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
redis_client = redis.Redis(host="redis", port=6379)


@app.get("/public_transport")
async def public_transport(request: Request):
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
28 changes: 28 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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

lunchmenu:
container_name: lunchmenu
depends_on:
- redis
build:
context: .
dockerfile: ./Dockerfile
restart: unless-stopped
ports:
- "443:443"
extra_hosts:
- "host.docker.internal:host-gateway"

volumes:
redis_data:
Loading

0 comments on commit 8906bba

Please sign in to comment.