Skip to content

Commit

Permalink
feat: add mealdoo restaurant support (#52)
Browse files Browse the repository at this point in the history
fix: improve print output upon error
docs: update docstrings
test: update tests
ci: update pre-commit config
  • Loading branch information
jkerola authored Aug 12, 2024
1 parent d525ee4 commit f96c409
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 142 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ exclude: "CHANGELOG.md"
repos:
# Detect secrets
- repo: https://github.com/Yelp/detect-secrets
rev: "v1.4.0"
rev: "v1.5.0"
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]
Expand All @@ -15,7 +15,7 @@ repos:
args: ["--cache=false"]
# Black formatter
- repo: https://github.com/psf/black
rev: 24.1.1
rev: 24.8.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
Expand All @@ -26,11 +26,11 @@ repos:
# Ruff linter
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: "v0.1.14"
rev: "v0.5.7"
hooks:
- id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
Expand Down
23 changes: 23 additions & 0 deletions src/jmenu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,26 @@
Documentation for the library can be found in the [project pages](https://jkerola.github.io/jmenu)
"""

from .classes import (
JamixApi,
JamixRestaurant,
Marker,
MealdooApi,
MealdooRestaurant,
MenuItem,
MenuItemFactory,
Restaurant,
)

__all__ = [
"Restaurant",
"MealdooRestaurant",
"JamixRestaurant",
"ApiEndpoint",
"JamixApi",
"MealdooApi",
"Marker",
"MenuItem",
"MenuItemFactory",
]
7 changes: 6 additions & 1 deletion src/jmenu/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from .main import run
"""
See main.py for more information.
"""

import sys

from .main import run

if __name__ == "__main__":
sys.exit(run())
78 changes: 0 additions & 78 deletions src/jmenu/api.py

This file was deleted.

205 changes: 191 additions & 14 deletions src/jmenu/classes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
"""
Contains dataclasses jmenu uses to manage data.
This file can be imported and exposes the following classes:
* MenuItem
* Restaurant
* Marker
The following collections are use-case specific to the University of Oulu:
Expand All @@ -13,8 +8,11 @@
* SKIPPED_ITEMS
"""

from typing import NamedTuple
from collections.abc import Iterable, Mapping
from datetime import datetime
from typing import NamedTuple

import requests


class MenuItem(NamedTuple):
Expand All @@ -38,7 +36,24 @@ def diets_to_string(self) -> str:
return " ".join(self.diets)


class Restaurant(NamedTuple):
class Restaurant:
name: str

def __init__(self, name: str):
self.name = name


class MealdooRestaurant(Restaurant):
menu_name: str
organization: str

def __init__(self, name: str, menu_name: str, organization: str):
self.menu_name = menu_name
self.organization = organization
Restaurant.__init__(self, name)


class JamixRestaurant(Restaurant):
"""Dataclass for relevant restaurant information
Attributes:
Expand All @@ -54,12 +69,18 @@ class Restaurant(NamedTuple):
menu names used for filtering out desserts etc.
"""

name: str
client_id: int
kitchen_id: int
menu_type: int
relevant_menus: Iterable[str]

def __init__(self, name, client_id, kitchen_id, menu_type, relevant_menus):
Restaurant.__init__(self, name)
self.client_id = client_id
self.menu_type = menu_type
self.kitchen_id = kitchen_id
self.relevant_menus = relevant_menus


class Marker(NamedTuple):
"""Dataclass for allergen information markings
Expand Down Expand Up @@ -99,12 +120,13 @@ def get_explanation(self, lang_code: str = "en"):
]

RESTAURANTS = [
Restaurant("Foobar", 93077, 69, 84, ["Foobar Salad and soup", "Foobar Rohee"]),
Restaurant("Foodoo", 93077, 48, 89, ["Foodoo Salad and soup", "Foodoo Reilu"]),
Restaurant("Kastari", 95663, 5, 2, ["Ruokalista"]),
Restaurant("Kylymä", 93077, 48, 92, ["Kylymä Rohee"]),
Restaurant("Mara", 93077, 49, 111, ["Salad and soup", "Ravintola Mara"]),
Restaurant("Napa", 93077, 48, 79, ["Napa Rohee"]),
JamixRestaurant("Foobar", 93077, 69, 84, ["Foobar Salad and soup", "Foobar Rohee"]),
JamixRestaurant("Foodoo", 93077, 48, 89, ["Foodoo Salad and soup", "Foodoo Reilu"]),
# JamixRestaurant("Kastari", 95663, 5, 2, ["Ruokalista"]),
JamixRestaurant("Kylymä", 93077, 48, 92, ["Kylymä Rohee"]),
MealdooRestaurant("Julinia", "ravintolajulinia", "uniresta"),
JamixRestaurant("Mara", 93077, 49, 111, ["Salad and soup", "Ravintola Mara"]),
JamixRestaurant("Napa", 93077, 48, 79, ["Napa Rohee"]),
]

MARKERS = [
Expand All @@ -125,3 +147,158 @@ def get_explanation(self, lang_code: str = "en"):
Marker("<3", {"fi": "Sydänmerkki", "en": "Better choice indicator"}),
Marker("VEG", {"fi": "Vegaani", "en": "Vegan"}),
]


class ApiEndpoint:
"""Base class for API endpoints."""

baseUrl: str

def create_url_for_restaurant(restaurant: Restaurant):
pass

def parse_items() -> list[MenuItem]:
pass


class MealdooApi(ApiEndpoint):
"""Utility class for parsing Mealdoo API responses."""

baseUrl = "https://api.fi.poweresta.com/publicmenu/dates"

def create_url_for_restaurant(self, res: MealdooRestaurant, date: datetime) -> str:
"""Generate a URL with appropriate parameters for given [Restaurant].
Args:
res (MealdooRestaurant): Restaurant with appropriate metadata.
date (datetime): Menu date.
Returns:
str: URL with formatted parameters.
"""
return f"{self.baseUrl}/{res.organization}/{res.name.lower()}/?menu={res.menu_name}&dates={date.strftime('%Y-%m-%d')}"

def parse_items(self, data: list[dict], lang_code: str) -> list[MenuItem]:
"""Create [MenuItems] based on response JSON.
Args:
data (list[dict]): Response JSON data.
lang_code (str): Language code. Either "fi" or "en".
Returns:
list[MenuItem]: List of [MenuItems]
"""
items = []
for result in data:
try:
options = result["data"]["mealOptions"]
for opt in options:
for row in opt["rows"]:
title = "???"
diets = []
for name in row["names"]:
if name["language"] == lang_code and name["name"]:
title, *extra_diets = name["name"].split(",")
title = " ".join(title.split(" ")[:-1])
diets.extend(extra_diets)

for diet in row["diets"]:
if diet["language"] == lang_code and diet["dietShorts"]:
diets.extend(diet["dietShorts"])
diets = set([diet.strip() for diet in diets])
items.append(MenuItem(title if title else "???", diets))
except Exception:
pass

return items


class JamixApi(ApiEndpoint):
"""Utility class for parsing Jamix API responses."""

baseUrl = "https://fi.jamix.cloud/apps/menuservice/rest/haku/menu"

def create_url_for_restaurant(
self,
res: JamixRestaurant,
date: datetime,
lang_code="en",
) -> str:
"""Returns the formatted URL with given restaurant metadata as parameters.
Args:
res (JamixRestaurant): Restaurant metadata.
date (datetime): Menu date.
lang_code (str, optional): Language. Defaults to "en".
Returns:
str: Formatted URL string.
"""
return f"{self.baseUrl}/{res.client_id}/{res.kitchen_id}?lang={lang_code}&date={date.strftime('%Y%m%d')}"

def parse_items(
self, data: list[dict], relevant_menus: list[str]
) -> list[MenuItem]:
"""Returns a list of [MenuItems] parsed from JSON data
Parameters:
data (list[dict]):
parsed JSON response from the jamix API, see api._fetch_restaurant
relevant_menus (list[str]):
list of menu names to filter when parsing
defaults to all menus
Returns:
(list[MenuItem]):
list of restaurant menu items
"""
menus = []
for kitchen in data:
for m_type in kitchen["menuTypes"]:
if len(relevant_menus) == 0 or m_type["menuTypeName"] in relevant_menus:
menus.extend(m_type["menus"])
if len(menus) == 0:
return []
items = []
for menu in menus:
day = menu["days"][0]
mealopts = day["mealoptions"]
sorted(mealopts, key=lambda x: x["orderNumber"])
for opt in mealopts:
for item in opt["menuItems"]:
if item["name"] not in SKIPPED_ITEMS and len(item["name"]) > 0:
items.append(MenuItem(item["name"], item["diets"].split(",")))
return items


class MenuItemFactory:
"""Factory function for creating and parsing requests to all restaurant APIs."""

jamix = JamixApi()
mealdoo = MealdooApi()

def get_menu_items(
self,
restaurant: JamixRestaurant | MealdooRestaurant,
date: datetime,
lang_code="en",
) -> list[MenuItem]:
"""Fetch and create menu items for given [Restaurant].
Args:
restaurant (JamixRestaurant | MealdooRestaurant): Restaurant metadata.
date (datetime): Menu date.
lang_code (str, optional): Result language. Defaults to "en".
Returns:
list[MenuItem]: List of [MenuItems].
"""
if type(restaurant) is JamixRestaurant:
url = self.jamix.create_url_for_restaurant(restaurant, date, lang_code)
data = requests.get(url, timeout=5).json()
return self.jamix.parse_items(data, restaurant.relevant_menus)

elif type(restaurant) is MealdooRestaurant:
url = self.mealdoo.create_url_for_restaurant(restaurant, date)
data = requests.get(url, timeout=5).json()
return self.mealdoo.parse_items(data, lang_code)
Loading

0 comments on commit f96c409

Please sign in to comment.