# -*- coding: utf-8 -*-
# Copyright © 2024 Ivan Konovalov
# This file is part of a Python library yadisk.
# This library is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from typing import Any, Optional
from .._typing_compat import Callable, Iterator
__all__ = ["YaDiskObject"]
[docs]class YaDiskObject:
"""
Base class for all objects mirroring the ones returned by Yandex.Disk REST API.
It must have a fixed number of fields, each field must have a type.
It also supports subscripting and access of fields through the . operator.
:param field_types: `dict` or `None`
:param yadisk: :any:`YaDisk` or `None`, `YaDisk` object
"""
FIELD_TYPES: dict
FIELDS: dict
ALIASES: dict
_yadisk: Optional[Any]
def __init__(self,
field_types: Optional[dict] = None,
yadisk: Optional[Any] = None):
if field_types is None:
field_types = {}
self.FIELD_TYPES = {}
self.FIELDS = {}
self.ALIASES = {}
self.set_field_types(field_types)
self._yadisk = yadisk
[docs] def set_field_types(self, field_types: dict) -> None:
"""
Set the field types of the object
:param field_types: `dict`, where keys are the field names and values are types (or factories)
"""
self.FIELD_TYPES = field_types
for field in field_types.keys():
self[field] = None
[docs] def set_field_type(self, field: str, type: Callable) -> None:
"""
Set field type.
:param field: `str`
:param type: type or factory
"""
self.FIELD_TYPES[field] = type
self[field] = None
[docs] def set_alias(self, alias: str, name: str) -> None:
"""
Set an alias.
:param alias: `str`, alias to add
:param name: `str`, field name
"""
self.ALIASES[alias] = name
[docs] def remove_alias(self, alias: str) -> None:
"""
Remove an alias.
:param alias: `str`
"""
self.ALIASES.pop(alias)
[docs] def remove_field(self, field: str) -> None:
"""
Remove field.
:param field: `str`
"""
self.FIELDS.pop(field)
self.FIELD_TYPES.pop(field)
[docs] def import_fields(self, source_dict: Optional[dict]) -> None:
"""
Set all the fields of the object to the values in `source_dict`.
All the other fields are ignored
:param source_dict: `dict` or `None` (nothing will be done in that case)
"""
if source_dict is not None:
for field in self.FIELDS:
try:
self[field] = source_dict[field]
except KeyError:
pass
for alias, field in self.ALIASES.items():
try:
self[field] = source_dict[alias]
except KeyError:
pass
def __setattr__(self, attr: str, value: Any) -> None:
if attr in ("FIELDS", "FIELD_TYPES", "ALIASES", "_yadisk"):
self.__dict__[attr] = value
return
attr = self.ALIASES.get(attr, attr)
if attr not in self.FIELD_TYPES:
raise AttributeError("Unknown attribute: %r" % (attr,))
datatype = self.FIELD_TYPES[attr]
self.FIELDS[attr] = datatype(value) if value is not None else None
def __getattr__(self, attr: str) -> Any:
attr = self.ALIASES.get(attr, attr)
if attr not in self.FIELD_TYPES:
raise AttributeError("Unknown attribute: %r" % (attr,))
return self.FIELDS[attr]
def __getitem__(self, key: str) -> Any:
return self.FIELDS[key]
def __setitem__(self, key: str, value: Any) -> None:
self.__setattr__(key, value)
def __delitem__(self, key: str) -> None:
del self.FIELDS[key]
def __iter__(self) -> Iterator[dict]:
return iter(self.FIELDS)
def __len__(self) -> int:
return len(self.FIELDS)
def __repr__(self) -> str:
return "<%s%r>" % (self.__class__.__name__, self.FIELDS)
def _repr_pretty_(self, p, cycle: bool) -> None:
"""IPython pretty-print implementation."""
if cycle:
p.text(f"<{self.__class__.__name__}{'{...}'}>")
else:
if not self.FIELDS:
p.text(f"<{self.__class__.__name__}{'{}'}>")
return
with p.group(4, f"<{self.__class__.__name__}{'{'}", "})>"):
p.breakable()
for idx, (k, v) in enumerate(self.FIELDS.items()):
if idx:
p.text(",")
p.breakable()
p.text(repr(k))
p.text(": ")
p.pretty(v)
[docs] def field(self, name: str) -> Any:
"""
Get value of field `name`, guarantee it's not :code:`None` or
raise a :any:`ValueError`.
:param name: `str`, name of the field
:raises ValueError: value of the given field is :code:`None`
:returns: field's value
"""
value = self.__getattr__(name)
if value is not None:
return value
raise ValueError(f"field {repr(name)} is None")
[docs] def __matmul__(self, name: str) -> Any:
"""
The :code:`@` operator. Same as :any:`YaDiskObject.field()`.
Can be used like this:
.. code:: python
# if embedded or embedded.total turn out to be None, we'll get a ValueError
file_count = client.get_meta("/some_folder") @ "embedded" @ "total"
print(f"/some_folder contains {file_count} files")
:param name: `str`, name of the field
:raises ValueError: value of the given field is :code:`None`
:returns: field's value
"""
return self.field(name)