-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement adapters for settings customization
- Loading branch information
Showing
13 changed files
with
276 additions
and
124 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from __future__ import annotations | ||
|
||
from django.conf import settings | ||
from django.http import HttpRequest | ||
from django.utils.module_loading import import_string | ||
|
||
from lippukala.adapter.base import LippukalaAdapter | ||
|
||
try: | ||
from functools import cache | ||
except ImportError: # Remove this when deprecating Python 3.9 support | ||
from functools import lru_cache | ||
|
||
cache = lru_cache(maxsize=None) | ||
|
||
DEFAULT_ADAPTER_REFERENCE = "lippukala.adapter.default.DefaultLippukalaAdapter" | ||
|
||
|
||
@cache | ||
def get_adapter_class() -> type[LippukalaAdapter]: | ||
adapter_class_name = getattr(settings, "LIPPUKALA_ADAPTER_CLASS", DEFAULT_ADAPTER_REFERENCE) | ||
return import_string(adapter_class_name) | ||
|
||
|
||
def get_adapter(request: HttpRequest) -> LippukalaAdapter: | ||
return get_adapter_class()(request) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from __future__ import annotations | ||
|
||
from abc import ABCMeta, abstractmethod | ||
|
||
from django.http import HttpRequest | ||
|
||
IMPLEMENT_IN_A_SUBCLASS = "Implement in a subclass" | ||
|
||
|
||
class LippukalaAdapter(metaclass=ABCMeta): | ||
def __init__(self, request: HttpRequest | None) -> None: | ||
self.request = request | ||
|
||
@abstractmethod | ||
def get_prefixes(self) -> dict[str, str]: | ||
raise NotImplementedError(IMPLEMENT_IN_A_SUBCLASS) | ||
|
||
@abstractmethod | ||
def get_literate_keyspace(self, prefix: str | None) -> list[str] | None: | ||
raise NotImplementedError(IMPLEMENT_IN_A_SUBCLASS) | ||
|
||
@abstractmethod | ||
def get_code_digit_range(self, prefix: str) -> range: | ||
raise NotImplementedError(IMPLEMENT_IN_A_SUBCLASS) | ||
|
||
@abstractmethod | ||
def get_code_allow_leading_zeroes(self, prefix: str) -> bool: | ||
raise NotImplementedError(IMPLEMENT_IN_A_SUBCLASS) | ||
|
||
@abstractmethod | ||
def get_print_logo_path(self, prefix: str) -> str | None: | ||
raise NotImplementedError(IMPLEMENT_IN_A_SUBCLASS) | ||
|
||
@abstractmethod | ||
def get_print_logo_size_cm(self, prefix: str) -> tuple[float, float]: | ||
raise NotImplementedError(IMPLEMENT_IN_A_SUBCLASS) | ||
|
||
def get_prefix_may_be_blank(self) -> bool: | ||
return not self.get_prefixes() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
from string import digits | ||
|
||
from django.conf import settings | ||
from django.core.exceptions import ImproperlyConfigured | ||
|
||
from lippukala.adapter.base import LippukalaAdapter | ||
|
||
|
||
def get_setting(name, default=None): | ||
return getattr(settings, f"LIPPUKALA_{name}", default) | ||
|
||
|
||
def get_integer_setting(name, default=0): | ||
try: | ||
value = get_setting(name, default) | ||
return int(value) | ||
except ValueError: # pragma: no cover | ||
raise ImproperlyConfigured(f"LIPPUKALA_{name} must be an integer (got {value!r})") | ||
|
||
|
||
class LippukalaSettings: | ||
def __init__(self) -> None: | ||
self.prefixes = get_setting("PREFIXES", {}) | ||
self.literate_keyspaces = get_setting("LITERATE_KEYSPACES", {}) | ||
self.code_min_n_digits = get_integer_setting("CODE_MIN_N_DIGITS", 10) | ||
self.code_max_n_digits = get_integer_setting("CODE_MAX_N_DIGITS", 10) | ||
self.code_allow_leading_zeroes = bool(get_setting("CODE_ALLOW_LEADING_ZEROES", True)) | ||
self.print_logo_path = get_setting("PRINT_LOGO_PATH") | ||
self.print_logo_size_cm = get_setting("PRINT_LOGO_SIZE_CM") | ||
|
||
if self.prefixes: | ||
self.prefix_choices = [(p, f"{p} [{t}]") for (p, t) in sorted(self.prefixes.items())] | ||
self.prefix_may_be_blank = False | ||
else: | ||
self.prefix_choices = [("", "---")] | ||
self.prefix_may_be_blank = True | ||
|
||
def validate(self) -> None: # pragma: no cover | ||
self._validate_code() | ||
self._validate_prefixes() | ||
self._validate_print() | ||
|
||
def _validate_code(self) -> None: | ||
if self.code_min_n_digits <= 5 or self.code_max_n_digits < self.code_min_n_digits: | ||
raise ImproperlyConfigured( | ||
f"The range ({self.code_min_n_digits} .. {self.code_max_n_digits}) for " | ||
f"Lippukala code digits is invalid" | ||
) | ||
|
||
def _validate_prefixes(self): | ||
key_lengths = [len(k) for k in self.prefixes] | ||
if key_lengths and not all(k == key_lengths[0] for k in key_lengths): | ||
raise ImproperlyConfigured("All LIPPUKALA_PREFIXES keys must be the same length!") | ||
for prefix in self.prefixes: | ||
if not all(c in digits for c in prefix): | ||
raise ImproperlyConfigured( | ||
f"The prefix {prefix!r} has invalid characters. Only digits are allowed." | ||
) | ||
for prefix, literate_keyspace in list(self.literate_keyspaces.items()): | ||
if isinstance(literate_keyspace, str): | ||
raise ImproperlyConfigured( | ||
f"A string ({literate_keyspace!r}) was passed as the " | ||
f"literate keyspace for prefix {prefix!r}" | ||
) | ||
too_short_keys = any(len(key) <= 1 for key in literate_keyspace) | ||
maybe_duplicate = len(set(literate_keyspace)) != len(literate_keyspace) | ||
if too_short_keys or maybe_duplicate: | ||
raise ImproperlyConfigured( | ||
f"The literate keyspace for prefix {prefix!r} has invalid or duplicate entries." | ||
) | ||
|
||
def _validate_print(self): | ||
if not self.print_logo_path: | ||
return | ||
if not os.path.isfile(self.print_logo_path): | ||
raise ImproperlyConfigured( | ||
f"PRINT_LOGO_PATH was defined, but does not exist ({self.print_logo_path!r})" | ||
) | ||
if not all(float(s) > 0 for s in self.print_logo_size_cm): | ||
raise ImproperlyConfigured(f"PRINT_LOGO_SIZE_CM values not valid: {self.print_logo_size_cm!r}") | ||
|
||
|
||
class DefaultLippukalaAdapter(LippukalaAdapter): | ||
_settings: LippukalaSettings | None = None | ||
|
||
@classmethod | ||
def get_settings(cls) -> LippukalaSettings: | ||
if not cls._settings: | ||
cls._settings = LippukalaSettings() | ||
cls._settings.validate() | ||
return cls._settings | ||
|
||
def get_prefixes(self) -> dict[str, str]: | ||
return self.get_settings().prefixes | ||
|
||
def get_literate_keyspace(self, prefix: str | None) -> list[str] | None: | ||
literate_keyspaces = self.get_settings().literate_keyspaces | ||
return literate_keyspaces.get(prefix) | ||
|
||
def get_code_digit_range(self, prefix: str) -> range: | ||
s = self.get_settings() | ||
return range(s.code_min_n_digits, s.code_max_n_digits + 1) | ||
|
||
def get_code_allow_leading_zeroes(self, prefix: str) -> bool: | ||
return self.get_settings().code_allow_leading_zeroes | ||
|
||
def get_print_logo_path(self, prefix: str) -> str | None: | ||
return self.get_settings().print_logo_path | ||
|
||
def get_print_logo_size_cm(self, prefix: str) -> tuple[float, float]: | ||
return self.get_settings().print_logo_size_cm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from __future__ import annotations | ||
|
||
from lippukala.adapter import LippukalaAdapter | ||
|
||
|
||
class AdapterMixin: | ||
_adapter: LippukalaAdapter | None = None | ||
|
||
def get_adapter(self) -> LippukalaAdapter: | ||
if not self._adapter: | ||
raise ValueError(f"An adapter needs to be set on {self.__class__.__name__}") | ||
return self._adapter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,4 @@ | ||
import os | ||
from string import digits | ||
|
||
from django.conf import settings | ||
from django.core.exceptions import ImproperlyConfigured | ||
|
||
|
||
def get_setting(name, default=None): | ||
return getattr(settings, f"LIPPUKALA_{name}", default) | ||
|
||
|
||
def get_integer_setting(name, default=0): | ||
try: | ||
value = get_setting(name, default) | ||
return int(value) | ||
except ValueError: # pragma: no cover | ||
raise ImproperlyConfigured(f"LIPPUKALA_{name} must be an integer (got {value!r})") | ||
|
||
|
||
PREFIXES = get_setting("PREFIXES", {}) | ||
LITERATE_KEYSPACES = get_setting("LITERATE_KEYSPACES", {}) | ||
CODE_MIN_N_DIGITS = get_integer_setting("CODE_MIN_N_DIGITS", 10) | ||
CODE_MAX_N_DIGITS = get_integer_setting("CODE_MAX_N_DIGITS", 10) | ||
CODE_ALLOW_LEADING_ZEROES = bool(get_setting("CODE_ALLOW_LEADING_ZEROES", True)) | ||
PRINT_LOGO_PATH = get_setting("PRINT_LOGO_PATH") | ||
PRINT_LOGO_SIZE_CM = get_setting("PRINT_LOGO_SIZE_CM") | ||
|
||
if PREFIXES: | ||
PREFIX_CHOICES = [(p, f"{p} [{t}]") for (p, t) in sorted(PREFIXES.items())] | ||
PREFIX_MAY_BE_BLANK = False | ||
else: | ||
PREFIX_CHOICES = [("", "---")] | ||
PREFIX_MAY_BE_BLANK = True | ||
|
||
|
||
def validate_settings(): # pragma: no cover | ||
_validate_code() | ||
_validate_prefixes() | ||
_validate_print() | ||
|
||
|
||
def _validate_code(): | ||
if CODE_MIN_N_DIGITS <= 5 or CODE_MAX_N_DIGITS < CODE_MIN_N_DIGITS: | ||
raise ImproperlyConfigured( | ||
"The range (%d .. %d) for Lippukala code digits is invalid" | ||
% (CODE_MIN_N_DIGITS, CODE_MAX_N_DIGITS) | ||
) | ||
|
||
|
||
def _validate_prefixes(): | ||
key_lengths = [len(k) for k in PREFIXES] | ||
if key_lengths and not all(k == key_lengths[0] for k in key_lengths): | ||
raise ImproperlyConfigured("All LIPPUKALA_PREFIXES keys must be the same length!") | ||
for prefix in PREFIXES: | ||
if not all(c in digits for c in prefix): | ||
raise ImproperlyConfigured( | ||
f"The prefix {prefix!r} has invalid characters. Only digits are allowed." | ||
) | ||
for prefix, literate_keyspace in list(LITERATE_KEYSPACES.items()): | ||
if isinstance(literate_keyspace, str): | ||
raise ImproperlyConfigured( | ||
f"A string ({literate_keyspace!r}) was passed as the literate keyspace for prefix {prefix!r}" | ||
) | ||
too_short_keys = any(len(key) <= 1 for key in literate_keyspace) | ||
maybe_duplicate = len(set(literate_keyspace)) != len(literate_keyspace) | ||
if too_short_keys or maybe_duplicate: | ||
raise ImproperlyConfigured( | ||
f"The literate keyspace for prefix {prefix!r} has invalid or duplicate entries." | ||
) | ||
|
||
|
||
def _validate_print(): | ||
if PRINT_LOGO_PATH: | ||
if not os.path.isfile(PRINT_LOGO_PATH): | ||
raise ImproperlyConfigured( | ||
f"PRINT_LOGO_PATH was defined, but does not exist ({PRINT_LOGO_PATH!r})" | ||
) | ||
if not all(float(s) > 0 for s in PRINT_LOGO_SIZE_CM): | ||
raise ImproperlyConfigured(f"PRINT_LOGO_SIZE_CM values not valid: {PRINT_LOGO_SIZE_CM!r}") | ||
|
||
|
||
validate_settings() | ||
del validate_settings # aaaand it's gone | ||
raise NotImplementedError( | ||
"Do not import anything from `lippukala.settings`! " | ||
"Please migrate your code to use LippukalaAdapter subclasses." | ||
) |
Oops, something went wrong.