From 9b96133b7a7a2abed3dc00671ccad4d4ccff5693 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 27 Sep 2017 08:58:37 +0300 Subject: [PATCH] Move skeleton for new types from https://bitbucket.org/illemius/demo_telegramobject --- aiogram/types/__init__.py | 135 +-------------------- aiogram/types/base.py | 248 +++++++++++++++++++++++--------------- aiogram/types/fields.py | 135 +++++++++++++++++++++ 3 files changed, 289 insertions(+), 229 deletions(-) create mode 100644 aiogram/types/fields.py diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 4a8fd58e..541f6ce8 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -1,133 +1,6 @@ from . import base -from .animation import Animation -from .audio import Audio -from .callback_query import CallbackQuery -from .chat import Chat, ChatType, ChatActions -from .chat_member import ChatMember, ChatMemberStatus -from .chat_photo import ChatPhoto -from .chosen_inline_result import ChosenInlineResult -from .contact import Contact -from .document import Document -from .file import File -from .force_reply import ForceReply -from .game import Game -from .game_high_score import GameHighScore -from .inline_keyboard import InlineKeyboardButton, InlineKeyboardMarkup -from .inline_query import InlineQuery -from .inline_query_result import InlineQueryResult, InlineQueryResultArticle, InlineQueryResultAudio, \ - InlineQueryResultCachedAudio, InlineQueryResultCachedDocument, InlineQueryResultCachedGif, \ - InlineQueryResultCachedMpeg4Gif, InlineQueryResultCachedPhoto, InlineQueryResultCachedSticker, \ - InlineQueryResultCachedVideo, InlineQueryResultCachedVoice, InlineQueryResultContact, InlineQueryResultDocument, \ - InlineQueryResultGame, InlineQueryResultGif, InlineQueryResultLocation, InlineQueryResultMpeg4Gif, \ - InlineQueryResultPhoto, InlineQueryResultVenue, InlineQueryResultVideo, InlineQueryResultVoice, \ - InputMessageContent, InputContactMessageContent, InputContactMessageContent, InputLocationMessageContent, \ - InputLocationMessageContent, InputMessageContent, InputTextMessageContent, InputTextMessageContent, \ - InputVenueMessageContent, InputVenueMessageContent -from .invoice import Invoice -from .labeled_price import LabeledPrice -from .location import Location -from .mask_position import MaskPosition -from .message import Message, ContentType, ParseMode -from .message_entity import MessageEntity -from .order_info import OrderInfo -from .photo_size import PhotoSize -from .pre_checkout_query import PreCheckoutQuery -from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove -from .response_parameters import ResponseParameters -from .shipping_address import ShippingAddress -from .shipping_option import ShippingOption -from .shipping_query import ShippingQuery -from .sticker import Sticker -from .sticker_set import StickerSet -from .successful_payment import SuccessfulPayment -from .update import Update -from .user import User -from .user_profile_photos import UserProfilePhotos -from .venue import Venue -from .video import Video -from .video_note import VideoNote -from .voice import Voice -from .webhook_info import WebhookInfo +from . import fields -__all__ = [ - 'base', - 'Animation', - 'Audio', - 'CallbackQuery', - 'Chat', - 'ChatActions', - 'ChatMember', - 'ChatMemberStatus', - 'ChatPhoto', - 'ChatType', - 'ChosenInlineResult', - 'Contact', - 'ContentType', - 'Document', - 'File', - 'ForceReply', - 'Game', - 'GameHighScore', - 'InlineKeyboardButton', - 'InlineKeyboardMarkup', - 'InlineQuery', - 'InlineQueryResult', - 'InlineQueryResultArticle', - 'InlineQueryResultAudio', - 'InlineQueryResultCachedAudio', - 'InlineQueryResultCachedDocument', - 'InlineQueryResultCachedGif', - 'InlineQueryResultCachedMpeg4Gif', - 'InlineQueryResultCachedPhoto', - 'InlineQueryResultCachedSticker', - 'InlineQueryResultCachedVideo', - 'InlineQueryResultCachedVoice', - 'InlineQueryResultContact', - 'InlineQueryResultDocument', - 'InlineQueryResultGame', - 'InlineQueryResultGif', - 'InlineQueryResultLocation', - 'InlineQueryResultMpeg4Gif', - 'InlineQueryResultPhoto', - 'InlineQueryResultVenue', - 'InlineQueryResultVideo', - 'InlineQueryResultVoice', - 'InputMessageContent', - 'InputContactMessageContent', - 'InputContactMessageContent', - 'InputLocationMessageContent', - 'InputLocationMessageContent', - 'InputMessageContent', - 'InputTextMessageContent', - 'InputTextMessageContent', - 'InputVenueMessageContent', - 'InputVenueMessageContent', - 'Invoice', - 'KeyboardButton', - 'LabeledPrice', - 'Location', - 'MaskPosition' - 'Message', - 'MessageEntity', - 'OrderInfo', - 'ParseMode', - 'PhotoSize', - 'PreCheckoutQuery', - 'ReplyKeyboardMarkup', - 'ReplyKeyboardRemove', - 'ResponseParameters', - 'ShippingAddress', - 'ShippingOption', - 'ShippingQuery', - 'Sticker', - 'StickerSet' - 'SuccessfulPayment', - 'Update', - 'User', - 'UserProfilePhotos', - 'Venue', - 'Video', - 'VideoNote', - 'Voice', - 'WebhookInfo' -] +__all__ = ( + 'base', 'fields' +) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 5cabbf3e..fb15f25a 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -1,119 +1,171 @@ -import datetime -import time +import typing +import ujson + +from .fields import BaseField + +PROPS_ATTR_NAME = '_props' +VALUES_ATTR_NAME = '_values' +ALIASES_ATTR_NAME = '_aliases' + +__all__ = ('MetaTelegramObject', 'TelegramObject') -def deserialize(deserializable, data): +class MetaTelegramObject(type): """ - Deserialize object if have data - - :param deserializable: :class:`aiogram.types.Deserializable` - :param data: - :return: + Metaclass for telegram objects """ - if data: - return deserializable.de_json(data) + _objects = {} + def __new__(mcs, name, bases, namespace, **kwargs): + cls = super(MetaTelegramObject, mcs).__new__(mcs, name, bases, namespace) -def deserialize_array(deserializable, array): - """ - Deserialize array of objects + props = {} + values = {} + aliases = {} - :param deserializable: - :param array: - :return: - """ - if array: - return [deserialize(deserializable, item) for item in array] - - -class Serializable: - """ - Subclasses of this class are guaranteed to be able to be created from a json-style dict. - """ - - def to_json(self): - """ - Returns a JSON representation of this class. - - :return: dict - """ - return {k: v.to_json() if hasattr(v, 'to_json') else v for k, v in self.__dict__.items() if - not k.startswith('_')} - - -class Deserializable: - """ - Subclasses of this class are guaranteed to be able to be created from a json-style dict or json formatted string. - All subclasses of this class must override de_json. - """ - - def to_json(self): - result = {} - for name, attr in self.__dict__.items(): - if not attr or name in ['_bot', '_parent']: + # Get props, values, aliases from parent objects + for base in bases: + if not isinstance(base, MetaTelegramObject): continue - if hasattr(attr, 'to_json'): - attr = getattr(attr, 'to_json')() - elif isinstance(attr, datetime.datetime): - attr = int(time.mktime(attr.timetuple())) - result[name] = attr + props.update(getattr(base, PROPS_ATTR_NAME)) + values.update(getattr(base, VALUES_ATTR_NAME)) + aliases.update(getattr(base, ALIASES_ATTR_NAME)) + + # Scan current object for props + for name, prop in ((name, prop) for name, prop in namespace.items() if isinstance(prop, BaseField)): + props[prop.alias] = prop + if prop.default is not None: + values[prop.alias] = prop.default + aliases[name] = prop.alias + + # Set attributes + setattr(cls, PROPS_ATTR_NAME, props) + setattr(cls, VALUES_ATTR_NAME, values) + setattr(cls, ALIASES_ATTR_NAME, aliases) + + mcs._objects[cls.__name__] = cls + return cls + + @property + def telegram_types(cls): + return cls._objects + + +class TelegramObject(metaclass=MetaTelegramObject): + """ + Abstract class for telegram objects + """ + + def __init__(self, conf=None, **kwargs): + """ + Deserialize object + + :param conf: + :param kwargs: + """ + if conf is None: + conf = {} + for key, value in kwargs.items(): + if key in self.props: + self.props[key].set_value(self, value) + else: + self.values[key] = value + self._conf = conf + + @property + def conf(self) -> typing.Dict[str, typing.Any]: + return self.conf + + @property + def props(self) -> typing.Dict[str, BaseField]: + """ + Get props + + :return: dict with props + """ + return getattr(self, PROPS_ATTR_NAME, {}) + + @property + def props_aliases(self) -> typing.Dict[str, str]: + """ + Get aliases for props + + :return: + """ + return getattr(self, ALIASES_ATTR_NAME, {}) + + @property + def values(self): + """ + Get values + + :return: + """ + return getattr(self, VALUES_ATTR_NAME, {}) + + @property + def telegram_types(self): + return type(self).telegram_types + + @classmethod + def to_object(cls, data): + """ + Deserialize object + + :param data: + :return: + """ + return cls(**data) + + def to_python(self) -> typing.Dict: + """ + Get object as JSON serializable + + :return: + """ + self.clean() + result = {} + for name, value in self.values.items(): + if name in self.props: + value = self.props[name].export(self) + if isinstance(value, TelegramObject): + value = value.to_python() + result[self.props_aliases.get(name, name)] = value return result - @classmethod - def _parse_date(cls, unix_time): - if unix_time: - return datetime.datetime.fromtimestamp(unix_time) - - @property - def bot(self) -> 'Bot': + def clean(self): """ - Bot instance + Remove empty values """ - if not hasattr(self, '_bot'): - raise AttributeError("{0} is not configured.".format(self.__class__.__name__)) - return getattr(self, '_bot') + for key, value in self.values.copy().items(): + if value is None: + del self.values[key] - @bot.setter - def bot(self, bot): - setattr(self, '_bot', bot) - for name, attr in self.__dict__.items(): - if hasattr(attr, 'de_json'): - attr.bot = bot - - @property - def parent(self): + def as_json(self) -> str: """ - Parent object + Get object as JSON string + + :return: JSON + :rtype: :obj:`str` """ - return getattr(self, '_parent', None) + return ujson.dumps(self.to_python()) - @parent.setter - def parent(self, value): - setattr(self, '_parent', value) - for name, attr in self.__dict__.items(): - if name.startswith('_'): - continue - if hasattr(attr, 'de_json'): - attr.parent = self - - @classmethod - def de_json(cls, raw_data): + def __str__(self) -> str: """ - Returns an instance of this class from the given json dict or string. + Return object as string. Alias for '.as_json()' - This function must be overridden by subclasses. - :return: an instance of this class created from the given json dict or string. + :return: str """ - raise NotImplementedError + return self.as_json() - def __str__(self): - return str(self.to_json()) + def __getitem__(self, item): + if item in self.props: + return getattr(self, item) + elif item in self.values: + return self.values[item] - def __repr__(self): - return str(self) - - @classmethod - def deserialize(cls, obj): - if isinstance(obj, list): - return deserialize_array(cls, obj) - return deserialize(cls, obj) + def __setitem__(self, key, value): + if key in self.props: + setattr(self, key, value) + else: + self.values[key] = value diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py new file mode 100644 index 00000000..b8af6076 --- /dev/null +++ b/aiogram/types/fields.py @@ -0,0 +1,135 @@ +import abc +import datetime + +__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField') + + +class BaseField(metaclass=abc.ABCMeta): + """ + Base field (prop) + """ + + def __init__(self, *, base=None, default=None, alias=None): + """ + Init prop + + :param base: class for child element + :param default: default value + :param alias: alias name (for e.g. field named 'from' must be has name 'from_user' + ('from' is builtin Python keyword) + """ + self.base_object = base + self.default = default + self.alias = alias + + def __set_name__(self, owner, name): + if self.alias is None: + self.alias = name + + def resolve_base(self, instance): + if self.base_object is None or hasattr(self.base_object, 'telegram_types'): + return + elif isinstance(self.base_object, str): + self.base_object = instance.telegram_types.get(self.base_object) + + def get_value(self, instance): + """ + Get value for current object instance + + :param instance: + :return: + """ + return instance.values.get(self.alias) + + def set_value(self, instance, value): + """ + Set prop value + + :param instance: + :param value: + :return: + """ + self.resolve_base(instance) + value = self.deserialize(value) + instance.values[self.alias] = value + + def __get__(self, instance, owner): + return self.get_value(instance) + + def __set__(self, instance, value): + self.set_value(instance, value) + + @abc.abstractmethod + def serialize(self, value): + """ + Serialize value to python + + :param value: + :return: + """ + pass + + @abc.abstractmethod + def deserialize(self, value): + """Deserialize python object value to TelegramObject value""" + pass + + def export(self, instance): + """ + Alias for `serialize` but for current Object instance + + :param instance: + :return: + """ + return self.serialize(self.get_value(instance)) + + +class Field(BaseField): + """ + Simple field + """ + + def serialize(self, value): + if self.base_object is not None: + return value.to_python() + return value + + def deserialize(self, value): + if self.base_object is not None and not hasattr(value, 'base_object'): + return self.base_object(**value) + return value + + +class ListField(Field): + """ + Field contains list ob objects + """ + + def serialize(self, value): + result = [] + serialize = super(ListField, self).serialize + for item in value: + result.append(serialize(item)) + return result + + def deserialize(self, value): + result = [] + deserialize = super(ListField, self).deserialize + for item in value: + result.append(deserialize(item)) + return result + + +class DateTimeField(BaseField): + """ + In this field stored datetime + + in: unixtime + out: datetime + """ + + def serialize(self, value: datetime.datetime): + return round(value.timestamp()) + + def deserialize(self, value): + return datetime.datetime.fromtimestamp(value)