aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md6
-rw-r--r--datamaps/__init__.py2
-rw-r--r--datamaps/api/__init__.py1
-rw-r--r--datamaps/api/api.py29
-rw-r--r--datamaps/core/temporal.py124
-rw-r--r--datamaps/plugins/dft/master.py111
-rw-r--r--datamaps/tests/test_api.py35
-rw-r--r--datamaps/tests/test_month.py42
-rw-r--r--pyproject.toml2
9 files changed, 302 insertions, 50 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f13ed2..ad1a09a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## v1.1.6
+
+* Added the `Month()` object to `datamaps.api`. You can now assign a month
+ to a `Master()` object using the function
+ `datamaps.api.project_data_from_master_month`.
+
## v1.1.5
* Additional version bump to bring into line with `bcompiler-engine`.
diff --git a/datamaps/__init__.py b/datamaps/__init__.py
index 9b102be..1436d8f 100644
--- a/datamaps/__init__.py
+++ b/datamaps/__init__.py
@@ -1 +1 @@
-__version__ = "1.1.5"
+__version__ = "1.1.6"
diff --git a/datamaps/api/__init__.py b/datamaps/api/__init__.py
index 3f6a35c..947f8a6 100644
--- a/datamaps/api/__init__.py
+++ b/datamaps/api/__init__.py
@@ -1 +1,2 @@
from .api import project_data_from_master_api as project_data_from_master
+from .api import project_data_from_master_month_api as project_data_from_master_month
diff --git a/datamaps/api/api.py b/datamaps/api/api.py
index 6cc6f4e..072d95f 100644
--- a/datamaps/api/api.py
+++ b/datamaps/api/api.py
@@ -13,3 +13,32 @@ def project_data_from_master_api(master_file: str, quarter: int, year: int):
"""
m = Master(Quarter(quarter, year), master_file)
return m
+
+
+def project_data_from_master_month_api(master_file: str, month: int, year: int) -> Master:
+ """Create a Master object directly without the need to explicitly pass
+ a Month object.
+
+ Args:
+ master_file (str): the path to a master file
+ month (int): an integer representing the month
+ year (int): an integer representing the year
+ """
+ # we need to work out what Quarter we are dealing with from the month
+ if month in [1, 2, 3]:
+ quarter = 4
+ elif month in [4, 5, 6]:
+ quarter = 1
+ elif month in [7, 8, 9]:
+ quarter = 2
+ elif month in [10, 11, 12]:
+ quarter = 3
+ else:
+ pass
+ # TODO: raise exception here
+
+ # from that, we can work out what quarter year we are dealing with
+ if quarter == 4:
+ year = year - 1
+ m = Master(Quarter(quarter, year), master_file, month)
+ return m
diff --git a/datamaps/core/temporal.py b/datamaps/core/temporal.py
index ddb7004..a927b23 100644
--- a/datamaps/core/temporal.py
+++ b/datamaps/core/temporal.py
@@ -1,4 +1,67 @@
+import calendar
import datetime
+import itertools
+from typing import List
+
+MONTHS = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+]
+
+
+class Month:
+ """An object representing a calendar Month."""
+
+ _end_ints = {
+ 1: 31,
+ 2: 28,
+ 3: 31,
+ 4: 30,
+ 5: 31,
+ 6: 30,
+ 7: 31,
+ 8: 31,
+ 9: 30,
+ 10: 31,
+ 11: 30,
+ 12: 31,
+ }
+
+ def __init__(self, month: int, year: int):
+ self.month_int = month
+ self.year = year
+
+ @property
+ def start_date(self):
+ return datetime.date(self.year, self.month_int, 1)
+
+ @property
+ def end_date(self):
+ if self.month_int == 2 and calendar.isleap(self.year):
+ return datetime.date(
+ self.year, self.month_int, Month._end_ints[self.month_int] + 1
+ )
+ else:
+ return datetime.date(
+ self.year, self.month_int, Month._end_ints[self.month_int]
+ )
+
+ @property
+ def name(self):
+ return MONTHS[self.month_int - 1]
+
+ def __repr__(self):
+ return f"Month({self.name})"
class FinancialYear:
@@ -26,27 +89,23 @@ class FinancialYear:
self.end_date = self.q4.end_date
@property
- def q1(self) -> datetime.date:
- """Quarter 1 as a :py:class:`datetime.date` object
- """
+ def q1(self):
+ """Quarter 1 as a :py:class:`datetime.date` object"""
return self._q1
@property
def q2(self):
- """Quarter 2 as a :py:class:`datetime.date` object
- """
+ """Quarter 2 as a :py:class:`datetime.date` object"""
return self._q2
@property
def q3(self):
- """Quarter 3 as a :py:class:`datetime.date` object
- """
+ """Quarter 3 as a :py:class:`datetime.date` object"""
return self._q3
@property
def q4(self):
- """Quarter 4 as a :py:class:`datetime.date` object
- """
+ """Quarter 4 as a :py:class:`datetime.date` object"""
return self._q4
def __str__(self):
@@ -55,34 +114,35 @@ class FinancialYear:
def _generate_quarters(self):
self.quarters = [Quarter(x, self.year) for x in range(1, 5)]
-
def __repr__(self):
return f"FinancialYear({self.year})"
class Quarter:
- """An object representing a financial quarter. This is mainly required for building
- a :py:class:`core.master.Master` object.
+ """An object representing a financial quarter.
+
+ This is mainly required for building a :py:class:`core.master.Master`
+ object.
Args:
quarter (int): e.g.1, 2, 3 or 4
year (int): e.g. 2013
"""
+
_start_months = {
- 1: (4, 'April'),
- 2: (7, 'July'),
- 3: (10, 'October'),
- 4: (1, 'January')
+ 1: (4, "April"),
+ 2: (7, "July"),
+ 3: (10, "October"),
+ 4: (1, "January"),
}
_end_months = {
- 1: (6, 'June', 30),
- 2: (9, 'September', 30),
- 3: (12, 'December', 31),
- 4: (3, 'March', 31),
+ 1: (6, "June", 30),
+ 2: (9, "September", 30),
+ 3: (12, "December", 31),
+ 4: (3, "March", 31),
}
-
def __init__(self, quarter: int, year: int) -> None:
if isinstance(quarter, int) and (quarter >= 1 and quarter <= 4):
@@ -93,10 +153,27 @@ class Quarter:
if isinstance(year, int) and (year in range(1950, 2100)):
self.year = year
else:
- raise ValueError("Year must be between 1950 and 2100 - surely that will do?")
+ raise ValueError(
+ "Year must be between 1950 and 2100 - surely that will do?"
+ )
self.start_date = self._start_date(self.quarter, self.year)
self.end_date = self._end_date(self.quarter, self.year)
+ self.months: List[Month] = []
+ self._populate_months()
+
+ def _populate_months(self):
+ months_ints = []
+ start_int = Quarter._start_months[self.quarter][0]
+ strt = itertools.count(start_int)
+ for x in range(3):
+ months_ints.append(next(strt))
+ for m in months_ints:
+ if self.quarter == 4:
+ year = self.year + 1
+ else:
+ year = self.year
+ self.months.append(Month(m, year))
def __str__(self):
return f"Q{self.quarter} {str(self.year)[2:]}/{str(self.year + 1)[2:]}"
@@ -116,6 +193,5 @@ class Quarter:
@property
def fy(self):
- """Return a :py:class:`core.temporal.FinancialYear` object.
- """
+ """Return a :py:class:`core.temporal.FinancialYear` object."""
return FinancialYear(self.year)
diff --git a/datamaps/plugins/dft/master.py b/datamaps/plugins/dft/master.py
index 744cc27..324b63d 100644
--- a/datamaps/plugins/dft/master.py
+++ b/datamaps/plugins/dft/master.py
@@ -1,23 +1,23 @@
-import re
import datetime
import logging
+import re
import unicodedata
from pathlib import Path
-from typing import List, Tuple, Iterable, Optional, Any
+from typing import Any, Iterable, List, Optional, Tuple
+from datamaps.core.temporal import Quarter
from datamaps.plugins.dft.portfolio import project_data_from_master
from datamaps.process.cleansers import DATE_REGEX_4
-from datamaps.core.temporal import Quarter
-
from openpyxl import load_workbook
-logger = logging.getLogger('bcompiler.utils')
+logger = logging.getLogger("bcompiler.utils")
class ProjectData:
"""
ProjectData class
"""
+
def __init__(self, d: dict) -> None:
"""
:py:func:`OrderedDict` is easiest to get from project_data_from_master[x]
@@ -37,7 +37,7 @@ class ProjectData:
data = [item for item in self._data.items() if key in item[0]]
if not data:
raise KeyError("Sorry, there is no matching data")
- return (data)
+ return data
def pull_keys(self, input_iter: Iterable, flat=False) -> List[Tuple[Any, ...]]:
"""
@@ -46,19 +46,54 @@ class ProjectData:
"""
if flat is True:
# search and replace troublesome EN DASH character
- xs = [item for item in self._data.items()
- for i in input_iter if item[0].strip().replace(unicodedata.lookup('EN DASH'), unicodedata.lookup('HYPHEN-MINUS')) == i]
+ xs = [
+ item
+ for item in self._data.items()
+ for i in input_iter
+ if item[0]
+ .strip()
+ .replace(
+ unicodedata.lookup("EN DASH"), unicodedata.lookup("HYPHEN-MINUS")
+ )
+ == i
+ ]
xs = [_convert_str_date_to_object(x) for x in xs]
- ts = sorted(xs, key=lambda x: input_iter.index(x[0].strip().replace(unicodedata.lookup('EN DASH'), unicodedata.lookup('HYPHEN-MINUS'))))
+ ts = sorted(
+ xs,
+ key=lambda x: input_iter.index(
+ x[0]
+ .strip()
+ .replace(
+ unicodedata.lookup("EN DASH"),
+ unicodedata.lookup("HYPHEN-MINUS"),
+ )
+ ),
+ )
ts = [item[1] for item in ts]
return ts
else:
- xs = [item for item in self._data.items()
- for i in input_iter if item[0].replace(unicodedata.lookup('EN DASH'), unicodedata.lookup('HYPHEN-MINUS')) == i]
- xs = [item for item in self._data.items()
- for i in input_iter if item[0] == i]
+ xs = [
+ item
+ for item in self._data.items()
+ for i in input_iter
+ if item[0].replace(
+ unicodedata.lookup("EN DASH"), unicodedata.lookup("HYPHEN-MINUS")
+ )
+ == i
+ ]
+ xs = [
+ item for item in self._data.items() for i in input_iter if item[0] == i
+ ]
xs = [_convert_str_date_to_object(x) for x in xs]
- ts = sorted(xs, key=lambda x: input_iter.index(x[0].replace(unicodedata.lookup('EN DASH'), unicodedata.lookup('HYPHEN-MINUS'))))
+ ts = sorted(
+ xs,
+ key=lambda x: input_iter.index(
+ x[0].replace(
+ unicodedata.lookup("EN DASH"),
+ unicodedata.lookup("HYPHEN-MINUS"),
+ )
+ ),
+ )
return ts
def __repr__(self):
@@ -69,7 +104,7 @@ def _convert_str_date_to_object(d_str: tuple) -> Tuple[str, Optional[datetime.da
try:
if re.match(DATE_REGEX_4, d_str[1]):
try:
- ds = d_str[1].split('-')
+ ds = d_str[1].split("-")
return (d_str[0], datetime.date(int(ds[0]), int(ds[1]), int(ds[2])))
except TypeError:
return d_str
@@ -114,12 +149,21 @@ class Master:
filename = m1.filename
..etc
"""
- def __init__(self, quarter: Quarter, path: str) -> None:
+
+ def __init__(
+ self, quarter: Quarter, path: str, declared_month: Optional[int] = None
+ ) -> None:
self._quarter = quarter
+ self._declared_month = declared_month
self.path = path
+ if self._declared_month:
+ idxs = [x.month_int for x in self._quarter.months]
+ m_idx = idxs.index(self._declared_month)
+ self.year = self._quarter.months[m_idx].year
+ else:
+ self.year = self._quarter.year
self._data = project_data_from_master(self.path)
self._project_titles = [item for item in self.data.keys()]
- self.year = self._quarter.year
def __getitem__(self, project_name):
return ProjectData(self._data[project_name])
@@ -158,16 +202,37 @@ class Master:
return self._quarter
@property
- def filename(self):
- """The filename of the master xlsx file, e.g. ``master_1_2017.xlsx``.
+ def month(self):
"""
+ Returns the ``Month`` object associated with the ``Master``.
+ """
+ months = {
+ 1: "January",
+ 2: "February",
+ 3: "March",
+ 4: "April",
+ 5: "May",
+ 6: "June",
+ 7: "July",
+ 8: "August",
+ 9: "September",
+ 10: "October",
+ 11: "November",
+ 12: "December",
+ }
+ return [
+ m for m in self.quarter.months if m.name == months[self._declared_month]
+ ][0]
+
+ @property
+ def filename(self):
+ """The filename of the master xlsx file, e.g. ``master_1_2017.xlsx``."""
p = Path(self.path)
return p.name
@property
def projects(self):
- """A list of project titles derived from the master xlsx.
- """
+ """A list of project titles derived from the master xlsx."""
return self._project_titles
def duplicate_keys(self, to_log=None):
@@ -194,7 +259,9 @@ class Master:
dups.add(x)
if to_log and len(dups) > 0:
for x in dups:
- logger.warning(f"{self.path} contains duplicate key: \"{x}\". Masters cannot contain duplicate keys. Rename them.")
+ logger.warning(
+ f'{self.path} contains duplicate key: "{x}". Masters cannot contain duplicate keys. Rename them.'
+ )
return True
elif to_log and len(dups) == 0:
logger.info(f"No duplicate keys in {self.path}")
diff --git a/datamaps/tests/test_api.py b/datamaps/tests/test_api.py
index 273f984..295f1fb 100644
--- a/datamaps/tests/test_api.py
+++ b/datamaps/tests/test_api.py
@@ -1,10 +1,41 @@
import datetime
-from ..api import project_data_from_master
+from ..api import project_data_from_master, project_data_from_master_month
+from ..core.temporal import Month
def test_get_project_data(master):
master = project_data_from_master(master, 1, 2019)
- assert master["Chutney Bridge.xlsm"]["Project/Programme Name"] == "Chutney Bridge Ltd"
+ assert (
+ master["Chutney Bridge.xlsm"]["Project/Programme Name"] == "Chutney Bridge Ltd"
+ )
assert master.quarter.quarter == 1
assert master.quarter.end_date == datetime.date(2019, 6, 30)
+
+
+def test_get_project_data_using_month(master):
+ m = project_data_from_master_month(master, 7, 2021)
+ m2 = project_data_from_master_month(master, 8, 2021)
+ m3 = project_data_from_master_month(master, 9, 2021)
+ m4 = project_data_from_master_month(master, 10, 2021)
+ m5 = project_data_from_master_month(master, 2, 2021) # this is q4
+ assert m["Chutney Bridge.xlsm"]["Project/Programme Name"] == "Chutney Bridge Ltd"
+ assert isinstance(m.month, Month)
+ assert m.month.name == "July"
+ assert m2.month.name == "August"
+ assert m3.month.name == "September"
+ assert m4.month.name == "October"
+ assert m.quarter.quarter == 2
+ assert m2.quarter.quarter == 2
+ assert m3.quarter.quarter == 2
+ assert m4.quarter.quarter == 3
+ assert m.quarter.end_date == datetime.date(2021, 9, 30)
+ assert m2.quarter.end_date == datetime.date(2021, 9, 30)
+ assert m3.quarter.end_date == datetime.date(2021, 9, 30)
+ assert m4.quarter.end_date == datetime.date(2021, 12, 31)
+
+ # year should be different if using this func
+ assert m.year == 2021
+ assert m2.year == 2021
+ assert m3.year == 2021
+ assert m5.year == 2021
diff --git a/datamaps/tests/test_month.py b/datamaps/tests/test_month.py
new file mode 100644
index 0000000..de53545
--- /dev/null
+++ b/datamaps/tests/test_month.py
@@ -0,0 +1,42 @@
+import datetime
+
+import pytest
+
+from ..core.temporal import Month, Quarter
+
+
+def test_quarter_month_objects_leap_years():
+ q = Quarter(4, 2023)
+ assert q.months[1].start_date == datetime.date(2024, 2, 1)
+ assert q.months[1].end_date == datetime.date(2024, 2, 29)
+
+
+def test_quarter_objects_have_months():
+ q = Quarter(1, 2021)
+ q2 = Quarter(4, 2021)
+ assert isinstance(q.months[0], Month)
+ assert q.months[0].start_date == datetime.date(2021, 4, 1)
+ assert q.months[1].start_date == datetime.date(2021, 5, 1)
+ assert q.months[2].start_date == datetime.date(2021, 6, 1)
+ with pytest.raises(IndexError):
+ q.months[4].start_date == datetime.date(2021, 6, 1)
+ assert q2.months[0].start_date == datetime.date(2022, 1, 1)
+ assert q2.months[1].start_date == datetime.date(2022, 2, 1)
+ assert q2.months[2].start_date == datetime.date(2022, 3, 1)
+ with pytest.raises(IndexError):
+ q2.months[4].start_date == datetime.date(2022, 6, 1)
+
+
+def test_month():
+ m1 = Month(1, 2021)
+ assert m1.name == "January"
+ assert m1.year == 2021
+ m2 = Month(9, 2021)
+ assert m2.name == "September"
+ assert m2.start_date == datetime.date(2021, 9, 1)
+ assert m2.end_date == datetime.date(2021, 9, 30)
+ # test leap year
+ m3 = Month(2, 2024)
+ m4 = Month(2, 2028)
+ assert m3.end_date == datetime.date(2024, 2, 29)
+ assert m4.end_date == datetime.date(2028, 2, 29)
diff --git a/pyproject.toml b/pyproject.toml
index 4241d53..87d347a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"]
[tool.poetry]
name = "datamaps"
-version = "1.1.4"
+version = "1.1.6"
homepage = "https://github.com/hammerheadlemon/datamaps"
repository = "https://github.com/hammerheadlemon/datamaps"
license = "MIT"