refactor: streamline kalente
module structure
All checks were successful
Python Package CI/CD / Publish to PyPI (push) Successful in 40s
All checks were successful
Python Package CI/CD / Publish to PyPI (push) Successful in 40s
Introduced a `Calendar` class to encapsulate calendar-related functionalities, removing redundant code and improving module organization. This refactor simplifies `__main__.py` by delegating calendar operations to the `Calendar` class, enhancing readability and maintainability. Adjusted import statements in `__init__.py` to reflect these changes, ensuring the module's public API remains clear and intuitive. The refactor also includes minor optimizations, such as consolidating validation logic and updating default image data, contributing to a leaner codebase. This change aims to make future extensions and maintenance of the `kalente` module more manageable, paving the way for further enhancements and features.
This commit is contained in:
parent
a8d1a8b4c3
commit
4a6c825394
5 changed files with 153 additions and 161 deletions
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "kalente"
|
name = "kalente"
|
||||||
version = "0.1.4"
|
version = "0.2.0"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Kumi Mitterer", email="kalente@kumi.email" },
|
{ name="Kumi Mitterer", email="kalente@kumi.email" },
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .classes import Calendar
|
||||||
|
|
||||||
|
__all__ = ["Calendar"]
|
|
@ -1,164 +1,16 @@
|
||||||
import holidays
|
import holidays
|
||||||
import pdfkit
|
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from datetime import date, timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
from locale import setlocale, LC_ALL
|
from locale import setlocale, LC_ALL
|
||||||
from base64 import b64encode
|
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
|
|
||||||
|
from .classes.calendar import Calendar
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
NO_LOGO = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>'
|
NO_LOGO = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||||
|
|
||||||
|
|
||||||
def get_day(
|
|
||||||
for_date: date = None,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
date_format: str = "%b %d, %Y",
|
|
||||||
):
|
|
||||||
for_date = for_date or date.today()
|
|
||||||
day = for_date
|
|
||||||
|
|
||||||
day_info = {
|
|
||||||
"date_obj": day,
|
|
||||||
"day": day.strftime("%A"),
|
|
||||||
"date": day.strftime(date_format),
|
|
||||||
"holiday": holidays.CountryHoliday(country_code, years=[for_date.year]).get(
|
|
||||||
day
|
|
||||||
),
|
|
||||||
"is_weekend": (day.weekday() in [5, 6]),
|
|
||||||
}
|
|
||||||
return day_info
|
|
||||||
|
|
||||||
|
|
||||||
def get_week(
|
|
||||||
for_date: date = None,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
date_format: str = "%b %d, %Y",
|
|
||||||
):
|
|
||||||
week_days = []
|
|
||||||
|
|
||||||
for_date = for_date or date.today()
|
|
||||||
week_start = for_date - timedelta(days=for_date.weekday())
|
|
||||||
week_end = week_start + timedelta(days=6)
|
|
||||||
|
|
||||||
if country_code:
|
|
||||||
holiday_list = holidays.CountryHoliday(
|
|
||||||
country_code, years=[for_date.year, week_end.year, week_start.year]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
holiday_list = {}
|
|
||||||
|
|
||||||
for i in range(7):
|
|
||||||
day = week_start + timedelta(days=i)
|
|
||||||
day_info = {
|
|
||||||
"date_obj": day,
|
|
||||||
"day": day.strftime("%A"),
|
|
||||||
"date": day.strftime(date_format),
|
|
||||||
"holiday": holiday_list.get(day),
|
|
||||||
"is_weekend": (day.weekday() in [5, 6]),
|
|
||||||
}
|
|
||||||
week_days.append(day_info)
|
|
||||||
return week_days
|
|
||||||
|
|
||||||
|
|
||||||
def get_month(
|
|
||||||
for_date: date = None,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
date_format: str = "%b %d, %Y",
|
|
||||||
):
|
|
||||||
for_date = for_date or date.today()
|
|
||||||
month_start = for_date.replace(day=1)
|
|
||||||
|
|
||||||
month_weeks = []
|
|
||||||
|
|
||||||
for i in range(6):
|
|
||||||
week = get_week(
|
|
||||||
for_date=month_start + timedelta(days=i * 7),
|
|
||||||
country_code=country_code,
|
|
||||||
date_format=date_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
week[0]["date_obj"].month != for_date.month
|
|
||||||
and week[-1]["date_obj"].month != for_date.month
|
|
||||||
):
|
|
||||||
break
|
|
||||||
|
|
||||||
month_weeks.append(week)
|
|
||||||
|
|
||||||
return month_weeks
|
|
||||||
|
|
||||||
|
|
||||||
def get_year(
|
|
||||||
for_date: date = None,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
date_format: str = "%b %d, %Y",
|
|
||||||
):
|
|
||||||
for_date = for_date or date.today()
|
|
||||||
year_start = for_date.replace(month=1, day=1)
|
|
||||||
|
|
||||||
year_months = []
|
|
||||||
|
|
||||||
for i in range(12):
|
|
||||||
month = get_month(
|
|
||||||
for_date=year_start.replace(month=i + 1),
|
|
||||||
country_code=country_code,
|
|
||||||
date_format=date_format,
|
|
||||||
)
|
|
||||||
year_months.append(month)
|
|
||||||
|
|
||||||
return year_months
|
|
||||||
|
|
||||||
|
|
||||||
def generate_html(
|
|
||||||
content, content_type, template_path: str = None, logo_path: str = None
|
|
||||||
):
|
|
||||||
if not template_path:
|
|
||||||
template_name = "{}.html".format(content_type)
|
|
||||||
file_loader = FileSystemLoader(Path(__file__).parent.absolute() / "templates")
|
|
||||||
else:
|
|
||||||
template_name = template_path
|
|
||||||
file_loader = FileSystemLoader()
|
|
||||||
|
|
||||||
if logo_path is None:
|
|
||||||
logo_path = Path(__file__).parent / "static" / "logo.png"
|
|
||||||
|
|
||||||
if logo_path:
|
|
||||||
# Let it throw, let it throw, let it throw...
|
|
||||||
with Path(logo_path).open("rb") as logo_file:
|
|
||||||
logo = b64encode(logo_file.read()).decode("utf-8")
|
|
||||||
mime_type = (
|
|
||||||
"image/png" if str(logo_path).endswith(".png") else "image/jpeg"
|
|
||||||
) # Doesn't matter much anyway.
|
|
||||||
data_uri = f"data:image/png;base64,{logo}"
|
|
||||||
|
|
||||||
env = Environment(loader=file_loader)
|
|
||||||
template = env.get_template(template_name)
|
|
||||||
|
|
||||||
context = {"logo": data_uri}
|
|
||||||
|
|
||||||
if content_type == "yearly":
|
|
||||||
return template.render(year=content, **context)
|
|
||||||
elif content_type == "monthly":
|
|
||||||
return template.render(month=content, **context)
|
|
||||||
elif content_type == "weekly":
|
|
||||||
return template.render(week=content, **context)
|
|
||||||
elif content_type == "daily":
|
|
||||||
return template.render(day=content, **context)
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid content type: {}".format(content_type))
|
|
||||||
|
|
||||||
|
|
||||||
def convert_html_to_pdf(content, output_filename, options=None):
|
|
||||||
options.setdefault("page-size", "A4")
|
|
||||||
options.setdefault("orientation", "Landscape")
|
|
||||||
pdfkit.from_string(content, output_filename, options=options)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -314,7 +166,7 @@ def main():
|
||||||
elif args.type == "yearly":
|
elif args.type == "yearly":
|
||||||
count = end_date.year - for_date.year + 1
|
count = end_date.year - for_date.year + 1
|
||||||
|
|
||||||
if not args.type in ["daily", "weekly", "monthly", "yearly"]:
|
if args.type not in ["daily", "weekly", "monthly", "yearly"]:
|
||||||
raise ValueError(f"Invalid calendar type: {args.type}")
|
raise ValueError(f"Invalid calendar type: {args.type}")
|
||||||
|
|
||||||
if args.no_logo:
|
if args.no_logo:
|
||||||
|
@ -322,16 +174,18 @@ def main():
|
||||||
else:
|
else:
|
||||||
logo_path = args.logo
|
logo_path = args.logo
|
||||||
|
|
||||||
|
generator = Calendar(country_code, args.date_format)
|
||||||
|
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
data = (
|
data = (
|
||||||
{
|
{
|
||||||
"daily": get_day,
|
"daily": generator.get_day,
|
||||||
"weekly": get_week,
|
"weekly": generator.get_week,
|
||||||
"monthly": get_month,
|
"monthly": generator.get_month,
|
||||||
"yearly": get_year,
|
"yearly": generator.get_year,
|
||||||
}[args.type]
|
}[args.type]
|
||||||
)(for_date, country_code, args.date_format)
|
)(for_date)
|
||||||
html_content = generate_html(data, args.type, args.template, logo_path)
|
html_content = Calendar.generate_html(data, args.type, args.template, logo_path)
|
||||||
pages.append(html_content)
|
pages.append(html_content)
|
||||||
for_date = {
|
for_date = {
|
||||||
"daily": lambda x: x["date_obj"] + timedelta(days=1),
|
"daily": lambda x: x["date_obj"] + timedelta(days=1),
|
||||||
|
@ -341,7 +195,9 @@ def main():
|
||||||
}[args.type](data)
|
}[args.type](data)
|
||||||
|
|
||||||
conversion_options = {"orientation": "Portrait"} if args.type == "daily" else {}
|
conversion_options = {"orientation": "Portrait"} if args.type == "daily" else {}
|
||||||
convert_html_to_pdf("\n".join(pages), args.output, options=conversion_options)
|
Calendar.convert_html_to_pdf(
|
||||||
|
"\n".join(pages), args.output, options=conversion_options
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
3
src/kalente/classes/__init__.py
Normal file
3
src/kalente/classes/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .calendar import Calendar
|
||||||
|
|
||||||
|
__all__ = ["Calendar"]
|
130
src/kalente/classes/calendar.py
Normal file
130
src/kalente/classes/calendar.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import holidays
|
||||||
|
import pdfkit
|
||||||
|
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
|
||||||
|
class Calendar:
|
||||||
|
def __init__(self, country_code: Optional[str] = None, date_format: str = "%b %d, %Y"):
|
||||||
|
self.country_code = country_code
|
||||||
|
self.date_format = date_format
|
||||||
|
|
||||||
|
def get_day(self, for_date: date = None):
|
||||||
|
for_date = for_date or date.today()
|
||||||
|
day = for_date
|
||||||
|
|
||||||
|
day_info = {
|
||||||
|
"date_obj": day,
|
||||||
|
"day": day.strftime("%A"),
|
||||||
|
"date": day.strftime(self.date_format),
|
||||||
|
"holiday": holidays.CountryHoliday(self.country_code, years=[for_date.year]).get(day),
|
||||||
|
"is_weekend": (day.weekday() in [5, 6]),
|
||||||
|
}
|
||||||
|
return day_info
|
||||||
|
|
||||||
|
def get_week(self, for_date: date = None):
|
||||||
|
week_days = []
|
||||||
|
|
||||||
|
for_date = for_date or date.today()
|
||||||
|
week_start = for_date - timedelta(days=for_date.weekday())
|
||||||
|
week_end = week_start + timedelta(days=6)
|
||||||
|
|
||||||
|
if self.country_code:
|
||||||
|
holiday_list = holidays.CountryHoliday(
|
||||||
|
self.country_code, years=[for_date.year, week_end.year, week_start.year]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
holiday_list = {}
|
||||||
|
|
||||||
|
for i in range(7):
|
||||||
|
day = week_start + timedelta(days=i)
|
||||||
|
day_info = {
|
||||||
|
"date_obj": day,
|
||||||
|
"day": day.strftime("%A"),
|
||||||
|
"date": day.strftime(self.date_format),
|
||||||
|
"holiday": holiday_list.get(day),
|
||||||
|
"is_weekend": (day.weekday() in [5, 6]),
|
||||||
|
}
|
||||||
|
week_days.append(day_info)
|
||||||
|
return week_days
|
||||||
|
|
||||||
|
def get_month(self, for_date: date = None):
|
||||||
|
for_date = for_date or date.today()
|
||||||
|
month_start = for_date.replace(day=1)
|
||||||
|
|
||||||
|
month_weeks = []
|
||||||
|
|
||||||
|
for i in range(6):
|
||||||
|
week = self.get_week(for_date=month_start + timedelta(days=i * 7))
|
||||||
|
|
||||||
|
if (
|
||||||
|
week[0]["date_obj"].month != for_date.month
|
||||||
|
and week[-1]["date_obj"].month != for_date.month
|
||||||
|
):
|
||||||
|
break
|
||||||
|
|
||||||
|
month_weeks.append(week)
|
||||||
|
|
||||||
|
return month_weeks
|
||||||
|
|
||||||
|
def get_year(self, for_date: date = None):
|
||||||
|
for_date = for_date or date.today()
|
||||||
|
year_start = for_date.replace(month=1, day=1)
|
||||||
|
|
||||||
|
year_months = []
|
||||||
|
|
||||||
|
for i in range(12):
|
||||||
|
month = self.get_month(for_date=year_start.replace(month=i + 1))
|
||||||
|
year_months.append(month)
|
||||||
|
|
||||||
|
return year_months
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_html(content, content_type, template_path: str = None, logo_path: str = None):
|
||||||
|
if not template_path:
|
||||||
|
template_name = "{}.html".format(content_type)
|
||||||
|
file_loader = FileSystemLoader(Path(__file__).parent.parent.absolute() / "templates")
|
||||||
|
else:
|
||||||
|
template_name = template_path
|
||||||
|
file_loader = FileSystemLoader(template_path)
|
||||||
|
|
||||||
|
if logo_path is None:
|
||||||
|
logo_path = Path(__file__).parent.parent.absolute() / "static" / "logo.png"
|
||||||
|
|
||||||
|
if str(logo_path).startswith("data:"):
|
||||||
|
data_uri = logo_path
|
||||||
|
|
||||||
|
elif logo_path:
|
||||||
|
with Path(logo_path).open("rb") as logo_file:
|
||||||
|
logo = b64encode(logo_file.read()).decode("utf-8")
|
||||||
|
mime_type = (
|
||||||
|
"image/png" if str(logo_path).endswith(".png") else "image/jpeg"
|
||||||
|
)
|
||||||
|
data_uri = f"data:{mime_type};base64,{logo}"
|
||||||
|
|
||||||
|
env = Environment(loader=file_loader)
|
||||||
|
template = env.get_template(template_name)
|
||||||
|
|
||||||
|
context = {"logo": data_uri}
|
||||||
|
|
||||||
|
if content_type == "yearly":
|
||||||
|
return template.render(year=content, **context)
|
||||||
|
elif content_type == "monthly":
|
||||||
|
return template.render(month=content, **context)
|
||||||
|
elif content_type == "weekly":
|
||||||
|
return template.render(week=content, **context)
|
||||||
|
elif content_type == "daily":
|
||||||
|
return template.render(day=content, **context)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid content type: {}".format(content_type))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_html_to_pdf(content, output_filename, options=None):
|
||||||
|
options.setdefault("page-size", "A4")
|
||||||
|
options.setdefault("orientation", "Landscape")
|
||||||
|
pdfkit.from_string(content, output_filename, options=options)
|
Loading…
Reference in a new issue