import functools
import warnings
from datetime import date, datetime
from dateutil import parser
from decimal import Decimal
from pyticketswitch.exceptions import InvalidParametersError
[docs]def date_range_str(start_date, end_date):
"""Convert a set of dates to string readable by the API
Args:
start_date (datetime.date): the start of the date range.
end_date (datetime.date): the end of the date range.
Returns:
str: a date range in the format of "YYYYMMDD:YYYYMMDD".
Missing either or both dates is acceptable and will return
"YYYYMMDD:", ":YYYYMMDD", ":".
Raises:
InvalidParametersError: when a start_date or end_date is specified and
it is not a datetime.date object.
"""
if start_date and not isinstance(start_date, date):
raise InvalidParametersError("start_date is not a datetime instance")
if end_date and not isinstance(end_date, date):
raise InvalidParametersError("end_date is not a datetime instance")
if start_date:
start_date = start_date.strftime('%Y%m%d')
else:
start_date = ''
if end_date:
end_date = end_date.strftime('%Y%m%d')
else:
end_date = ''
if start_date or end_date:
date_range = '{}:{}'.format(start_date, end_date)
else:
date_range = ''
return date_range
[docs]def isostr_to_datetime(date_str):
"""Convert an iso datetime string to a :py:class:`datetime.datetime` object.
Args:
date_str (str): the string to convert.
Returns:
:py:class:`datetime.datetime`: the python representation of the date
and time.
Raises:
ValueError: when the date_str is empty or None.
"""
if not date_str:
raise ValueError('{} is not a valid datetime string'.format(date_str))
dt = parser.parse(date_str)
return dt
[docs]def yyyymmdd_to_date(date_str):
"""Convert a YYYYMMDDD formated date to python :py:class:`datetime.date` object.
Args:
date_str (str): the string to convert.
Returns:
:py:class:`datetime.date`: the python representation of the date.
Raises:
ValueError: when the date_str is empty or None.
"""
if not date_str:
raise ValueError('{} is not a valid datetime string'.format(date_str))
date = datetime.strptime(date_str, '%Y%m%d')
if date:
return date.date()
[docs]def specific_dates_from_api_data(dates):
MONTHS = {
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4,
'may': 5, 'jun': 6, 'jul': 7, 'aug': 8,
'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12,
}
return [
date(int(year.split('_')[1]), MONTHS.get(month), int(day.split('_')[1]))
for year, months in dates.items()
if year.startswith('year_')
for month, days in months.items()
for day, valid in days.items()
if valid is True
]
[docs]def bitmask_to_boolean_list(mask):
"""Convert a bitmask to boolean list
Args:
mask (int): the mask returned by the API
Returns:
list: list of booleans.
"""
of_length = max(1, mask.bit_length())
return [
bool(mask >> i & 1)
for i in range(of_length)
]
[docs]def bitmask_to_numbered_list(mask):
"""Convert a bitmask to a numbered list
Args:
mask (int): the mask returned by the API
Returns:
list: list of integers
"""
if mask is None:
return []
return [
i+1
for i in range(mask.bit_length() + 1)
if bool(mask >> i & 1)
]
[docs]def get_price(data, key):
"""Extracts a price as a float from some data
Args:
data (dict): the data containing the price
key (str): the key of the target price.
Returns:
float: the price as a float.
When the dictionary is missing the requested key, returns :obj:`None`
"""
price = data.get(key)
if price is not None:
price = float(price)
return price
[docs]def add_prices(*prices):
"""Adds two or more prices together
Args:
*prices: Prices to add together. They, can be a
:py:class:`float`, :py:class:`int`,
:py:class:`Decimal <decimal.Decimal>` or :py:class:`str`
Returns:
:class:`Decimal <decimal.Decimal>`, str, float or int.
The sum of the prices, using the most specific of these types that
is used in the input arguments, where specificity is in the order
- :py:class:`Decimal <decimal.Decimal>`
- :py:class:`str`
- :py:class:`float`
- :py:class:`int`
Raises:
TypeError: when fewer than 2 prices are provided
decimal.InvalidOperation: when the string representation
of an argument cannot be parsed as a decimal
"""
if len(prices) < 2:
raise TypeError(
'add_prices expected at least 2 arguments, got {}'.format(len(prices))
)
converted = [
Decimal(str(price)) if price is not None else None
for price in prices
]
combined = sum(converted)
if any(isinstance(price, Decimal) for price in prices):
return Decimal(combined)
if any(isinstance(price, str) for price in prices):
return str(combined)
if any(isinstance(price, float) for price in prices):
return float(combined)
return int(combined)
[docs]def filter_none_from_parameters(params):
"""Removes parameters whos value is :obj:None
Args:
params (dict): dictionary of parameters to be passed to the API.
Returns:
dict: the original parameters with any parameters whos value was
:obj:`None` removed.
"""
return {
key: value
for key, value in params.items()
if value is not None
}
[docs]def deprecation_warning(message, stacklevel=2):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.warn(message,
category=DeprecationWarning,
stacklevel=stacklevel)
warnings.simplefilter('default', DeprecationWarning) # reset filter
[docs]def deprecated(func):
"""Mark a function as deprecated and raise a warning
This decorator is used to mark functions as deprecated. It will result in
a warning being emitted when the function is called.
Args:
func: the function to be wrapped
Returns:
the wrapped function that raises a warning
"""
@functools.wraps(func)
def wrapped_func(*args, **kwargs):
deprecation_warning(
"Call to deprecated function {}".format(func.__name__)
)
return func(*args, **kwargs)
# Mark the function as deprecated
wrapped_func.is_deprecated = True
return wrapped_func