This commit is contained in:
Hadrian Burkhardt
2023-11-30 04:26:43 +01:00
committed by f4lco
parent 58a53e608f
commit 9e37940334
6 changed files with 174 additions and 15 deletions
+102 -6
View File
@@ -3,15 +3,21 @@ from datetime import date
class Builder: class Builder:
"""A class method for creating a new class."""
def __init__(self): def __init__(self):
"""Initialize the object for the OpenMensa Feed Doc XML."""
self._doc = minidom.Document() self._doc = minidom.Document()
self._om = self._doc.createElement("openmensa") self._om = self._doc.createElement("openmensa")
self._om.setAttribute("version", "2.1") self._om.setAttribute("version", "2.1")
self._om.setAttribute("xmlns", "http://openmensa.org/open-mensa-v2") self._om.setAttribute("xmlns", "http://openmensa.org/open-mensa-v2")
self._om.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") self._om.setAttribute(
"xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"
)
self._om.setAttribute( self._om.setAttribute(
"xsi:schemaLocation", "xsi:schemaLocation",
"http://openmensa.org/open-mensa-v2 http://openmensa.org/open-mensa-v2.xsd", "http://openmensa.org/open-mensa-v2 "
+ "http://openmensa.org/open-mensa-v2.xsd",
) )
self._version = None self._version = None
self._name = None self._name = None
@@ -28,6 +34,11 @@ class Builder:
@property @property
def version(self): def version(self):
"""The version of the device .
Returns:
[type]: [description]
"""
return self._version return self._version
@version.setter @version.setter
@@ -40,6 +51,11 @@ class Builder:
@property @property
def name(self): def name(self):
"""Name of the canteen .
Returns:
[type]: [description]
"""
return self._name return self._name
@name.setter @name.setter
@@ -52,12 +68,17 @@ class Builder:
@property @property
def address(self): def address(self):
"""The address of the canteen .
Returns:
[type]: [description]
"""
return self._address return self._address
@address.setter @address.setter
def address(self, value: tuple[str, str, str]): def address(self, value: tuple[str, str, str]):
street_nr, zip, city = value street_nr, zip_code, city = value
self._address = f"{street_nr}, {zip} {city}" self._address = f"{street_nr}, {zip_code} {city}"
@address.deleter @address.deleter
def address(self): def address(self):
@@ -65,6 +86,11 @@ class Builder:
@property @property
def city(self): def city(self):
"""Get the city of the canteen .
Returns:
[type]: [description]
"""
return self._city return self._city
@city.setter @city.setter
@@ -77,6 +103,11 @@ class Builder:
@property @property
def phone(self): def phone(self):
"""The phone number .
Returns:
[type]: [description]
"""
return self._phone return self._phone
@phone.setter @phone.setter
@@ -89,6 +120,11 @@ class Builder:
@property @property
def email(self): def email(self):
"""The email address of the canteen .
Returns:
[type]: [description]
"""
return self._email return self._email
@email.setter @email.setter
@@ -101,6 +137,11 @@ class Builder:
@property @property
def location(self): def location(self):
"""Get a tuple containing the location as latitude and longitude .
Returns:
[type]: [description]
"""
return (self._longitude, self._latitude) return (self._longitude, self._latitude)
@location.setter @location.setter
@@ -115,6 +156,11 @@ class Builder:
@property @property
def availability(self): def availability(self):
"""Whether the canteen is public or restriced.
Returns:
[type]: [description]
"""
return self._availability return self._availability
@availability.setter @availability.setter
@@ -130,11 +176,22 @@ class Builder:
@property @property
def times(self): def times(self):
"""Get the opening times the canteen.
Returns:
[type]: [description]
"""
return self._times return self._times
@times.setter @times.setter
def times(self, value: dict[str, str]): def times(self, value: dict[str, str]):
def attach_weekday(tag: str, value: str): def attach_weekday(tag: str, value: str):
"""Attach a week tag to the week .
Args:
tag (str): [description]
value (str): [description]
"""
if value == "": if value == "":
return return
d = self._doc.createElement(tag) d = self._doc.createElement(tag)
@@ -167,17 +224,31 @@ class Builder:
@property @property
def feed(self): def feed(self):
"""Get a feed object .
Returns:
[type]: [description]
"""
return self._feed return self._feed
@feed.setter @feed.setter
def feed(self, value: dict): def feed(self, value: dict):
"""Set the feed element .
Args:
value (dict): [description]
"""
name: str = value.get("name") name: str = value.get("name")
priority: int = value.get("priority") priority: int = value.get("priority")
url: str = value.get("url") url: str = value.get("url")
source: str = value.get("source") source: str = value.get("source")
hour: int = value.get("hour") hour: int = value.get("hour")
dayOfMonth: int | str = value.get("dayOfMonth") if value.get("dayOfMonth") else "*" dayOfMonth: int | str = (
dayOfWeek: int | str = value.get("dayOfWeek") if value.get("dayOfWeek") else "*" value.get("dayOfMonth") if value.get("dayOfMonth") else "*"
)
dayOfWeek: int | str = (
value.get("dayOfWeek") if value.get("dayOfWeek") else "*"
)
month: int | str = value.get("month") if value.get("month") else "*" month: int | str = value.get("month") if value.get("month") else "*"
minute: int = value.get("minute") if value.get("minute") else 0 minute: int = value.get("minute") if value.get("minute") else 0
retry: str = value.get("retry") retry: str = value.get("retry")
@@ -211,6 +282,11 @@ class Builder:
@property @property
def day(self): def day(self):
"""Returns the number of day of the week .
Returns:
[type]: [description]
"""
return self._day return self._day
@day.setter @day.setter
@@ -229,6 +305,15 @@ class Builder:
prices: dict[str, float], prices: dict[str, float],
note: str = None, note: str = None,
): ):
"""Add a meal element to the document .
Args:
date (date): [description]
category (str): [description]
name (str): [description]
prices (dict[str, float]): [description]
note (str, optional): [description]. Defaults to None.
"""
meal = self._doc.createElement("meal") meal = self._doc.createElement("meal")
node = self._doc.createElement("name") node = self._doc.createElement("name")
meal.appendChild(node) meal.appendChild(node)
@@ -270,12 +355,23 @@ class Builder:
meal_list.append(meal) meal_list.append(meal)
def __append_node(self, tag: str, value: str): def __append_node(self, tag: str, value: str):
"""Create a node with a tag and text .
Args:
tag (str): [description]
value (str): [description]
"""
elem = self._doc.createElement(tag) elem = self._doc.createElement(tag)
self._canteen.appendChild(elem) self._canteen.appendChild(elem)
node = self._doc.createTextNode(value) node = self._doc.createTextNode(value)
elem.appendChild(node) elem.appendChild(node)
def toXML(self): def toXML(self):
"""Return a XML string representing the canteen.
Returns:
[type]: [description]
"""
self._doc.appendChild(self._om) self._doc.appendChild(self._om)
if self.version: if self.version:
self.__append_node("version", self.version) self.__append_node("version", self.version)
+33 -5
View File
@@ -6,9 +6,16 @@ import json
class SWP_Webspeiseplan_API: class SWP_Webspeiseplan_API:
"""This class is used download content from SWP_Webspeiseplan.
Returns:
[type]: [description]
"""
URL_BASE = "https://swp.webspeiseplan.de" URL_BASE = "https://swp.webspeiseplan.de"
def __init__(self): def __init__(self):
"""Initialize the configuration for the web service ."""
logging.basicConfig() logging.basicConfig()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.__parse_token() self.__parse_token()
@@ -20,7 +27,9 @@ class SWP_Webspeiseplan_API:
"_": int(time.time() * 1000), "_": int(time.time() * 1000),
} }
self.outlets = {outlet["name"]: outlet for outlet in self.__parse_model(params)} self.outlets = {
outlet["name"]: outlet for outlet in self.__parse_model(params)
}
self.menus = {} self.menus = {}
self.meal_categories = {} self.meal_categories = {}
for outlet in self.outlets.values(): for outlet in self.outlets.values():
@@ -38,6 +47,7 @@ class SWP_Webspeiseplan_API:
self.meal_categories[outlet["name"]] = id2cat self.meal_categories[outlet["name"]] = id2cat
def __parse_token(self): def __parse_token(self):
"""Get the token from the proxy server."""
req = urllib.request.Request(self.URL_BASE) req = urllib.request.Request(self.URL_BASE)
with urllib.request.urlopen(req) as resp: with urllib.request.urlopen(req) as resp:
txt = resp.read().decode("utf-8") txt = resp.read().decode("utf-8")
@@ -50,14 +60,23 @@ class SWP_Webspeiseplan_API:
self.logger.debug(f"__parse_token: PROXY_TOKEN {self.proxy_token}") self.logger.debug(f"__parse_token: PROXY_TOKEN {self.proxy_token}")
def __spoof_req_headers(req: urllib.request.Request): def __spoof_req_headers(req: urllib.request.Request):
req.add_header("Accept", "application/json, text/javascript, */*; q=0.01") """Add headers to a request .
Args:
req (urllib.request.Request): [description]
"""
req.add_header(
"Accept", "application/json, text/javascript, */*; q=0.01"
)
req.add_header("Accept-Language", "en-US,en;q=0.9") req.add_header("Accept-Language", "en-US,en;q=0.9")
req.add_header("Connection", "keep-alive") req.add_header("Connection", "keep-alive")
req.add_header("Host", "swp.webspeiseplan.de") req.add_header("Host", "swp.webspeiseplan.de")
req.add_header("Referer", "https://swp.webspeiseplan.de/InitialConfig") req.add_header("Referer", "https://swp.webspeiseplan.de/InitialConfig")
req.add_header( req.add_header(
"Sec-Ch-Ua", "Sec-Ch-Ua",
'"Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"', '"Not/A)Brand";v="99", '
+ '"Google Chrome";v="115", '
+ '"Chromium";v="115"',
) )
req.add_header("Sec-Ch-Ua-Mobile", "?0") req.add_header("Sec-Ch-Ua-Mobile", "?0")
req.add_header("Sec-Ch-Ua-Platform", "Linux") req.add_header("Sec-Ch-Ua-Platform", "Linux")
@@ -66,11 +85,21 @@ class SWP_Webspeiseplan_API:
req.add_header("Sec-Fetch-Site", "same-origin") req.add_header("Sec-Fetch-Site", "same-origin")
req.add_header( req.add_header(
"User-Agent", "User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/115.0.0.0 Safari/537.36",
) )
req.add_header("X-Requested-With", "XMLHttpRequest") req.add_header("X-Requested-With", "XMLHttpRequest")
def __parse_model(self, params: dict): def __parse_model(self, params: dict):
"""Retrieve data from host.
Args:
params (dict): [description]
Returns:
[type]: [description]
"""
url = f"{self.URL_BASE}/index.php?" + "&".join( url = f"{self.URL_BASE}/index.php?" + "&".join(
[f"{k}={v}" for k, v in params.items()] [f"{k}={v}" for k, v in params.items()]
) )
@@ -80,4 +109,3 @@ class SWP_Webspeiseplan_API:
with urllib.request.urlopen(req) as resp: with urllib.request.urlopen(req) as resp:
data = resp.read() data = resp.read()
return json.loads(data)["content"] return json.loads(data)["content"]
+28 -2
View File
@@ -5,6 +5,8 @@ from stw_potsdam.swp_webspeiseplan_api import SWP_Webspeiseplan_API
class SWP_Webspeiseplan_Parser: class SWP_Webspeiseplan_Parser:
"""Class method to parse SWP_Webspeiseplan."""
def __init__( def __init__(
self, self,
menu_data: list[dict], menu_data: list[dict],
@@ -12,6 +14,14 @@ class SWP_Webspeiseplan_Parser:
outlet_data: dict, outlet_data: dict,
url: str, url: str,
): ):
"""Initialize the parser .
Args:
menu_data (list[dict]): [description]
meal_categories (list[dict]): [description]
outlet_data (dict): [description]
url (str): [description]
"""
logging.basicConfig() logging.basicConfig()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.menu_data = menu_data self.menu_data = menu_data
@@ -24,6 +34,11 @@ class SWP_Webspeiseplan_Parser:
self.__parse_meals() self.__parse_meals()
def __parse_canteen(self, outlet: dict): def __parse_canteen(self, outlet: dict):
"""Parse the outlet data from outlet.
Args:
outlet (dict): [description]
"""
canteen = self._builder canteen = self._builder
canteen.name = outlet["name"] canteen.name = outlet["name"]
canteen.address = ( canteen.address = (
@@ -53,13 +68,16 @@ class SWP_Webspeiseplan_Parser:
} }
times = { times = {
k: v.replace("None, None", "").replace("None,", "").replace(", None", "") k: v.replace("None, None", "")
.replace("None,", "")
.replace(", None", "")
for k, v in times.items() for k, v in times.items()
} }
canteen.times = times canteen.times = times
def __parse_feed(self): def __parse_feed(self):
"""Parse feed and set feed."""
feed = { feed = {
"name": "full", "name": "full",
"priority": 0, "priority": 0,
@@ -71,6 +89,7 @@ class SWP_Webspeiseplan_Parser:
self._builder.feed = feed self._builder.feed = feed
def __parse_meals(self): def __parse_meals(self):
"""Parse the menu and adds it to the builder."""
for menu in self.menu_data: for menu in self.menu_data:
for meal in menu["speiseplanGerichtData"]: for meal in menu["speiseplanGerichtData"]:
info = meal["speiseplanAdvancedGericht"] info = meal["speiseplanAdvancedGericht"]
@@ -78,7 +97,9 @@ class SWP_Webspeiseplan_Parser:
additional_info = meal["zusatzinformationen"] additional_info = meal["zusatzinformationen"]
self._builder.add_meal( self._builder.add_meal(
date=date, date=date,
category=self.meal_categories[info["gerichtkategorieID"]]["name"], category=self.meal_categories[info["gerichtkategorieID"]][
"name"
],
name=info["gerichtname"], name=info["gerichtname"],
prices={ prices={
"student": additional_info["mitarbeiterpreisDecimal2"], "student": additional_info["mitarbeiterpreisDecimal2"],
@@ -89,4 +110,9 @@ class SWP_Webspeiseplan_Parser:
@property @property
def xml_feed(self): def xml_feed(self):
"""Return the XML string of the builder.
Returns:
[type]: [description]
"""
return self._builder.toXML() return self._builder.toXML()
+3 -1
View File
@@ -45,6 +45,7 @@ def get_menu():
log.debug("Downloading menu for SWP") log.debug("Downloading menu for SWP")
return SWP_Webspeiseplan_API() return SWP_Webspeiseplan_API()
@app.route("/canteens/<canteen_name>") @app.route("/canteens/<canteen_name>")
@app.route("/canteens/<canteen_name>/xml") @app.route("/canteens/<canteen_name>/xml")
def canteen_xml_feed(canteen_name): def canteen_xml_feed(canteen_name):
@@ -63,9 +64,10 @@ def canteen_xml_feed(canteen_name):
) )
xml = swp_parser.xml_feed.decode() xml = swp_parser.xml_feed.decode()
response = make_response(xml) response = make_response(xml)
response.mimetype = 'text/xml' response.mimetype = "text/xml"
return response return response
@app.route("/") @app.route("/")
@app.route("/canteens") @app.route("/canteens")
def canteen_index(): def canteen_index():
+5
View File
@@ -25,6 +25,7 @@ def _read_feed(resource_name):
with io.open(_resource_path(resource_name), encoding='utf-8') as xml: with io.open(_resource_path(resource_name), encoding='utf-8') as xml:
return xml.read() return xml.read()
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_meta_consistency(): def test_meta_consistency():
raise NotImplementedError() raise NotImplementedError()
@@ -34,6 +35,7 @@ def test_meta_consistency():
expected = _read_feed('meta_output.xml') expected = _read_feed('meta_output.xml')
assert expected == actual assert expected == actual
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_menu_consistency(): def test_menu_consistency():
raise NotImplementedError() raise NotImplementedError()
@@ -42,6 +44,7 @@ def test_menu_consistency():
expected = _read_feed('menu_output.xml') expected = _read_feed('menu_output.xml')
assert expected == actual assert expected == actual
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_empty_menu(): def test_empty_menu():
raise NotImplementedError() raise NotImplementedError()
@@ -50,6 +53,7 @@ def test_empty_menu():
expected = _read_feed('empty_menu_output.xml') expected = _read_feed('empty_menu_output.xml')
assert expected == actual assert expected == actual
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_offers_dictionary(): def test_offers_dictionary():
raise NotImplementedError() raise NotImplementedError()
@@ -58,6 +62,7 @@ def test_offers_dictionary():
expected = _read_feed('offers-dict-output.xml') expected = _read_feed('offers-dict-output.xml')
assert expected == actual assert expected == actual
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_missing_category(): def test_missing_category():
raise NotImplementedError() raise NotImplementedError()
+3 -1
View File
@@ -48,11 +48,13 @@ def test_canteen_not_found(client, url):
def test_canteen_menu_api_unavailable(client, api_offline): def test_canteen_menu_api_unavailable(client, api_offline):
_request_check_meals(client) _request_check_meals(client)
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_canteen_menu_request(client, api_online_one_shot): def test_canteen_menu_request(client, api_online_one_shot):
raise NotImplementedError() raise NotImplementedError()
_request_check_meals(client) _request_check_meals(client)
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_canteen_menu_cached(client, api_online_one_shot): def test_canteen_menu_cached(client, api_online_one_shot):
raise NotImplementedError() raise NotImplementedError()
@@ -66,8 +68,8 @@ def test_canteen_menu_second_request_indeed_fails(client, api_online_one_shot):
views.cache.clear() views.cache.clear()
_request_check_meals(client) _request_check_meals(client)
@pytest.mark.xfail(strict=True)
@pytest.mark.xfail(strict=True)
def _request_check_meals(client): def _request_check_meals(client):
raise NotImplementedError() raise NotImplementedError()
response = client.get("/canteens/griebnitzsee/xml") response = client.get("/canteens/griebnitzsee/xml")