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]
name = "kalente"
version = "0.1.4"
version = "0.2.0"
authors = [
{ 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 pdfkit
from argparse import ArgumentParser
from datetime import date, timedelta
from pathlib import Path
from typing import Optional
from datetime import timedelta
from locale import setlocale, LC_ALL
from base64 import b64encode
from jinja2 import Environment, FileSystemLoader
from dateutil.parser import parse
from .classes.calendar import Calendar
import math
NO_LOGO = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>'
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)
NO_LOGO = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
def main():
@ -314,7 +166,7 @@ def main():
elif args.type == "yearly":
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}")
if args.no_logo:
@ -322,16 +174,18 @@ def main():
else:
logo_path = args.logo
generator = Calendar(country_code, args.date_format)
for i in range(count):
data = (
{
"daily": get_day,
"weekly": get_week,
"monthly": get_month,
"yearly": get_year,
"daily": generator.get_day,
"weekly": generator.get_week,
"monthly": generator.get_month,
"yearly": generator.get_year,
}[args.type]
)(for_date, country_code, args.date_format)
html_content = generate_html(data, args.type, args.template, logo_path)
)(for_date)
html_content = Calendar.generate_html(data, args.type, args.template, logo_path)
pages.append(html_content)
for_date = {
"daily": lambda x: x["date_obj"] + timedelta(days=1),
@ -341,7 +195,9 @@ def main():
}[args.type](data)
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__":

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)