Source code for octo_api.products

#!/usr/bin/env python3
#
#  products.py
"""
Classes to model products and tariffs.
"""
#
#  Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
from datetime import datetime
from itertools import chain
from typing import Any, Dict, Iterable, List, MutableMapping, NamedTuple, Optional, TypeVar, Union

# 3rd party
import attr
from attr_utils.pprinter import register_pretty
from attr_utils.serialise import serde
from domdf_python_tools.doctools import prettify_docstrings
from domdf_python_tools.stringlist import DelimitedList

# this package
from octo_api.utils import add_repr, from_iso_zulu

__all__ = [
		"BaseProduct",
		"Product",
		"DetailedProduct",
		"Tariff",
		"RateInfo",
		"RegionalTariffs",
		"RegionalQuotes",
		]


def _term_converter(term: Optional[int]) -> Optional[int]:
	"""
	Converter function for ``term`` in :class:`~.BaseProduct``.

	:param term: The number of months that a product lasts for if it is fixed length.
	"""

	if term is None:
		return None
	else:
		return int(term)


def _links_converter(iterable: Iterable[MutableMapping[str, Any]]) -> List[MutableMapping[str, Any]]:
	return list(iterable)


[docs]@serde @prettify_docstrings @add_repr @attr.s(slots=True, frozen=True, repr=False) class BaseProduct: """ Represents an Octopus Energy product. """ #: The date from which the product is available. available_from: Optional[datetime] = attr.ib(converter=from_iso_zulu) #: The date until which the product is available. available_to: Optional[datetime] = attr.ib(converter=from_iso_zulu) #: The brand under which the product is sold. brand: str = attr.ib(converter=str) #: The code of the product. code: str = attr.ib(converter=str) #: A description of the product. description: str = attr.ib(converter=str) #: The display name of the product. display_name: str = attr.ib(converter=str) #: The name of the product. full_name: str = attr.ib(converter=str) #: Whether the product is for businesses. is_business: bool = attr.ib(converter=bool) #: Whether the product is green. is_green: bool = attr.ib(converter=bool) #: Whether the product is prepay. is_prepay: bool = attr.ib(converter=bool) #: Whether the product is restricted. is_restricted: bool = attr.ib(converter=bool) #: Whether the product tracks the wholesale electricity rate. is_tracker: bool = attr.ib(converter=bool) #: Whether the product has a variable tariff. is_variable: bool = attr.ib(converter=bool) #: Links associated with this product. links: List[MutableMapping[str, Any]] = attr.ib(converter=_links_converter) #: The number of months that a product lasts for if it is fixed length. term: Optional[int] = attr.ib(converter=_term_converter)
[docs]@prettify_docstrings @attr.s(slots=True, frozen=True, repr=False) class Product(BaseProduct): """ Represents an Octopus Energy product. """ #: The direction of the product (supply to the customer or supply to the grid). direction: str = attr.ib(converter=str)
def _parse_tariffs(tariffs_dict: Dict[str, Dict[str, Dict[str, Any]]]) -> "RegionalTariffs": """ Parse tariff data for a :class:`~.DetailedProduct`. :param tariffs_dict: """ tariffs: RegionalTariffs = RegionalTariffs() for gsp, payment_methods in tariffs_dict.items(): tariffs[gsp] = {} for method, tariff in payment_methods.items(): tariffs[gsp][method] = Tariff(**tariff) return tariffs def _parse_quotes(quotes_dict: Dict[str, Dict[str, Dict[str, Any]]]) -> "RegionalQuotes": """ Parse quote data for a :class:`~.DetailedProduct`. :param quotes_dict: """ quotes: RegionalQuotes = RegionalQuotes() for gsp, payment_methods in quotes_dict.items(): quotes[gsp] = {} for method, fuels in payment_methods.items(): quotes[gsp][method] = {} for fuel, quote in fuels.items(): quotes[gsp][method][fuel] = Quote(**quote) return quotes
[docs]@serde @prettify_docstrings @attr.s(slots=True, frozen=True, repr=False) class DetailedProduct(BaseProduct): """ Represents an Octopus Energy product, with detailed tariff information. Each ``*_tariffs`` object will have up to 14 keys; one for each GSP. For each GSP the applicable tariffs are listed under their associated payment method, e.g. direct_debit_monthly. * The ``standard_unit_rate_*`` values are listed in p/kWh (pence per kilowatt hour). * The ``standing_charge_*`` values are listed in p/day (pence per day). * The ``annual_cost_*`` values are listed in p (pence). """ # Historical charges can be browsed using the URLs contained under the key links. #: tariffs_active_at: Optional[datetime] = attr.ib(converter=from_iso_zulu) #: Mapping of GSPs to applicable tariffs for each payment method, e.g. direct_debit_monthly. single_register_electricity_tariffs: Dict = attr.ib(converter=_parse_tariffs) #: Mapping of GSPs to applicable tariffs for each payment method, e.g. direct_debit_monthly. dual_register_electricity_tariffs: Dict = attr.ib(converter=_parse_tariffs) #: Mapping of GSPs to applicable tariffs for each payment method, e.g. direct_debit_monthly. single_register_gas_tariffs: Dict = attr.ib(converter=_parse_tariffs) #: sample_quotes: Dict = attr.ib(converter=_parse_quotes) #: sample_consumption: Dict = attr.ib()
[docs]@serde @prettify_docstrings @add_repr @attr.s(slots=True, frozen=True, repr=False) class Tariff: """ Represents a tariff for a product. """ #: The tariff code. code: str = attr.ib(converter=str) #: In p/day (pence per day). standing_charge_exc_vat: float = attr.ib(converter=float) #: In p/day (pence per day). standing_charge_inc_vat: float = attr.ib(converter=float) online_discount_exc_vat: int = attr.ib(converter=int) online_discount_inc_vat: int = attr.ib(converter=int) dual_fuel_discount_exc_vat: int = attr.ib(converter=int) dual_fuel_discount_inc_vat: int = attr.ib(converter=int) exit_fees_exc_vat: int = attr.ib(converter=int) exit_fees_inc_vat: int = attr.ib(converter=int) links: List[Dict[str, Any]] = attr.ib(converter=list) #: In p/kWh (pence per kilowatt hour). standard_unit_rate_exc_vat: Optional[float] = attr.ib(default=None) #: In p/kWh (pence per kilowatt hour). standard_unit_rate_inc_vat: Optional[float] = attr.ib(default=None) #: In p/kWh (pence per kilowatt hour). day_unit_rate_exc_vat: Optional[float] = attr.ib(default=None) #: In p/kWh (pence per kilowatt hour). day_unit_rate_inc_vat: Optional[float] = attr.ib(default=None) #: In p/kWh (pence per kilowatt hour). night_unit_rate_exc_vat: Optional[float] = attr.ib(default=None) #: In p/kWh (pence per kilowatt hour). night_unit_rate_inc_vat: Optional[float] = attr.ib(default=None)
@prettify_docstrings class Quote(NamedTuple): """ Represents a quote for a product. """ annual_cost_inc_vat: float annual_cost_exc_vat: float
[docs]@serde @prettify_docstrings @add_repr @attr.s(slots=True, frozen=True, repr=False) class RateInfo: """ Represents the unit rate of a tariff at a particular period in time. """ #: In p/kWh (pence per kilowatt hour). value_exc_vat: float = attr.ib(converter=float) #: In p/kWh (pence per kilowatt hour). value_inc_vat: float = attr.ib(converter=float) #: The date and time from which this rate is in effect. valid_from: datetime = attr.ib(converter=from_iso_zulu) #: The date and time until which this rate is in effect, or :py:obj:`None` if this rate continues in perpetuity. valid_to: Optional[datetime] = attr.ib(converter=from_iso_zulu)
_T = TypeVar("_T") def _sortedset(iterable: Iterable[str]) -> DelimitedList: return DelimitedList(sorted(set(iterable)))
[docs]@prettify_docstrings class RegionalTariffs(Dict[str, Dict[str, Tariff]]): """ Mapping of GSP regions to a mapping of payment methods to :class:`Tariffs <.Tariff>`. """
[docs] def __str__(self) -> str: payment_methods = _sortedset(chain.from_iterable(k.keys() for k in self.values())) return f"{self.__class__.__name__}(['{payment_methods:, }'])"
@prettify_docstrings class RegionalQuotes(Dict[str, Dict[str, Dict[str, Quote]]]): """ Mapping of GSP regions to a mapping of payment methods to a mapping of fuel types to :class:`Quotes <.Quote>`. """ def __str__(self) -> str: fuel_types = _sortedset( chain.from_iterable(kk.keys() for kk in chain.from_iterable(k.values() for k in self.values())) ) return f"{self.__class__.__name__}([{', '.join(fuel_types)}])" @register_pretty(RegionalQuotes) @register_pretty(RegionalTariffs) def pretty_regional_tariffs(value: Union[RegionalTariffs, RegionalQuotes], ctx: Any) -> str: return str(value)