key_mapper.py 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. import re
  2. from typing import Tuple
  3. from pydantic import field_validator
  4. from clean_python import DomainService
  5. from clean_python import Id
  6. __all__ = ["KeyMapper"]
  7. def _maybe_coerce_int(x: str) -> Id:
  8. try:
  9. return int(x)
  10. except ValueError:
  11. return x
  12. class KeyMapper(DomainService):
  13. """Maps one or multiple ids to a string and vice versa.
  14. The mapping is configured using a python formatting string with standard
  15. {} placeholders. Additionally, the key can be prefixed with a tenant id
  16. when multitenant=True.
  17. """
  18. pattern: str = "{}"
  19. @field_validator("pattern")
  20. @classmethod
  21. def validate_pattern(cls, v):
  22. if isinstance(v, str):
  23. assert not v.startswith("/"), "pattern should not start with '/'"
  24. assert v.endswith("{}"), "pattern cannot have a suffix"
  25. try:
  26. v.format(*((2,) * v.count("{}")))
  27. except KeyError:
  28. raise ValueError("invalid pattern")
  29. return v
  30. @property
  31. def n_placeholders(self) -> int:
  32. return self.pattern.count("{}")
  33. def get_named_pattern(self, *names: str) -> str:
  34. return self.pattern.format(*[f"{{{x}}}" for x in names])
  35. @property
  36. def regex(self) -> str:
  37. return "^" + self.pattern.replace("{}", "(.+)") + "$"
  38. def to_key(self, *args: Id) -> str:
  39. assert len(args) == self.n_placeholders
  40. return self.pattern.format(*args)
  41. def to_key_prefix(self, *args: Id) -> str:
  42. return self.to_key(*(args + ("",)))
  43. def from_key(self, key: str) -> Tuple[Id, ...]:
  44. match = re.fullmatch(self.regex, key)
  45. if match is None:
  46. raise ValueError("key does not match expected pattern")
  47. return tuple(_maybe_coerce_int(x) for x in match.groups())