126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import date, datetime
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
|
|
class FieldType(StrEnum):
|
|
STRING = "string"
|
|
INTEGER = "integer"
|
|
DOUBLE = "double"
|
|
DATE = "date"
|
|
PASSWORD = "password"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class FieldDescription:
|
|
name: str
|
|
type: FieldType = FieldType.STRING
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Field:
|
|
content: Any
|
|
|
|
@classmethod
|
|
def with_content(cls, content: Any) -> "Field":
|
|
if content is None:
|
|
raise ValueError("content must not be None")
|
|
return cls(content=content)
|
|
|
|
@property
|
|
def type(self) -> FieldType:
|
|
if isinstance(self.content, bool):
|
|
return FieldType.STRING
|
|
if isinstance(self.content, int):
|
|
return FieldType.INTEGER
|
|
if isinstance(self.content, float):
|
|
return FieldType.DOUBLE
|
|
if isinstance(self.content, (date, datetime)):
|
|
return FieldType.DATE
|
|
if isinstance(self.content, (bytes, bytearray)):
|
|
return FieldType.PASSWORD
|
|
return FieldType.STRING
|
|
|
|
def as_string(self) -> str:
|
|
if isinstance(self.content, (bytes, bytearray)):
|
|
return self.content.decode("utf-8")
|
|
if isinstance(self.content, (date, datetime)):
|
|
return self.content.isoformat()
|
|
return str(self.content)
|
|
|
|
|
|
@dataclass
|
|
class FieldConfiguration:
|
|
fields: list[FieldDescription] = field(default_factory=list)
|
|
|
|
def add_field_at_end(self, field_description: FieldDescription) -> "FieldConfiguration":
|
|
return self.add_field_at_position(len(self.fields), field_description)
|
|
|
|
def add_field_at_start(self, field_description: FieldDescription) -> "FieldConfiguration":
|
|
return self.add_field_at_position(0, field_description)
|
|
|
|
def add_field_at_position(self, position: int, field_description: FieldDescription) -> "FieldConfiguration":
|
|
if self.has_field(field_description.name):
|
|
raise ValueError(f"field already exists: {field_description.name}")
|
|
position = max(0, min(position, len(self.fields)))
|
|
self.fields.insert(position, field_description)
|
|
return self
|
|
|
|
def has_field(self, name: str) -> bool:
|
|
return any(f.name == name for f in self.fields)
|
|
|
|
def get_field_description(self, name: str) -> FieldDescription | None:
|
|
return next((f for f in self.fields if f.name == name), None)
|
|
|
|
def get_field_names(self) -> list[str]:
|
|
return [f.name for f in self.fields]
|
|
|
|
|
|
@dataclass
|
|
class FieldContents:
|
|
field_config: FieldConfiguration
|
|
field_map: dict[str, Field] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
for field_description in self.field_config.fields:
|
|
self.ensure_field(field_description)
|
|
|
|
def ensure_field(self, field_description: FieldDescription) -> None:
|
|
if field_description.name in self.field_map:
|
|
return
|
|
match field_description.type:
|
|
case FieldType.INTEGER:
|
|
value = 0
|
|
case FieldType.DOUBLE:
|
|
value = 0.0
|
|
case FieldType.DATE:
|
|
value = date.today()
|
|
case FieldType.PASSWORD:
|
|
value = b""
|
|
case _:
|
|
value = ""
|
|
self.field_map[field_description.name] = Field.with_content(value)
|
|
|
|
def get_field_content_from_name(self, name: str) -> Field:
|
|
try:
|
|
return self.field_map[name]
|
|
except KeyError as exc:
|
|
raise KeyError(f"unknown field: {name}") from exc
|
|
|
|
def set_field_content_for_name(self, name: str, value: Field | Any) -> bool:
|
|
if name not in self.field_map:
|
|
return False
|
|
if not isinstance(value, Field):
|
|
value = Field.with_content(value)
|
|
expected = self.field_map[name].type
|
|
if expected != value.type and expected != FieldType.PASSWORD:
|
|
raise TypeError(f"field {name!r} expects {expected}, got {value.type}")
|
|
self.field_map[name] = value
|
|
return True
|
|
|
|
def as_value_map(self, prefix: str) -> dict[str, str]:
|
|
return {f"{prefix}::{name}": field.as_string() for name, field in self.field_map.items()}
|