Skip to content

Commit

Permalink
feat: restructure package as library (#27)
Browse files Browse the repository at this point in the history
* restructure app for library use

* add error tracking, exit codes

* add unit tests

* add unit test workflow

* revamp versioning system
  • Loading branch information
jkerola authored Sep 13, 2023
1 parent 3a86d35 commit 6209c01
Show file tree
Hide file tree
Showing 13 changed files with 824 additions and 110 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/analyze-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,14 @@ jobs:
uses: psf/black@stable
with:
options: "--check"
run-tests:
name: Tests pass
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
with:
set-safe-directory: true
- name: Setup environment
uses: ./.github/actions/setup-env
- name: Run unit tests
run: pytest
19 changes: 14 additions & 5 deletions .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ jobs:
set-safe-directory: true
- name: Setup environment
uses: ./.github/actions/setup-env
- name: Export version information
run: |
echo "Version ${{ github.ref_name }}"
echo "VERSION = '${{ github.ref_name }}'" > src/jmenu/version.py
- name: Build executable
run: python3 -m build
run: |
rm -rf dist
python3 -m build
- name: Install sanity test
run: pip install dist/*.whl
- name: Import sanity test
shell: python
run: |
try:
import jmenu.main
print(jmenu.main.get_version())
exit(0)
except ModuleNotFoundError:
exit(1)
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ build
jmenu.spec
.ruff_cache
*.egg-info
.pytest_cache
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ build-backend = "setuptools.build_meta"

[project.scripts]
jmenu = "jmenu.main:main"

[tool.pytest.ini_options]
pythonpath = "src"
addopts = [
"--import-mode=importlib"
]
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
build==1.0.0
certifi==2023.7.22
charset-normalizer==3.2.0
exceptiongroup==1.1.3
idna==3.4
iniconfig==2.0.0
packaging==23.1
pluggy==1.3.0
pyproject_hooks==1.0.0
pytest==7.4.2
requests==2.31.0
tomli==2.0.1
urllib3==2.0.4
98 changes: 98 additions & 0 deletions src/jmenu/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""api.py
Contains functions used to wrangle the JAMIX API.
This file can be imported and exposes the following functions:
* fetch_restaurant
* parse_items_from_response
* get_menu_items
The following constants are also exposed:
* API_URL
"""


import requests
from datetime import datetime
from .classes import Restaurant, MenuItem, SKIPPED_ITEMS

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


def fetch_restaurant(rest: Restaurant, fetch_date: datetime) -> list[dict]:
"""Return the JSON response containing all menu information for [Restaurant]
Parameters
----------
rest : Restaurant
dataclass containing relevant restaurant information, see classes.RESTAURANTS
fetch_date : datetime
datetime object used to fetch the date specified menu
Returns
-------
list[dict]
parsed response json
"""
response = requests.get(
f"{API_URL}/{rest.client_id}/{rest.kitchen_id}?lang=fi&date={fetch_date.strftime('%Y%m%d')}",
timeout=5,
)
return response.json()


def get_menu_items(rest: Restaurant, fetch_date: datetime) -> list[MenuItem]:
"""Returns a list of restaurant [MenuItems]
Parameters
----------
rest : Restaurant
dataclass containing relevant restaurant information, see classes.RESTAURANTS
fetch_date : datetime
datetime object used to fetch the date specified menu
Returns
-------
list[MenuItem]
list of restaurant menu items, see classes.MenuItem
"""
data = fetch_restaurant(rest, fetch_date)
items = parse_items_from_response(data, rest.relevant_menus)
return items


def parse_items_from_response(
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 consider when parsing
defaults to empty list
Returns
-------
list[MenuItem]
list of restaurant menu items, see classes.MenuItem"""
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:
items.append(MenuItem(item["name"], item["diets"]))
return items
102 changes: 102 additions & 0 deletions src/jmenu/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Classes.py
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:
* MARKERS
* RESTAURANTS
* SKIPPED_ITEMS
"""

from collections import namedtuple


_MenuItem = namedtuple("MenuItem", ["name", "diets"])


class MenuItem(_MenuItem):
"""Dataclass for single menu items and their properties
Attributes
---
name : str
name of the dish
diets : str
list of allergen markers
"""


_Restaurant = namedtuple(
"Restaurant", ["name", "client_id", "kitchen_id", "menu_type", "relevant_menus"]
)


class Restaurant(_Restaurant):
"""Dataclass for relevant restaurant information
Attributes
---
name : str
name of the restaurant
client_id : str
internal jamix identifier used for restaurant providers
kitchen_id : str
internal jamix identifier used to assign menu content
menu_type : str
internal jamix identifier used to classify menus based on content
relevant_menus : str
menu names used for filtering out desserts etc.
"""


_Marker = namedtuple("Marker", ["letters", "explanation"])


class Marker(_Marker):
"""Dataclass for allergen information markings
Attributes
---
letters : str
allergen markings
explanation : str
extended information about the marker
"""


SKIPPED_ITEMS = [
"proteiinilisäke",
"Täysjyväriisi",
"Lämmin kasvislisäke",
"Höyryperunat",
"Tumma pasta",
"Meillä tehty perunamuusi",
]

RESTAURANTS = [
Restaurant("Foobar", 93077, 49, 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"]),
]

MARKERS = [
Marker("G", "Gluteeniton"),
Marker("M", "Maidoton"),
Marker("L", "Laktoositon"),
Marker("SO", "Sisältää soijaa"),
Marker("SE", "Sisältää selleriä"),
Marker("MU", "Munaton"),
Marker("[S], *", "Kelan korkeakouluruokailunsuosituksen mukainen"),
Marker("SIN", "Sisältää sinappia"),
Marker("<3", "Sydänmerkki"),
Marker("VEG", "Vegaani"),
]
Loading

0 comments on commit 6209c01

Please sign in to comment.