Source code for aladhan.data_classes

from datetime import datetime
from functools import partial
from typing import Dict, Iterable, List, Optional, Union

import pytz

from .enums import LatitudeAdjustmentMethods, MidnightModes, Schools, Shafaq
from .exceptions import (
    InvalidAdjustment,
    InvalidLatAdjMethod,
    InvalidMethod,
    InvalidMidnightMode,
    InvalidSchool,
    InvalidShafaq,
    InvalidTimezone,
    InvalidTune,
)
from .methods import ISNA, Method, all_methods

__all__ = (
    "Data",
    "Date",
    "DateType",
    "Meta",
    "Timings",
    "Tune",
    "Prayer",
    "TimingsDateArg",
    "CalendarDateArg",
    "Parameters",
    "Qibla",
    "Ism",
    "Method",
    "BaseDate",
    "NextPrayerData",
)


[docs]class Tune: """ Represents a Tune obj that is returned from API. Can be used to make an obj that will be used as a tune param in :class:`Parameters` Attributes ---------- imsak: :class:`int` The tune value for imsak. fajr: :class:`int` The tune value for fajr. sunrise: :class:`int` The tune value for sunrise. asr: :class:`int` The tune value for asr. maghrib: :class:`int` The tune value for maghrib. sunset: :class:`int` The tune value for sunset. isha: :class:`int` The tune value for isha. midnight: :class:`int` The tune value for midnight. *New in v1.0.: __iter__* """ __slots__ = ( "imsak", "fajr", "sunrise", "dhuhr", "asr", "maghrib", "sunset", "isha", "midnight", ) def __init__( self, Imsak: int = 0, Fajr: int = 0, Sunrise: int = 0, Dhuhr: int = 0, Asr: int = 0, Maghrib: int = 0, Sunset: int = 0, Isha: int = 0, Midnight: int = 0, ): self.imsak = Imsak self.fajr = Fajr self.sunrise = Sunrise self.dhuhr = Dhuhr self.asr = Asr self.maghrib = Maghrib self.sunset = Sunset self.isha = Isha self.midnight = Midnight @property def value(self): """:class:`str`: The string value that will be used to get response. Format: imsak,fajr,sunrise,dhuhr,asr,maghrib,sunset,isha,midnight """ return ( "{0.imsak},{0.fajr},{0.sunrise},{0.dhuhr},{0.asr}," "{0.maghrib},{0.sunset},{0.isha},{0.midnight}".format(self) )
[docs] @classmethod def from_str(cls, s: str) -> "Tune": """Makes a Tune obj from a value string. Returns ------- :class:`Tune` The created obj. Raises ------ :exc:`AsserationError` Invalid string format. """ args = s.split(",") assert ( len(args) == 9 ), "Invalid string format, must be in Tune.value format." return cls(*map(int, args))
def __iter__(self): yield from map(int, self.value.split(",")) def __repr__(self): return "<Tune = {}>".format(self.value) def __hash__(self): return hash(self.value)
[docs]class Qibla: """Represents a Qibla obj. Do not create this class yourself. Only get it through a getter. Attributes ---------- longitude: :class:`float` Longitude coordinate. latitude: :class:`float` Latitude coordinate. direction: :class:`float` Qibla direction. *New in v0.1.3* """ __slots__ = ("longitude", "latitude", "direction") def __init__(self, longitude: float, latitude: float, direction: float): self.longitude = longitude self.latitude = latitude self.direction = direction def __repr__(self): return "<Qibla longitude={0.longitude} latitude={0.latitude}>".format( self ) def __hash__(self): return hash((self.longitude, self.latitude, self.direction))
[docs]class Ism: """Represents an Ism obj. Do not create this class yourself. Only get it through a getter. Attributes ---------- name: :class:`str` The name in arabic. transliteration: :class:`str` The transliteration of the name. number: :class:`int` Ism's number/id. en: :class:`str` The name in english. *New in v0.1.3* """ __slots__ = ("name", "transliteration", "number", "en") def __init__(self, name: str, transliteration: str, number: int, en: dict): self.name = name self.transliteration = transliteration self.number = number self.en = en["meaning"] def __repr__(self): return "<Ism name={0.name} en={0.en}>".format(self) def __hash__(self): return hash(self.name)
[docs]class Prayer: """Represents a Prayer obj. Do not create this class yourself. Only get it through a getter. Attributes ---------- data: :class:`Data` or :class:`NextPrayerData` Source data. name: :class:`str` Prayer name. time: :class:`datetime.datetime` Prayer's time. time_utc: Optional[:class:`datetime.datetime`] Prayer's time in utc, might be None when time doesn't exist because of a daylight savings switch. str_time: :class:`str` Better looking string format for prayer's time. *New in v0.1.2: timings, time_utc* *Changed in v1.2.0*: timings is removed and replaced with data. """ __slots__ = ("data", "name", "time", "time_utc", "str_time") def __init__(self, name: str, time: str, data): d = datetime.strptime(data.date.readable, "%d %b %Y") time = time.split()[0] self.data = data self.name = name self.time = datetime.strptime(time, "%H:%M").replace( d.year, d.month, d.day ) try: self.time_utc = self.time - data.meta.timezone.utcoffset(self.time) except pytz.exceptions.NonExistentTimeError: # pragma: no cover self.time_utc = None self.str_time = self.time.strftime("%H:%M %d-%m-%Y") @property def remaining(self): """Optional[:class:`datetime.timedelta`]: remaining time for prayer for utc. *New in v0.1.2* """ return self.time_utc and self.time_utc - datetime.utcnow() def __repr__(self): return "<Prayer name={0.name!r}, time=D{0.str_time!r}>".format(self) def __hash__(self): return hash(self.name)
[docs]class CalendarDateArg: """ Class to make an obj that will be used as a date param in calendar getters Parameters ---------- year: :class:`int` Required argument for calendar's year. month: Optional[:class:`int`] If this was not giving, or 0 was giving instead it will return a whole year calendar instead which is set to by default hijri: :class:`bool` whether `year` is a hijri year or not. Default: False Attributes ---------- year: :class:`int` Calendar's year. month: :class:`int` Calendar's month, set to 0 if it wasn't given. annual: :class:`str` Whether a year calender going to be returned ot not. "true" if month was not given otherwise "false". hijri: :class:`bool` Whether `year` given is a hijri year or not. Raises ------ :exc:`ValueError` Month passed is not in 1-12 range. """ __slots__ = ("year", "month", "hijri", "annual") def __init__( self, year: int, month: Optional[int] = None, hijri: bool = False, ): if month: if month not in range(1, 13): raise ValueError( "month argument expected to be in range 1-12" " got {}".format(month) ) self.month = month self.annual = "false" else: self.month = 0 self.annual = "true" self.year = year self.hijri = hijri @property def as_dict(self): return {"year": self.year, "annual": self.annual, "month": self.month} def __hash__(self): return hash((self.year, self.annual, self.month))
[docs]class TimingsDateArg: """ Class to make an obj that will be used as a date param in timings getters Parameters ---------- date: Optional[:class:`int` or :class:`str` or :class:`datetime.datetime`] Can be either int representing the UNIX format or a str in DD-MM-YYYY format or a datetime obj. Default: current date. Attributes ---------- date: :class:`str` A date string in DD-MM-YYYY format. Raises ------ :exc:`AsserationError` Invalid date string format. """ __slots__ = "date" def __init__(self, date: Optional[Union[str, int, datetime]] = None): if date is None: date = datetime.utcnow() elif isinstance(date, int): date = datetime.utcfromtimestamp(date) if isinstance(date, datetime): date = date.strftime("%d-%m-%Y") else: # it is a str try: datetime.strptime(date, "%d-%m-%Y") except ValueError: raise ValueError( "Expected DD-MM-YYYY date format got {!r} ".format(date) ) self.date: str = date # noqa def __hash__(self): return hash(self.date)
[docs]class Parameters: """ Class to make an obj that will be used as a defaults param in getters. Parameters ---------- method: :class:`methods.Method` or :class:`int` A prayer time calculation method, you can look into all methods from :meth:`Client.get_all_methods()`. Default: ISNA (Islamic Society of North America). tune: Optional[:class:`Tune`] To offset returned timings. Default: Tune() school: :class:`int` or :class:`~enums.School` 0 for Shafi (standard), 1 for Hanafi. Default: Shafi midnightMode: :class:`int` or :class:`~enums.MidnightMode` 0 for Standard (Mid Sunset to Sunrise), 1 for Jafari (Mid Sunset to Fajr). Default: Standard timezonestring: Optional[:class:`str`] A valid timezone name as specified on https://www.php.net/manual/en/timezones.php Example: Europe/London. Calculated using the co-ordinates provided by default. *This should be used only in getters that uses co-ordinates or it will be ignored.* *New in v0.2.* latitudeAdjustmentMethod: :class:`int` or \ :class:`~enums.LatitudeAdjustmentMethod` Method for adjusting times higher latitudes. For instance, if you are checking timings in the UK or Sweden. 1 - Middle of the Night 2 - One Seventh 3 - Angle Based Default: Angle Based adjustment: :class:`int` Number of days to adjust hijri date(s) Default: 0 shafaq: :class:`str` or :class:`~aladhan.enums.Shafaq` Which Shafaq to use if the method is Moonsighting Commitee Worldwide. Default: 'general' *New in v1.2.0* Attributes ---------- method: :class:`int` Method id. method_params: Optional[dict[str, int or "null"]] Method's parameters. ``None`` if method wasn't custom. *New in v0.2.* tune: :class:`str` Tune Value. school: :class:`int` midnightMode: :class:`int` timezonestring: Optional[:class:`str`] *New in v0.2.0* latitudeAdjustmentMethod: :class:`int` adjustment: :class:`int` shafaq: :class:`str` *New in v1.2.0* Raises ------ :exc:`~aladhan.exceptions.InvalidMethod` Method id passed was not in 0-15, Or passed an integer (99) instead of Method obj for a custom method use. :exc:`~aladhan.exceptions.InvalidTune` Tune with a bad value was passed. :exc:`~aladhan.exceptions.InvalidSchool` :exc:`~aladhan.exceptions.InvalidMidnightMode` :exc:`~aladhan.exceptions.InvalidTimezone` :exc:`~aladhan.exceptions.InvalidLatAdjMethod` :exc:`~aladhan.exceptions.InvalidAdjustment` :exc:`~aladhan.exceptions.InvalidShafaq` """ __slots__ = ( "method", "method_params", "tune", "school", "midnightMode", "timezonestring", "latitudeAdjustmentMethod", "adjustment", "shfaq", ) def __init__( self, method: Union[Method, int] = ISNA, tune: Optional[Tune] = None, school: Union[int, Schools] = Schools.SHAFI, midnightMode: Union[ # noqa int, MidnightModes ] = MidnightModes.STANDARD, timezonestring: Optional[str] = None, latitudeAdjustmentMethod: Union[ # noqa int, LatitudeAdjustmentMethods ] = LatitudeAdjustmentMethods.ANGLE_BASED, adjustment: int = 0, shafaq: Union[str, Shafaq] = Shafaq.GENERAL, ): # method self.method_params = None if isinstance(method, Method): if method.id == 99: self.method_params = method.params_str method = method.id elif method == 99: raise InvalidMethod( "Pass Method object instead if you want to use custom method." " for more info https://aladhanpy.readthedocs.io/en/latest/" "api.html#aladhan.methods.Method" ) if method not in range(16) and method != 99: raise InvalidMethod( "Expected method in 0-15 range or 99 got {!r}".format(method) ) self.method: int = method # tune if tune is None: tune_val = Tune().value elif isinstance(tune, Tune): tune_val = tune.value ts = tune_val.split(",") if len(ts) != 9 or not all(x.isdigit() for x in ts): raise InvalidTune( "Invalid tune argument was passed. (tune.value = %s)" % tune_val ) else: raise InvalidTune( "'tune' argument must be `Tune` object." " got `%s` instead." % type(tune).__name__ ) self.tune: str = tune_val # school if isinstance(school, Schools): school = school.value if school not in (0, 1): raise InvalidSchool( "School argument can only be either 0 or 1 got {!r}".format( school ) ) self.school = school # midnight mode if isinstance(midnightMode, MidnightModes): midnightMode = midnightMode.value if midnightMode not in (0, 1): raise InvalidMidnightMode( "midnightMode argument can only be either 0 or 1" " got {!r}".format(midnightMode) ) self.midnightMode = midnightMode # timezone string if timezonestring and timezonestring not in pytz.all_timezones_set: raise InvalidTimezone( "Invalid timezone ({!r}).".format(timezonestring) + " https://www.php.net/manual/en/timezones.php " "for valid timezones." ) self.timezonestring: Optional[str] = timezonestring # lat adj methods if isinstance(latitudeAdjustmentMethod, LatitudeAdjustmentMethods): latitudeAdjustmentMethod = latitudeAdjustmentMethod.value if latitudeAdjustmentMethod not in (1, 2, 3): raise InvalidLatAdjMethod( "latitudeAdjustmentMethod argument can only be either 1, 2" " or 3 got {!r}".format(latitudeAdjustmentMethod) ) self.latitudeAdjustmentMethod = latitudeAdjustmentMethod # adj if not isinstance(adjustment, int): raise InvalidAdjustment( "Expected adjustment argument to be `int` got" " `{}`".format(type(adjustment).__name__) ) self.adjustment = adjustment # shafaq if isinstance(shafaq, Shafaq): shafaq = shafaq.value if shafaq not in ("general", "abyad", "ahmer"): raise InvalidShafaq( "Expected Shfaq argument to be in `aladhan.Shafaq` enums" "got `{!r}`".format(shafaq) ) self.shfaq = shafaq @property def as_dict(self): dct = { "method": self.method, "tune": self.tune, "school": self.school, "midnightMode": self.midnightMode, "latitudeAdjustmentMethod": self.latitudeAdjustmentMethod, "adjustment": self.adjustment, } if self.method == 99: dct["methodSettings"] = self.method_params if self.timezonestring: dct["timezonestring"] = self.timezonestring if self.method == 15: dct["shafaq"] = self.shfaq return dct def __hash__(self): return hash(tuple(self.as_dict.values()))
[docs]class Meta: """Represents the meta that is in returned :class:`Data` Do not create this class yourself. Only get it through a getter. Attributes ---------- data: :class:`Data` or :class:`NextPrayerData` Original fetched Data. longitude: :class:`float` Longitude coordinate. latitude: :class:`float` Latitude coordinate. timezone: :class:`pytz.UTC` Used timezone to calculate. method: Optional[:class:`Method`] Calculation Method. ``None`` if it was a custom method. latitudeAdjustmentMethod: :class:`str` midnightMode: :class:`str` school: :class:`str` offset: :class:`Tune` Used offset to tune timings. """ __slots__ = ( "data", "longitude", "latitude", "timezone", "method", "latitudeAdjustmentMethod", "midnightMode", "school", "offset", ) def __init__( self, data, longitude: float, latitude: float, timezone: str, method: dict, latitudeAdjustmentMethod: str, midnightMode: str, school: str, offset: dict, ): self.data = data self.longitude = longitude self.latitude = latitude self.timezone = pytz.timezone(timezone) self.method = all_methods.get(method.get("id")) self.latitudeAdjustmentMethod = latitudeAdjustmentMethod self.midnightMode = midnightMode self.school = school self.offset = Tune(**offset) @property def parameters(self): """:class:`Parameters`: returns a Parameters obj .. warning:: This will set method to ISNA if method was custom, and it will always set adjustment to 0. """ return Parameters( self.method or 2, self.offset, getattr(Schools, self.school.upper()), getattr(MidnightModes, self.midnightMode.upper()), self.timezone.zone, getattr( LatitudeAdjustmentMethods, self.latitudeAdjustmentMethod.upper(), ) # can't get adjustment ... ) def __repr__(self): return ( "<Meta longitude={0.longitude!r}, latitude={0.latitude!r}, " "method={0.method!r}, latitudeAdjustmentMethod=" "{0.latitudeAdjustmentMethod!r}, midnightMode={0.midnightMode!r}, " "school={0.school!r}, offset={0.offset!r}>" ).format(self) def __hash__(self): return hash( ( self.method, self.longitude, self.latitude, self.timezone, self.method, self.latitudeAdjustmentMethod, self.method, self.school, self.offset, ) )
[docs]class DateType: """A class for gregorian/hijri date. Do not create this class yourself. Only get it through a getter. Attributes ---------- name: :class:`str` gregorian or hijri. date: :class:`str` Date string. format: :class:`str` Date's format day: :class:`int` Date's day. weekday: dict[:class:`str`, :class:`str`] A dict with 2 keys, "en" and "ar" for hijri and only 1 key "en" for gregorian. month: dict[:class:`str`, :class:`int` or :class:`str`] A dict with 3 keys "number", "en", "ar" for hijri and 2 keys "number", "en" for gregorian. year: :class:`int` Date's year. designation: dict[:class:`str`, :class:`str`] A dict with 2 keys, "abbreviated" and "expanded". holidays: Optional[:class:`list` of :class:`str`] A list of holidays might be empty for hijri, always None for gregorian. lunarSighting: Optional[:class:`bool`] A boolean for gregorian and None for hijri. *New in v1.2.2* lunarSighting: Optional[:class:`bool`] A boolean for hijri and None for gregorian. *New in v1.2.2* method: Optional[:class:`str`] A string for hijri and None for gregorian. *New in v1.2.2* """ __slots__ = ( "name", "date", "format", "day", "weekday", "month", "year", "designation", "holidays", "lunarSighting", "adjustedHolidays", "method" ) def __init__( self, name: str, date: str, format: str, # noqa day: str, weekday: Dict[str, str], month: Dict[str, Union[int, str]], year: str, designation: Dict[str, str], holidays: Optional[List[str]] = None, lunarSighting: Optional[bool] = None, adjustedHolidays: Optional[list] = None, method: Optional[str] = None, ): self.name = name self.date = date self.format = format self.day = int(day) self.weekday = weekday self.month = month self.year = int(year) self.designation = designation self.holidays = holidays self.lunarSighting = lunarSighting self.adjustedHolidays = adjustedHolidays self.method = method def __repr__(self): return "<DateType name={0.name!r}, date={0.date!r}>".format(self) def __hash__(self): return hash((self.name, self.date))
[docs]class BaseDate: """ Do not create this class yourself. Only get it through a getter. *New in v1.2.0* Attributes ---------- readable: Optional[:class:`str`] Date in readable format. None if it wasn't from a timings getter. timestamp: Optional[:class:`int`] Date in UNIX format. None if it wasn't from a timings getter. """ __slots__ = "readable", "timestamp" def __init__(self, readable: Optional[str], timestamp: Optional[str]): self.readable = readable self.timestamp = timestamp and int(timestamp) def __hash__(self): return hash((self.readable, self.timestamp)) def __repr__(self): return ( "<BaseDate readable={0.readable!r} timestamp={0.timestamp!r}>" ).format(self)
[docs]class Date(BaseDate): """Represents the date that is in returned :class:`Data` Do not create this class yourself. Only get it through a getter. Attributes ---------- data: Optional[:class:`Data`] Original fetched Data. None if it wasn't from a timings getter. *Changed in v1.1.0: changed to Optional* readable: Optional[:class:`str`] Date in readable format. None if it wasn't from a timings getter. *Changed in v1.1.0: changed to Optional* timestamp: Optional[:class:`int`] Date in UNIX format. None if it wasn't from a timings getter. *Changed in v1.1.0: changed to Optional* gregorian: :class:`DateType` Gregorian date. hijri: :class:`DateType` Hijri date. """ __slots__ = ("data", "gregorian", "hijri") def __init__( self, gregorian: dict, hijri: dict, data: Optional["Data"] = None, readable: Optional[str] = None, timestamp: Optional[str] = None, ): self.data = data self.gregorian = DateType("Gregorian", **gregorian) self.hijri = DateType("Hijri", **hijri) super().__init__(readable, timestamp) def __repr__(self): return "<gregorian={0.gregorian!r}, hijri={0.hijri!r}>".format(self) def __hash__(self): return hash(self.timestamp)
[docs]class Timings: """Represents the timings that is in returned :class:`Data` Do not create this class yourself. Only get it through a getter. Attributes ---------- data: :class:`Data` Original fetched Data. imsak: :class:`Prayer` Imsak time. fajr: :class:`Prayer` Fajr prayer time. sunrise: :class:`Prayer` Sunrise time. asr: :class:`Prayer` Asr prayer time. maghrib: :class:`Prayer` Maghrib prayer time. sunset: :class:`Prayer` Sunset time. isha: :class:`Prayer` Isha prayer time. midnight: :class:`Prayer` Midnight time. first_third: :class:`Prayer` First third time. *New in v1.2.0* last_third: :class:`Prayer` Last third time. *New in v1.2.0* *New in v0.1.4: __iter__* """ __slots__ = ( "data", "imsak", "fajr", "sunrise", "dhuhr", "asr", "maghrib", "sunset", "isha", "midnight", "first_third", "last_third", ) def __init__( self, data: "Data", Imsak: str, Fajr: str, Sunrise: str, Dhuhr: str, Asr: str, Maghrib: str, Sunset: str, Isha: str, Midnight: str, Firstthird: str, Lastthird: str, ): self.data = data _Prayer = partial(Prayer, data=data) self.imsak: Prayer = _Prayer("Imsak", Imsak) self.fajr: Prayer = _Prayer("Fajr", Fajr) self.sunrise: Prayer = _Prayer("Sunrise", Sunrise) self.dhuhr: Prayer = _Prayer("Dhuhr", Dhuhr) self.asr: Prayer = _Prayer("Asr", Asr) self.sunset: Prayer = _Prayer("Sunset", Sunset) self.maghrib: Prayer = _Prayer("Maghrib", Maghrib) self.isha: Prayer = _Prayer("Isha", Isha) self.midnight: Prayer = _Prayer("Midnight", Midnight) self.first_third: Prayer = _Prayer("Firstthird", Firstthird) self.last_third: Prayer = _Prayer("Lastthird", Lastthird) @property def as_dict(self) -> Dict[str, Prayer]: """dict[:class:`str`, :class:`Prayer`]: A dict of all 5 prayers and the other times *New in v0.1.4*""" return { "Imsak": self.imsak, "Fajr": self.fajr, "Sunrise": self.sunrise, "Dhuhr": self.dhuhr, "Asr": self.asr, "Sunset": self.sunset, "Maghrib": self.maghrib, "Isha": self.isha, "Midnight": self.midnight, "Fristthird": self.first_third, "Lastthird": self.last_third, } @property def prayers_only(self) -> Dict[str, Prayer]: """dict[:class:`str`, :class:`Prayer`]: A dict of the 5 prayers.""" return { "Fajr": self.fajr, "Dhuhr": self.dhuhr, "Asr": self.asr, "Maghrib": self.maghrib, "Isha": self.isha, }
[docs] def next_prayer(self) -> Optional[Prayer]: """ Get the next upcoming prayer. Returns ``None`` if the upcoming wasn't in the date, so this will return ``None`` if it was from an old date. Returns ------- Optional[:class:`Prayer`] The upcoming prayer. *Changed in v1.0.: Returns None instead of recursive calls, and no longer awaitable.* """ meta = self.data.meta now = datetime.utcnow() now = now + meta.timezone.utcoffset(now) for key, val in self.prayers_only.items(): if now < val.time: return val return None # pragma: no cover
def __iter__(self) -> Iterable[Prayer]: yield from self.as_dict.values() def __repr__(self): return ( "<Timings imsak={0.imsak}, fajr={0.fajr}, sunrise={0.sunrise}, " "dhuhr={0.dhuhr}, asr={0.asr}, sunset={0.sunset}, " "maghrib={0.maghrib}, isha={0.isha}, midnight={0.midnight}" "first_third={0.first_third}, last_third={0.last_third}>" ).format(self) def __hash__(self): return hash(tuple(self.as_dict.values()))
[docs]class Data: """ Main class Representing the data returned from a timings request to API Do not create this class yourself. Only get it through a getter. Attributes ---------- meta: :class:`Meta` Represents the meta part. date: :class:`Date` Represents the date part. timings: :class:`Timings` Represents the timings part. client: :class:`Client` Represents the client that the Data were fetched from. """ __slots__ = ("meta", "date", "timings", "client") def __init__(self, timings: dict, date: dict, meta: dict, client): self.meta = Meta(**meta, data=self) self.date = Date(**date, data=self) self.timings = Timings(**timings, data=self) self.client = client def __repr__(self): return "<Data object | {0.gregorian.date}>".format(self.date) def __hash__(self): return hash((self.meta, self.date))
[docs]class NextPrayerData: """ Main class Representing the data returned from a next prayer request to API Do not create this class yourself. Only get it through a getter. Attributes ---------- meta: :class:`Meta` Represents the meta part. date: :class:`Date` Represents the date part. prayer: :class:`Prayer` Represents the Prayer part. client: :class:`Client` Represents the client that the Data were fetched from. *New in v1.2.0* """ def __init__(self, meta, date, timings, client): ((prayer, time),) = timings.items() self.meta = Meta(data=self, **meta) self.date = Date(**date) self.prayer = Prayer(prayer, time, data=self) self.client = client def __repr__(self): return f"<NextPrayerData | {self.prayer.name} {self.date.readable}>" def __hash__(self): return hash((self.meta, self.date))