"""Base constructs for build package objects
"""
import re
import string
import json
from typing import (
Optional,
TypeVar,
Generic,
Any,
Union,
AbstractSet,
Iterable,
)
from collections.abc import Mapping, KeysView, ValuesView, ItemsView
from collections import Counter
from pydantic import BaseModel, Field
from pydantic.generics import GenericModel
from bgameb.errors import ComponentNameError, ComponentClassError
from loguru._logger import Logger
from loguru import logger
logger.disable('bgameb')
[docs]def log_enable(
log_path: str = './logs/game.log',
log_level: str = 'DEBUG'
) -> None:
"""Enable logging
Args:
log_path (str, optional): path to log file.
Defaults to './logs/game.log'.
log_level (str, optional): logging level. Defaults to 'DEBUG'.
"""
logger.remove()
logger.add(
sink=log_path,
level=log_level,
format='{extra[classname]} -> func {function} | ' +
'{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}',
)
logger.enable('bgameb')
IntStr = Union[int, str]
AbstractSetIntStr = AbstractSet[IntStr]
MappingIntStrAny = Mapping[IntStr, Any]
# TODO: test me
[docs]class PropertyBaseModel(BaseModel):
"""
Serializing properties with pydantic
https://github.com/samuelcolvin/pydantic/issues/935
https://github.com/pydantic/pydantic/issues/935#issuecomment-554378904
https://github.com/pydantic/pydantic/issues/935#issuecomment-1152457432
"""
[docs] @classmethod
def get_properties(cls):
return [
prop for prop
in dir(cls)
if isinstance(getattr(cls, prop), property)
and prop not in ("__values__", "fields")
]
[docs] def dict(
self,
*,
include: Union[AbstractSetIntStr, MappingIntStrAny] = None,
exclude: Union[AbstractSetIntStr, MappingIntStrAny] = None,
by_alias: bool = False,
skip_defaults: bool = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> dict[str, Any]:
attribs = super().dict(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none
)
props = self.get_properties()
# Include and exclude properties
if include:
props = [prop for prop in props if prop in include]
if exclude:
props = [prop for prop in props if prop not in exclude]
# Update the attribute dict with the properties
if props:
attribs.update({prop: getattr(self, prop) for prop in props})
return attribs
[docs]class Base(PropertyBaseModel):
"""Base class for game, players, tools and items
..
Attr:
id (str): id of stuff
_counter (Counter): Counter object.
Isn't represented in final json or dict.
Is initialized automaticaly by __init__.
Counter is a collection.Counter.
_logger (Logger): loguru logger
Counter is a `collection.Counter
<https://docs.python.org/3/library/collections.html#collections.Counter>`_
"""
id: str
_counter: Counter[Any] = Field(default_factory=Counter)
_logger: Logger = Field(...)
def __init__(self, **data):
super().__init__(**data)
self._counter = Counter()
self._logger = logger.bind(classname=self.__class__.__name__)
[docs] class Config:
underscore_attrs_are_private = True
[docs]class BaseGame(Base):
"""Base class for games
"""
def __init__(self, **data):
super().__init__(**data)
self._logger.info('===========NEW GAME============')
self._logger.info(f'{self.__class__.__name__} created.')
[docs]class BasePlayer(Base):
"""Base class for players
"""
[docs]class BaseItem(Base):
"""Base class for items (like dices or cards)
"""
V = TypeVar('V', bound=BaseItem)
[docs]class Components(GenericModel, Generic[V], Mapping[str, V]):
"""Components mapping represents a collection of objects,
used for create instance of game items, like dices or decks
"""
def __init__(
self,
*args: tuple[dict[str, V]],
**kwargs: dict[str, V]
) -> None:
for arg in args:
if isinstance(arg, dict):
for k, v, in arg.items():
self.__dict__[k] = v
else:
raise AttributeError('Args must be a dict of dicts')
if kwargs:
for k, v, in kwargs.items():
self.__dict__[k] = v
def __iter__(self):
return iter(self.__dict__)
def __setattr__(self, attr: str, value: V) -> None:
self.__setitem__(attr, value)
def __getattr__(self, attr: str) -> V:
try:
return self.__getitem__(attr)
except KeyError:
raise AttributeError(attr)
def __delattr__(self, attr: str) -> None:
try:
self.__delitem__(attr)
except KeyError:
raise AttributeError(attr)
def __setitem__(self, attr: str, value: V) -> None:
if issubclass(value.__class__, BaseItem):
name = self._make_name(attr)
if name not in self.__dict__.keys():
self.__dict__[name] = value
else:
raise ComponentNameError(name)
else:
raise ComponentClassError(value)
def __getitem__(self, attr: str) -> V:
return self.__dict__[attr] # type: ignore
def __delitem__(self, attr: str) -> None:
del self.__dict__[attr]
def __repr__(self) -> str:
items = list(
{f"{k}: {v!r}" for k, v in self.items()}
)
return f"{{{', '.join(items)}}}"
def __len__(self) -> int:
return len(self.__dict__)
[docs] def keys(self) -> KeysView[str]:
return self.__dict__.keys()
[docs] def values(self) -> ValuesView[V]:
return self.__dict__.values()
[docs] def items(self) -> ItemsView[str, V]:
return self.__dict__.items()
[docs] def to_json(self) -> str:
return json.dumps(self.__dict__, default=lambda c: c.dict())
def _is_valid(self, name: str) -> bool:
"""Chek is name of stuff contains correct symbols
match [a-zA-Z_][a-zA-Z0-9_]*$ expression:
* a-z, A-Z, 0-9 symbols
* first letter not a number amd not a _
* can be used _ symbol in subsequent symbols
Args:
name (str): name of stuff
Raises:
ComponentNameError: name is not valid
Returns:
Trye: is valid
"""
if not re.match("[a-z][a-z0-9_]*$", str(name)):
raise ComponentNameError(name)
return True
def _make_name(self, name: str) -> str:
"""
Replace spaces and other specific characters
in the name with _
Args:
name (str): name of stuff
Returns:
name (str): safe name of stuff
"""
name = str(name).lower()
available = set(string.ascii_letters.lower() + string.digits + '_')
if " " in name:
name = name.replace(' ', '_')
diff = set(name).difference(available)
if diff:
for char in diff:
name = name.replace(char, '_')
self._is_valid(name)
return name
[docs] def update(
self,
stuff: V,
name: Optional[str] = None,
) -> None:
"""Update Component dict with safe name
Args:
stuff (BaseItem): item added to Components
name (Optional[str]). keys to update dict. Defult to None.
"""
if name is None:
name = self._make_name(stuff.id)
else:
name = self._make_name(name)
comp = stuff.__class__(**stuff.dict())
self.__dict__[name] = comp
@property
def ids(self) -> list[str]:
"""Get ids of all items in Components
Returns:
list[str]: list of stuff ids
"""
return [stuff.id for stuff in self.values()]
[docs] def by_id(self, id: str) -> Optional[V]:
"""Get item object by its id
Args:
id (str): item id
Returns:
V, optional: item object
"""
for comp in self.values():
if comp.id == id:
return comp
return None