# coding=utf-8
"""
Модуль содержит:
- класс :class:`.CinemateConfig` для работы с настройками;
- метакласс :class:`.CommonMeta` для реализации служебных методов;
- класс :class:`.BaseCinemate` от которого наследуются все остальные
классы проекта;
- классы :class:`.BaseImage` и :class:`.BaseSlug` в которые вынесен общий
код для :class:`person.Photo`, :class:`movie.Poster`,
:class:`movie.Country`, :class:`movie.Genre`;
- декоратор :func:`require`, который проверяет у экземпляра класса наличие
указаных аттрибутов;
- функции :func:`parse_date`, :func:`parse_datetime` для разбора дат и
времени в формате ISO.
"""
import yaml
import __main__ as main
from collections import defaultdict
from datetime import datetime
from functools import wraps
from getpass import getpass
from os.path import exists, expanduser, join
from six import add_metaclass, callable, iteritems, iterkeys, PY2
from six.moves import input
def _open(*args, **kwargs): # pragma: no cover
""" Используется для подмены содержимого при тестировании
"""
return open(*args, **kwargs)
def _exists(*args, **kwargs): # pragma: no cover
return exists(*args, **kwargs)
class CinemateConfig(object):
""" Класс для чтения и сохранения конфигурации.
"""
module = yaml
filename = join(expanduser('~'), '.cinemate')
_auth = {}.fromkeys(('username', 'password', 'apikey', 'passkey'))
_config = {'auth': _auth}
def __init__(self):
interactive = not hasattr(main, '__file__')
if interactive and not self.exists: # pragma: no cover
self.interactive_input()
self.save()
elif not self.exists:
msg = (
'Config file {cfg} does not exists. Create it manually '
'or create cinemate instance in interactive mode:\n'
'>>> from cinemate import Cinemate\n'
'>>> cin = Cinemate()'
)
raise IOError(msg.format(cfg=self.filename))
else:
self.load()
@property
def exists(self, filename=None):
return _exists(filename or self.filename)
def interactive_input(self): # pragma: no cover
""" Запрашивает у пользователя данные и сохраняет их.
"""
self._auth['username'] = input('Username: ')
self._auth['password'] = getpass('Password: ')
self._auth['passkey'] = getpass('Passkey: ')
self._auth['apikey'] = getpass('Apikey: ')
def apply(self, obj):
""" Сохраняет пользовательские настройки в указанный объект.
"""
for field, value in iteritems(self._auth):
setattr(obj, field, value)
def load(self, filename=None):
""" Загружает пользовательские данные из файла.
:param filename: имя файла, если не задано используется значение
по умолчанию: ``~/.cinemate`` или ``%HOME%\.cinemate``
в зависимости от операционной системы
:type filename: :py:class:`str`
"""
with _open(filename or self.filename) as cfg:
self._config = self.module.load(cfg)
self._auth = self._config['auth']
def save(self, filename=None):
""" Сохраняет пользовательские настройки в файл.
:param filename: имя файла
:type filename: :py:class:`str`
"""
for field, value in iteritems(self._auth):
self._config['auth'][field] = value
with _open(filename or self.filename, 'w') as cfg:
self.module.dump(self._config, cfg, default_flow_style=False)
class CompareMixin(object):
id = None
def __eq__(self, other):
return type(self) is type(other) and self.id == other.id
class FieldsCompareMixin(object):
fields = []
def __eq__(self, other):
is_dict = isinstance(self.fields, dict)
fields = iterkeys(self.fields) if is_dict else self.fields
return all(getattr(self, f) == getattr(other, f) for f in fields)
class CommonMeta(type):
""" Метакласс для реализации __служебных_методов__.
"""
_instances = defaultdict(dict)
def __new__(mcs, name, bases, attrs):
method = attrs.get('__unicode__')
if method:
def to_str(x):
return PY2 and method(x).encode('utf-8') or method(x)
attrs.setdefault('__str__', to_str)
attrs.setdefault('__repr__', to_str)
fetch = attrs.get('fetch')
if callable(fetch):
def wrapper(*args, **kwargs):
""" Обёртка для метода fetch. Каждый раз после вызова метода
экземпляр класса добавляется в _instances.
"""
instance = args[0] # self
result = fetch(*args, **kwargs)
mcs._instances[name][instance.id] = instance
return result
attrs['fetch'] = wrapper
return super(CommonMeta, mcs).__new__(mcs, name, bases, attrs)
def __call__(cls, *args, **kwargs):
instance = super(CommonMeta, cls).__call__(*args, **kwargs)
fetch = getattr(cls, 'fetch', None)
if not callable(fetch): # у объекта нет метода fetch
return instance
instances = cls._instances[cls.__name__]
return instances.get(instance.id, instance)
class CinemateDuckMeta(type):
""" Метакласс для проверки того, что объект содержит экземпляр cinemate.
"""
def __instancecheck__(self, instance):
return hasattr(instance, 'cinemate')
@add_metaclass(CinemateDuckMeta)
class Cinematable(object):
""" Утинная типизация для всех классов использующихся в проекте.
"""
@add_metaclass(CommonMeta)
class BaseCinemate(object):
""" От этого класса наследуются все остальные классы проекта.
"""
class BaseImage(FieldsCompareMixin, BaseCinemate):
fields = 'small', 'medium', 'big'
def __init__(self, small, medium, big):
self.small = small
self.medium = medium
self.big = big
@classmethod
def from_dict(cls, dct):
""" Изображение из словаря, возвращаемого API.
:param dct: словарь, возвращаемый API
:type dct: :py:class:`dict`
:return: изображение
:rtype: :class:`{module_name}.{class_name}`
""".format(
module_name=cls.__module__,
class_name=cls.__name__,
)
if dct is None:
return
fields = {k: dct.get(k).get('url') for k in cls.fields if k in dct}
return cls(**fields)
def __unicode__(self):
sizes = '/'.join(k for k, v in sorted(iteritems(self.__dict__)) if v)
return '<{class_name} {sizes}>'.format(
class_name=self.__class__.__name__,
sizes=sizes,
)
class BaseSlug(BaseCinemate):
def __init__(self, name, slug=None):
self.name = name
self.slug = slug or self.slug_by_name(name)
@classmethod
def from_dict(cls, dct):
""" Задать объект из словаря, возвращаемого API.
:param dct: словарь, возвращаемый API
:type dct: :py:class:`dict`
:return: объект
:rtype: :class:`{module_name}.{class_name}`
""".format(
module_name=cls.__module__,
class_name=cls.__name__,
)
name = dct.get('name')
slug = dct.get('slug', cls.slug_by_name(name))
return cls(name=name, slug=slug)
@classmethod
def slug_by_name(cls, name):
""" Получение slug объекта по его названию на русском языке.
:param name: имя объекта на русском языке
:return: slug объекта
:rtype: :py:class:`str`
"""
finder = (slug for slug, rus in iteritems(cls.mapping) if rus == name)
return next(finder, None)
def __eq__(self, other):
return self.slug == other.slug or self.name == other.name
def __unicode__(self):
return '<{class_name}: {name}>'.format(
class_name=self.__class__.__name__,
name=self.slug or self.name,
)
# noinspection PyPep8Naming
class require(object):
""" Декоратор проверяет наличие указанных атрибутов у объекта cinemate.
"""
def __init__(self, *attr_names):
"""
:param attr_names: имена требуемых аттрибутов
:type attr_names: :py:class:`str` or :py:class:`tuple`
"""
self.attr_names = attr_names
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
""" Декорируемая функция.
:param args: неименованные параметры декорируемой функции
:type args: :py:class:`tuple`
:param kwargs: именованные параметры декорируемой функции
:type kwargs: :py:class:`dict`
"""
cinemate = self.get_cinemate(args[0]) # args[0] == self or cls
if not all(getattr(cinemate, a, None) for a in self.attr_names):
msg = '{attr} required to use {cls}.{method} method'.format(
attr=', '.join(self.attr_names),
cls=args[0].__class__.__name__,
method=func.__name__
)
raise AttributeError(msg)
return func(*args, **kwargs)
return wrapper
@staticmethod
def get_cinemate(instance):
""" Получение объекта cinemate хранящегося в аттрибутах.
:param instance: экземпляр какого-нибудь класса
:return: объект cinemate
:raises AttributeError: Вызывается, если объект не содержит
требуемых полей или экземпляра cinemate
"""
if isinstance(instance, Cinematable):
return getattr(instance, 'cinemate', instance)
else:
raise AttributeError('Object has not cinemate attribute')
def parse_datetime(source):
""" Парсинг дат и времени формата ISO. Например: 2011-04-09T15:38:30
:param source: исходная строка с датой и временем
:type source: :py:class:`str`
"""
if source:
return datetime.strptime(source, '%Y-%m-%dT%H:%M:%S')
def parse_date(source):
""" Парсинг дат формата 2011-04-07
:param source: исходная строка с датой
:type source: :py:class:`str`
"""
if source:
return datetime.strptime(source, '%Y-%m-%d')