#!/usr/bin/env python3
#
# utils.py
"""
Utility functions.
"""
#
# 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
import sys
import textwrap
from typing import Any, Dict, NamedTuple, Optional, Type, Union
# 3rd party
import attr
import prettyprinter # type: ignore[import]
from domdf_python_tools.doctools import prettify_docstrings
from domdf_python_tools.stringlist import StringList
from enum_tools import StrEnum
__all__ = [
"from_iso_zulu",
"RateType",
"Region",
"MeterPointDetails",
"add_repr",
]
# stdlib
from datetime import datetime, timedelta, timezone
if sys.version_info[:2] < (3, 7):
# 3rd party
from backports.datetime_fromisoformat import MonkeyPatch # nodep
MonkeyPatch.patch_fromisoformat()
# def format_datetime(dt: datetime) -> str:
# """
# Format a :class:`datetime.datetime` object to a string in
# :wikipedia:`ISO 8601` format.
#
# :param dt:
# """
#
# return dt.strftime("%Y-%m-%dT:")
[docs]def from_iso_zulu(the_datetime: Union[str, datetime, None]) -> Optional[datetime]:
"""
Constructs a :class:`datetime.datetime` object from an
`ISO 8601 <https://en.wikipedia.org/wiki/ISO_8601>`_ format string.
This function understands the character ``Z`` as meaning Zulu time (GMT/UTC).
:param the_datetime:
""" # noqa: D400
if the_datetime is None:
return the_datetime
elif isinstance(the_datetime, datetime):
return the_datetime
else:
return datetime.fromisoformat(the_datetime.replace('Z', "+00:00"))
[docs]class RateType(StrEnum):
"""
Enumeration of different rate types.
"""
StandingCharge = "standing-charges"
StandardUnitRate = "standard-unit-rates"
DayUnitRate = "day-unit-rates"
NightUnitRate = "night-unit-rates"
[docs]class Region(StrEnum):
"""
Enumeration of different electricity supply regions.
The different regions can be seen on the following map:
.. image:: pes_boundaries.png
:width: 300
:alt: Electricity Regions
"""
Eastern = "_A" # Eastern Electricity
EastMidlands = "_B" # East Midlands Electricity
London = "_C" # London Electricity
Merseyside = "_D" # Merseyside and North Wales Electricity Board
NorthWales = "_D" # Merseyside and North Wales Electricity Board
Midlands = "_E" # Midlands Electricity
NorthEastern = "_F" # North Eastern Electricity Board
NorthWestern = "_G" # North Western Electricity Board
Southern = "_H" # Southern Electric
SouthEastern = "_J" # South Eastern Electricity Board
SouthWales = "_K" # South Wales Electricity
SouthWestern = "_L" # South Western Electricity
Yorkshire = "_M" # Yorkshire Electricity
SouthScotland = "_N" # South of Scotland Electricity Board
NorthScotland = "_P" # North of Scotland Hydro Board
[docs]@prettify_docstrings
class MeterPointDetails(NamedTuple):
"""
Information about a meter point.
:param mpan: The meter point access number.
:param gsp: The grid supply point/region that the meter point is located in.
:param profile_class: The profile class of the meter point.
* **Profile Class 1** -- Domestic Unrestricted Customers
* **Profile Class 2** -- Domestic Economy 7 Customers
* **Profile Class 3** -- Non-Domestic Unrestricted Customers
* **Profile Class 4** -- Non-Domestic Economy 7 Customers
* **Profile Class 5** -- Non-Domestic Maximum Demand (MD) Customers with a Peak Load Factor (LF) of less than 20%
* **Profile Class 6** -- Non-Domestic Maximum Demand Customers with a Peak Load Factor between 20% and 30%
* **Profile Class 7** -- Non-Domestic Maximum Demand Customers with a Peak Load Factor between 30% and 40%
* **Profile Class 8** -- Non-Domestic Maximum Demand Customers with a Peak Load Factor over 40%
Information from https://www.elexon.co.uk/knowledgebase/profile-classes/
.. seealso:: `Load Profiles and their use in Electricity Settlement <https://www.elexon.co.uk/documents/training-guidance/bsc-guidance-notes/load-profiles/>`_ by Elexon
"""
mpan: str
gsp: Region
profile_class: int
@classmethod
def _from_dict(cls, octopus_dict: Dict[str, Any]) -> "MeterPointDetails":
return MeterPointDetails(
mpan=str(octopus_dict["mpan"]),
gsp=Region(octopus_dict["gsp"]),
profile_class=int(octopus_dict["profile_class"]),
)
#: The British Summer Time timezone (UTC+1).
bst = timezone(timedelta(seconds=3600))
#: The Greenwich Mean Time timezone (aka UTC).
gmt = timezone.utc
utc = gmt
[docs]def add_repr(cls: Type) -> Type:
"""
Add a pretty-printed ``__repr__`` function to the decorated attrs class.
:param cls:
.. seealso:: :func:`attr_utils.pprinter.pretty_repr`.
"""
if attr.has(cls):
def __repr__(self) -> str:
buf = StringList()
buf.indent_type = " "
buf.append(f"{self.__class__.__module__}.{self.__class__.__qualname__}(")
with buf.with_indent_size(1):
for attrib in attr.fields(self.__class__):
value = getattr(self, attrib.name)
if isinstance(value, datetime):
buf.append(f"{attrib.name}={value.isoformat()!r},")
elif isinstance(value, str):
lines = textwrap.wrap(value, width=80 - len(attrib.name) - 1)
buf.append(f"{attrib.name}={lines.pop(0)!r}")
for line in lines:
buf.append(' ' * len(attrib.name) + ' ' + repr(line))
buf[-1] = f"{buf[-1][len(buf.indent_type) * buf.indent_size:]},"
elif value is None:
buf.append(f"{attrib.name}=None,")
else:
buf.append(f"{attrib.name}={prettyprinter.pformat(value)},")
buf.append(')')
return str(buf)
__repr__.__doc__ = f"Return a string representation of the :class:`~.{cls.__name__}`."
cls.__repr__ = __repr__ # type: ignore[assignment]
cls.__repr__.__qualname__ = f"{cls.__name__}.__repr__"
cls.__repr__.__module__ = cls.__module__
return cls