Skip to content

Commit

Permalink
Refactor core classes
Browse files Browse the repository at this point in the history
Refactor Region, Country, HolidayGenerator
Optimize emitters, utils
Use @DataClass for 'Holiday'
Add type hints
Replace date parsing with DSL
Replace custom date functions with DSL
Rearrange package structure

Signed-off-by: Thomas Lauf <[email protected]>
  • Loading branch information
lauft committed Aug 20, 2024
1 parent cfbcf34 commit 628a77f
Show file tree
Hide file tree
Showing 73 changed files with 1,314 additions and 1,310 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Refactor core classes, replace date parsing and custom date functions with DSL
- Test holidays are only defined for valid regions
- Enable holidata to be calculated outside confirmed range

Expand Down
76 changes: 38 additions & 38 deletions src/holidata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,67 @@
from .emitters import Emitter
from .holidays import *
from typing import Iterator


def get_country_for(identifier):
country_class = Country.get(identifier)

if not country_class:
raise ValueError(f"No country found for id '{identifier}'!")

return country_class()


def get_emitter_for(identifier):
emitter_class = Emitter.get(identifier)

if not emitter_class:
raise ValueError(f"Unsupported output format '{identifier}'!")

return emitter_class()


def for_locale(country_id, lang_id=None):
country = get_country_for(country_id)
lang_id = country.validate_language_or_get_default(lang_id)

return Locale(country, lang_id)
from holidata.emitters import Emitter
from holidata.holiday import Country, Holiday
from holidata.holidays import *


class Holidata:
emitter = None
holidays = None

def __init__(self, holidays, emitter=None):
def __init__(self, holidays, emitter: Emitter = None):
self.holidays = holidays
self.emitter = emitter

def __str__(self):
def __str__(self) -> str:
return self.emitter.output(self.holidays)

def formatted_as(self, format_id):
def formatted_as(self, format_id: str) -> 'Holidata':
self.emitter = get_emitter_for(format_id)

return self


class Locale:
def __init__(self, country, lang):
def __init__(self, country: Country, lang: str):
self.country = country
self.lang = lang

def get_holidays_of(self, year):
def get_holidays_of(self, year: int) -> Iterator[Holiday]:
return self.country.get_holidays_of(year, self.lang)

def holidays_of(self, year):
return Holidata(self.country.get_holidays_of(self._parse_year(year), self.lang))
def holidays_of(self, year: str) -> Holidata:
return Holidata(self.country.get_holidays_of(Locale._parse_year(year), self.lang))

@staticmethod
def _parse_year(year):
def _parse_year(year: str) -> int:
try:
return int(year)
except ValueError:
raise ValueError(f"Invalid year '{year}'! Has to be an integer.")

@property
def id(self):
def id(self) -> str:
return f"{self.lang}-{self.country.id}"


def get_country_for(identifier: str) -> Country:
country_class = Country.get(identifier)

if not country_class:
raise ValueError(f"No country found for id '{identifier}'!")

return country_class()


def get_emitter_for(identifier: str) -> Emitter:
emitter_class = Emitter.get(identifier)

if not emitter_class:
raise ValueError(f"Unsupported output format '{identifier}'!")

return emitter_class()


def for_locale(country_id: str, lang_id: str = None) -> Locale:
country = get_country_for(country_id)
lang_id = country.validate_language_or_get_default(lang_id)

return Locale(country, lang_id)
94 changes: 54 additions & 40 deletions src/holidata/emitters.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,108 @@
import csv
import io
import json
from typing import List, Dict, Any, Callable

from holidata.holiday import Holiday
from holidata.plugin import PluginMount


class Emitter(metaclass=PluginMount):
type = None
type: str = None

def __init__(self):
if self.type is None:
raise ValueError(f"Emitter {self.__class__.__name__} does not provide its type!")

@staticmethod
def get(identifier):
def get(identifier: str) -> Callable[[], 'Emitter']:
return Emitter.get_plugin(identifier, "type")

def output(self, holidays):
pass
def output(self, holidays: List[Holiday]) -> str:
raise NotImplementedError


class JsonEmitter(Emitter):
type = "json"
type: str = "json"

def output(self, holidays):
export_data = [h.as_dict() for h in holidays]
def output(self, holidays: List[Holiday]) -> str:
export_data: List[Dict[str, Any]] = [h.as_dict() for h in holidays]
export_data.sort(key=lambda x: (x["date"], x["description"], x["region"]))
return "\n".join([json.dumps(h, ensure_ascii=False, sort_keys=False, indent=None, separators=(",", ":")) for h in export_data]) + "\n"
return json.dumps(export_data, ensure_ascii=False, sort_keys=False, indent=None, separators=(",", ":")) + "\n"


class CsvEmitter(Emitter):
type = "csv"
type: str = "csv"

def output(self, holidays):
export_data = [h.as_dict() for h in holidays]
def output(self, holidays: List[Holiday]) -> str:
export_data: List[Dict[str, Any]] = [h.as_dict() for h in holidays]
export_data.sort(key=lambda x: (x["date"], x["description"], x["region"]))
result = io.StringIO()
result: io.StringIO = io.StringIO()

writer = csv.DictWriter(result,
["locale", "region", "date", "description", "type", "notes"],
quoting=csv.QUOTE_ALL,
lineterminator="\n")
writer: csv.DictWriter = csv.DictWriter(
result,
["locale", "region", "date", "description", "type", "notes"],
quoting=csv.QUOTE_ALL,
lineterminator="\n")
writer.writeheader()
writer.writerows(export_data)

return result.getvalue()


class YamlEmitter(Emitter):
type = "yaml"
type: str = "yaml"

def output(self, holidays):
export_data = [h.as_dict() for h in holidays]
@staticmethod
def _format_yaml(holiday: Dict[str, Any]) -> str:
output: str = " holiday:\n"
for key in ["locale", "region", "date", "description", "type", "notes"]:
value: Any = holiday[key]

if value is not None and value != "":
output += f" {key}: {value}\n"
else:
output += f" {key}:\n"

return output

def output(self, holidays: List[Holiday]) -> str:
export_data: List[Dict[str, Any]] = [h.as_dict() for h in holidays]
export_data.sort(key=lambda x: (x["date"], x["description"], x["region"]))

output = "%YAML 1.1\n"
output: str = "%YAML 1.1\n"
output += "---\n"
for holiday in export_data:
output += " holiday:\n"

for key in ["locale", "region", "date", "description", "type", "notes"]:
value = holiday[key]

if value is not None and value != "":
output += f" {key}: {value}\n"
else:
output += f" {key}:\n"
for holiday in export_data:
output += YamlEmitter._format_yaml(holiday)

output += "...\n"
return output


class XmlEmitter(Emitter):
type = "xml"
type: str = "xml"

def output(self, holidays):
export_data = [h.as_dict() for h in holidays]
@staticmethod
def _format_xml(holiday: Dict[str, Any]) -> str:
output: str = " <holiday>\n"

for key in ["locale", "region", "date", "description", "type", "notes"]:
value: Any = holiday[key] if key in holiday else ""
output += f" <{key}>{value if value is not None else ''}</{key}>\n"

output += " </holiday>\n"
return output

def output(self, holidays: List[Holiday]) -> str:
export_data: List[Dict[str, Any]] = [h.as_dict() for h in holidays]
export_data.sort(key=lambda x: (x["date"], x["description"], x["region"]))

output = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
output: str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
output += "<holidays>\n"

for holiday in export_data:
output += " <holiday>\n"

for key in ["locale", "region", "date", "description", "type", "notes"]:
value = holiday[key] if key in holiday else ""
output += f" <{key}>{value if value is not None else ''}</{key}>\n"

output += " </holiday>\n"
output += XmlEmitter._format_xml(holiday)

output += "</holidays>\n"
return output
Loading

0 comments on commit 628a77f

Please sign in to comment.