refactor: streamline kalente module structure
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:
Kumi 2024-05-28 12:49:59 +02:00
parent a8d1a8b4c3
commit 4a6c825394
Signed by: kumi
GPG key ID: ECBCC9082395383F
5 changed files with 153 additions and 161 deletions

View file

@ -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" },
] ]

View file

@ -0,0 +1,3 @@
from .classes import Calendar
__all__ = ["Calendar"]

View file

@ -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__":

View file

@ -0,0 +1,3 @@
from .calendar import Calendar
__all__ = ["Calendar"]

View 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)