diff --git a/docs/docs/getting-started/install.md b/docs/docs/getting-started/install.md index 3f2aaf113fe3..737d20e48228 100644 --- a/docs/docs/getting-started/install.md +++ b/docs/docs/getting-started/install.md @@ -23,6 +23,11 @@ docker run \ ``` +!!! tip "Default Credentials" + **Username:** changeme@email.com + + **Password:** MyPassword + ## Docker Compose with SQLite Deployment with docker-compose is the recommended method for deployment. The example below will create an instance of mealie available on port `9925` with the data volume mounted from the local directory. To use, create a docker-compose.yml file, paste the contents below and save. In the terminal run `docker-compose up -d` to start the container. diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html index 5249b0d34e31..2219e40add74 100644 --- a/docs/docs/overrides/api.html +++ b/docs/docs/overrides/api.html @@ -14,7 +14,7 @@
diff --git a/mealie/services/scraper/cleaner.py b/mealie/services/scraper/cleaner.py index 9a0dba2a1b17..7028ad590e79 100644 --- a/mealie/services/scraper/cleaner.py +++ b/mealie/services/scraper/cleaner.py @@ -1,6 +1,6 @@ import html import re -from datetime import datetime +from datetime import datetime, timedelta from typing import List from slugify import slugify @@ -10,8 +10,6 @@ class Cleaner: """A Namespace for utility function to clean recipe data extracted from a url and returns a dictionary that is ready for import into the database. Cleaner.clean is the main entrypoint - - """ @staticmethod @@ -44,7 +42,7 @@ class Cleaner: @staticmethod def category(category: str): - if isinstance(category, str): + if isinstance(category, str) and category != "": return [category] else: return [] @@ -144,7 +142,100 @@ class Cleaner: def time(time_entry): if time_entry is None: return None + elif isinstance(time_entry, timedelta): + pretty_print_timedelta(time_entry) elif isinstance(time_entry, datetime): print(time_entry) + elif isinstance(time_entry, str): + if re.match("PT.*H.*M", time_entry): + time_delta_object = parse_duration(time_entry) + return pretty_print_timedelta(time_delta_object) else: return str(time_entry) + + +# ! TODO: Cleanup Code Below + + +def parse_duration(iso_duration): + """Parses an ISO 8601 duration string into a datetime.timedelta instance. + Args: + iso_duration: an ISO 8601 duration string. + Returns: + a datetime.timedelta instance + """ + m = re.match(r"^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:.\d+)?)S)?$", iso_duration) + if m is None: + raise ValueError("invalid ISO 8601 duration string") + + days = 0 + hours = 0 + minutes = 0 + seconds = 0.0 + + # Years and months are not being utilized here, as there is not enough + # information provided to determine which year and which month. + # Python's time_delta class stores durations as days, seconds and + # microseconds internally, and therefore we'd have to + # convert parsed years and months to specific number of days. + + if m[3]: + days = int(m[3]) + if m[4]: + hours = int(m[4]) + if m[5]: + minutes = int(m[5]) + if m[6]: + seconds = float(m[6]) + + return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) + + +def pretty_print_timedelta(t, max_components=None, max_decimal_places=2): + """ + Print a pretty string for a timedelta. + For example datetime.timedelta(days=2, seconds=17280) will be printed as '2 days, 4 hours, 48 minutes'. Setting max_components to e.g. 1 will change this to '2.2 days', where the + number of decimal points can also be set. + """ + time_scales = [ + timedelta(days=365), + timedelta(days=1), + timedelta(hours=1), + timedelta(minutes=1), + timedelta(seconds=1), + timedelta(microseconds=1000), + timedelta(microseconds=1), + ] + time_scale_names_dict = { + timedelta(days=365): "year", + timedelta(days=1): "day", + timedelta(hours=1): "Hour", + timedelta(minutes=1): "Minute", + timedelta(seconds=1): "Second", + timedelta(microseconds=1000): "millisecond", + timedelta(microseconds=1): "microsecond", + } + count = 0 + txt = "" + first = True + for scale in time_scales: + if t >= scale: + count += 1 + n = t / scale if count == max_components else int(t / scale) + t -= n * scale + + n_txt = str(round(n, max_decimal_places)) + if n_txt[-2:] == ".0": + n_txt = n_txt[:-2] + txt += "{}{} {}{}".format( + "" if first else " ", + n_txt, + time_scale_names_dict[scale], + "s" if n > 1 else "", + ) + if first: + first = False + + if len(txt) == 0: + txt = "none" + return txt diff --git a/tests/unit_tests/test_cleaner.py b/tests/unit_tests/test_cleaner.py index 9c805cbb6055..8d574ddf56db 100644 --- a/tests/unit_tests/test_cleaner.py +++ b/tests/unit_tests/test_cleaner.py @@ -1,5 +1,6 @@ import json import re +from datetime import timedelta import pytest from mealie.services.scraper.cleaner import Cleaner @@ -89,3 +90,11 @@ def test_html_with_recipe_data(): assert url_validation_regex.match(recipe_data["image"]) assert len(recipe_data["recipeIngredient"]) == 13 assert len(recipe_data["recipeInstructions"]) == 4 + + +def test_time_cleaner(): + + my_time_delta = "PT2H30M" + return_delta = Cleaner.time(my_time_delta) + + assert return_delta == "2 Hours 30 Minutes"