|
| 1 | +#!/usr/bin/env python |
| 2 | +# |
| 3 | +# A library that provides a Python interface to the Telegram Bot API |
| 4 | +# Copyright (C) 2015-2018 |
| 5 | +# Leandro Toledo de Souza <devs@python-telegram-bot.org> |
| 6 | +# |
| 7 | +# This program is free software: you can redistribute it and/or modify |
| 8 | +# it under the terms of the GNU Lesser Public License as published by |
| 9 | +# the Free Software Foundation, either version 3 of the License, or |
| 10 | +# (at your option) any later version. |
| 11 | +# |
| 12 | +# This program is distributed in the hope that it will be useful, |
| 13 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | +# GNU Lesser Public License for more details. |
| 16 | +# |
| 17 | +# You should have received a copy of the GNU Lesser Public License |
| 18 | +# along with this program. If not, see [http://www.gnu.org/licenses/]. |
| 19 | +"""This module contains the BasePersistence class.""" |
| 20 | +import pickle |
| 21 | +from collections import defaultdict |
| 22 | + |
| 23 | + |
| 24 | +class PicklePersistence(object): |
| 25 | + """Using python's builtin pickle for making you bot persistent. |
| 26 | +
|
| 27 | + Attributes: |
| 28 | + filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` |
| 29 | + is false this will be used as a prefix. |
| 30 | + store_user_data (:obj:`bool`): Optional. Whether user_data should be saved by this |
| 31 | + persistence class. |
| 32 | + store_chat_data (:obj:`bool`): Optional. Whether user_data should be saved by this |
| 33 | + persistence class. |
| 34 | + single_file (:obj:`bool`): Optional. When ``False`` will store 3 sperate files of |
| 35 | + `filename_user_data`, `filename_chat_data` and `filename_conversations`. Default is |
| 36 | + ``True``. |
| 37 | + on_flush (:obj:`bool`): Optional. When ``True` will only save to file when :attr:`flush` is |
| 38 | + called and keep data in memory until that happens. When False will store data on any |
| 39 | + transaction. Default is ``False``. |
| 40 | +
|
| 41 | + Args: |
| 42 | + filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` |
| 43 | + is false this will be used as a prefix. |
| 44 | + store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this |
| 45 | + persistence class. Default is ``True``. |
| 46 | + store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this |
| 47 | + persistence class. Default is ``True``. |
| 48 | + single_file (:obj:`bool`, optional): When ``False`` will store 3 sperate files of |
| 49 | + `filename_user_data`, `filename_chat_data` and `filename_conversations`. Default is |
| 50 | + ``True``. |
| 51 | + on_flush (:obj:`bool`, optional): When ``True` will only save to file when :attr:`flush` is |
| 52 | + called and keep data in memory until that happens. When False will store data on any |
| 53 | + transaction. Default is ``False``. |
| 54 | + """ |
| 55 | + |
| 56 | + def __init__(self, filename, store_user_data=True, store_chat_data=True, singe_file=True, |
| 57 | + on_flush=False): |
| 58 | + self.filename = filename |
| 59 | + self.store_user_data = store_user_data |
| 60 | + self.store_chat_data = store_chat_data |
| 61 | + self.single_file = singe_file |
| 62 | + self.on_flush = on_flush |
| 63 | + self.user_data = None |
| 64 | + self.chat_data = None |
| 65 | + self.conversations = None |
| 66 | + |
| 67 | + def load_singlefile(self): |
| 68 | + try: |
| 69 | + filename = self.filename |
| 70 | + with open(self.filename, "rb") as f: |
| 71 | + all = pickle.load(f) |
| 72 | + self.user_data = defaultdict(dict, all['user_data']) |
| 73 | + self.chat_data = defaultdict(dict, all['chat_data']) |
| 74 | + self.conversations = all['conversations'] |
| 75 | + except FileNotFoundError: |
| 76 | + self.conversations = {} |
| 77 | + self.user_data = defaultdict(dict) |
| 78 | + self.chat_data = defaultdict(dict) |
| 79 | + except pickle.UnpicklingError: |
| 80 | + raise TypeError("File {} does not contain valid pickle data".format(filename)) |
| 81 | + |
| 82 | + def load_file(self, filename): |
| 83 | + try: |
| 84 | + with open(filename, "rb") as f: |
| 85 | + return pickle.load(f) |
| 86 | + except FileNotFoundError: |
| 87 | + return None |
| 88 | + except pickle.UnpicklingError: |
| 89 | + raise TypeError("File {} does not contain valid pickle data".format(filename)) |
| 90 | + |
| 91 | + def dump_singlefile(self): |
| 92 | + with open(self.filename, "wb") as f: |
| 93 | + all = {'conversations': self.conversations, 'user_data': self.user_data, |
| 94 | + 'chat_data': self.chat_data} |
| 95 | + pickle.dump(all, f) |
| 96 | + |
| 97 | + def dump_file(self, filename, data): |
| 98 | + with open(filename, "wb") as f: |
| 99 | + pickle.dump(data, f) |
| 100 | + |
| 101 | + def get_user_data(self): |
| 102 | + """"Returns the user_data from the pickle file if it exsists or an empty defaultdict. |
| 103 | +
|
| 104 | + Returns: |
| 105 | + :obj:'defaultdict`: The restored user data. |
| 106 | + """ |
| 107 | + if self.user_data: |
| 108 | + pass |
| 109 | + elif not self.single_file: |
| 110 | + filename = "{}_user_data".format(self.filename) |
| 111 | + data = self.load_file(filename) |
| 112 | + if not data: |
| 113 | + data = defaultdict(dict) |
| 114 | + else: |
| 115 | + data = defaultdict(dict, data) |
| 116 | + self.user_data = data |
| 117 | + else: |
| 118 | + self.load_singlefile() |
| 119 | + return self.user_data.copy() |
| 120 | + |
| 121 | + def get_chat_data(self): |
| 122 | + """"Returns the chat_data from the pickle file if it exsists or an empty defaultdict. |
| 123 | +
|
| 124 | + Returns: |
| 125 | + :obj:'defaultdict`: The restored chat data. |
| 126 | + """ |
| 127 | + if self.chat_data: |
| 128 | + pass |
| 129 | + elif not self.single_file: |
| 130 | + filename = "{}_chat_data".format(self.filename) |
| 131 | + data = self.load_file(filename) |
| 132 | + if not data: |
| 133 | + data = defaultdict(dict) |
| 134 | + else: |
| 135 | + data = defaultdict(dict, data) |
| 136 | + self.chat_data = data |
| 137 | + else: |
| 138 | + self.load_singlefile() |
| 139 | + return self.chat_data.copy() |
| 140 | + |
| 141 | + def get_conversations(self, name): |
| 142 | + """"Returns the conversations from the pickle file if it exsists or an empty defaultdict. |
| 143 | +
|
| 144 | + Args: |
| 145 | + name (:obj:`str`): The handlers name. |
| 146 | +
|
| 147 | + Returns: |
| 148 | + :obj:'dict`: The restored conversations for the handler. |
| 149 | + """ |
| 150 | + if self.conversations: |
| 151 | + pass |
| 152 | + elif not self.single_file: |
| 153 | + filename = "{}_conversations".format(self.filename) |
| 154 | + data = self.load_file(filename) |
| 155 | + if not data: |
| 156 | + data = {name: {}} |
| 157 | + self.conversations = data |
| 158 | + else: |
| 159 | + self.load_singlefile() |
| 160 | + return self.conversations.get(name, {}).copy() |
| 161 | + |
| 162 | + def update_conversations(self, name, conversations): |
| 163 | + """Will update the conversations for the given handler and depending on :attr:`on_flush` |
| 164 | + save the pickle file. |
| 165 | +
|
| 166 | + Args: |
| 167 | + name (:obj:`str`): The handlers name. |
| 168 | + conversations (:obj:'dict`): The :attr:`telegram.ext.ConversationHandler.conversations` |
| 169 | + dict to store. |
| 170 | + """ |
| 171 | + if self.conversations[name] == conversations: |
| 172 | + return |
| 173 | + self.conversations[name] = conversations |
| 174 | + if not self.on_flush: |
| 175 | + if not self.single_file: |
| 176 | + filename = "{}_conversations".format(self.filename) |
| 177 | + self.dump_file(filename, self.conversations) |
| 178 | + else: |
| 179 | + self.dump_singlefile() |
| 180 | + |
| 181 | + def update_user_data(self, user_data): |
| 182 | + """"Will update the user_data (if changed) and depending on :attr:`on_flush` save the |
| 183 | + pickle file. |
| 184 | +
|
| 185 | + Args: |
| 186 | + user_data (:obj:'defaultdict`): The :attr:`telegram.ext.dispatcher.user_data` |
| 187 | + defaultdict to store. |
| 188 | + """ |
| 189 | + if self.user_data == user_data: |
| 190 | + return |
| 191 | + self.user_data = user_data |
| 192 | + if not self.on_flush: |
| 193 | + if not self.single_file: |
| 194 | + filename = "{}_user_data".format(self.filename) |
| 195 | + self.dump_file(filename, self.user_data) |
| 196 | + else: |
| 197 | + self.dump_singlefile() |
| 198 | + |
| 199 | + def update_chat_data(self, chat_data): |
| 200 | + """"Will update the chat_data (if changed) and depending on :attr:`on_flush` save the |
| 201 | + pickle file. |
| 202 | +
|
| 203 | + Args: |
| 204 | + chat_data (:obj:'defaultdict`): The :attr:`telegram.ext.dispatcher.chat_data` |
| 205 | + defaultdict to store. |
| 206 | + """ |
| 207 | + if self.chat_data == chat_data: |
| 208 | + return |
| 209 | + self.chat_data = chat_data |
| 210 | + if not self.on_flush: |
| 211 | + if not self.single_file: |
| 212 | + filename = "{}_chat_data".format(self.filename) |
| 213 | + self.dump_file(filename, self.chat_data) |
| 214 | + else: |
| 215 | + self.dump_singlefile() |
| 216 | + |
| 217 | + def flush(self): |
| 218 | + """If :attr:`on_flush` is set to ``True``. Will save all data in memory to pickle file(s). If |
| 219 | + it's ``False`` will just pass. |
| 220 | + """ |
| 221 | + if not self.on_flush: |
| 222 | + pass |
| 223 | + else: |
| 224 | + if self.single_file: |
| 225 | + self.dump_singlefile() |
| 226 | + else: |
| 227 | + self.dump_file("{}_user_data".format(self.filename), self.user_data) |
| 228 | + self.dump_file("{}_chat_data".format(self.filename), self.chat_data) |
| 229 | + self.dump_file("{}_conversations".format(self.filename), self.conversations) |
0 commit comments