Prvotní nástřel fungující aplikace

This commit is contained in:
Martin Berka
2023-06-01 23:05:51 +02:00
parent bf379e13ed
commit 12583e6efb
59 changed files with 2194 additions and 1011 deletions

10
food_api/Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM python:3.9
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./food_service.py /app
COPY ./food_api.py /app
CMD ["uvicorn", "food_api:app", "--host", "0.0.0.0", "--port", "3002"]

43
food_api/README.md Normal file
View File

@@ -0,0 +1,43 @@
# TODO
Následující informace jsou neaktuální. Už nemáme Flask, místo WSGI jedeme přes ASGI apod. Místo tohoto dokumentu využijte nadřazený README.md.
# POMPSZČPS
POMPSZČPS, neboli Parser Obědových Menu Plzeňských Stravovacích Zařízení v Části Plzeň-Slovany, je Python aplikace poskytující na jednom místě aktuální obědové menu pro několik stravovacích zařízení v městské části Plzeň 2-Slovany. Aktuálně podporuje následující podniky:
- [Pivnice Sladovnická](https://sladovnicka.unasplzenchutna.cz)
- [Restaurace U Motlíků](https://www.umotliku.cz)
- [Restaurace TechTower](https://www.equifarm.cz/restaurace-techtower)
Pro tyto podniky umožňuje získání aktuálního obědového menu, a to buďto barevným výpisem do konzole (přímým spuštěním `food_service.py`) nebo v podobě [WSGI](https://cs.wikipedia.org/wiki/Web_Server_Gateway_Interface) endpointu (`wsgi.py`), který vrací zmíněná menu jako strukturovaný JSON objekt pro další použití v jiných aplikacích.
## Závislosti
- [Python 3.x](https://www.python.org)
Pro použití jako konzolová aplikace
- [beautifulsoup4](https://pypi.org/project/beautifulsoup4)
Pro použití jako API endpoint
- [beautifulsoup4](https://pypi.org/project/beautifulsoup4)
- [Flask](https://pypi.org/project/Flask)
- [gunicorn](https://pypi.org/project/gunicorn)
## Použití
```bash
python -m venv venv
(Unix): source venv/bin/activate
(Windows): venv\Scripts\activate.bat
pip install -r requirements.txt
```
- Jako konzolová aplikace: `python food_service.py`
- Vypíše přehledně pod sebe menu všech aktuálně integrovaných podniků
- Jako JSON API endpoint
- TODO
## TODO
- Umožnit zadat a zobrazit menu pro jiné dny
- umožnit zadání datumem nebo názvem dne v týdnu
- validace - žádná sobota, neděle
- validace - datum musí být tento týden
- minimálně pro Motlíky to znamená úpravu URL a parseru
- Otestovat rozchození - vytvoření venv, instalace requirements, spuštění jako konzole
- Umožnit konfiguračně určit pro které podniky se bude menu získávat a zobrazovat (vyberu si jen ty, které mě zajímají)
- Umožnit konfiguračně nastavit výrazy pro detekci polévky

20
food_api/food_api.py Normal file
View File

@@ -0,0 +1,20 @@
from food_service import getMenuSladovnicka, getMenuTechTower, getMenuUMotliku
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def read_root():
return {
'sladovnicka': getMenuSladovnicka(),
'uMotliku:': getMenuUMotliku(),
'techTower': getMenuTechTower()
}

252
food_api/food_service.py Executable file
View File

@@ -0,0 +1,252 @@
#!/usr/bin/env python3
import datetime
from typing import List
from bs4 import BeautifulSoup
import tempfile
import sys
import os
import urllib.request
from datetime import date, timedelta
URL_SLADOVNICKA = "https://sladovnicka.unasplzenchutna.cz/cz/denni-nabidka"
URL_MOTLICI = "https://www.umotliku.cz"
URL_TECHTOWER = "https://www.equifarm.cz/restaurace-techtower"
DAY_NAMES = ['pondělí', 'úterý', 'středa',
'čtvrtek', 'pátek', 'sobota', 'neděle']
# Fráze v názvech jídel, které naznačují že se jedná o polévku
SOUP_NAMES = ['polévka', 'česnečka', 'česnekový krém', 'cibulačka', 'vývar']
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
class Food:
name = None
amount = None
price = None
is_soup = False
def __init__(self, name, amount, price, is_soup=False) -> None:
self.name = name
self.amount = amount
self.price = price
self.is_soup = is_soup
def getOrDownloadHtml(prefix: str, url: str):
'''Vrátí HTML pro daný prefix pro aktuální den.
Pokud v tempu neexistuje, provede jeho stažení z předané URL a uložení.'''
filename = prefix + "_" + date.today().strftime("%Y_%m_%d") + ".html"
filepath = os.path.join(tempfile.gettempdir(), filename)
if not os.path.isfile(filepath):
urllib.request.urlretrieve(url, filepath)
file = open(filepath, "r")
contents = file.read()
file.close()
return contents
def isNameOfDay(text: str):
'''Vrátí True, pokud předaný text představuje název dne v týdnu (např. "pondělí")'''
return text.strip().lower() in DAY_NAMES
def getDayNameOfDate(date: datetime.datetime):
'''Vrátí název dne v týdnu - např. pondělí, úterý, ...'''
return DAY_NAMES[date.weekday()]
def getStartOfWeekDate():
'''Vrátí datetime představující pondělí v aktuálním týdnu.'''
today = datetime.datetime.now()
return today - timedelta(days=today.weekday())
def isTextSoupName(text: str):
'''Vrátí True, pokud se předaný text jeví jako název polévky.
Používá se tam, kde nemáme lepší způsob detekce (TechTower).'''
for name in SOUP_NAMES:
if name in text.lower():
return True
return False
def printMenu(name: str, foodList: List[Food]):
'''Vytiskne jídelní lístek na obrazovku.'''
print(f"{bcolors.OKGREEN}{name}{bcolors.ENDC}\n---------------------------------------------------------------------------------")
maxLength = 0
for jidlo in foodList:
if len(jidlo.name) > maxLength:
maxLength = len(jidlo.name)
for jidlo in foodList:
barva = bcolors.HEADER if jidlo.is_soup else bcolors.WARNING
print(f"{barva}{jidlo.amount}\t{jidlo.name.ljust(maxLength)}\t{bcolors.ENDC}{bcolors.OKCYAN}{jidlo.price}{bcolors.ENDC}")
print('\n')
def getMenuSladovnicka() -> List[Food]:
html = getOrDownloadHtml('sladovnicka', URL_SLADOVNICKA)
soup = BeautifulSoup(html, "html.parser")
div = soup.select_one("div.tab-pane.fade.in.active")
datumDen = div.find("h2").text
split = datumDen.split(".")
denMesic = split[0] + "." + split[1] + "."
# nazevDen = split[2]
dnesniDatum = date.today().strftime("%-d.%-m.")
if denMesic != dnesniDatum:
print('Chyba: neočekávané datum na stránce Sladovnické (' +
denMesic + '), očekáváno ' + dnesniDatum, file=sys.stderr)
sys.exit(1)
tables = div.find_all("table", {"class": "simple"})
if len(tables) != 2:
print('Chyba: neočekávaný počet tabulek na stránce Sladovnické (' +
str(len(tables)) + '), očekávány 2', file=sys.stderr)
sys.exit(1)
foodList: List[Food] = []
polevkaValues = tables[0].find_all("td")
amount = polevkaValues[0].text.strip()
name = polevkaValues[1].text.strip()
price = polevkaValues[2].text.strip()
foodList.append(Food(name, amount, price, True))
foodTables = tables[1].find_all("tr")
for food in foodTables:
rows = food.find_all("td")
if (len(rows) != 3):
print("Neočekávaný počet řádek hlavního jídla Sladovnické (" +
str(len(rows)) + ", očekávány 3, přeskakuji...")
continue
amount = rows[0].text.strip()
name = rows[1].text.strip()
price = rows[2].text.strip()
foodList.append(Food(name, amount, price))
return foodList
def getMenuUMotliku() -> List[Food]:
html = getOrDownloadHtml('u_motliku', URL_MOTLICI)
soup = BeautifulSoup(html, "html.parser")
table = soup.find("table", {"class": "Xtable-striped"})
rows = table.find_all("tr")
if len(rows) < 4:
print('Chyba: neočekávaný celkový počet řádek tabulky (' +
str(len(rows)) + '), očekáváno 4 a více', file=sys.stderr)
sys.exit(1)
foodList: List[Food] = []
if rows[0].td.text.strip() == 'Polévka':
tds = rows[1].find_all("td")
if len(tds) != 3:
print('Chyba: neočekávaný počet <td> elementů v řádce polévky (' +
str(len(tds)) + '), očekáváno 3', file=sys.stderr)
sys.exit(1)
amount = tds[0].text.strip()
name = tds[1].text.strip()
price = tds[2].text.strip().replace(',-', '')
foodList.append(Food(name, amount, price, True))
rows = rows[2:]
if rows[0].td.text.strip() == 'Hlavní jídlo':
for i in range(1, len(rows)):
tds = rows[i].find_all("td")
if len(tds) != 3:
print("Neočekávaný počet <td> elementů (" + str(len(tds)
) + ") pro hlavní jídlo " + str(i) + ", přeskakuji")
continue
amount = tds[0].text.strip()
name = tds[1].text.strip()
price = tds[2].text.strip().replace(',-', '')
foodList.append(Food(name, amount, price))
return foodList
def getMenuTechTower() -> List[Food]:
html = getOrDownloadHtml('techtower', URL_TECHTOWER)
soup = BeautifulSoup(html, "html.parser")
fonts = soup.find_all("font", {"class": ["wsw-41"]})
font = None
for f in fonts:
if (f.text.strip().startswith("Obědy")):
font = f
if font is None:
print('Chyba: nenalezen <font> pro obědy v HTML Techtower.', file=sys.stderr)
sys.exit(1)
siblings = font.parent.parent.find_next_siblings("p")
# dayNumber = date.today().strftime("%w")
currentDayName = getDayNameOfDate(datetime.datetime.now())
foodList = []
doParse = False
for i in range(0, len(siblings)):
text = siblings[i].text.strip().replace('\t', '').replace('\n', ' ')
if isNameOfDay(text):
if text == currentDayName:
# Našli jsme dnešní den, odtud začínáme parsovat jídla
doParse = True
elif doParse == True:
# Už parsujeme jídla, ale narazili jsme na následující den - končíme
break
elif doParse:
if len(text.strip()) == 0:
# Prázdná řádka - končíme (je za pátečním menu TechTower)
break
price = '? Kč'
if text.endswith(''):
split = text.rsplit(' ', 2)
price = " ".join(split[1:])
text = split[0]
foodList.append(Food(text, '-', price, isTextSoupName(text)))
return foodList
if __name__ == "__main__":
if len(sys.argv) > 1:
input = sys.argv[1].lower()
selectedDate = None
if input[0].isalpha():
matches = []
for day in DAY_NAMES:
if day.startswith(input):
matches.append(day)
if len(matches) == 1:
print("Match - den v týdnu - " + matches[0])
selectedDate = getStartOfWeekDate(
) + timedelta(DAY_NAMES.index(matches[0]))
elif len(matches) == 0:
# TODO zkusit v, z (včera, zítra)
if 'zítra'.startswith(input):
print("Match - zítra")
selectedDate = datetime.datetime.now() + timedelta(days=1)
elif 'včera'.startswith(input):
print("Match - včera")
selectedDate = datetime.datetime.now() + timedelta(days=-1)
elif 'dneska'.startswith(input):
print("Match - dnes")
selectedDate = datetime.datetime.now()
else:
print('Nejasný parametr "' + input +
'" - může znamenat jednu z možností: ' + ', '.join(matches), file=sys.stderr)
sys.exit(1)
else:
# TODO implementovat zadání datem
print('Zadání datem není aktuálně implementováno', file=sys.stderr)
sys.exit(1)
print("Datum: " + selectedDate.strftime('%d.%m.%Y'))
print("Den: " + getDayNameOfDate(selectedDate))
# printMenu('Sladovnická', getMenuSladovnicka())
# printMenu('U Motlíků', getMenuUMotliku())
# printMenu('TechTower', getMenuTechTower())

View File

@@ -0,0 +1,3 @@
beautifulsoup4==4.12.2
fastapi==0.95.2
uvicorn==0.22.0

8
food_api/run_dev.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
cd $dir
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
uvicorn food_api:app --port 3002 --reload