commit 6f25cb4691b5a64c86ddeca670cf9c2cb7074108 Author: mdivecky <matej@divecky.com> Date: Thu Oct 19 18:37:19 2023 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce2ccaa --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*/__pycache__ +.env +*.pyc \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d51543 --- /dev/null +++ b/README.md @@ -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é \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bea5316 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/env.sample b/env.sample new file mode 100644 index 0000000..2387a6f --- /dev/null +++ b/env.sample @@ -0,0 +1 @@ +API_KEY= \ No newline at end of file diff --git a/flask/Dockerfile b/flask/Dockerfile new file mode 100644 index 0000000..e21b756 --- /dev/null +++ b/flask/Dockerfile @@ -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"] diff --git a/flask/app.py b/flask/app.py new file mode 100644 index 0000000..31c8cf0 --- /dev/null +++ b/flask/app.py @@ -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" diff --git a/flask/requirements.txt b/flask/requirements.txt new file mode 100644 index 0000000..0615934 --- /dev/null +++ b/flask/requirements.txt @@ -0,0 +1,3 @@ +requests +gunicorn +Flask diff --git a/flask/templates/index.html b/flask/templates/index.html new file mode 100644 index 0000000..f4a2ffb --- /dev/null +++ b/flask/templates/index.html @@ -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> + + + diff --git a/flask/wsgi.py b/flask/wsgi.py new file mode 100644 index 0000000..b0efe6e --- /dev/null +++ b/flask/wsgi.py @@ -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) \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..ce35eee --- /dev/null +++ b/nginx/Dockerfile @@ -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;'"] \ No newline at end of file diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..c8a8bcd --- /dev/null +++ b/nginx/default.conf @@ -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"; + } + +} \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..d72a39b --- /dev/null +++ b/nginx/nginx.conf @@ -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; + +} \ No newline at end of file diff --git a/nginx/start.sh b/nginx/start.sh new file mode 100644 index 0000000..d3c7ac2 --- /dev/null +++ b/nginx/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +envsubst '$FLASK_SERVER_ADDR' < /tmp/default.conf > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;' \ No newline at end of file