2024-07-07 15:50:27 +00:00
|
|
|
"""Utility functions for Habitica."""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2025-01-13 12:20:15 +00:00
|
|
|
from dataclasses import asdict, fields
|
2024-07-07 15:50:27 +00:00
|
|
|
import datetime
|
2024-11-09 15:04:10 +00:00
|
|
|
from math import floor
|
2024-12-29 14:00:31 +00:00
|
|
|
from typing import TYPE_CHECKING
|
2024-07-07 15:50:27 +00:00
|
|
|
|
2024-10-30 03:53:49 +00:00
|
|
|
from dateutil.rrule import (
|
|
|
|
DAILY,
|
|
|
|
FR,
|
|
|
|
MO,
|
|
|
|
MONTHLY,
|
|
|
|
SA,
|
|
|
|
SU,
|
|
|
|
TH,
|
|
|
|
TU,
|
|
|
|
WE,
|
|
|
|
WEEKLY,
|
|
|
|
YEARLY,
|
|
|
|
rrule,
|
|
|
|
)
|
2024-12-29 14:00:31 +00:00
|
|
|
from habiticalib import ContentData, Frequency, TaskData, UserData
|
2024-10-30 03:53:49 +00:00
|
|
|
|
2024-07-07 15:50:27 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
def next_due_date(task: TaskData, today: datetime.datetime) -> datetime.date | None:
|
2024-07-07 15:50:27 +00:00
|
|
|
"""Calculate due date for dailies and yesterdailies."""
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
if task.everyX == 0 or not task.nextDue: # grey dailies never become due
|
2024-10-08 12:08:16 +00:00
|
|
|
return None
|
2025-01-13 12:20:15 +00:00
|
|
|
if task.frequency is Frequency.WEEKLY and not any(asdict(task.repeat).values()):
|
|
|
|
return None
|
2024-10-08 12:08:16 +00:00
|
|
|
|
2024-09-22 10:08:50 +00:00
|
|
|
if TYPE_CHECKING:
|
2024-12-29 14:00:31 +00:00
|
|
|
assert task.startDate
|
2024-09-22 10:08:50 +00:00
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
if task.isDue is True and not task.completed:
|
|
|
|
return dt_util.as_local(today).date()
|
2024-09-22 10:08:50 +00:00
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
if task.startDate > today:
|
|
|
|
if task.frequency is Frequency.DAILY or (
|
|
|
|
task.frequency in (Frequency.MONTHLY, Frequency.YEARLY) and task.daysOfMonth
|
2024-09-22 10:08:50 +00:00
|
|
|
):
|
2024-12-29 14:00:31 +00:00
|
|
|
return dt_util.as_local(task.startDate).date()
|
2024-09-22 10:08:50 +00:00
|
|
|
|
|
|
|
if (
|
2024-12-29 14:00:31 +00:00
|
|
|
task.frequency in (Frequency.WEEKLY, Frequency.MONTHLY)
|
|
|
|
and (nextdue := task.nextDue[0])
|
|
|
|
and task.startDate > nextdue
|
2024-09-22 10:08:50 +00:00
|
|
|
):
|
2024-12-29 14:00:31 +00:00
|
|
|
return dt_util.as_local(task.nextDue[1]).date()
|
2024-09-22 10:08:50 +00:00
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
return dt_util.as_local(task.nextDue[0]).date()
|
2024-07-07 15:50:27 +00:00
|
|
|
|
|
|
|
|
2024-10-30 03:53:49 +00:00
|
|
|
FREQUENCY_MAP = {"daily": DAILY, "weekly": WEEKLY, "monthly": MONTHLY, "yearly": YEARLY}
|
|
|
|
WEEKDAY_MAP = {"m": MO, "t": TU, "w": WE, "th": TH, "f": FR, "s": SA, "su": SU}
|
|
|
|
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
def build_rrule(task: TaskData) -> rrule:
|
2024-10-30 03:53:49 +00:00
|
|
|
"""Build rrule string."""
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert task.frequency
|
|
|
|
assert task.everyX
|
|
|
|
rrule_frequency = FREQUENCY_MAP.get(task.frequency, DAILY)
|
|
|
|
weekdays = [day for key, day in WEEKDAY_MAP.items() if getattr(task.repeat, key)]
|
2024-10-30 03:53:49 +00:00
|
|
|
bymonthday = (
|
2024-12-29 14:00:31 +00:00
|
|
|
task.daysOfMonth if rrule_frequency == MONTHLY and task.daysOfMonth else None
|
2024-10-30 03:53:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
bysetpos = None
|
2024-12-29 14:00:31 +00:00
|
|
|
if rrule_frequency == MONTHLY and task.weeksOfMonth:
|
|
|
|
bysetpos = task.weeksOfMonth
|
2024-10-30 03:53:49 +00:00
|
|
|
weekdays = weekdays if weekdays else [MO]
|
|
|
|
|
|
|
|
return rrule(
|
|
|
|
freq=rrule_frequency,
|
2024-12-29 14:00:31 +00:00
|
|
|
interval=task.everyX,
|
|
|
|
dtstart=dt_util.start_of_local_day(task.startDate),
|
2024-10-30 03:53:49 +00:00
|
|
|
byweekday=weekdays if rrule_frequency in [WEEKLY, MONTHLY] else None,
|
|
|
|
bymonthday=bymonthday,
|
|
|
|
bysetpos=bysetpos,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def get_recurrence_rule(recurrence: rrule) -> str:
|
|
|
|
r"""Extract and return the recurrence rule portion of an RRULE.
|
|
|
|
|
|
|
|
This function takes an RRULE representing a task's recurrence pattern,
|
|
|
|
builds the RRULE string, and extracts the recurrence rule part.
|
|
|
|
|
|
|
|
'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
recurrence : rrule
|
|
|
|
An RRULE object.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
str
|
|
|
|
The recurrence rule portion of the RRULE string, starting with 'FREQ='.
|
|
|
|
|
|
|
|
Example
|
|
|
|
-------
|
|
|
|
>>> rule = get_recurrence_rule(task)
|
|
|
|
>>> print(rule)
|
|
|
|
'FREQ=YEARLY;INTERVAL=2'
|
|
|
|
|
|
|
|
"""
|
|
|
|
return str(recurrence).split("RRULE:")[1]
|
2024-11-09 15:04:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_attribute_points(
|
2024-12-29 14:00:31 +00:00
|
|
|
user: UserData, content: ContentData, attribute: str
|
2024-11-09 15:04:10 +00:00
|
|
|
) -> dict[str, float]:
|
2024-12-29 14:00:31 +00:00
|
|
|
"""Get modifiers contributing to STR/INT/CON/PER attributes."""
|
2024-11-09 15:04:10 +00:00
|
|
|
|
|
|
|
equipment = sum(
|
2024-12-29 14:00:31 +00:00
|
|
|
getattr(stats, attribute)
|
|
|
|
for gear in fields(user.items.gear.equipped)
|
|
|
|
if (equipped := getattr(user.items.gear.equipped, gear.name))
|
|
|
|
and (stats := content.gear.flat[equipped])
|
2024-11-09 15:04:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
class_bonus = sum(
|
2024-12-29 14:00:31 +00:00
|
|
|
getattr(stats, attribute) / 2
|
|
|
|
for gear in fields(user.items.gear.equipped)
|
|
|
|
if (equipped := getattr(user.items.gear.equipped, gear.name))
|
|
|
|
and (stats := content.gear.flat[equipped])
|
|
|
|
and stats.klass == user.stats.Class
|
2024-11-09 15:04:10 +00:00
|
|
|
)
|
2024-12-29 14:00:31 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert user.stats.lvl
|
2024-11-09 15:04:10 +00:00
|
|
|
|
|
|
|
return {
|
2024-12-29 14:00:31 +00:00
|
|
|
"level": min(floor(user.stats.lvl / 2), 50),
|
2024-11-09 15:04:10 +00:00
|
|
|
"equipment": equipment,
|
|
|
|
"class": class_bonus,
|
2024-12-29 14:00:31 +00:00
|
|
|
"allocated": getattr(user.stats, attribute),
|
|
|
|
"buffs": getattr(user.stats.buffs, attribute),
|
2024-11-09 15:04:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
def get_attributes_total(user: UserData, content: ContentData, attribute: str) -> int:
|
2024-11-09 15:04:10 +00:00
|
|
|
"""Get total attribute points."""
|
|
|
|
return floor(
|
|
|
|
sum(value for value in get_attribute_points(user, content, attribute).values())
|
|
|
|
)
|
2025-01-11 20:31:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
def inventory_list(
|
|
|
|
user: UserData, content: ContentData, item_type: str
|
|
|
|
) -> dict[str, int]:
|
|
|
|
"""List inventory items of given type."""
|
|
|
|
return {
|
|
|
|
getattr(content, item_type)[k].text: v
|
|
|
|
for k, v in getattr(user.items, item_type, {}).items()
|
|
|
|
if k != "Saddle"
|
|
|
|
}
|