Prvotní nástřel fungující aplikace
This commit is contained in:
10
food_api/Dockerfile
Normal file
10
food_api/Dockerfile
Normal 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
43
food_api/README.md
Normal 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
20
food_api/food_api.py
Normal 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
252
food_api/food_service.py
Executable 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('Kč'):
|
||||
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())
|
||||
3
food_api/requirements.txt
Normal file
3
food_api/requirements.txt
Normal 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
8
food_api/run_dev.sh
Executable 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
|
||||
Reference in New Issue
Block a user