This commit is contained in:
mdivecky 2023-10-19 18:37:19 +02:00
commit 6f25cb4691
13 changed files with 327 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*/__pycache__
.env
*.pyc

16
README.md Normal file
View file

@ -0,0 +1,16 @@
Aktuálně jsou hardcodované dvě zastávky v proměnné na začátku kódu, dalo by se hodit do proměné třeba jako json.
Ryšánka - směr Budějovická (C) - U632Z3P
Na Vrstevnici - směr Přístaviště (A) - U436Z1P
Název zastávky a její stanoviště (A,B,C,D...) je možné najít na [webu PID](https://pid.cz/zastavkova-tabla/?stop=Na+Vrstevnici&stanoviste=&tab=3)
Název je pak možné hodit do [Swaggeru od Golemia](https://api.golemio.cz/pid/docs/openapi/#/%F0%9F%A7%BE%20GTFS%20Static/get_gtfs_stops) (get_gtfs_stops) a ve výstupu najít odpovídající ID zastávky (`stop_id`)
Před prvním spuštěním je potřeba dostat API key od Golemia, jde to zdarma [zde](https://api.golemio.cz/api-keys/auth/sign-in). Ten pak vložit do souboru .env jako proměnnou `API_KEY`.
"Dockerizováno" primárně podle návodu [tady](https://github.com/docker/awesome-compose/blob/master/nginx-wsgi-flask/compose.yaml)
## Todo
* Auto refresh?
* Cache pro data z Golemia?
* Přesun IDs zastávek do proměnné

31
docker-compose.yml Normal file
View file

@ -0,0 +1,31 @@
version: "3.3"
services:
nginx-proxy:
build: nginx
restart: always
volumes:
- ./nginx/default.conf:/tmp/default.conf
environment:
- FLASK_SERVER_ADDR=flask-app:8000
ports:
- "127.0.0.1:7880:80"
depends_on:
- flask-app
healthcheck:
test: ["CMD-SHELL", "curl --silent --fail localhost:80/health-check || exit 1"]
interval: 10s
timeout: 10s
retries: 3
command: /app/start.sh
flask-app:
build: flask
restart: always
environment:
- API_KEY=${API_KEY}
healthcheck:
test: ["CMD-SHELL", "curl --silent --fail localhost:8000/flask-health-check || exit 1"]
interval: 10s
timeout: 10s
retries: 3
command: gunicorn -w 3 -t 60 -b 0.0.0.0:8000 app:app

1
env.sample Normal file
View file

@ -0,0 +1 @@
API_KEY=

32
flask/Dockerfile Normal file
View file

@ -0,0 +1,32 @@
FROM python:alpine
# upgrade pip
RUN pip install --upgrade pip
# get curl for healthchecks
RUN apk add curl
# permissions and nonroot user for tightened security
RUN adduser -D nonroot
RUN mkdir /home/app/ && chown -R nonroot:nonroot /home/app
RUN mkdir -p /var/log/flask-app && touch /var/log/flask-app/flask-app.err.log && touch /var/log/flask-app/flask-app.out.log
RUN chown -R nonroot:nonroot /var/log/flask-app
WORKDIR /home/app
USER nonroot
# copy all the files to the container
COPY --chown=nonroot:nonroot . .
# venv
ENV VIRTUAL_ENV=/home/app/venv
# python setup
RUN python -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN export FLASK_APP=app.py
RUN pip install -r requirements.txt
# define the port number the container should expose
EXPOSE 5000
CMD ["python", "app.py"]

79
flask/app.py Normal file
View file

@ -0,0 +1,79 @@
import os
import requests
from pprint import pprint
from flask import Flask, render_template
import datetime
API_KEY = os.environ['API_KEY']
API_URL = "https://api.golemio.cz/v2/pid/departureboards"
STOPS = ["U632Z3P", "U436Z1P"]
#STOPS = ["U632Z3P"]
CAR_AMOUNT = 10
app = Flask(__name__)
def get_departures(api, key, stop):
headers = {"accept": "application/json", "X-Access-Token": key}
params = {"ids": stop, "limit": CAR_AMOUNT}
r = requests.get(api, params=params, headers=headers)
return r.json()
def get_cars(stops):
cars = []
for stop in stops:
deps = get_departures(API_URL, API_KEY, stop)
stop_name = deps["stops"][0]["stop_name"]
stop_platform = deps["stops"][0]["platform_code"]
stop_display = stop_name #+ " (" + stop_platform + ")"
stop_cars = []
for car in deps["departures"]:
# print(car)
car_id = car["route"]["short_name"]
car_dir = car["trip"]["headsign"]
car_ac = car["trip"]["is_air_conditioned"]
car_departure = car["departure_timestamp"]["minutes"]
if car["delay"]["is_available"]:
# car_ahead = "+ "
car_delay_sec = (car["delay"]["minutes"] * 60) + car["delay"]["seconds"]
# if car_delay_sec < 0:
# car_ahead = "- "
# elif car_delay_sec == 0:
# car_ahead = ""
# m, s = divmod(car_delay_sec, 60)
# car_delay = car_ahead + str(m) + "min, " + str(s) + "sec"
car_delay = car_delay_sec
else:
car_delay = "?"
car_data = {
"id": car_id,
"dir": car_dir,
"ac": car_ac,
"departure": car_departure,
"delay": car_delay,
}
stop_cars.append(car_data)
cars.append({"id": stop, "name": stop_display, "cars": stop_cars})
return cars
@app.route("/")
def hello_world():
return render_template('index.html', data=get_cars(STOPS))
@app.route('/flask-health-check')
def flask_health_check():
return "success"

3
flask/requirements.txt Normal file
View file

@ -0,0 +1,3 @@
requests
gunicorn
Flask

View file

@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<title>Hello, world!</title>
</head>
<body>
<div class="container">
{% for stop in data %}
<p style="font-size: 24px">{{ stop.name }}</p>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Linka</th>
<th scope="col">Odjezd za</th>
<!-- <th scope="col">Směr</th> -->
<th scope="col">Zpoždění</th>
<th scope="col">AC</th>
<th></th>
</tr>
</thead>
<tbody>
{% for car in stop.cars %}
<tr>
<th scope="row">{{ car.id }}</th>
<td>{{ car.departure }} min</td>
<!-- <td>{{ car.dir }}</td> -->
<td>{{ car.delay }} sec</td>
<td>{% if car.ac %} <i class="bi bi-snow"></i> {% endif %} </td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
<caption>Odjezdy jsou včetně zpoždění</caption>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>

5
flask/wsgi.py Normal file
View file

@ -0,0 +1,5 @@
from app import app
import os
if __name__ == "__main__":
app.run(host='0.0.0.0', port=os.environ.get("FLASK_SERVER_PORT"), debug=True)

32
nginx/Dockerfile Normal file
View file

@ -0,0 +1,32 @@
FROM nginx:1.25.2-alpine
# Add bash for boot cmd
RUN apk add bash
# Add nginx.conf to container
COPY --chown=nginx:nginx nginx.conf /etc/nginx/nginx.conf
COPY --chown=nginx:nginx start.sh /app/start.sh
# set workdir
WORKDIR /app
# permissions and nginx user for tightened security
RUN chown -R nginx:nginx /app && chmod -R 755 /app && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chmod -R 755 /var/log/nginx; \
chown -R nginx:nginx /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && chown -R nginx:nginx /var/run/nginx.pid
# # Uncomment to keep the nginx logs inside the container - Leave commented for logging to stdout and stderr
# RUN mkdir -p /var/log/nginx
# RUN unlink /var/log/nginx/access.log \
# && unlink /var/log/nginx/error.log \
# && touch /var/log/nginx/access.log \
# && touch /var/log/nginx/error.log \
# && chown nginx /var/log/nginx/*log \
# && chmod 644 /var/log/nginx/*log
USER nginx
CMD ["nginx", "-g", "'daemon off;'"]

18
nginx/default.conf Normal file
View file

@ -0,0 +1,18 @@
proxy_cache_path /tmp/cache levels=1:2 keys_zone=cache:10m max_size=500m inactive=60m use_temp_path=off;
server {
listen 80;
location / {
proxy_pass http://$FLASK_SERVER_ADDR;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /health-check {
add_header Content-Type text/plain;
return 200 "success";
}
}

50
nginx/nginx.conf Normal file
View file

@ -0,0 +1,50 @@
worker_processes auto;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Define the format of log messages.
log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'"$host" sn="$server_name" '
'rt=$request_time '
'ua="$upstream_addr" us="$upstream_status" '
'ut="$upstream_response_time" ul="$upstream_response_length" '
'cs=$upstream_cache_status' ;
access_log /var/log/nginx/access.log main_ext;
error_log /var/log/nginx/error.log warn;
sendfile on;
keepalive_timeout 65;
# Enable Compression
gzip on;
# Disable Display of NGINX Version
server_tokens off;
# Size Limits
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 8m;
large_client_header_buffers 2 1k;
# # SSL / TLS Settings - Suggested for Security
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_session_timeout 15m;
# ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# ssl_prefer_server_ciphers on;
# ssl_session_tickets off;
include /etc/nginx/conf.d/*.conf;
}

2
nginx/start.sh Normal file
View file

@ -0,0 +1,2 @@
#!/bin/bash
envsubst '$FLASK_SERVER_ADDR' < /tmp/default.conf > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'