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 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))