diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..5683115 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +.github/ @noam09 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f02b4d1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,124 @@ +name: Build eggs + +on: + push: + branches: [deluge-2.1.1] + +jobs: + + release: + name: Create Github Release + # if: contains(github.ref, 'tags/v') + # needs: [test] + runs-on: ubuntu-latest + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1.1.4 + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Deluge 2.1.1 + draft: true + prerelease: false + - name: Output Release URL File + run: | + echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt + pwd + ls -alh + - name: Save Release URL File for publish + uses: actions/upload-artifact@v4.4.0 + with: + name: release_url + path: release_url.txt + + build: + runs-on: ubuntu-latest + needs: [release] + strategy: + # By default, GitHub will maximize the number of jobs run in parallel + # depending on the available runners on GitHub-hosted virtual machines. + # max-parallel: 8 + fail-fast: false + matrix: + include: + - python-version: "3.7" + - python-version: "3.8" + - python-version: "3.9" + - python-version: "3.10" + - python-version: "3.11" + - python-version: "3.12" + + steps: + - uses: actions/checkout@v3 + + - uses: actions/checkout@v1 + - name: Load Release URL File from release job + uses: actions/download-artifact@v4.1.8 + with: + name: release_url + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Python build + run: | + echo "Python ${{ matrix.python-version }}" + python --version + PYTHON_VERSION=${{ matrix.python-version }} ./build-all.sh + ls -alh out + ls -alh out/dist + + - name: Get md5sum + run: | + echo "::set-output name=FILEHASH::$(md5sum ./out/dist/Telegramer-2.1.1.3-py${{ matrix.python-version }}.egg | cut -d ' ' -f 1)" + id: filehash + + # - name: Create Release + # id: create_release + # uses: actions/create-release@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + # with: + # body: "**MD5:** `${{ steps.filehash.outputs.FILEHASH }}`" + # tag_name: v2.1.1 + # release_name: Deluge 2.1.1 + # draft: true + # prerelease: true + + - name: Get Release File Name & Upload URL + id: get_release_info + run: | + pwd + ls -alh + echo ::set-output name=file_name::${REPOSITORY_NAME##*/}-${TAG_REF_NAME##*/v} # RepositoryName-v1.0.0 + value=`cat release_url.txt` + echo ::set-output name=upload_url::$value + env: + TAG_REF_NAME: ${{ github.ref }} + REPOSITORY_NAME: ${{ github.repository }} + + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + with: + upload_url: ${{ steps.get_release_info.outputs.upload_url }} + asset_path: ./out/dist/Telegramer-2.1.1.3-py${{ matrix.python-version }}.egg + asset_name: 'Telegramer-2.1.1.3-py${{ matrix.python-version }}.egg' + asset_content_type: application/octet-stream + + # - name: Upload Release Asset + # id: upload-release-asset + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + # asset_path: ./out/dist/Telegramer-2.1.1.3-py${{ matrix.python-version }}.egg + # asset_name: 'Telegramer-2.1.1.3-py${{ matrix.python-version }}.egg' + # asset_content_type: application/octet-stream diff --git a/Dockerfile b/Dockerfile index f65df59..95c6942 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:2.7.18-alpine3.11 AS base +FROM python:3.10-alpine3.15 AS base RUN mkdir -p /usr/src/app RUN mkdir -p /output diff --git a/Dockerfile-py b/Dockerfile-py new file mode 100644 index 0000000..c2af183 --- /dev/null +++ b/Dockerfile-py @@ -0,0 +1,14 @@ +ARG PYTHON_VERSION=3.11 +FROM python:${PYTHON_VERSION}-alpine AS base + +RUN mkdir -p /usr/src/app +RUN mkdir -p /output +WORKDIR /usr/src/app + +RUN pip install --no-cache-dir setuptools + +COPY telegramer /usr/src/app/telegramer +COPY setup.py /usr/src/app/setup.py +COPY LICENSE /usr/src/app/LICENSE + +RUN python setup.py bdist_egg diff --git a/README.md b/README.md index d85d785..086fc85 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

Telegramer

-![GitHub All Releases](https://img.shields.io/github/downloads/noam09/deluge-telegramer/total?style=flat-square) ![Deluge Version](https://img.shields.io/badge/deluge-1.3.15-blue?style=flat-square&logo=deluge) +![GitHub All Releases](https://img.shields.io/github/downloads/noam09/deluge-telegramer/total?style=flat-square) [![Deluge Version](https://img.shields.io/badge/deluge-1.3.15-blue?style=flat-square&logo=deluge)](https://github.com/noam09/deluge-telegramer/releases/tag/v1.3.1) [![Deluge Version](https://img.shields.io/badge/deluge-2.1.1-yellowgreen?style=flat-square&logo=deluge)](https://github.com/noam09/deluge-telegramer/releases/tag/2.1.1.0) [Telegramer](https://github.com/noam09/deluge-telegramer) is a [Deluge](https://deluge-torrent.org) plugin for sending notifications, adding and viewing torrents using [Telegram](https://telegram.org/) messenger. It features both a GTK and Web UI. @@ -20,7 +20,7 @@ Since the bot runs locally and is owned by the same user running it, Telegramer ## Requirements -Currently Telegramer has been tested mainly on Linux using Deluge 1.3.15. Support for Windows is also available. +Currently Telegramer has been tested mainly on Linux using Deluge 1.3.15. Support for Windows is also available. A [**beta** version](https://github.com/noam09/deluge-telegramer/releases/tag/2.1.1.0) of the plugin is available for Deluge 2, tested on v2.1.1. Make sure you have Python [`setuptools`](https://pypi.python.org/pypi/setuptools#installation-instructions) installed in order to build the plugin. The plugin itself works with the [`python-telegram-bot`](https://github.com/python-telegram-bot/python-telegram-bot) wrapper, which comes pre-packaged. diff --git a/build-all.sh b/build-all.sh new file mode 100755 index 0000000..261f4bc --- /dev/null +++ b/build-all.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +docker build -f Dockerfile-py --build-arg PYTHON_VERSION=${PYTHON_VERSION} --no-cache -t telegramer.build . \ + && docker run -v $(pwd)/out:/tmp/out --rm -i telegramer.build sh -s << COMMANDS +python setup.py bdist_egg +chown -R $(id -u):$(id -g) dist +cp -ar dist/ /tmp/out/ +COMMANDS diff --git a/setup.py b/setup.py index 1df357f..74c7b84 100755 --- a/setup.py +++ b/setup.py @@ -43,10 +43,11 @@ # from setuptools import setup, find_packages + __plugin_name__ = "Telegramer" __author__ = "Noam" __author_email__ = "noamgit@gmail.com" -__version__ = "1.3.1" +__version__ = "2.1.1.3" __url__ = "https://github.com/noam09" __license__ = "GPLv3" __description__ = "Control Deluge using Telegram" @@ -68,14 +69,15 @@ long_description=__long_description__ if __long_description__ else __description__, packages=packages, package_data=__pkg_data__, - entry_points=""" - [deluge.plugin.core] - %s = %s:CorePlugin - [deluge.plugin.gtkui] - %s = %s:GtkUIPlugin - [deluge.plugin.web] - %s = %s:WebUIPlugin - [telegramer.libpaths] - include = telegramer.include - """ % ((__plugin_name__, __plugin_name__.lower())*3) + entry_points="""[deluge.plugin.core] +%s = %s:CorePlugin +[deluge.plugin.gtkui] +%s = %s:GtkUIPlugin +[deluge.plugin.web] +%s = %s:WebUIPlugin +[deluge.plugin.gtk3ui] +%s = %s:Gtk3UIPlugin +[telegramer.libpaths] +include = telegramer.include +""" % ((__plugin_name__, __plugin_name__.lower())*4) ) diff --git a/telegramer/__init__.py b/telegramer/__init__.py index 41eb2dc..8adfc54 100755 --- a/telegramer/__init__.py +++ b/telegramer/__init__.py @@ -54,14 +54,15 @@ def load_libs(): for name in egg.get_entry_map("telegramer.libpaths"): ep = egg.get_entry_info("telegramer.libpaths", name) location = "%s/%s" % (egg.location, ep.module_name.replace(".", "/")) - sys.path.append(location) - log.error("Appending to sys.path: '%s'" % location) + if location not in sys.path: + sys.path.append(location) + log.error("NOTANERROR: Appending to sys.path: '%s'" % location) class CorePlugin(PluginInitBase): def __init__(self, plugin_name): load_libs() - from core import Core as _plugin_cls + from .core import Core as _plugin_cls self._plugin_cls = _plugin_cls super(CorePlugin, self).__init__(plugin_name) @@ -69,7 +70,7 @@ def __init__(self, plugin_name): class GtkUIPlugin(PluginInitBase): def __init__(self, plugin_name): load_libs() - from gtkui import GtkUI as _plugin_cls + from .gtkui import GtkUI as _plugin_cls self._plugin_cls = _plugin_cls super(GtkUIPlugin, self).__init__(plugin_name) @@ -77,6 +78,16 @@ def __init__(self, plugin_name): class WebUIPlugin(PluginInitBase): def __init__(self, plugin_name): load_libs() - from webui import WebUI as _plugin_cls + from .webui import WebUI as _plugin_cls self._plugin_cls = _plugin_cls super(WebUIPlugin, self).__init__(plugin_name) + + +class Gtk3UIPlugin(PluginInitBase): + def __init__(self, plugin_name): + load_libs() + from .gtk3ui import Gtk3UI as GtkUIPluginClass + self._plugin_cls = GtkUIPluginClass + super(Gtk3UIPlugin, self).__init__(plugin_name) + + diff --git a/telegramer/core.py b/telegramer/core.py index b39cedf..ba97835 100755 --- a/telegramer/core.py +++ b/telegramer/core.py @@ -47,7 +47,10 @@ import logging import traceback from time import strftime -from deluge.log import LOG as log +import time +# from deluge.log import LOG as log +import logging +log = logging.getLogger(__name__) # import sys # reload(sys) @@ -64,10 +67,10 @@ def prelog(): try: import re - import urllib2 - from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove, Bot) - from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, - RegexHandler, ConversationHandler, BaseFilter) + import urllib.request, urllib.error, urllib.parse + from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove, Bot, Update) + from telegram.ext import (Updater, CallbackContext, CommandHandler, MessageHandler, + ConversationHandler, Filters) from telegram.utils.request import Request import threading from base64 import b64encode @@ -87,12 +90,12 @@ def prelog(): log.error(prelog() + 'Import error - %s\n%s' % (str(e), traceback.format_exc())) -class FilterMagnets(BaseFilter): - def filter(self, message): - return 'magnet:?' in message.text +# class _FilterMagnets(BaseFilter): +# def filter(self, message): +# return 'magnet:?' in message.text -CATEGORY, SET_LABEL, TORRENT_TYPE, ADD_MAGNET, ADD_TORRENT, ADD_URL, RSS_FEED, FILE_NAME, REGEX = range(9) +CATEGORY, SET_LABEL, TORRENT_TYPE, ADD_MAGNET, ADD_TORRENT, ADD_URL, RSS_FEED, FILE_NAME, REGEX = list(range(9)) DEFAULT_PREFS = {"telegram_token": "Contact @BotFather and create a new bot", "telegram_user": "Contact @MyIDbot", @@ -122,11 +125,11 @@ def filter(self, message): 'snow': 'CAADAgADZQUAAgi3GQJyjRNCuIA54gI', 'borat': 'CAADBAADmwQAAjJQbQAB5DpM4iETWoQC'} -EMOJI = {'seeding': u'\u23eb', - 'queued': u'\u23ef', - 'paused': u'\u23f8', - 'error': u'\u2757\ufe0f', - 'downloading': u'\u23ec'} +EMOJI = {'seeding': '\u23eb', + 'queued': '\u23ef', + 'paused': '\u23f8', + 'error': '\u2757\ufe0f', + 'downloading': '\u23ec'} REGEX_SUBS_WORD = r"NAME" @@ -163,7 +166,7 @@ def filter(self, message): INFO_DICT = (('queue', lambda i, s: i != -1 and str(i) or '#'), ('state', None), - ('name', lambda i, s: u' %s *%s* ' % + ('name', lambda i, s: ' %s *%s* ' % (s['state'] if s['state'].lower() not in EMOJI else EMOJI[s['state'].lower()], i)), @@ -183,7 +186,7 @@ def filter(self, message): INFOS = [i[0] for i in INFO_DICT] -filter_magnets = FilterMagnets() +# filter_magnets = _FilterMagnets() def is_int(s): @@ -200,7 +203,7 @@ def format_torrent_info(torrent): log.debug(status) status_string = '' try: - status_string = u''.join([f(status[i], status) for i, f in INFO_DICT if f is not None]) + status_string = ''.join([f(status[i], status) for i, f in INFO_DICT if f is not None]) # except UnicodeDecodeError as e: except Exception as e: status_string = '' @@ -240,7 +243,7 @@ def enable(self): 'seeding': self.cmd_up, 'paused': self.cmd_paused, 'queued': self.cmd_paused, - '?': self.cmd_help, + # '?': self.cmd_help, 'cancel': self.cancel, 'help': self.cmd_help, 'start': self.cmd_help, @@ -248,7 +251,8 @@ def enable(self): # 'rss': self.cmd_add_rss, 'commands': self.cmd_help} - log.debug(prelog() + 'Initialize bot') + self.torrent_manager = component.get("TorrentManager") + log.info(prelog() + 'Initialize bot') if self.config['telegram_token'] != DEFAULT_PREFS['telegram_token']: if self.config['telegram_user']: @@ -256,14 +260,14 @@ def enable(self): self.whitelist.append(str(self.config['telegram_user'])) self.notifylist.append(str(self.config['telegram_user'])) if self.config['telegram_users']: - telegram_user_list = filter(None, [x.strip() for x in - str(self.config['telegram_users']).split(',')]) + telegram_user_list = [_f for _f in [x.strip() for x in + str(self.config['telegram_users']).split(',')] if _f] # Merge with whitelist and remove duplicates - order will be lost self.whitelist = list(set(self.whitelist + telegram_user_list)) log.debug(prelog() + 'Whitelist: ' + str(self.whitelist)) if self.config['telegram_users_notify']: - n = filter(None, [x.strip() for x in - str(self.config['telegram_users_notify']).split(',')]) + n = [_f for _f in [x.strip() for x in + str(self.config['telegram_users_notify']).split(',')] if _f] telegram_user_list_notify = [a for a in n if is_int(a)] # Merge with notifylist and remove duplicates - order will be lost self.notifylist = list(set(self.notifylist + @@ -290,7 +294,7 @@ def enable(self): self.bot = Bot(self.config['telegram_token'], request=bot_request) # Create the EventHandler and pass it bot's token. # self.updater = Updater(self.config['telegram_token'], bot=self.bot, request_kwargs=REQUEST_KWARGS) - self.updater = Updater(bot=self.bot, request_kwargs=REQUEST_KWARGS) + self.updater = Updater(bot=self.bot, use_context=True, request_kwargs=REQUEST_KWARGS) # Get the dispatcher to register handlers dp = self.updater.dispatcher # Add conversation handler with the different states @@ -334,9 +338,9 @@ def enable(self): dp.add_handler(conv_handler_paused) dp.add_handler(conv_handler_rss) dp.add_handler(MessageHandler(Filters.document, self.add_torrent)) - dp.add_handler(MessageHandler(filter_magnets, self.find_magnet)) + dp.add_handler(MessageHandler(Filters.regex(r'magnet:\?'), self.find_magnet)) - for key, value in self.COMMANDS.iteritems(): + for key, value in self.COMMANDS.items(): dp.add_handler(CommandHandler(key, value)) # Log all errors @@ -358,8 +362,8 @@ def enable(self): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def error(self, bot, update, error): - log.warn('Update "%s" caused error "%s"' % (update, error)) + def error(self, update: Update, context: CallbackContext): + log.warn('Update "%s" caused error "%s"' % (update, context.error)) def disable(self): try: @@ -384,7 +388,7 @@ def telegram_send(self, message, to=None, parse_mode=None): to = self.config['telegram_user'] else: log.debug(prelog() + 'send_message, to set') - if not isinstance(to, (list,)): + if not isinstance(to, list): log.debug(prelog() + 'Convert "to" to list') to = [to] log.debug(prelog() + "[to] " + str(to)) @@ -445,7 +449,7 @@ def telegram_poll_stop(self): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def cancel(self, bot, update): + def cancel(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: log.info("User %s canceled the conversation." % str(update.message.chat.id)) @@ -456,7 +460,7 @@ def cancel(self, bot, update): self.yarss_data.clear() return ConversationHandler.END - def cmd_help(self, bot, update): + def cmd_help(self, update: Update, context: CallbackContext): log.debug(prelog() + "Entered cmd_help") if str(update.message.chat.id) in self.whitelist: log.debug(prelog() + str(update.message.chat.id) + " in whitelist") @@ -475,7 +479,7 @@ def cmd_help(self, bot, update): to=[update.message.chat.id], parse_mode='Markdown') - def cmd_list(self, bot, update): + def cmd_list(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: # log.error(self.list_torrents()) self.telegram_send(self.list_torrents(lambda t: @@ -485,21 +489,21 @@ def cmd_list(self, bot, update): to=[update.message.chat.id], parse_mode='Markdown') - def cmd_down(self, bot, update): + def cmd_down(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: self.telegram_send(self.list_torrents(lambda t: t.get_status(('state',))['state'] == 'Downloading'), to=[update.message.chat.id], parse_mode='Markdown') - def cmd_up(self, bot, update): + def cmd_up(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: self.telegram_send(self.list_torrents(lambda t: t.get_status(('state',))['state'] == 'Seeding'), to=[update.message.chat.id], parse_mode='Markdown') - def cmd_paused(self, bot, update): + def cmd_paused(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: self.telegram_send(self.list_torrents(lambda t: t.get_status(('state',))['state'] in @@ -507,43 +511,42 @@ def cmd_paused(self, bot, update): to=[update.message.chat.id], parse_mode='Markdown') - def add(self, bot, update): - # log.error(type(update.message.chat.id) + str(update.message.chat.id)) + def add(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: self.opts = {} self.is_rss = False self.magnet_only = False - return self.prepare_categories(bot, update) + return self.prepare_categories(update, context) """ if "YaRSS2" in component.get('Core').get_available_plugins(): - return self.prepare_torrent_or_rss(bot, update) + return self.prepare_torrent_or_rss(update, context) else: - return self.prepare_categories(bot, update) + return self.prepare_categories(update, context) """ - def add_paused(self, bot, update): + def add_paused(self, update: Update, context: CallbackContext): # log.error(type(update.message.chat.id) + str(update.message.chat.id)) if str(update.message.chat.id) in self.whitelist: self.opts = {} self.opts["addpaused"] = True self.is_rss = False self.magnet_only = False - return self.prepare_categories(bot, update) + return self.prepare_categories(update, context) - def cmd_add_rss(self, bot, update): + def cmd_add_rss(self, update: Update, context: CallbackContext): # log.error(type(update.message.chat.id) + str(update.message.chat.id)) if str(update.message.chat.id) in self.whitelist: if "YaRSS2" in component.get('Core').get_available_plugins(): - return self.add_rss(bot, update) + return self.add_rss(update, context) else: update.message.reply_text('YaRSS2 plugin not available', reply_markup=ReplyKeyboardRemove()) self.is_rss = False self.yarss_data.clear() return ConversationHandler.END - # return self.prepare_categories(bot, update) + # return self.prepare_categories(update, context) - # def prepare_torrent_or_rss(self, bot, update): + # def prepare_torrent_or_rss(self, update: Update, context: CallbackContext): # try: # keyboard_options = [[STRINGS['torrent']], [STRINGS['rss']]] # update.message.reply_text( @@ -555,29 +558,29 @@ def cmd_add_rss(self, bot, update): # except Exception as e: # log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def torrent_or_rss(self, bot, update): + def torrent_or_rss(self, update: Update, context: CallbackContext): try: if str(update.message.chat.id) not in self.whitelist: return if STRINGS['torrent'] == update.message.text: - return self.prepare_categories(bot, update) + return self.prepare_categories(update, context) if STRINGS['rss'] == update.message.text: - return self.add_rss(bot, update) + return self.add_rss(update, context) except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def prepare_categories(self, bot, update): + def prepare_categories(self, update: Update, context: CallbackContext): try: keyboard_options = [] - filtered_dict = {c: d for c, d in self.config["categories"].iteritems() if os.path.isdir(d)} - missing = {c: d for c, d in self.config["categories"].iteritems() if not os.path.isdir(d)} + filtered_dict = {c: d for c, d in self.config["categories"].items() if os.path.isdir(d)} + missing = {c: d for c, d in self.config["categories"].items() if not os.path.isdir(d)} if len(missing) > 0: - for k in missing.keys(): + for k in list(missing.keys()): log.error(prelog() + "Missing directory for category {} ({})".format(k, missing[k])) - for c, d in filtered_dict.iteritems(): + for c, d in filtered_dict.items(): log.error(prelog() + c + ' : ' + d) keyboard_options.append([c]) @@ -590,7 +593,7 @@ def prepare_categories(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def prepare_torrent_type(self, update): + def prepare_torrent_type(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: # Request torrent type @@ -607,13 +610,13 @@ def prepare_torrent_type(self, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def category(self, bot, update): + def category(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: if STRINGS['no_category'] == update.message.text: self.opts = self.opts else: - if update.message.text in self.config["categories"].keys(): + if update.message.text in list(self.config["categories"].keys()): # move_completed_path vs download_location self.opts["move_completed_path"] = self.config["categories"][update.message.text] self.opts["move_completed"] = True @@ -641,7 +644,7 @@ def category(self, bot, update): traceback.format_exc()) if self.is_rss: log.debug(prelog() + "is_rss, calling RSS_APPLY") - return self.rss_apply(bot, update) + return self.rss_apply(update, context) log.debug(prelog() + "Label segment") keyboard_options = [] @@ -676,7 +679,7 @@ def category(self, bot, update): return SET_LABEL else: if self.is_rss: - return self.prepare_rss_feed(bot, update) + return self.prepare_rss_feed(update, context) else: return self.prepare_torrent_type(update) """ @@ -684,21 +687,21 @@ def category(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def prepare_rss_feed(self, bot, update): + def prepare_rss_feed(self, update: Update, context: CallbackContext): if self.yarss_plugin is None: self.yarss_plugin = component.get('CorePlugin.YaRSS2') if self.yarss_plugin: self.yarss_config = self.yarss_plugin.get_config() feeds = {} # Create dictionary with feed name:url - for rss_feed in self.yarss_config["rssfeeds"].values(): + for rss_feed in list(self.yarss_config["rssfeeds"].values()): feeds[rss_feed["name"]] = rss_feed["url"] # If feed(s) found count = 0 if len(feeds) > 0: count = count + 1 - feedlist = "\n".join(["{}) [{}]({})".format(count, f, feeds[f]) for f in feeds.keys()]) - keyboard_options = [feeds.keys()] + feedlist = "\n".join(["{}) [{}]({})".format(count, f, feeds[f]) for f in list(feeds.keys())]) + keyboard_options = [list(feeds.keys())] update.message.reply_text( '%s\n\n%s\n\n%s' % (STRINGS['which_rss_feed'], feedlist, STRINGS['cancel']), reply_markup=ReplyKeyboardMarkup(keyboard_options, one_time_keyboard=True), @@ -713,12 +716,12 @@ def prepare_rss_feed(self, bot, update): return ConversationHandler.END - def rss_feed(self, bot, update): + def rss_feed(self, update: Update, context: CallbackContext): if not str(update.message.chat.id) in self.whitelist: return self.is_rss = True try: - rss_feed = next(rss_feed for rss_feed in self.yarss_config["rssfeeds"].values() + rss_feed = next(rss_feed for rss_feed in list(self.yarss_config["rssfeeds"].values()) if rss_feed["name"] == update.message.text) self.yarss_data.subscription_data["rssfeed_key"] = rss_feed["key"] @@ -726,7 +729,7 @@ def rss_feed(self, bot, update): log.debug(self.config["regex_exp"]) - keyboard_options = [[regex_name] for regex_name in self.config["regex_exp"].keys() if regex_name != ''] + keyboard_options = [[regex_name] for regex_name in list(self.config["regex_exp"].keys()) if regex_name != ''] if len(keyboard_options) > 0: update.message.reply_text( @@ -741,7 +744,7 @@ def rss_feed(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def regex(self, bot, update): + def regex(self, update: Update, context: CallbackContext): if not str(update.message.chat.id) in self.whitelist: return if REGEX_SUBS_WORD in self.config["regex_exp"][update.message.text]: @@ -753,13 +756,13 @@ def regex(self, bot, update): reply_markup=ReplyKeyboardRemove()) return FILE_NAME else: - keyboard_options = [[regex_name] for regex_name in self.config["regex_exp"].keys()] + keyboard_options = [[regex_name] for regex_name in list(self.config["regex_exp"].keys())] update.message.reply_text( '%s\n%s' % (STRINGS['no_name'], STRINGS['cancel']), reply_markup=ReplyKeyboardMarkup(keyboard_options, one_time_keyboard=True)) return REGEX - def rss_file_name(self, bot, update): + def rss_file_name(self, update: Update, context: CallbackContext): if not str(update.message.chat.id) in self.whitelist: return @@ -775,9 +778,9 @@ def rss_file_name(self, bot, update): self.yarss_data.subscription_data["label"] = self.label self.yarss_data.subscription_data["name"] = update.message.text - return self.prepare_categories(bot, update) + return self.prepare_categories(update, context) - def rss_apply(self, bot, update): + def rss_apply(self, update: Update, context: CallbackContext): log.debug(prelog() + "entered rss_apply") if str(update.message.chat.id) in self.whitelist: log.debug(prelog() + "in whitelist") @@ -796,19 +799,19 @@ def rss_apply(self, bot, update): else: log.debug(prelog() + "not in whitelist") - def set_label(self, bot, update): + def set_label(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: # user = update.message.chat.id self.label = update.message.text log.debug(prelog() + "Label: %s" % (update.message.text)) - return self.prepare_torrent_type(update) + return self.prepare_torrent_type(update, context) """ # Request torrent type if self.is_rss: - return self.prepare_rss_feed(bot, update) + return self.prepare_rss_feed(update, context) else: return self.prepare_torrent_type(update) """ @@ -816,7 +819,7 @@ def set_label(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def torrent_type(self, bot, update): + def torrent_type(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: user = update.message.chat.id @@ -845,7 +848,7 @@ def torrent_type(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def add_magnet(self, bot, update): + def add_magnet(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: if self.magnet_only: @@ -891,7 +894,7 @@ def add_magnet(self, bot, update): # except Exception as e: # log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def find_magnet(self, bot, update): + def find_magnet(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: log.debug("Find magnets in message") @@ -901,7 +904,7 @@ def find_magnet(self, bot, update): if len(m) > 0: mag = m[0] self.magnet_only = True - return self.add_magnet(bot, update) + return self.add_magnet(update, context) else: log.debug("Magnet not found in message") update.message.reply_text(STRINGS['no_magnet_found'], @@ -915,7 +918,7 @@ def find_magnet(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def add_torrent(self, bot, update): + def add_torrent(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: user = update.message.chat.id @@ -926,10 +929,10 @@ def add_torrent(self, bot, update): # Get file info file_info = self.bot.getFile(update.message.document.file_id) # Download file - request = urllib2.Request(file_info.file_path, headers=HEADERS) - status_code = urllib2.urlopen(request).getcode() + request = urllib.request.Request(file_info.file_path, headers=HEADERS) + status_code = urllib.request.urlopen(request).getcode() if status_code == 200: - file_contents = urllib2.urlopen(request).read() + file_contents = urllib.request.urlopen(request).read() # Base64 encode file data metainfo = b64encode(file_contents) if self.opts is None: @@ -950,7 +953,7 @@ def add_torrent(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def add_url(self, bot, update): + def add_url(self, update: Update, context: CallbackContext): if str(update.message.chat.id) in self.whitelist: try: user = update.message.chat.id @@ -959,11 +962,11 @@ def add_url(self, bot, update): if is_url(update.message.text): try: # Download file - request = urllib2.Request(update.message.text.strip(), + request = urllib.request.Request(update.message.text.strip(), headers=HEADERS) - status_code = urllib2.urlopen(request).getcode() + status_code = urllib.request.urlopen(request).getcode() if status_code == 200: - file_contents = urllib2.urlopen(request).read() + file_contents = urllib.request.urlopen(request).read() # Base64 encode file data metainfo = b64encode(file_contents) if self.opts is None: @@ -988,12 +991,12 @@ def add_url(self, bot, update): except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) - def add_rss(self, bot, update): + def add_rss(self, update: Update, context: CallbackContext): try: component.get('Core').enable_plugin('YaRSS2') self.is_rss = True - return self.prepare_rss_feed(bot, update) - # return self.prepare_categories(bot, update) + return self.prepare_rss_feed(update, context) + # return self.prepare_categories(update, context) except Exception as e: log.error(prelog() + str(e) + '\n' + traceback.format_exc()) @@ -1021,7 +1024,7 @@ def update_stats(self): def check_speed(self): log.debug("Minimum speed: %s", self.config["minimum_speed"]) try: - for t in component.get('TorrentManager').torrents.values(): + for t in list(component.get('TorrentManager').torrents.values()): if t.get_status(("state",))["state"] == "Downloading": if t.status.download_rate < (self.config['minimum_speed'] * 1024): message = _('Torrent *%(name)s* is slower than minimum speed!') % t.get_status({}) @@ -1044,19 +1047,29 @@ def disconnect_events(self): event_manager.deregister_event_handler('TorrentAddedEvent', self.on_torrent_added) - def on_torrent_added(self, torrent_id): + def on_torrent_added(self, torrent_id, from_state): if (self.config['telegram_notify_added'] is False): return try: custom_message = False - # get_torrent_status - # torrent_id = str(alert.handle.info_hash()) torrent = component.get('TorrentManager')[torrent_id] - torrent_status = torrent.get_status({}) + torrent_status = torrent.get_status(['name']) + # get the torrent added time from get_status + torrent_added = torrent.get_status(['time_added']) + # check if torrent_added time is in the last 5 minutes + # if it is, send the message + # if it is not, do not send the message + # this is to prevent the bot from sending a message when the bot is restarted + # and all the torrents are added + # this is a hacky way to do it, but it works + # if the torrent was added more than 5 minutes ago, do not send the message + if (torrent_added["time_added"] < (time.time() - 300)): + return # Check if label shows up here log.debug("get_status for {}".format(torrent_id)) log.debug(torrent_status) message = _('Added Torrent *%(name)s*') % torrent_status + log.info(prelog() + 'Torrent added: %s' % torrent_added) # Check if custom message if self.config["message_added"] is not DEFAULT_PREFS["message_added"] \ and len(self.config["message_added"]) > 0: @@ -1076,12 +1089,9 @@ def on_torrent_finished(self, torrent_id): try: if (self.config['telegram_notify_finished'] is False): return - # torrent_id = str(alert.handle.info_hash()) custom_message = False - # get_torrent_status - # torrent_id = str(alert.handle.info_hash()) torrent = component.get('TorrentManager')[torrent_id] - torrent_status = torrent.get_status({}) + torrent_status = torrent.get_status(['name']) # Check if label shows up here log.debug("get_status for {}".format(torrent_id)) log.debug(torrent_status) @@ -1102,18 +1112,23 @@ def on_torrent_finished(self, torrent_id): log.error(prelog() + 'Error in alert %s' % str(e) + '\n' + traceback.format_exc()) - def list_torrents(self, filter=lambda _: True): - return '\n'.join([format_torrent_info(t) for t - in component.get('TorrentManager').torrents.values() - if filter(t)] or [STRINGS['no_items']]) + def list_torrents(self, filterz=lambda _: True): + selected_torrents = [] + torrents = list(self.torrent_manager.torrents.values()) + for t in torrents: + if filterz(t): + selected_torrents.append(format_torrent_info(t)) + if len(selected_torrents) == 0: + return STRINGS['no_items'] + return "\n".join(selected_torrents) @export def set_config(self, config): """Sets the config dictionary""" log.debug(prelog() + 'Set config') dirty = False - for key in config.keys(): - if ("categories" == key and cmp(self.config[key], config[key])) or \ + for key in list(config.keys()): + if ("categories" == key and self.config[key] == config[key]) or \ self.config[key] != config[key]: dirty = True self.config[key] = config[key] diff --git a/telegramer/data/config.ui b/telegramer/data/config.ui new file mode 100644 index 0000000..5333426 --- /dev/null +++ b/telegramer/data/config.ui @@ -0,0 +1,1001 @@ + + + + + + + True + Telegramer + + + True + True + + + True + False + + + True + False + 0.019999999552965164 + 10 + <b><i>Bot Settings</i></b> + True + + + True + True + 0 + + + + + True + False + + + True + False + 0.10000000149011612 + 3 + Telegram Bot Token: + + + False + False + 8 + 0 + + + + + True + True + + Contact @BotFather and create a new bot + True + False + False + True + True + + + True + True + 8 + 1 + + + + + False + False + 2 + 1 + + + + + True + False + + + True + False + 0.10000000149011612 + 3 + Telegram User ID: + + + False + False + 8 + 0 + + + + + True + True + + Contact @MyIDbot + True + False + False + True + True + + + True + True + 8 + 1 + + + + + False + False + 2 + 2 + + + + + True + False + + + True + False + 0.10000000149011612 + 3 + Additional IDs: + + + False + False + 8 + 0 + + + + + True + True + + IDs should be comma-separated + True + False + False + True + True + + + True + True + 8 + 1 + + + + + False + False + 2 + 3 + + + + + True + False + + + True + False + 0.10000000149011612 + 3 + Notify IDs: + + + False + False + 8 + 0 + + + + + True + True + + IDs should be comma-separated + True + False + False + True + True + + + True + True + 8 + 1 + + + + + False + False + 2 + 4 + + + + + True + False + 0.019999999552965164 + 10 + <b><i>Notifications</i></b> + True + + + True + True + 5 + + + + + Send Telegram notification when torrents are added + True + True + False + True + True + + + False + False + 8 + 6 + + + + + True + True + Message to send when new torrents are added + 80 + + Added Torrent *TORRENTNAME* + True + False + False + True + True + + + True + True + 7 + + + + + Send Telegram notification when torrents finish + True + True + False + True + True + + + False + False + 8 + 8 + + + + + True + True + Message to send when torrents finish downloading + 80 + + Finished Downloading *TORRENTNAME* + True + False + False + True + True + + + True + True + 9 + + + + + True + False + 0.019999999552965164 + 10 + <b><i>Sorting</i></b> + True + + + True + True + 10 + + + + + True + False + + + True + False + + + True + False + Category + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + + + True + True + 2 + + + + + True + True + + True + False + False + True + True + + + True + True + 3 + + + + + False + False + 0 + + + + + True + False + + + True + False + Directory + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + + + True + True + 2 + + + + + True + True + + True + False + False + True + True + + + True + True + 3 + + + + + True + True + 1 + + + + + False + False + 8 + 11 + + + + + True + False + 0.019999999552965164 + 10 + <b><i>Proxy Configuration</i></b> + True + + + True + True + 12 + + + + + True + False + + + True + False + 0.10000000149011612 + 3 + Proxy URL: + + + False + False + 8 + 0 + + + + + True + True + Example: socks5://127.0.0.1:9150 + + True + False + False + True + True + + + True + True + 8 + 1 + + + + + False + False + 2 + 13 + + + + + True + False + + + True + False + 0.10000000149011612 + 3 + Username: + + + False + False + 8 + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 8 + 1 + + + + + False + False + 2 + 14 + + + + + True + False + + + True + False + 0.10000000149011612 + 3 + Password: + + + False + False + 8 + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 8 + 1 + + + + + False + False + 2 + 15 + + + + + False + False + 0 + + + + + True + False + + + True + False + 0.019999999552965164 + 10 + <b><i>Regex Template</i></b> + True + + + True + True + 0 + + + + + True + False + + + True + False + + + True + False + Name + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + + + True + True + 2 + + + + + True + True + + True + False + False + True + True + + + True + True + 3 + + + + + False + False + 0 + + + + + True + False + + + True + False + Regex + + + True + True + 0 + + + + + True + True + Example: NAME.*amd64.*\.iso +NAME is a placeholder for new RSS filters + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + + + True + True + 2 + + + + + True + True + + True + False + False + True + True + + + True + True + 3 + + + + + True + True + 1 + + + + + False + False + 8 + 1 + + + + + True + True + 1 + + + + + True + False + + + True + True + + + True + False + 0.019999999552965164 + 10 + <b><i>Minimum Download Speed (KB/s) (-1 means no minimum)</i></b> + True + + + True + True + 0 + + + + + True + True + + 50 + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 0 + + + + + True + True + + + True + False + 0.019999999552965164 + 10 + <b><i>Check for Slow Torrents Every X seconds</i></b> + True + + + True + True + 0 + + + + + True + True + + 60 + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 1 + + + + + True + True + 2 + + + + + True + False + 10 + + + True + False + 12 + True + center + + + Save + True + True + True + + + + True + True + 8 + 0 + + + + + Test + True + True + True + + + + True + True + 8 + 1 + + + + + Reload + True + True + True + + + + True + True + 8 + 2 + + + + + True + False + 8 + end + 0 + + + + + False + False + 10 + end + 4 + + + + + + diff --git a/telegramer/gtk3ui.py b/telegramer/gtk3ui.py new file mode 100755 index 0000000..6d09954 --- /dev/null +++ b/telegramer/gtk3ui.py @@ -0,0 +1,148 @@ +# +# gtkui.py +# +# Copyright (C) 2016-2019 Noam +# https://github.com/noam09 +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# + + +try: + from deluge.log import LOG as log +except Exception as e: + print('Telegramer: Exception - %s' % str(e)) + +try: + from gi.repository import Gtk + import deluge.common + from .common import get_resource + from deluge.ui.client import client + import deluge.component as component + from deluge.plugins.pluginbase import Gtk3PluginBase +except ImportError as e: + log.error('Telegramer: Import error - %s', str(e)) + +REGEX_SUBS_WORD = "NAME" + + +class Gtk3UI(Gtk3PluginBase): + def enable(self): + self.builder = Gtk.Builder.new_from_file(get_resource("config.ui")) + self.builder.connect_signals({ + "on_button_test_clicked": self.on_button_test_clicked, + "on_button_save_clicked": self.on_button_save_clicked, + "on_button_reload_clicked": self.on_button_reload_clicked + }) + component.get("Preferences").add_page("Telegramer", self.builder.get_object("prefs_box")) + component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) + component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) + + def disable(self): + component.get("Preferences").remove_page("Telegramer") + component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs) + component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs) + + def on_apply_prefs(self): + log.debug("Telegramer: applying prefs for Telegramer") + config = { + "telegram_notify_added": self.builder.get_object("telegram_notify_added").get_active(), + "telegram_notify_finished": self.builder.get_object("telegram_notify_finished").get_active(), + "telegram_token": self.builder.get_object("telegram_token").get_text(), + "telegram_user": self.builder.get_object("telegram_user").get_text(), + "telegram_users": self.builder.get_object("telegram_users").get_text(), + "telegram_users_notify": self.builder.get_object("telegram_users_notify").get_text(), + "minimum_speed": self.builder.get_object("minimum_speed").get_text(), + "user_timer": self.builder.get_object("user_timer").get_text(), + "proxy_url": self.builder.get_object("proxy_url").get_text(), + "urllib3_proxy_kwargs_username": self.builder.get_object("urllib3_proxy_kwargs_username").get_text(), + "urllib3_proxy_kwargs_password": self.builder.get_object("urllib3_proxy_kwargs_password").get_text(), + "categories": {self.builder.get_object("cat1").get_text(): + self.builder.get_object("dir1").get_text(), + self.builder.get_object("cat2").get_text(): + self.builder.get_object("dir2").get_text(), + self.builder.get_object("cat3").get_text(): + self.builder.get_object("dir3").get_text() + }, + "regex_exp": {self.builder.get_object("rname1").get_text(): + self.builder.get_object("reg1").get_text(), + self.builder.get_object("rname2").get_text(): + self.builder.get_object("reg2").get_text(), + self.builder.get_object("rname3").get_text(): + self.builder.get_object("reg3").get_text() + } + } + + client.telegramer.set_config(config) + for ind, (n, r) in enumerate(config["regex_exp"].items()): + if REGEX_SUBS_WORD not in r: + log.error("Your regex " + n + " template does not contain the " + + REGEX_SUBS_WORD + " keyword") + break + + def on_show_prefs(self): + client.telegramer.get_config().addCallback(self.cb_get_config) + + def cb_get_config(self, config): + "callback for on show_prefs" + self.builder.get_object("telegram_notify_added").set_active(config["telegram_notify_added"]) + self.builder.get_object("telegram_notify_finished").set_active(config["telegram_notify_finished"]) + self.builder.get_object("telegram_token").set_text(config["telegram_token"]) + self.builder.get_object("telegram_user").set_text(config["telegram_user"]) + self.builder.get_object("telegram_users").set_text(config["telegram_users"]) + self.builder.get_object("telegram_users_notify").set_text(config["telegram_users_notify"]) + # Slow + self.builder.get_object("minimum_speed").set_text(str(config["minimum_speed"])) + self.builder.get_object("user_timer").set_text(str(config["user_timer"])) + # Proxy + self.builder.get_object("proxy_url").set_text(config["proxy_url"]), + self.builder.get_object('urllib3_proxy_kwargs_username').set_text(config["urllib3_proxy_kwargs_username"]), + self.builder.get_object("urllib3_proxy_kwargs_password").set_text(config["urllib3_proxy_kwargs_password"]), + # Categories + for ind, (c, d) in enumerate(config["categories"].items()): + self.builder.get_object("cat"+str(ind+1)).set_text(c) + self.builder.get_object("dir"+str(ind+1)).set_text(d) + # RSS + for ind, (n, r) in enumerate(config["regex_exp"].items()): + self.builder.get_object("rname"+str(ind+1)).set_text(n) + self.builder.get_object("reg"+str(ind+1)).set_text(r) + + def on_button_test_clicked(self, Event=None): + client.telegramer.telegram_do_test() + + def on_button_save_clicked(self, Event=None): + self.on_apply_prefs() + + def on_button_reload_clicked(self, Event=None): + client.telegramer.restart_telegramer() diff --git a/telegramer/gtkui.py b/telegramer/gtkui.py index a3f1f50..9570a26 100755 --- a/telegramer/gtkui.py +++ b/telegramer/gtkui.py @@ -42,7 +42,7 @@ try: from deluge.log import LOG as log except Exception as e: - print 'Telegramer: Exception - %s' % str(e) + print("Telegramer: Exception - {}".format(str(e))) try: import gtk diff --git a/telegramer/include/apscheduler/__init__.py b/telegramer/include/apscheduler/__init__.py new file mode 100644 index 0000000..968169a --- /dev/null +++ b/telegramer/include/apscheduler/__init__.py @@ -0,0 +1,10 @@ +from pkg_resources import get_distribution, DistributionNotFound + +try: + release = get_distribution('APScheduler').version.split('-')[0] +except DistributionNotFound: + release = '3.5.0' + +version_info = tuple(int(x) if x.isdigit() else x for x in release.split('.')) +version = __version__ = '.'.join(str(x) for x in version_info[:3]) +del get_distribution, DistributionNotFound diff --git a/telegramer/include/apscheduler/events.py b/telegramer/include/apscheduler/events.py new file mode 100644 index 0000000..016da03 --- /dev/null +++ b/telegramer/include/apscheduler/events.py @@ -0,0 +1,94 @@ +__all__ = ('EVENT_SCHEDULER_STARTED', 'EVENT_SCHEDULER_SHUTDOWN', 'EVENT_SCHEDULER_PAUSED', + 'EVENT_SCHEDULER_RESUMED', 'EVENT_EXECUTOR_ADDED', 'EVENT_EXECUTOR_REMOVED', + 'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED', 'EVENT_ALL_JOBS_REMOVED', + 'EVENT_JOB_ADDED', 'EVENT_JOB_REMOVED', 'EVENT_JOB_MODIFIED', 'EVENT_JOB_EXECUTED', + 'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED', 'EVENT_JOB_SUBMITTED', 'EVENT_JOB_MAX_INSTANCES', + 'SchedulerEvent', 'JobEvent', 'JobExecutionEvent', 'JobSubmissionEvent') + + +EVENT_SCHEDULER_STARTED = EVENT_SCHEDULER_START = 2 ** 0 +EVENT_SCHEDULER_SHUTDOWN = 2 ** 1 +EVENT_SCHEDULER_PAUSED = 2 ** 2 +EVENT_SCHEDULER_RESUMED = 2 ** 3 +EVENT_EXECUTOR_ADDED = 2 ** 4 +EVENT_EXECUTOR_REMOVED = 2 ** 5 +EVENT_JOBSTORE_ADDED = 2 ** 6 +EVENT_JOBSTORE_REMOVED = 2 ** 7 +EVENT_ALL_JOBS_REMOVED = 2 ** 8 +EVENT_JOB_ADDED = 2 ** 9 +EVENT_JOB_REMOVED = 2 ** 10 +EVENT_JOB_MODIFIED = 2 ** 11 +EVENT_JOB_EXECUTED = 2 ** 12 +EVENT_JOB_ERROR = 2 ** 13 +EVENT_JOB_MISSED = 2 ** 14 +EVENT_JOB_SUBMITTED = 2 ** 15 +EVENT_JOB_MAX_INSTANCES = 2 ** 16 +EVENT_ALL = (EVENT_SCHEDULER_STARTED | EVENT_SCHEDULER_SHUTDOWN | EVENT_SCHEDULER_PAUSED | + EVENT_SCHEDULER_RESUMED | EVENT_EXECUTOR_ADDED | EVENT_EXECUTOR_REMOVED | + EVENT_JOBSTORE_ADDED | EVENT_JOBSTORE_REMOVED | EVENT_ALL_JOBS_REMOVED | + EVENT_JOB_ADDED | EVENT_JOB_REMOVED | EVENT_JOB_MODIFIED | EVENT_JOB_EXECUTED | + EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_JOB_SUBMITTED | EVENT_JOB_MAX_INSTANCES) + + +class SchedulerEvent(object): + """ + An event that concerns the scheduler itself. + + :ivar code: the type code of this event + :ivar alias: alias of the job store or executor that was added or removed (if applicable) + """ + + def __init__(self, code, alias=None): + super(SchedulerEvent, self).__init__() + self.code = code + self.alias = alias + + def __repr__(self): + return '<%s (code=%d)>' % (self.__class__.__name__, self.code) + + +class JobEvent(SchedulerEvent): + """ + An event that concerns a job. + + :ivar code: the type code of this event + :ivar job_id: identifier of the job in question + :ivar jobstore: alias of the job store containing the job in question + """ + + def __init__(self, code, job_id, jobstore): + super(JobEvent, self).__init__(code) + self.code = code + self.job_id = job_id + self.jobstore = jobstore + + +class JobSubmissionEvent(JobEvent): + """ + An event that concerns the submission of a job to its executor. + + :ivar scheduled_run_times: a list of datetimes when the job was intended to run + """ + + def __init__(self, code, job_id, jobstore, scheduled_run_times): + super(JobSubmissionEvent, self).__init__(code, job_id, jobstore) + self.scheduled_run_times = scheduled_run_times + + +class JobExecutionEvent(JobEvent): + """ + An event that concerns the running of a job within its executor. + + :ivar scheduled_run_time: the time when the job was scheduled to be run + :ivar retval: the return value of the successfully executed job + :ivar exception: the exception raised by the job + :ivar traceback: a formatted traceback for the exception + """ + + def __init__(self, code, job_id, jobstore, scheduled_run_time, retval=None, exception=None, + traceback=None): + super(JobExecutionEvent, self).__init__(code, job_id, jobstore) + self.scheduled_run_time = scheduled_run_time + self.retval = retval + self.exception = exception + self.traceback = traceback diff --git a/telegramer/include/apscheduler/executors/__init__.py b/telegramer/include/apscheduler/executors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegramer/include/apscheduler/executors/asyncio.py b/telegramer/include/apscheduler/executors/asyncio.py new file mode 100644 index 0000000..06fc7f9 --- /dev/null +++ b/telegramer/include/apscheduler/executors/asyncio.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import + +import sys + +from apscheduler.executors.base import BaseExecutor, run_job +from apscheduler.util import iscoroutinefunction_partial + +try: + from apscheduler.executors.base_py3 import run_coroutine_job +except ImportError: + run_coroutine_job = None + + +class AsyncIOExecutor(BaseExecutor): + """ + Runs jobs in the default executor of the event loop. + + If the job function is a native coroutine function, it is scheduled to be run directly in the + event loop as soon as possible. All other functions are run in the event loop's default + executor which is usually a thread pool. + + Plugin alias: ``asyncio`` + """ + + def start(self, scheduler, alias): + super(AsyncIOExecutor, self).start(scheduler, alias) + self._eventloop = scheduler._eventloop + self._pending_futures = set() + + def shutdown(self, wait=True): + # There is no way to honor wait=True without converting this method into a coroutine method + for f in self._pending_futures: + if not f.done(): + f.cancel() + + self._pending_futures.clear() + + def _do_submit_job(self, job, run_times): + def callback(f): + self._pending_futures.discard(f) + try: + events = f.result() + except BaseException: + self._run_job_error(job.id, *sys.exc_info()[1:]) + else: + self._run_job_success(job.id, events) + + if iscoroutinefunction_partial(job.func): + if run_coroutine_job is not None: + coro = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name) + f = self._eventloop.create_task(coro) + else: + raise Exception('Executing coroutine based jobs is not supported with Trollius') + else: + f = self._eventloop.run_in_executor(None, run_job, job, job._jobstore_alias, run_times, + self._logger.name) + + f.add_done_callback(callback) + self._pending_futures.add(f) diff --git a/telegramer/include/apscheduler/executors/base.py b/telegramer/include/apscheduler/executors/base.py new file mode 100644 index 0000000..4c09fc1 --- /dev/null +++ b/telegramer/include/apscheduler/executors/base.py @@ -0,0 +1,146 @@ +from abc import ABCMeta, abstractmethod +from collections import defaultdict +from datetime import datetime, timedelta +from traceback import format_tb +import logging +import sys + +from pytz import utc +import six + +from apscheduler.events import ( + JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED) + + +class MaxInstancesReachedError(Exception): + def __init__(self, job): + super(MaxInstancesReachedError, self).__init__( + 'Job "%s" has already reached its maximum number of instances (%d)' % + (job.id, job.max_instances)) + + +class BaseExecutor(six.with_metaclass(ABCMeta, object)): + """Abstract base class that defines the interface that every executor must implement.""" + + _scheduler = None + _lock = None + _logger = logging.getLogger('apscheduler.executors') + + def __init__(self): + super(BaseExecutor, self).__init__() + self._instances = defaultdict(lambda: 0) + + def start(self, scheduler, alias): + """ + Called by the scheduler when the scheduler is being started or when the executor is being + added to an already running scheduler. + + :param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting + this executor + :param str|unicode alias: alias of this executor as it was assigned to the scheduler + + """ + self._scheduler = scheduler + self._lock = scheduler._create_lock() + self._logger = logging.getLogger('apscheduler.executors.%s' % alias) + + def shutdown(self, wait=True): + """ + Shuts down this executor. + + :param bool wait: ``True`` to wait until all submitted jobs + have been executed + """ + + def submit_job(self, job, run_times): + """ + Submits job for execution. + + :param Job job: job to execute + :param list[datetime] run_times: list of datetimes specifying + when the job should have been run + :raises MaxInstancesReachedError: if the maximum number of + allowed instances for this job has been reached + + """ + assert self._lock is not None, 'This executor has not been started yet' + with self._lock: + if self._instances[job.id] >= job.max_instances: + raise MaxInstancesReachedError(job) + + self._do_submit_job(job, run_times) + self._instances[job.id] += 1 + + @abstractmethod + def _do_submit_job(self, job, run_times): + """Performs the actual task of scheduling `run_job` to be called.""" + + def _run_job_success(self, job_id, events): + """ + Called by the executor with the list of generated events when :func:`run_job` has been + successfully called. + + """ + with self._lock: + self._instances[job_id] -= 1 + if self._instances[job_id] == 0: + del self._instances[job_id] + + for event in events: + self._scheduler._dispatch_event(event) + + def _run_job_error(self, job_id, exc, traceback=None): + """Called by the executor with the exception if there is an error calling `run_job`.""" + with self._lock: + self._instances[job_id] -= 1 + if self._instances[job_id] == 0: + del self._instances[job_id] + + exc_info = (exc.__class__, exc, traceback) + self._logger.error('Error running job %s', job_id, exc_info=exc_info) + + +def run_job(job, jobstore_alias, run_times, logger_name): + """ + Called by executors to run the job. Returns a list of scheduler events to be dispatched by the + scheduler. + + """ + events = [] + logger = logging.getLogger(logger_name) + for run_time in run_times: + # See if the job missed its run time window, and handle + # possible misfires accordingly + if job.misfire_grace_time is not None: + difference = datetime.now(utc) - run_time + grace_time = timedelta(seconds=job.misfire_grace_time) + if difference > grace_time: + events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias, + run_time)) + logger.warning('Run time of job "%s" was missed by %s', job, difference) + continue + + logger.info('Running job "%s" (scheduled at %s)', job, run_time) + try: + retval = job.func(*job.args, **job.kwargs) + except BaseException: + exc, tb = sys.exc_info()[1:] + formatted_tb = ''.join(format_tb(tb)) + events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time, + exception=exc, traceback=formatted_tb)) + logger.exception('Job "%s" raised an exception', job) + + # This is to prevent cyclic references that would lead to memory leaks + if six.PY2: + sys.exc_clear() + del tb + else: + import traceback + traceback.clear_frames(tb) + del tb + else: + events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time, + retval=retval)) + logger.info('Job "%s" executed successfully', job) + + return events diff --git a/telegramer/include/apscheduler/executors/base_py3.py b/telegramer/include/apscheduler/executors/base_py3.py new file mode 100644 index 0000000..7111d2a --- /dev/null +++ b/telegramer/include/apscheduler/executors/base_py3.py @@ -0,0 +1,43 @@ +import logging +import sys +import traceback +from datetime import datetime, timedelta +from traceback import format_tb + +from pytz import utc + +from apscheduler.events import ( + JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED) + + +async def run_coroutine_job(job, jobstore_alias, run_times, logger_name): + """Coroutine version of run_job().""" + events = [] + logger = logging.getLogger(logger_name) + for run_time in run_times: + # See if the job missed its run time window, and handle possible misfires accordingly + if job.misfire_grace_time is not None: + difference = datetime.now(utc) - run_time + grace_time = timedelta(seconds=job.misfire_grace_time) + if difference > grace_time: + events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias, + run_time)) + logger.warning('Run time of job "%s" was missed by %s', job, difference) + continue + + logger.info('Running job "%s" (scheduled at %s)', job, run_time) + try: + retval = await job.func(*job.args, **job.kwargs) + except BaseException: + exc, tb = sys.exc_info()[1:] + formatted_tb = ''.join(format_tb(tb)) + events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time, + exception=exc, traceback=formatted_tb)) + logger.exception('Job "%s" raised an exception', job) + traceback.clear_frames(tb) + else: + events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time, + retval=retval)) + logger.info('Job "%s" executed successfully', job) + + return events diff --git a/telegramer/include/apscheduler/executors/debug.py b/telegramer/include/apscheduler/executors/debug.py new file mode 100644 index 0000000..ac739ae --- /dev/null +++ b/telegramer/include/apscheduler/executors/debug.py @@ -0,0 +1,20 @@ +import sys + +from apscheduler.executors.base import BaseExecutor, run_job + + +class DebugExecutor(BaseExecutor): + """ + A special executor that executes the target callable directly instead of deferring it to a + thread or process. + + Plugin alias: ``debug`` + """ + + def _do_submit_job(self, job, run_times): + try: + events = run_job(job, job._jobstore_alias, run_times, self._logger.name) + except BaseException: + self._run_job_error(job.id, *sys.exc_info()[1:]) + else: + self._run_job_success(job.id, events) diff --git a/telegramer/include/apscheduler/executors/gevent.py b/telegramer/include/apscheduler/executors/gevent.py new file mode 100644 index 0000000..1235bb6 --- /dev/null +++ b/telegramer/include/apscheduler/executors/gevent.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import +import sys + +from apscheduler.executors.base import BaseExecutor, run_job + + +try: + import gevent +except ImportError: # pragma: nocover + raise ImportError('GeventExecutor requires gevent installed') + + +class GeventExecutor(BaseExecutor): + """ + Runs jobs as greenlets. + + Plugin alias: ``gevent`` + """ + + def _do_submit_job(self, job, run_times): + def callback(greenlet): + try: + events = greenlet.get() + except BaseException: + self._run_job_error(job.id, *sys.exc_info()[1:]) + else: + self._run_job_success(job.id, events) + + gevent.spawn(run_job, job, job._jobstore_alias, run_times, self._logger.name).\ + link(callback) diff --git a/telegramer/include/apscheduler/executors/pool.py b/telegramer/include/apscheduler/executors/pool.py new file mode 100644 index 0000000..c85896e --- /dev/null +++ b/telegramer/include/apscheduler/executors/pool.py @@ -0,0 +1,71 @@ +from abc import abstractmethod +import concurrent.futures + +from apscheduler.executors.base import BaseExecutor, run_job + +try: + from concurrent.futures.process import BrokenProcessPool +except ImportError: + BrokenProcessPool = None + + +class BasePoolExecutor(BaseExecutor): + @abstractmethod + def __init__(self, pool): + super(BasePoolExecutor, self).__init__() + self._pool = pool + + def _do_submit_job(self, job, run_times): + def callback(f): + exc, tb = (f.exception_info() if hasattr(f, 'exception_info') else + (f.exception(), getattr(f.exception(), '__traceback__', None))) + if exc: + self._run_job_error(job.id, exc, tb) + else: + self._run_job_success(job.id, f.result()) + + try: + f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name) + except BrokenProcessPool: + self._logger.warning('Process pool is broken; replacing pool with a fresh instance') + self._pool = self._pool.__class__(self._pool._max_workers) + f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name) + + f.add_done_callback(callback) + + def shutdown(self, wait=True): + self._pool.shutdown(wait) + + +class ThreadPoolExecutor(BasePoolExecutor): + """ + An executor that runs jobs in a concurrent.futures thread pool. + + Plugin alias: ``threadpool`` + + :param max_workers: the maximum number of spawned threads. + :param pool_kwargs: dict of keyword arguments to pass to the underlying + ThreadPoolExecutor constructor + """ + + def __init__(self, max_workers=10, pool_kwargs=None): + pool_kwargs = pool_kwargs or {} + pool = concurrent.futures.ThreadPoolExecutor(int(max_workers), **pool_kwargs) + super(ThreadPoolExecutor, self).__init__(pool) + + +class ProcessPoolExecutor(BasePoolExecutor): + """ + An executor that runs jobs in a concurrent.futures process pool. + + Plugin alias: ``processpool`` + + :param max_workers: the maximum number of spawned processes. + :param pool_kwargs: dict of keyword arguments to pass to the underlying + ProcessPoolExecutor constructor + """ + + def __init__(self, max_workers=10, pool_kwargs=None): + pool_kwargs = pool_kwargs or {} + pool = concurrent.futures.ProcessPoolExecutor(int(max_workers), **pool_kwargs) + super(ProcessPoolExecutor, self).__init__(pool) diff --git a/telegramer/include/apscheduler/executors/tornado.py b/telegramer/include/apscheduler/executors/tornado.py new file mode 100644 index 0000000..3b97eec --- /dev/null +++ b/telegramer/include/apscheduler/executors/tornado.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import + +import sys +from concurrent.futures import ThreadPoolExecutor + +from tornado.gen import convert_yielded + +from apscheduler.executors.base import BaseExecutor, run_job + +try: + from apscheduler.executors.base_py3 import run_coroutine_job + from apscheduler.util import iscoroutinefunction_partial +except ImportError: + def iscoroutinefunction_partial(func): + return False + + +class TornadoExecutor(BaseExecutor): + """ + Runs jobs either in a thread pool or directly on the I/O loop. + + If the job function is a native coroutine function, it is scheduled to be run directly in the + I/O loop as soon as possible. All other functions are run in a thread pool. + + Plugin alias: ``tornado`` + + :param int max_workers: maximum number of worker threads in the thread pool + """ + + def __init__(self, max_workers=10): + super(TornadoExecutor, self).__init__() + self.executor = ThreadPoolExecutor(max_workers) + + def start(self, scheduler, alias): + super(TornadoExecutor, self).start(scheduler, alias) + self._ioloop = scheduler._ioloop + + def _do_submit_job(self, job, run_times): + def callback(f): + try: + events = f.result() + except BaseException: + self._run_job_error(job.id, *sys.exc_info()[1:]) + else: + self._run_job_success(job.id, events) + + if iscoroutinefunction_partial(job.func): + f = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name) + else: + f = self.executor.submit(run_job, job, job._jobstore_alias, run_times, + self._logger.name) + + f = convert_yielded(f) + f.add_done_callback(callback) diff --git a/telegramer/include/apscheduler/executors/twisted.py b/telegramer/include/apscheduler/executors/twisted.py new file mode 100644 index 0000000..c7bcf64 --- /dev/null +++ b/telegramer/include/apscheduler/executors/twisted.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import + +from apscheduler.executors.base import BaseExecutor, run_job + + +class TwistedExecutor(BaseExecutor): + """ + Runs jobs in the reactor's thread pool. + + Plugin alias: ``twisted`` + """ + + def start(self, scheduler, alias): + super(TwistedExecutor, self).start(scheduler, alias) + self._reactor = scheduler._reactor + + def _do_submit_job(self, job, run_times): + def callback(success, result): + if success: + self._run_job_success(job.id, result) + else: + self._run_job_error(job.id, result.value, result.tb) + + self._reactor.getThreadPool().callInThreadWithCallback( + callback, run_job, job, job._jobstore_alias, run_times, self._logger.name) diff --git a/telegramer/include/apscheduler/job.py b/telegramer/include/apscheduler/job.py new file mode 100644 index 0000000..445d9a8 --- /dev/null +++ b/telegramer/include/apscheduler/job.py @@ -0,0 +1,302 @@ +from inspect import ismethod, isclass +from uuid import uuid4 + +import six + +from apscheduler.triggers.base import BaseTrigger +from apscheduler.util import ( + ref_to_obj, obj_to_ref, datetime_repr, repr_escape, get_callable_name, check_callable_args, + convert_to_datetime) + +try: + from collections.abc import Iterable, Mapping +except ImportError: + from collections import Iterable, Mapping + + +class Job(object): + """ + Contains the options given when scheduling callables and its current schedule and other state. + This class should never be instantiated by the user. + + :var str id: the unique identifier of this job + :var str name: the description of this job + :var func: the callable to execute + :var tuple|list args: positional arguments to the callable + :var dict kwargs: keyword arguments to the callable + :var bool coalesce: whether to only run the job once when several run times are due + :var trigger: the trigger object that controls the schedule of this job + :var str executor: the name of the executor that will run this job + :var int misfire_grace_time: the time (in seconds) how much this job's execution is allowed to + be late (``None`` means "allow the job to run no matter how late it is") + :var int max_instances: the maximum number of concurrently executing instances allowed for this + job + :var datetime.datetime next_run_time: the next scheduled run time of this job + + .. note:: + The ``misfire_grace_time`` has some non-obvious effects on job execution. See the + :ref:`missed-job-executions` section in the documentation for an in-depth explanation. + """ + + __slots__ = ('_scheduler', '_jobstore_alias', 'id', 'trigger', 'executor', 'func', 'func_ref', + 'args', 'kwargs', 'name', 'misfire_grace_time', 'coalesce', 'max_instances', + 'next_run_time', '__weakref__') + + def __init__(self, scheduler, id=None, **kwargs): + super(Job, self).__init__() + self._scheduler = scheduler + self._jobstore_alias = None + self._modify(id=id or uuid4().hex, **kwargs) + + def modify(self, **changes): + """ + Makes the given changes to this job and saves it in the associated job store. + + Accepted keyword arguments are the same as the variables on this class. + + .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.modify_job` + + :return Job: this job instance + + """ + self._scheduler.modify_job(self.id, self._jobstore_alias, **changes) + return self + + def reschedule(self, trigger, **trigger_args): + """ + Shortcut for switching the trigger on this job. + + .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.reschedule_job` + + :return Job: this job instance + + """ + self._scheduler.reschedule_job(self.id, self._jobstore_alias, trigger, **trigger_args) + return self + + def pause(self): + """ + Temporarily suspend the execution of this job. + + .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.pause_job` + + :return Job: this job instance + + """ + self._scheduler.pause_job(self.id, self._jobstore_alias) + return self + + def resume(self): + """ + Resume the schedule of this job if previously paused. + + .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.resume_job` + + :return Job: this job instance + + """ + self._scheduler.resume_job(self.id, self._jobstore_alias) + return self + + def remove(self): + """ + Unschedules this job and removes it from its associated job store. + + .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.remove_job` + + """ + self._scheduler.remove_job(self.id, self._jobstore_alias) + + @property + def pending(self): + """ + Returns ``True`` if the referenced job is still waiting to be added to its designated job + store. + + """ + return self._jobstore_alias is None + + # + # Private API + # + + def _get_run_times(self, now): + """ + Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive). + + :type now: datetime.datetime + :rtype: list[datetime.datetime] + + """ + run_times = [] + next_run_time = self.next_run_time + while next_run_time and next_run_time <= now: + run_times.append(next_run_time) + next_run_time = self.trigger.get_next_fire_time(next_run_time, now) + + return run_times + + def _modify(self, **changes): + """ + Validates the changes to the Job and makes the modifications if and only if all of them + validate. + + """ + approved = {} + + if 'id' in changes: + value = changes.pop('id') + if not isinstance(value, six.string_types): + raise TypeError("id must be a nonempty string") + if hasattr(self, 'id'): + raise ValueError('The job ID may not be changed') + approved['id'] = value + + if 'func' in changes or 'args' in changes or 'kwargs' in changes: + func = changes.pop('func') if 'func' in changes else self.func + args = changes.pop('args') if 'args' in changes else self.args + kwargs = changes.pop('kwargs') if 'kwargs' in changes else self.kwargs + + if isinstance(func, six.string_types): + func_ref = func + func = ref_to_obj(func) + elif callable(func): + try: + func_ref = obj_to_ref(func) + except ValueError: + # If this happens, this Job won't be serializable + func_ref = None + else: + raise TypeError('func must be a callable or a textual reference to one') + + if not hasattr(self, 'name') and changes.get('name', None) is None: + changes['name'] = get_callable_name(func) + + if isinstance(args, six.string_types) or not isinstance(args, Iterable): + raise TypeError('args must be a non-string iterable') + if isinstance(kwargs, six.string_types) or not isinstance(kwargs, Mapping): + raise TypeError('kwargs must be a dict-like object') + + check_callable_args(func, args, kwargs) + + approved['func'] = func + approved['func_ref'] = func_ref + approved['args'] = args + approved['kwargs'] = kwargs + + if 'name' in changes: + value = changes.pop('name') + if not value or not isinstance(value, six.string_types): + raise TypeError("name must be a nonempty string") + approved['name'] = value + + if 'misfire_grace_time' in changes: + value = changes.pop('misfire_grace_time') + if value is not None and (not isinstance(value, six.integer_types) or value <= 0): + raise TypeError('misfire_grace_time must be either None or a positive integer') + approved['misfire_grace_time'] = value + + if 'coalesce' in changes: + value = bool(changes.pop('coalesce')) + approved['coalesce'] = value + + if 'max_instances' in changes: + value = changes.pop('max_instances') + if not isinstance(value, six.integer_types) or value <= 0: + raise TypeError('max_instances must be a positive integer') + approved['max_instances'] = value + + if 'trigger' in changes: + trigger = changes.pop('trigger') + if not isinstance(trigger, BaseTrigger): + raise TypeError('Expected a trigger instance, got %s instead' % + trigger.__class__.__name__) + + approved['trigger'] = trigger + + if 'executor' in changes: + value = changes.pop('executor') + if not isinstance(value, six.string_types): + raise TypeError('executor must be a string') + approved['executor'] = value + + if 'next_run_time' in changes: + value = changes.pop('next_run_time') + approved['next_run_time'] = convert_to_datetime(value, self._scheduler.timezone, + 'next_run_time') + + if changes: + raise AttributeError('The following are not modifiable attributes of Job: %s' % + ', '.join(changes)) + + for key, value in six.iteritems(approved): + setattr(self, key, value) + + def __getstate__(self): + # Don't allow this Job to be serialized if the function reference could not be determined + if not self.func_ref: + raise ValueError( + 'This Job cannot be serialized since the reference to its callable (%r) could not ' + 'be determined. Consider giving a textual reference (module:function name) ' + 'instead.' % (self.func,)) + + # Instance methods cannot survive serialization as-is, so store the "self" argument + # explicitly + func = self.func + if ismethod(func) and not isclass(func.__self__) and obj_to_ref(func) == self.func_ref: + args = (func.__self__,) + tuple(self.args) + else: + args = self.args + + return { + 'version': 1, + 'id': self.id, + 'func': self.func_ref, + 'trigger': self.trigger, + 'executor': self.executor, + 'args': args, + 'kwargs': self.kwargs, + 'name': self.name, + 'misfire_grace_time': self.misfire_grace_time, + 'coalesce': self.coalesce, + 'max_instances': self.max_instances, + 'next_run_time': self.next_run_time + } + + def __setstate__(self, state): + if state.get('version', 1) > 1: + raise ValueError('Job has version %s, but only version 1 can be handled' % + state['version']) + + self.id = state['id'] + self.func_ref = state['func'] + self.func = ref_to_obj(self.func_ref) + self.trigger = state['trigger'] + self.executor = state['executor'] + self.args = state['args'] + self.kwargs = state['kwargs'] + self.name = state['name'] + self.misfire_grace_time = state['misfire_grace_time'] + self.coalesce = state['coalesce'] + self.max_instances = state['max_instances'] + self.next_run_time = state['next_run_time'] + + def __eq__(self, other): + if isinstance(other, Job): + return self.id == other.id + return NotImplemented + + def __repr__(self): + return '' % (repr_escape(self.id), repr_escape(self.name)) + + def __str__(self): + return repr_escape(self.__unicode__()) + + def __unicode__(self): + if hasattr(self, 'next_run_time'): + status = ('next run at: ' + datetime_repr(self.next_run_time) if + self.next_run_time else 'paused') + else: + status = 'pending' + + return u'%s (trigger: %s, %s)' % (self.name, self.trigger, status) diff --git a/telegramer/include/apscheduler/jobstores/__init__.py b/telegramer/include/apscheduler/jobstores/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegramer/include/apscheduler/jobstores/base.py b/telegramer/include/apscheduler/jobstores/base.py new file mode 100644 index 0000000..9cff66c --- /dev/null +++ b/telegramer/include/apscheduler/jobstores/base.py @@ -0,0 +1,143 @@ +from abc import ABCMeta, abstractmethod +import logging + +import six + + +class JobLookupError(KeyError): + """Raised when the job store cannot find a job for update or removal.""" + + def __init__(self, job_id): + super(JobLookupError, self).__init__(u'No job by the id of %s was found' % job_id) + + +class ConflictingIdError(KeyError): + """Raised when the uniqueness of job IDs is being violated.""" + + def __init__(self, job_id): + super(ConflictingIdError, self).__init__( + u'Job identifier (%s) conflicts with an existing job' % job_id) + + +class TransientJobError(ValueError): + """ + Raised when an attempt to add transient (with no func_ref) job to a persistent job store is + detected. + """ + + def __init__(self, job_id): + super(TransientJobError, self).__init__( + u'Job (%s) cannot be added to this job store because a reference to the callable ' + u'could not be determined.' % job_id) + + +class BaseJobStore(six.with_metaclass(ABCMeta)): + """Abstract base class that defines the interface that every job store must implement.""" + + _scheduler = None + _alias = None + _logger = logging.getLogger('apscheduler.jobstores') + + def start(self, scheduler, alias): + """ + Called by the scheduler when the scheduler is being started or when the job store is being + added to an already running scheduler. + + :param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting + this job store + :param str|unicode alias: alias of this job store as it was assigned to the scheduler + """ + + self._scheduler = scheduler + self._alias = alias + self._logger = logging.getLogger('apscheduler.jobstores.%s' % alias) + + def shutdown(self): + """Frees any resources still bound to this job store.""" + + def _fix_paused_jobs_sorting(self, jobs): + for i, job in enumerate(jobs): + if job.next_run_time is not None: + if i > 0: + paused_jobs = jobs[:i] + del jobs[:i] + jobs.extend(paused_jobs) + break + + @abstractmethod + def lookup_job(self, job_id): + """ + Returns a specific job, or ``None`` if it isn't found.. + + The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of + the returned job to point to the scheduler and itself, respectively. + + :param str|unicode job_id: identifier of the job + :rtype: Job + """ + + @abstractmethod + def get_due_jobs(self, now): + """ + Returns the list of jobs that have ``next_run_time`` earlier or equal to ``now``. + The returned jobs must be sorted by next run time (ascending). + + :param datetime.datetime now: the current (timezone aware) datetime + :rtype: list[Job] + """ + + @abstractmethod + def get_next_run_time(self): + """ + Returns the earliest run time of all the jobs stored in this job store, or ``None`` if + there are no active jobs. + + :rtype: datetime.datetime + """ + + @abstractmethod + def get_all_jobs(self): + """ + Returns a list of all jobs in this job store. + The returned jobs should be sorted by next run time (ascending). + Paused jobs (next_run_time == None) should be sorted last. + + The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of + the returned jobs to point to the scheduler and itself, respectively. + + :rtype: list[Job] + """ + + @abstractmethod + def add_job(self, job): + """ + Adds the given job to this store. + + :param Job job: the job to add + :raises ConflictingIdError: if there is another job in this store with the same ID + """ + + @abstractmethod + def update_job(self, job): + """ + Replaces the job in the store with the given newer version. + + :param Job job: the job to update + :raises JobLookupError: if the job does not exist + """ + + @abstractmethod + def remove_job(self, job_id): + """ + Removes the given job from this store. + + :param str|unicode job_id: identifier of the job + :raises JobLookupError: if the job does not exist + """ + + @abstractmethod + def remove_all_jobs(self): + """Removes all jobs from this store.""" + + def __repr__(self): + return '<%s>' % self.__class__.__name__ diff --git a/telegramer/include/apscheduler/jobstores/memory.py b/telegramer/include/apscheduler/jobstores/memory.py new file mode 100644 index 0000000..abfe7c6 --- /dev/null +++ b/telegramer/include/apscheduler/jobstores/memory.py @@ -0,0 +1,108 @@ +from __future__ import absolute_import + +from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError +from apscheduler.util import datetime_to_utc_timestamp + + +class MemoryJobStore(BaseJobStore): + """ + Stores jobs in an array in RAM. Provides no persistence support. + + Plugin alias: ``memory`` + """ + + def __init__(self): + super(MemoryJobStore, self).__init__() + # list of (job, timestamp), sorted by next_run_time and job id (ascending) + self._jobs = [] + self._jobs_index = {} # id -> (job, timestamp) lookup table + + def lookup_job(self, job_id): + return self._jobs_index.get(job_id, (None, None))[0] + + def get_due_jobs(self, now): + now_timestamp = datetime_to_utc_timestamp(now) + pending = [] + for job, timestamp in self._jobs: + if timestamp is None or timestamp > now_timestamp: + break + pending.append(job) + + return pending + + def get_next_run_time(self): + return self._jobs[0][0].next_run_time if self._jobs else None + + def get_all_jobs(self): + return [j[0] for j in self._jobs] + + def add_job(self, job): + if job.id in self._jobs_index: + raise ConflictingIdError(job.id) + + timestamp = datetime_to_utc_timestamp(job.next_run_time) + index = self._get_job_index(timestamp, job.id) + self._jobs.insert(index, (job, timestamp)) + self._jobs_index[job.id] = (job, timestamp) + + def update_job(self, job): + old_job, old_timestamp = self._jobs_index.get(job.id, (None, None)) + if old_job is None: + raise JobLookupError(job.id) + + # If the next run time has not changed, simply replace the job in its present index. + # Otherwise, reinsert the job to the list to preserve the ordering. + old_index = self._get_job_index(old_timestamp, old_job.id) + new_timestamp = datetime_to_utc_timestamp(job.next_run_time) + if old_timestamp == new_timestamp: + self._jobs[old_index] = (job, new_timestamp) + else: + del self._jobs[old_index] + new_index = self._get_job_index(new_timestamp, job.id) + self._jobs.insert(new_index, (job, new_timestamp)) + + self._jobs_index[old_job.id] = (job, new_timestamp) + + def remove_job(self, job_id): + job, timestamp = self._jobs_index.get(job_id, (None, None)) + if job is None: + raise JobLookupError(job_id) + + index = self._get_job_index(timestamp, job_id) + del self._jobs[index] + del self._jobs_index[job.id] + + def remove_all_jobs(self): + self._jobs = [] + self._jobs_index = {} + + def shutdown(self): + self.remove_all_jobs() + + def _get_job_index(self, timestamp, job_id): + """ + Returns the index of the given job, or if it's not found, the index where the job should be + inserted based on the given timestamp. + + :type timestamp: int + :type job_id: str + + """ + lo, hi = 0, len(self._jobs) + timestamp = float('inf') if timestamp is None else timestamp + while lo < hi: + mid = (lo + hi) // 2 + mid_job, mid_timestamp = self._jobs[mid] + mid_timestamp = float('inf') if mid_timestamp is None else mid_timestamp + if mid_timestamp > timestamp: + hi = mid + elif mid_timestamp < timestamp: + lo = mid + 1 + elif mid_job.id > job_id: + hi = mid + elif mid_job.id < job_id: + lo = mid + 1 + else: + return mid + + return lo diff --git a/telegramer/include/apscheduler/jobstores/mongodb.py b/telegramer/include/apscheduler/jobstores/mongodb.py new file mode 100644 index 0000000..5a00f94 --- /dev/null +++ b/telegramer/include/apscheduler/jobstores/mongodb.py @@ -0,0 +1,141 @@ +from __future__ import absolute_import +import warnings + +from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError +from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime +from apscheduler.job import Job + +try: + import cPickle as pickle +except ImportError: # pragma: nocover + import pickle + +try: + from bson.binary import Binary + from pymongo.errors import DuplicateKeyError + from pymongo import MongoClient, ASCENDING +except ImportError: # pragma: nocover + raise ImportError('MongoDBJobStore requires PyMongo installed') + + +class MongoDBJobStore(BaseJobStore): + """ + Stores jobs in a MongoDB database. Any leftover keyword arguments are directly passed to + pymongo's `MongoClient + `_. + + Plugin alias: ``mongodb`` + + :param str database: database to store jobs in + :param str collection: collection to store jobs in + :param client: a :class:`~pymongo.mongo_client.MongoClient` instance to use instead of + providing connection arguments + :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the + highest available + """ + + def __init__(self, database='apscheduler', collection='jobs', client=None, + pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args): + super(MongoDBJobStore, self).__init__() + self.pickle_protocol = pickle_protocol + + if not database: + raise ValueError('The "database" parameter must not be empty') + if not collection: + raise ValueError('The "collection" parameter must not be empty') + + if client: + self.client = maybe_ref(client) + else: + connect_args.setdefault('w', 1) + self.client = MongoClient(**connect_args) + + self.collection = self.client[database][collection] + + def start(self, scheduler, alias): + super(MongoDBJobStore, self).start(scheduler, alias) + self.collection.create_index('next_run_time', sparse=True) + + @property + def connection(self): + warnings.warn('The "connection" member is deprecated -- use "client" instead', + DeprecationWarning) + return self.client + + def lookup_job(self, job_id): + document = self.collection.find_one(job_id, ['job_state']) + return self._reconstitute_job(document['job_state']) if document else None + + def get_due_jobs(self, now): + timestamp = datetime_to_utc_timestamp(now) + return self._get_jobs({'next_run_time': {'$lte': timestamp}}) + + def get_next_run_time(self): + document = self.collection.find_one({'next_run_time': {'$ne': None}}, + projection=['next_run_time'], + sort=[('next_run_time', ASCENDING)]) + return utc_timestamp_to_datetime(document['next_run_time']) if document else None + + def get_all_jobs(self): + jobs = self._get_jobs({}) + self._fix_paused_jobs_sorting(jobs) + return jobs + + def add_job(self, job): + try: + self.collection.insert_one({ + '_id': job.id, + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol)) + }) + except DuplicateKeyError: + raise ConflictingIdError(job.id) + + def update_job(self, job): + changes = { + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol)) + } + result = self.collection.update_one({'_id': job.id}, {'$set': changes}) + if result and result.matched_count == 0: + raise JobLookupError(job.id) + + def remove_job(self, job_id): + result = self.collection.delete_one({'_id': job_id}) + if result and result.deleted_count == 0: + raise JobLookupError(job_id) + + def remove_all_jobs(self): + self.collection.delete_many({}) + + def shutdown(self): + self.client.close() + + def _reconstitute_job(self, job_state): + job_state = pickle.loads(job_state) + job = Job.__new__(Job) + job.__setstate__(job_state) + job._scheduler = self._scheduler + job._jobstore_alias = self._alias + return job + + def _get_jobs(self, conditions): + jobs = [] + failed_job_ids = [] + for document in self.collection.find(conditions, ['_id', 'job_state'], + sort=[('next_run_time', ASCENDING)]): + try: + jobs.append(self._reconstitute_job(document['job_state'])) + except BaseException: + self._logger.exception('Unable to restore job "%s" -- removing it', + document['_id']) + failed_job_ids.append(document['_id']) + + # Remove all the jobs we failed to restore + if failed_job_ids: + self.collection.delete_many({'_id': {'$in': failed_job_ids}}) + + return jobs + + def __repr__(self): + return '<%s (client=%s)>' % (self.__class__.__name__, self.client) diff --git a/telegramer/include/apscheduler/jobstores/redis.py b/telegramer/include/apscheduler/jobstores/redis.py new file mode 100644 index 0000000..5bb69d6 --- /dev/null +++ b/telegramer/include/apscheduler/jobstores/redis.py @@ -0,0 +1,150 @@ +from __future__ import absolute_import +from datetime import datetime + +from pytz import utc +import six + +from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError +from apscheduler.util import datetime_to_utc_timestamp, utc_timestamp_to_datetime +from apscheduler.job import Job + +try: + import cPickle as pickle +except ImportError: # pragma: nocover + import pickle + +try: + from redis import Redis +except ImportError: # pragma: nocover + raise ImportError('RedisJobStore requires redis installed') + + +class RedisJobStore(BaseJobStore): + """ + Stores jobs in a Redis database. Any leftover keyword arguments are directly passed to redis's + :class:`~redis.StrictRedis`. + + Plugin alias: ``redis`` + + :param int db: the database number to store jobs in + :param str jobs_key: key to store jobs in + :param str run_times_key: key to store the jobs' run times in + :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the + highest available + """ + + def __init__(self, db=0, jobs_key='apscheduler.jobs', run_times_key='apscheduler.run_times', + pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args): + super(RedisJobStore, self).__init__() + + if db is None: + raise ValueError('The "db" parameter must not be empty') + if not jobs_key: + raise ValueError('The "jobs_key" parameter must not be empty') + if not run_times_key: + raise ValueError('The "run_times_key" parameter must not be empty') + + self.pickle_protocol = pickle_protocol + self.jobs_key = jobs_key + self.run_times_key = run_times_key + self.redis = Redis(db=int(db), **connect_args) + + def lookup_job(self, job_id): + job_state = self.redis.hget(self.jobs_key, job_id) + return self._reconstitute_job(job_state) if job_state else None + + def get_due_jobs(self, now): + timestamp = datetime_to_utc_timestamp(now) + job_ids = self.redis.zrangebyscore(self.run_times_key, 0, timestamp) + if job_ids: + job_states = self.redis.hmget(self.jobs_key, *job_ids) + return self._reconstitute_jobs(six.moves.zip(job_ids, job_states)) + return [] + + def get_next_run_time(self): + next_run_time = self.redis.zrange(self.run_times_key, 0, 0, withscores=True) + if next_run_time: + return utc_timestamp_to_datetime(next_run_time[0][1]) + + def get_all_jobs(self): + job_states = self.redis.hgetall(self.jobs_key) + jobs = self._reconstitute_jobs(six.iteritems(job_states)) + paused_sort_key = datetime(9999, 12, 31, tzinfo=utc) + return sorted(jobs, key=lambda job: job.next_run_time or paused_sort_key) + + def add_job(self, job): + if self.redis.hexists(self.jobs_key, job.id): + raise ConflictingIdError(job.id) + + with self.redis.pipeline() as pipe: + pipe.multi() + pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(), + self.pickle_protocol)) + if job.next_run_time: + pipe.zadd(self.run_times_key, + {job.id: datetime_to_utc_timestamp(job.next_run_time)}) + + pipe.execute() + + def update_job(self, job): + if not self.redis.hexists(self.jobs_key, job.id): + raise JobLookupError(job.id) + + with self.redis.pipeline() as pipe: + pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(), + self.pickle_protocol)) + if job.next_run_time: + pipe.zadd(self.run_times_key, + {job.id: datetime_to_utc_timestamp(job.next_run_time)}) + else: + pipe.zrem(self.run_times_key, job.id) + + pipe.execute() + + def remove_job(self, job_id): + if not self.redis.hexists(self.jobs_key, job_id): + raise JobLookupError(job_id) + + with self.redis.pipeline() as pipe: + pipe.hdel(self.jobs_key, job_id) + pipe.zrem(self.run_times_key, job_id) + pipe.execute() + + def remove_all_jobs(self): + with self.redis.pipeline() as pipe: + pipe.delete(self.jobs_key) + pipe.delete(self.run_times_key) + pipe.execute() + + def shutdown(self): + self.redis.connection_pool.disconnect() + + def _reconstitute_job(self, job_state): + job_state = pickle.loads(job_state) + job = Job.__new__(Job) + job.__setstate__(job_state) + job._scheduler = self._scheduler + job._jobstore_alias = self._alias + return job + + def _reconstitute_jobs(self, job_states): + jobs = [] + failed_job_ids = [] + for job_id, job_state in job_states: + try: + jobs.append(self._reconstitute_job(job_state)) + except BaseException: + self._logger.exception('Unable to restore job "%s" -- removing it', job_id) + failed_job_ids.append(job_id) + + # Remove all the jobs we failed to restore + if failed_job_ids: + with self.redis.pipeline() as pipe: + pipe.hdel(self.jobs_key, *failed_job_ids) + pipe.zrem(self.run_times_key, *failed_job_ids) + pipe.execute() + + return jobs + + def __repr__(self): + return '<%s>' % self.__class__.__name__ diff --git a/telegramer/include/apscheduler/jobstores/rethinkdb.py b/telegramer/include/apscheduler/jobstores/rethinkdb.py new file mode 100644 index 0000000..d8a78cd --- /dev/null +++ b/telegramer/include/apscheduler/jobstores/rethinkdb.py @@ -0,0 +1,155 @@ +from __future__ import absolute_import + +from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError +from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime +from apscheduler.job import Job + +try: + import cPickle as pickle +except ImportError: # pragma: nocover + import pickle + +try: + from rethinkdb import RethinkDB +except ImportError: # pragma: nocover + raise ImportError('RethinkDBJobStore requires rethinkdb installed') + + +class RethinkDBJobStore(BaseJobStore): + """ + Stores jobs in a RethinkDB database. Any leftover keyword arguments are directly passed to + rethinkdb's `RethinkdbClient `_. + + Plugin alias: ``rethinkdb`` + + :param str database: database to store jobs in + :param str collection: collection to store jobs in + :param client: a :class:`rethinkdb.net.Connection` instance to use instead of providing + connection arguments + :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the + highest available + """ + + def __init__(self, database='apscheduler', table='jobs', client=None, + pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args): + super(RethinkDBJobStore, self).__init__() + + if not database: + raise ValueError('The "database" parameter must not be empty') + if not table: + raise ValueError('The "table" parameter must not be empty') + + self.database = database + self.table_name = table + self.table = None + self.client = client + self.pickle_protocol = pickle_protocol + self.connect_args = connect_args + self.r = RethinkDB() + self.conn = None + + def start(self, scheduler, alias): + super(RethinkDBJobStore, self).start(scheduler, alias) + + if self.client: + self.conn = maybe_ref(self.client) + else: + self.conn = self.r.connect(db=self.database, **self.connect_args) + + if self.database not in self.r.db_list().run(self.conn): + self.r.db_create(self.database).run(self.conn) + + if self.table_name not in self.r.table_list().run(self.conn): + self.r.table_create(self.table_name).run(self.conn) + + if 'next_run_time' not in self.r.table(self.table_name).index_list().run(self.conn): + self.r.table(self.table_name).index_create('next_run_time').run(self.conn) + + self.table = self.r.db(self.database).table(self.table_name) + + def lookup_job(self, job_id): + results = list(self.table.get_all(job_id).pluck('job_state').run(self.conn)) + return self._reconstitute_job(results[0]['job_state']) if results else None + + def get_due_jobs(self, now): + return self._get_jobs(self.r.row['next_run_time'] <= datetime_to_utc_timestamp(now)) + + def get_next_run_time(self): + results = list( + self.table + .filter(self.r.row['next_run_time'] != None) # noqa + .order_by(self.r.asc('next_run_time')) + .map(lambda x: x['next_run_time']) + .limit(1) + .run(self.conn) + ) + return utc_timestamp_to_datetime(results[0]) if results else None + + def get_all_jobs(self): + jobs = self._get_jobs() + self._fix_paused_jobs_sorting(jobs) + return jobs + + def add_job(self, job): + job_dict = { + 'id': job.id, + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol)) + } + results = self.table.insert(job_dict).run(self.conn) + if results['errors'] > 0: + raise ConflictingIdError(job.id) + + def update_job(self, job): + changes = { + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol)) + } + results = self.table.get_all(job.id).update(changes).run(self.conn) + skipped = False in map(lambda x: results[x] == 0, results.keys()) + if results['skipped'] > 0 or results['errors'] > 0 or not skipped: + raise JobLookupError(job.id) + + def remove_job(self, job_id): + results = self.table.get_all(job_id).delete().run(self.conn) + if results['deleted'] + results['skipped'] != 1: + raise JobLookupError(job_id) + + def remove_all_jobs(self): + self.table.delete().run(self.conn) + + def shutdown(self): + self.conn.close() + + def _reconstitute_job(self, job_state): + job_state = pickle.loads(job_state) + job = Job.__new__(Job) + job.__setstate__(job_state) + job._scheduler = self._scheduler + job._jobstore_alias = self._alias + return job + + def _get_jobs(self, predicate=None): + jobs = [] + failed_job_ids = [] + query = (self.table.filter(self.r.row['next_run_time'] != None).filter(predicate) # noqa + if predicate else self.table) + query = query.order_by('next_run_time', 'id').pluck('id', 'job_state') + + for document in query.run(self.conn): + try: + jobs.append(self._reconstitute_job(document['job_state'])) + except Exception: + self._logger.exception('Unable to restore job "%s" -- removing it', document['id']) + failed_job_ids.append(document['id']) + + # Remove all the jobs we failed to restore + if failed_job_ids: + self.r.expr(failed_job_ids).for_each( + lambda job_id: self.table.get_all(job_id).delete()).run(self.conn) + + return jobs + + def __repr__(self): + connection = self.conn + return '<%s (connection=%s)>' % (self.__class__.__name__, connection) diff --git a/telegramer/include/apscheduler/jobstores/sqlalchemy.py b/telegramer/include/apscheduler/jobstores/sqlalchemy.py new file mode 100644 index 0000000..dcfd3e5 --- /dev/null +++ b/telegramer/include/apscheduler/jobstores/sqlalchemy.py @@ -0,0 +1,154 @@ +from __future__ import absolute_import + +from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError +from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime +from apscheduler.job import Job + +try: + import cPickle as pickle +except ImportError: # pragma: nocover + import pickle + +try: + from sqlalchemy import ( + create_engine, Table, Column, MetaData, Unicode, Float, LargeBinary, select, and_) + from sqlalchemy.exc import IntegrityError + from sqlalchemy.sql.expression import null +except ImportError: # pragma: nocover + raise ImportError('SQLAlchemyJobStore requires SQLAlchemy installed') + + +class SQLAlchemyJobStore(BaseJobStore): + """ + Stores jobs in a database table using SQLAlchemy. + The table will be created if it doesn't exist in the database. + + Plugin alias: ``sqlalchemy`` + + :param str url: connection string (see + :ref:`SQLAlchemy documentation ` on this) + :param engine: an SQLAlchemy :class:`~sqlalchemy.engine.Engine` to use instead of creating a + new one based on ``url`` + :param str tablename: name of the table to store jobs in + :param metadata: a :class:`~sqlalchemy.schema.MetaData` instance to use instead of creating a + new one + :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the + highest available + :param str tableschema: name of the (existing) schema in the target database where the table + should be + :param dict engine_options: keyword arguments to :func:`~sqlalchemy.create_engine` + (ignored if ``engine`` is given) + """ + + def __init__(self, url=None, engine=None, tablename='apscheduler_jobs', metadata=None, + pickle_protocol=pickle.HIGHEST_PROTOCOL, tableschema=None, engine_options=None): + super(SQLAlchemyJobStore, self).__init__() + self.pickle_protocol = pickle_protocol + metadata = maybe_ref(metadata) or MetaData() + + if engine: + self.engine = maybe_ref(engine) + elif url: + self.engine = create_engine(url, **(engine_options or {})) + else: + raise ValueError('Need either "engine" or "url" defined') + + # 191 = max key length in MySQL for InnoDB/utf8mb4 tables, + # 25 = precision that translates to an 8-byte float + self.jobs_t = Table( + tablename, metadata, + Column('id', Unicode(191, _warn_on_bytestring=False), primary_key=True), + Column('next_run_time', Float(25), index=True), + Column('job_state', LargeBinary, nullable=False), + schema=tableschema + ) + + def start(self, scheduler, alias): + super(SQLAlchemyJobStore, self).start(scheduler, alias) + self.jobs_t.create(self.engine, True) + + def lookup_job(self, job_id): + selectable = select([self.jobs_t.c.job_state]).where(self.jobs_t.c.id == job_id) + job_state = self.engine.execute(selectable).scalar() + return self._reconstitute_job(job_state) if job_state else None + + def get_due_jobs(self, now): + timestamp = datetime_to_utc_timestamp(now) + return self._get_jobs(self.jobs_t.c.next_run_time <= timestamp) + + def get_next_run_time(self): + selectable = select([self.jobs_t.c.next_run_time]).\ + where(self.jobs_t.c.next_run_time != null()).\ + order_by(self.jobs_t.c.next_run_time).limit(1) + next_run_time = self.engine.execute(selectable).scalar() + return utc_timestamp_to_datetime(next_run_time) + + def get_all_jobs(self): + jobs = self._get_jobs() + self._fix_paused_jobs_sorting(jobs) + return jobs + + def add_job(self, job): + insert = self.jobs_t.insert().values(**{ + 'id': job.id, + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol) + }) + try: + self.engine.execute(insert) + except IntegrityError: + raise ConflictingIdError(job.id) + + def update_job(self, job): + update = self.jobs_t.update().values(**{ + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol) + }).where(self.jobs_t.c.id == job.id) + result = self.engine.execute(update) + if result.rowcount == 0: + raise JobLookupError(job.id) + + def remove_job(self, job_id): + delete = self.jobs_t.delete().where(self.jobs_t.c.id == job_id) + result = self.engine.execute(delete) + if result.rowcount == 0: + raise JobLookupError(job_id) + + def remove_all_jobs(self): + delete = self.jobs_t.delete() + self.engine.execute(delete) + + def shutdown(self): + self.engine.dispose() + + def _reconstitute_job(self, job_state): + job_state = pickle.loads(job_state) + job_state['jobstore'] = self + job = Job.__new__(Job) + job.__setstate__(job_state) + job._scheduler = self._scheduler + job._jobstore_alias = self._alias + return job + + def _get_jobs(self, *conditions): + jobs = [] + selectable = select([self.jobs_t.c.id, self.jobs_t.c.job_state]).\ + order_by(self.jobs_t.c.next_run_time) + selectable = selectable.where(and_(*conditions)) if conditions else selectable + failed_job_ids = set() + for row in self.engine.execute(selectable): + try: + jobs.append(self._reconstitute_job(row.job_state)) + except BaseException: + self._logger.exception('Unable to restore job "%s" -- removing it', row.id) + failed_job_ids.add(row.id) + + # Remove all the jobs we failed to restore + if failed_job_ids: + delete = self.jobs_t.delete().where(self.jobs_t.c.id.in_(failed_job_ids)) + self.engine.execute(delete) + + return jobs + + def __repr__(self): + return '<%s (url=%s)>' % (self.__class__.__name__, self.engine.url) diff --git a/telegramer/include/apscheduler/jobstores/zookeeper.py b/telegramer/include/apscheduler/jobstores/zookeeper.py new file mode 100644 index 0000000..5253069 --- /dev/null +++ b/telegramer/include/apscheduler/jobstores/zookeeper.py @@ -0,0 +1,178 @@ +from __future__ import absolute_import + +from datetime import datetime + +from pytz import utc +from kazoo.exceptions import NoNodeError, NodeExistsError + +from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError +from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime +from apscheduler.job import Job + +try: + import cPickle as pickle +except ImportError: # pragma: nocover + import pickle + +try: + from kazoo.client import KazooClient +except ImportError: # pragma: nocover + raise ImportError('ZooKeeperJobStore requires Kazoo installed') + + +class ZooKeeperJobStore(BaseJobStore): + """ + Stores jobs in a ZooKeeper tree. Any leftover keyword arguments are directly passed to + kazoo's `KazooClient + `_. + + Plugin alias: ``zookeeper`` + + :param str path: path to store jobs in + :param client: a :class:`~kazoo.client.KazooClient` instance to use instead of + providing connection arguments + :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the + highest available + """ + + def __init__(self, path='/apscheduler', client=None, close_connection_on_exit=False, + pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args): + super(ZooKeeperJobStore, self).__init__() + self.pickle_protocol = pickle_protocol + self.close_connection_on_exit = close_connection_on_exit + + if not path: + raise ValueError('The "path" parameter must not be empty') + + self.path = path + + if client: + self.client = maybe_ref(client) + else: + self.client = KazooClient(**connect_args) + self._ensured_path = False + + def _ensure_paths(self): + if not self._ensured_path: + self.client.ensure_path(self.path) + self._ensured_path = True + + def start(self, scheduler, alias): + super(ZooKeeperJobStore, self).start(scheduler, alias) + if not self.client.connected: + self.client.start() + + def lookup_job(self, job_id): + self._ensure_paths() + node_path = self.path + "/" + str(job_id) + try: + content, _ = self.client.get(node_path) + doc = pickle.loads(content) + job = self._reconstitute_job(doc['job_state']) + return job + except BaseException: + return None + + def get_due_jobs(self, now): + timestamp = datetime_to_utc_timestamp(now) + jobs = [job_def['job'] for job_def in self._get_jobs() + if job_def['next_run_time'] is not None and job_def['next_run_time'] <= timestamp] + return jobs + + def get_next_run_time(self): + next_runs = [job_def['next_run_time'] for job_def in self._get_jobs() + if job_def['next_run_time'] is not None] + return utc_timestamp_to_datetime(min(next_runs)) if len(next_runs) > 0 else None + + def get_all_jobs(self): + jobs = [job_def['job'] for job_def in self._get_jobs()] + self._fix_paused_jobs_sorting(jobs) + return jobs + + def add_job(self, job): + self._ensure_paths() + node_path = self.path + "/" + str(job.id) + value = { + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': job.__getstate__() + } + data = pickle.dumps(value, self.pickle_protocol) + try: + self.client.create(node_path, value=data) + except NodeExistsError: + raise ConflictingIdError(job.id) + + def update_job(self, job): + self._ensure_paths() + node_path = self.path + "/" + str(job.id) + changes = { + 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), + 'job_state': job.__getstate__() + } + data = pickle.dumps(changes, self.pickle_protocol) + try: + self.client.set(node_path, value=data) + except NoNodeError: + raise JobLookupError(job.id) + + def remove_job(self, job_id): + self._ensure_paths() + node_path = self.path + "/" + str(job_id) + try: + self.client.delete(node_path) + except NoNodeError: + raise JobLookupError(job_id) + + def remove_all_jobs(self): + try: + self.client.delete(self.path, recursive=True) + except NoNodeError: + pass + self._ensured_path = False + + def shutdown(self): + if self.close_connection_on_exit: + self.client.stop() + self.client.close() + + def _reconstitute_job(self, job_state): + job_state = job_state + job = Job.__new__(Job) + job.__setstate__(job_state) + job._scheduler = self._scheduler + job._jobstore_alias = self._alias + return job + + def _get_jobs(self): + self._ensure_paths() + jobs = [] + failed_job_ids = [] + all_ids = self.client.get_children(self.path) + for node_name in all_ids: + try: + node_path = self.path + "/" + node_name + content, _ = self.client.get(node_path) + doc = pickle.loads(content) + job_def = { + 'job_id': node_name, + 'next_run_time': doc['next_run_time'] if doc['next_run_time'] else None, + 'job_state': doc['job_state'], + 'job': self._reconstitute_job(doc['job_state']), + 'creation_time': _.ctime + } + jobs.append(job_def) + except BaseException: + self._logger.exception('Unable to restore job "%s" -- removing it' % node_name) + failed_job_ids.append(node_name) + + # Remove all the jobs we failed to restore + if failed_job_ids: + for failed_id in failed_job_ids: + self.remove_job(failed_id) + paused_sort_key = datetime(9999, 12, 31, tzinfo=utc) + return sorted(jobs, key=lambda job_def: (job_def['job'].next_run_time or paused_sort_key, + job_def['creation_time'])) + + def __repr__(self): + self._logger.exception('<%s (client=%s)>' % (self.__class__.__name__, self.client)) + return '<%s (client=%s)>' % (self.__class__.__name__, self.client) diff --git a/telegramer/include/apscheduler/schedulers/__init__.py b/telegramer/include/apscheduler/schedulers/__init__.py new file mode 100644 index 0000000..bd8a790 --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/__init__.py @@ -0,0 +1,12 @@ +class SchedulerAlreadyRunningError(Exception): + """Raised when attempting to start or configure the scheduler when it's already running.""" + + def __str__(self): + return 'Scheduler is already running' + + +class SchedulerNotRunningError(Exception): + """Raised when attempting to shutdown the scheduler when it's not running.""" + + def __str__(self): + return 'Scheduler is not running' diff --git a/telegramer/include/apscheduler/schedulers/asyncio.py b/telegramer/include/apscheduler/schedulers/asyncio.py new file mode 100644 index 0000000..70ebede --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/asyncio.py @@ -0,0 +1,74 @@ +from __future__ import absolute_import +from functools import wraps, partial + +from apscheduler.schedulers.base import BaseScheduler +from apscheduler.util import maybe_ref + +try: + import asyncio +except ImportError: # pragma: nocover + try: + import trollius as asyncio + except ImportError: + raise ImportError( + 'AsyncIOScheduler requires either Python 3.4 or the asyncio package installed') + + +def run_in_event_loop(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + wrapped = partial(func, self, *args, **kwargs) + self._eventloop.call_soon_threadsafe(wrapped) + return wrapper + + +class AsyncIOScheduler(BaseScheduler): + """ + A scheduler that runs on an asyncio (:pep:`3156`) event loop. + + The default executor can run jobs based on native coroutines (``async def``). + + Extra options: + + ============== ============================================================= + ``event_loop`` AsyncIO event loop to use (defaults to the global event loop) + ============== ============================================================= + """ + + _eventloop = None + _timeout = None + + def start(self, paused=False): + if not self._eventloop: + self._eventloop = asyncio.get_event_loop() + + super(AsyncIOScheduler, self).start(paused) + + @run_in_event_loop + def shutdown(self, wait=True): + super(AsyncIOScheduler, self).shutdown(wait) + self._stop_timer() + + def _configure(self, config): + self._eventloop = maybe_ref(config.pop('event_loop', None)) + super(AsyncIOScheduler, self)._configure(config) + + def _start_timer(self, wait_seconds): + self._stop_timer() + if wait_seconds is not None: + self._timeout = self._eventloop.call_later(wait_seconds, self.wakeup) + + def _stop_timer(self): + if self._timeout: + self._timeout.cancel() + del self._timeout + + @run_in_event_loop + def wakeup(self): + self._stop_timer() + wait_seconds = self._process_jobs() + self._start_timer(wait_seconds) + + def _create_default_executor(self): + from apscheduler.executors.asyncio import AsyncIOExecutor + return AsyncIOExecutor() diff --git a/telegramer/include/apscheduler/schedulers/background.py b/telegramer/include/apscheduler/schedulers/background.py new file mode 100644 index 0000000..bb8f77d --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/background.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import + +from threading import Thread, Event + +from apscheduler.schedulers.base import BaseScheduler +from apscheduler.schedulers.blocking import BlockingScheduler +from apscheduler.util import asbool + + +class BackgroundScheduler(BlockingScheduler): + """ + A scheduler that runs in the background using a separate thread + (:meth:`~apscheduler.schedulers.base.BaseScheduler.start` will return immediately). + + Extra options: + + ========== ============================================================================= + ``daemon`` Set the ``daemon`` option in the background thread (defaults to ``True``, see + `the documentation + `_ + for further details) + ========== ============================================================================= + """ + + _thread = None + + def _configure(self, config): + self._daemon = asbool(config.pop('daemon', True)) + super(BackgroundScheduler, self)._configure(config) + + def start(self, *args, **kwargs): + if self._event is None or self._event.is_set(): + self._event = Event() + + BaseScheduler.start(self, *args, **kwargs) + self._thread = Thread(target=self._main_loop, name='APScheduler') + self._thread.daemon = self._daemon + self._thread.start() + + def shutdown(self, *args, **kwargs): + super(BackgroundScheduler, self).shutdown(*args, **kwargs) + self._thread.join() + del self._thread diff --git a/telegramer/include/apscheduler/schedulers/base.py b/telegramer/include/apscheduler/schedulers/base.py new file mode 100644 index 0000000..444de8e --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/base.py @@ -0,0 +1,1026 @@ +from __future__ import print_function + +from abc import ABCMeta, abstractmethod +from threading import RLock +from datetime import datetime, timedelta +from logging import getLogger +import warnings +import sys + +from pkg_resources import iter_entry_points +from tzlocal import get_localzone +import six + +from apscheduler.schedulers import SchedulerAlreadyRunningError, SchedulerNotRunningError +from apscheduler.executors.base import MaxInstancesReachedError, BaseExecutor +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.jobstores.base import ConflictingIdError, JobLookupError, BaseJobStore +from apscheduler.jobstores.memory import MemoryJobStore +from apscheduler.job import Job +from apscheduler.triggers.base import BaseTrigger +from apscheduler.util import ( + asbool, asint, astimezone, maybe_ref, timedelta_seconds, undefined, TIMEOUT_MAX) +from apscheduler.events import ( + SchedulerEvent, JobEvent, JobSubmissionEvent, EVENT_SCHEDULER_START, EVENT_SCHEDULER_SHUTDOWN, + EVENT_JOBSTORE_ADDED, EVENT_JOBSTORE_REMOVED, EVENT_ALL, EVENT_JOB_MODIFIED, EVENT_JOB_REMOVED, + EVENT_JOB_ADDED, EVENT_EXECUTOR_ADDED, EVENT_EXECUTOR_REMOVED, EVENT_ALL_JOBS_REMOVED, + EVENT_JOB_SUBMITTED, EVENT_JOB_MAX_INSTANCES, EVENT_SCHEDULER_RESUMED, EVENT_SCHEDULER_PAUSED) + +try: + from collections.abc import MutableMapping +except ImportError: + from collections import MutableMapping + +#: constant indicating a scheduler's stopped state +STATE_STOPPED = 0 +#: constant indicating a scheduler's running state (started and processing jobs) +STATE_RUNNING = 1 +#: constant indicating a scheduler's paused state (started but not processing jobs) +STATE_PAUSED = 2 + + +class BaseScheduler(six.with_metaclass(ABCMeta)): + """ + Abstract base class for all schedulers. + + Takes the following keyword arguments: + + :param str|logging.Logger logger: logger to use for the scheduler's logging (defaults to + apscheduler.scheduler) + :param str|datetime.tzinfo timezone: the default time zone (defaults to the local timezone) + :param int|float jobstore_retry_interval: the minimum number of seconds to wait between + retries in the scheduler's main loop if the job store raises an exception when getting + the list of due jobs + :param dict job_defaults: default values for newly added jobs + :param dict jobstores: a dictionary of job store alias -> job store instance or configuration + dict + :param dict executors: a dictionary of executor alias -> executor instance or configuration + dict + + :ivar int state: current running state of the scheduler (one of the following constants from + ``apscheduler.schedulers.base``: ``STATE_STOPPED``, ``STATE_RUNNING``, ``STATE_PAUSED``) + + .. seealso:: :ref:`scheduler-config` + """ + + _trigger_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.triggers')) + _trigger_classes = {} + _executor_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.executors')) + _executor_classes = {} + _jobstore_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.jobstores')) + _jobstore_classes = {} + + # + # Public API + # + + def __init__(self, gconfig={}, **options): + super(BaseScheduler, self).__init__() + self._executors = {} + self._executors_lock = self._create_lock() + self._jobstores = {} + self._jobstores_lock = self._create_lock() + self._listeners = [] + self._listeners_lock = self._create_lock() + self._pending_jobs = [] + self.state = STATE_STOPPED + self.configure(gconfig, **options) + + def __getstate__(self): + raise TypeError("Schedulers cannot be serialized. Ensure that you are not passing a " + "scheduler instance as an argument to a job, or scheduling an instance " + "method where the instance contains a scheduler as an attribute.") + + def configure(self, gconfig={}, prefix='apscheduler.', **options): + """ + Reconfigures the scheduler with the given options. + + Can only be done when the scheduler isn't running. + + :param dict gconfig: a "global" configuration dictionary whose values can be overridden by + keyword arguments to this method + :param str|unicode prefix: pick only those keys from ``gconfig`` that are prefixed with + this string (pass an empty string or ``None`` to use all keys) + :raises SchedulerAlreadyRunningError: if the scheduler is already running + + """ + if self.state != STATE_STOPPED: + raise SchedulerAlreadyRunningError + + # If a non-empty prefix was given, strip it from the keys in the + # global configuration dict + if prefix: + prefixlen = len(prefix) + gconfig = dict((key[prefixlen:], value) for key, value in six.iteritems(gconfig) + if key.startswith(prefix)) + + # Create a structure from the dotted options + # (e.g. "a.b.c = d" -> {'a': {'b': {'c': 'd'}}}) + config = {} + for key, value in six.iteritems(gconfig): + parts = key.split('.') + parent = config + key = parts.pop(0) + while parts: + parent = parent.setdefault(key, {}) + key = parts.pop(0) + parent[key] = value + + # Override any options with explicit keyword arguments + config.update(options) + self._configure(config) + + def start(self, paused=False): + """ + Start the configured executors and job stores and begin processing scheduled jobs. + + :param bool paused: if ``True``, don't start job processing until :meth:`resume` is called + :raises SchedulerAlreadyRunningError: if the scheduler is already running + :raises RuntimeError: if running under uWSGI with threads disabled + + """ + if self.state != STATE_STOPPED: + raise SchedulerAlreadyRunningError + + self._check_uwsgi() + + with self._executors_lock: + # Create a default executor if nothing else is configured + if 'default' not in self._executors: + self.add_executor(self._create_default_executor(), 'default') + + # Start all the executors + for alias, executor in six.iteritems(self._executors): + executor.start(self, alias) + + with self._jobstores_lock: + # Create a default job store if nothing else is configured + if 'default' not in self._jobstores: + self.add_jobstore(self._create_default_jobstore(), 'default') + + # Start all the job stores + for alias, store in six.iteritems(self._jobstores): + store.start(self, alias) + + # Schedule all pending jobs + for job, jobstore_alias, replace_existing in self._pending_jobs: + self._real_add_job(job, jobstore_alias, replace_existing) + del self._pending_jobs[:] + + self.state = STATE_PAUSED if paused else STATE_RUNNING + self._logger.info('Scheduler started') + self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_START)) + + if not paused: + self.wakeup() + + @abstractmethod + def shutdown(self, wait=True): + """ + Shuts down the scheduler, along with its executors and job stores. + + Does not interrupt any currently running jobs. + + :param bool wait: ``True`` to wait until all currently executing jobs have finished + :raises SchedulerNotRunningError: if the scheduler has not been started yet + + """ + if self.state == STATE_STOPPED: + raise SchedulerNotRunningError + + self.state = STATE_STOPPED + + # Shut down all executors + with self._executors_lock, self._jobstores_lock: + for executor in six.itervalues(self._executors): + executor.shutdown(wait) + + # Shut down all job stores + for jobstore in six.itervalues(self._jobstores): + jobstore.shutdown() + + self._logger.info('Scheduler has been shut down') + self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_SHUTDOWN)) + + def pause(self): + """ + Pause job processing in the scheduler. + + This will prevent the scheduler from waking up to do job processing until :meth:`resume` + is called. It will not however stop any already running job processing. + + """ + if self.state == STATE_STOPPED: + raise SchedulerNotRunningError + elif self.state == STATE_RUNNING: + self.state = STATE_PAUSED + self._logger.info('Paused scheduler job processing') + self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_PAUSED)) + + def resume(self): + """Resume job processing in the scheduler.""" + if self.state == STATE_STOPPED: + raise SchedulerNotRunningError + elif self.state == STATE_PAUSED: + self.state = STATE_RUNNING + self._logger.info('Resumed scheduler job processing') + self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_RESUMED)) + self.wakeup() + + @property + def running(self): + """ + Return ``True`` if the scheduler has been started. + + This is a shortcut for ``scheduler.state != STATE_STOPPED``. + + """ + return self.state != STATE_STOPPED + + def add_executor(self, executor, alias='default', **executor_opts): + """ + Adds an executor to this scheduler. + + Any extra keyword arguments will be passed to the executor plugin's constructor, assuming + that the first argument is the name of an executor plugin. + + :param str|unicode|apscheduler.executors.base.BaseExecutor executor: either an executor + instance or the name of an executor plugin + :param str|unicode alias: alias for the scheduler + :raises ValueError: if there is already an executor by the given alias + + """ + with self._executors_lock: + if alias in self._executors: + raise ValueError('This scheduler already has an executor by the alias of "%s"' % + alias) + + if isinstance(executor, BaseExecutor): + self._executors[alias] = executor + elif isinstance(executor, six.string_types): + self._executors[alias] = executor = self._create_plugin_instance( + 'executor', executor, executor_opts) + else: + raise TypeError('Expected an executor instance or a string, got %s instead' % + executor.__class__.__name__) + + # Start the executor right away if the scheduler is running + if self.state != STATE_STOPPED: + executor.start(self, alias) + + self._dispatch_event(SchedulerEvent(EVENT_EXECUTOR_ADDED, alias)) + + def remove_executor(self, alias, shutdown=True): + """ + Removes the executor by the given alias from this scheduler. + + :param str|unicode alias: alias of the executor + :param bool shutdown: ``True`` to shut down the executor after + removing it + + """ + with self._executors_lock: + executor = self._lookup_executor(alias) + del self._executors[alias] + + if shutdown: + executor.shutdown() + + self._dispatch_event(SchedulerEvent(EVENT_EXECUTOR_REMOVED, alias)) + + def add_jobstore(self, jobstore, alias='default', **jobstore_opts): + """ + Adds a job store to this scheduler. + + Any extra keyword arguments will be passed to the job store plugin's constructor, assuming + that the first argument is the name of a job store plugin. + + :param str|unicode|apscheduler.jobstores.base.BaseJobStore jobstore: job store to be added + :param str|unicode alias: alias for the job store + :raises ValueError: if there is already a job store by the given alias + + """ + with self._jobstores_lock: + if alias in self._jobstores: + raise ValueError('This scheduler already has a job store by the alias of "%s"' % + alias) + + if isinstance(jobstore, BaseJobStore): + self._jobstores[alias] = jobstore + elif isinstance(jobstore, six.string_types): + self._jobstores[alias] = jobstore = self._create_plugin_instance( + 'jobstore', jobstore, jobstore_opts) + else: + raise TypeError('Expected a job store instance or a string, got %s instead' % + jobstore.__class__.__name__) + + # Start the job store right away if the scheduler isn't stopped + if self.state != STATE_STOPPED: + jobstore.start(self, alias) + + # Notify listeners that a new job store has been added + self._dispatch_event(SchedulerEvent(EVENT_JOBSTORE_ADDED, alias)) + + # Notify the scheduler so it can scan the new job store for jobs + if self.state != STATE_STOPPED: + self.wakeup() + + def remove_jobstore(self, alias, shutdown=True): + """ + Removes the job store by the given alias from this scheduler. + + :param str|unicode alias: alias of the job store + :param bool shutdown: ``True`` to shut down the job store after removing it + + """ + with self._jobstores_lock: + jobstore = self._lookup_jobstore(alias) + del self._jobstores[alias] + + if shutdown: + jobstore.shutdown() + + self._dispatch_event(SchedulerEvent(EVENT_JOBSTORE_REMOVED, alias)) + + def add_listener(self, callback, mask=EVENT_ALL): + """ + add_listener(callback, mask=EVENT_ALL) + + Adds a listener for scheduler events. + + When a matching event occurs, ``callback`` is executed with the event object as its + sole argument. If the ``mask`` parameter is not provided, the callback will receive events + of all types. + + :param callback: any callable that takes one argument + :param int mask: bitmask that indicates which events should be + listened to + + .. seealso:: :mod:`apscheduler.events` + .. seealso:: :ref:`scheduler-events` + + """ + with self._listeners_lock: + self._listeners.append((callback, mask)) + + def remove_listener(self, callback): + """Removes a previously added event listener.""" + + with self._listeners_lock: + for i, (cb, _) in enumerate(self._listeners): + if callback == cb: + del self._listeners[i] + + def add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None, + misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined, + next_run_time=undefined, jobstore='default', executor='default', + replace_existing=False, **trigger_args): + """ + add_job(func, trigger=None, args=None, kwargs=None, id=None, \ + name=None, misfire_grace_time=undefined, coalesce=undefined, \ + max_instances=undefined, next_run_time=undefined, \ + jobstore='default', executor='default', \ + replace_existing=False, **trigger_args) + + Adds the given job to the job list and wakes up the scheduler if it's already running. + + Any option that defaults to ``undefined`` will be replaced with the corresponding default + value when the job is scheduled (which happens when the scheduler is started, or + immediately if the scheduler is already running). + + The ``func`` argument can be given either as a callable object or a textual reference in + the ``package.module:some.object`` format, where the first half (separated by ``:``) is an + importable module and the second half is a reference to the callable object, relative to + the module. + + The ``trigger`` argument can either be: + #. the alias name of the trigger (e.g. ``date``, ``interval`` or ``cron``), in which case + any extra keyword arguments to this method are passed on to the trigger's constructor + #. an instance of a trigger class + + :param func: callable (or a textual reference to one) to run at the given time + :param str|apscheduler.triggers.base.BaseTrigger trigger: trigger that determines when + ``func`` is called + :param list|tuple args: list of positional arguments to call func with + :param dict kwargs: dict of keyword arguments to call func with + :param str|unicode id: explicit identifier for the job (for modifying it later) + :param str|unicode name: textual description of the job + :param int misfire_grace_time: seconds after the designated runtime that the job is still + allowed to be run (or ``None`` to allow the job to run no matter how late it is) + :param bool coalesce: run once instead of many times if the scheduler determines that the + job should be run more than once in succession + :param int max_instances: maximum number of concurrently running instances allowed for this + job + :param datetime next_run_time: when to first run the job, regardless of the trigger (pass + ``None`` to add the job as paused) + :param str|unicode jobstore: alias of the job store to store the job in + :param str|unicode executor: alias of the executor to run the job with + :param bool replace_existing: ``True`` to replace an existing job with the same ``id`` + (but retain the number of runs from the existing one) + :rtype: Job + + """ + job_kwargs = { + 'trigger': self._create_trigger(trigger, trigger_args), + 'executor': executor, + 'func': func, + 'args': tuple(args) if args is not None else (), + 'kwargs': dict(kwargs) if kwargs is not None else {}, + 'id': id, + 'name': name, + 'misfire_grace_time': misfire_grace_time, + 'coalesce': coalesce, + 'max_instances': max_instances, + 'next_run_time': next_run_time + } + job_kwargs = dict((key, value) for key, value in six.iteritems(job_kwargs) if + value is not undefined) + job = Job(self, **job_kwargs) + + # Don't really add jobs to job stores before the scheduler is up and running + with self._jobstores_lock: + if self.state == STATE_STOPPED: + self._pending_jobs.append((job, jobstore, replace_existing)) + self._logger.info('Adding job tentatively -- it will be properly scheduled when ' + 'the scheduler starts') + else: + self._real_add_job(job, jobstore, replace_existing) + + return job + + def scheduled_job(self, trigger, args=None, kwargs=None, id=None, name=None, + misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined, + next_run_time=undefined, jobstore='default', executor='default', + **trigger_args): + """ + scheduled_job(trigger, args=None, kwargs=None, id=None, \ + name=None, misfire_grace_time=undefined, \ + coalesce=undefined, max_instances=undefined, \ + next_run_time=undefined, jobstore='default', \ + executor='default',**trigger_args) + + A decorator version of :meth:`add_job`, except that ``replace_existing`` is always + ``True``. + + .. important:: The ``id`` argument must be given if scheduling a job in a persistent job + store. The scheduler cannot, however, enforce this requirement. + + """ + def inner(func): + self.add_job(func, trigger, args, kwargs, id, name, misfire_grace_time, coalesce, + max_instances, next_run_time, jobstore, executor, True, **trigger_args) + return func + return inner + + def modify_job(self, job_id, jobstore=None, **changes): + """ + Modifies the properties of a single job. + + Modifications are passed to this method as extra keyword arguments. + + :param str|unicode job_id: the identifier of the job + :param str|unicode jobstore: alias of the job store that contains the job + :return Job: the relevant job instance + + """ + with self._jobstores_lock: + job, jobstore = self._lookup_job(job_id, jobstore) + job._modify(**changes) + if jobstore: + self._lookup_jobstore(jobstore).update_job(job) + + self._dispatch_event(JobEvent(EVENT_JOB_MODIFIED, job_id, jobstore)) + + # Wake up the scheduler since the job's next run time may have been changed + if self.state == STATE_RUNNING: + self.wakeup() + + return job + + def reschedule_job(self, job_id, jobstore=None, trigger=None, **trigger_args): + """ + Constructs a new trigger for a job and updates its next run time. + + Extra keyword arguments are passed directly to the trigger's constructor. + + :param str|unicode job_id: the identifier of the job + :param str|unicode jobstore: alias of the job store that contains the job + :param trigger: alias of the trigger type or a trigger instance + :return Job: the relevant job instance + + """ + trigger = self._create_trigger(trigger, trigger_args) + now = datetime.now(self.timezone) + next_run_time = trigger.get_next_fire_time(None, now) + return self.modify_job(job_id, jobstore, trigger=trigger, next_run_time=next_run_time) + + def pause_job(self, job_id, jobstore=None): + """ + Causes the given job not to be executed until it is explicitly resumed. + + :param str|unicode job_id: the identifier of the job + :param str|unicode jobstore: alias of the job store that contains the job + :return Job: the relevant job instance + + """ + return self.modify_job(job_id, jobstore, next_run_time=None) + + def resume_job(self, job_id, jobstore=None): + """ + Resumes the schedule of the given job, or removes the job if its schedule is finished. + + :param str|unicode job_id: the identifier of the job + :param str|unicode jobstore: alias of the job store that contains the job + :return Job|None: the relevant job instance if the job was rescheduled, or ``None`` if no + next run time could be calculated and the job was removed + + """ + with self._jobstores_lock: + job, jobstore = self._lookup_job(job_id, jobstore) + now = datetime.now(self.timezone) + next_run_time = job.trigger.get_next_fire_time(None, now) + if next_run_time: + return self.modify_job(job_id, jobstore, next_run_time=next_run_time) + else: + self.remove_job(job.id, jobstore) + + def get_jobs(self, jobstore=None, pending=None): + """ + Returns a list of pending jobs (if the scheduler hasn't been started yet) and scheduled + jobs, either from a specific job store or from all of them. + + If the scheduler has not been started yet, only pending jobs can be returned because the + job stores haven't been started yet either. + + :param str|unicode jobstore: alias of the job store + :param bool pending: **DEPRECATED** + :rtype: list[Job] + + """ + if pending is not None: + warnings.warn('The "pending" option is deprecated -- get_jobs() always returns ' + 'scheduled jobs if the scheduler has been started and pending jobs ' + 'otherwise', DeprecationWarning) + + with self._jobstores_lock: + jobs = [] + if self.state == STATE_STOPPED: + for job, alias, replace_existing in self._pending_jobs: + if jobstore is None or alias == jobstore: + jobs.append(job) + else: + for alias, store in six.iteritems(self._jobstores): + if jobstore is None or alias == jobstore: + jobs.extend(store.get_all_jobs()) + + return jobs + + def get_job(self, job_id, jobstore=None): + """ + Returns the Job that matches the given ``job_id``. + + :param str|unicode job_id: the identifier of the job + :param str|unicode jobstore: alias of the job store that most likely contains the job + :return: the Job by the given ID, or ``None`` if it wasn't found + :rtype: Job + + """ + with self._jobstores_lock: + try: + return self._lookup_job(job_id, jobstore)[0] + except JobLookupError: + return + + def remove_job(self, job_id, jobstore=None): + """ + Removes a job, preventing it from being run any more. + + :param str|unicode job_id: the identifier of the job + :param str|unicode jobstore: alias of the job store that contains the job + :raises JobLookupError: if the job was not found + + """ + jobstore_alias = None + with self._jobstores_lock: + # Check if the job is among the pending jobs + if self.state == STATE_STOPPED: + for i, (job, alias, replace_existing) in enumerate(self._pending_jobs): + if job.id == job_id and jobstore in (None, alias): + del self._pending_jobs[i] + jobstore_alias = alias + break + else: + # Otherwise, try to remove it from each store until it succeeds or we run out of + # stores to check + for alias, store in six.iteritems(self._jobstores): + if jobstore in (None, alias): + try: + store.remove_job(job_id) + jobstore_alias = alias + break + except JobLookupError: + continue + + if jobstore_alias is None: + raise JobLookupError(job_id) + + # Notify listeners that a job has been removed + event = JobEvent(EVENT_JOB_REMOVED, job_id, jobstore_alias) + self._dispatch_event(event) + + self._logger.info('Removed job %s', job_id) + + def remove_all_jobs(self, jobstore=None): + """ + Removes all jobs from the specified job store, or all job stores if none is given. + + :param str|unicode jobstore: alias of the job store + + """ + with self._jobstores_lock: + if self.state == STATE_STOPPED: + if jobstore: + self._pending_jobs = [pending for pending in self._pending_jobs if + pending[1] != jobstore] + else: + self._pending_jobs = [] + else: + for alias, store in six.iteritems(self._jobstores): + if jobstore in (None, alias): + store.remove_all_jobs() + + self._dispatch_event(SchedulerEvent(EVENT_ALL_JOBS_REMOVED, jobstore)) + + def print_jobs(self, jobstore=None, out=None): + """ + print_jobs(jobstore=None, out=sys.stdout) + + Prints out a textual listing of all jobs currently scheduled on either all job stores or + just a specific one. + + :param str|unicode jobstore: alias of the job store, ``None`` to list jobs from all stores + :param file out: a file-like object to print to (defaults to **sys.stdout** if nothing is + given) + + """ + out = out or sys.stdout + with self._jobstores_lock: + if self.state == STATE_STOPPED: + print(u'Pending jobs:', file=out) + if self._pending_jobs: + for job, jobstore_alias, replace_existing in self._pending_jobs: + if jobstore in (None, jobstore_alias): + print(u' %s' % job, file=out) + else: + print(u' No pending jobs', file=out) + else: + for alias, store in sorted(six.iteritems(self._jobstores)): + if jobstore in (None, alias): + print(u'Jobstore %s:' % alias, file=out) + jobs = store.get_all_jobs() + if jobs: + for job in jobs: + print(u' %s' % job, file=out) + else: + print(u' No scheduled jobs', file=out) + + @abstractmethod + def wakeup(self): + """ + Notifies the scheduler that there may be jobs due for execution. + Triggers :meth:`_process_jobs` to be run in an implementation specific manner. + """ + + # + # Private API + # + + def _configure(self, config): + # Set general options + self._logger = maybe_ref(config.pop('logger', None)) or getLogger('apscheduler.scheduler') + self.timezone = astimezone(config.pop('timezone', None)) or get_localzone() + self.jobstore_retry_interval = float(config.pop('jobstore_retry_interval', 10)) + + # Set the job defaults + job_defaults = config.get('job_defaults', {}) + self._job_defaults = { + 'misfire_grace_time': asint(job_defaults.get('misfire_grace_time', 1)), + 'coalesce': asbool(job_defaults.get('coalesce', True)), + 'max_instances': asint(job_defaults.get('max_instances', 1)) + } + + # Configure executors + self._executors.clear() + for alias, value in six.iteritems(config.get('executors', {})): + if isinstance(value, BaseExecutor): + self.add_executor(value, alias) + elif isinstance(value, MutableMapping): + executor_class = value.pop('class', None) + plugin = value.pop('type', None) + if plugin: + executor = self._create_plugin_instance('executor', plugin, value) + elif executor_class: + cls = maybe_ref(executor_class) + executor = cls(**value) + else: + raise ValueError( + 'Cannot create executor "%s" -- either "type" or "class" must be defined' % + alias) + + self.add_executor(executor, alias) + else: + raise TypeError( + "Expected executor instance or dict for executors['%s'], got %s instead" % + (alias, value.__class__.__name__)) + + # Configure job stores + self._jobstores.clear() + for alias, value in six.iteritems(config.get('jobstores', {})): + if isinstance(value, BaseJobStore): + self.add_jobstore(value, alias) + elif isinstance(value, MutableMapping): + jobstore_class = value.pop('class', None) + plugin = value.pop('type', None) + if plugin: + jobstore = self._create_plugin_instance('jobstore', plugin, value) + elif jobstore_class: + cls = maybe_ref(jobstore_class) + jobstore = cls(**value) + else: + raise ValueError( + 'Cannot create job store "%s" -- either "type" or "class" must be ' + 'defined' % alias) + + self.add_jobstore(jobstore, alias) + else: + raise TypeError( + "Expected job store instance or dict for jobstores['%s'], got %s instead" % + (alias, value.__class__.__name__)) + + def _create_default_executor(self): + """Creates a default executor store, specific to the particular scheduler type.""" + return ThreadPoolExecutor() + + def _create_default_jobstore(self): + """Creates a default job store, specific to the particular scheduler type.""" + return MemoryJobStore() + + def _lookup_executor(self, alias): + """ + Returns the executor instance by the given name from the list of executors that were added + to this scheduler. + + :type alias: str + :raises KeyError: if no executor by the given alias is not found + + """ + try: + return self._executors[alias] + except KeyError: + raise KeyError('No such executor: %s' % alias) + + def _lookup_jobstore(self, alias): + """ + Returns the job store instance by the given name from the list of job stores that were + added to this scheduler. + + :type alias: str + :raises KeyError: if no job store by the given alias is not found + + """ + try: + return self._jobstores[alias] + except KeyError: + raise KeyError('No such job store: %s' % alias) + + def _lookup_job(self, job_id, jobstore_alias): + """ + Finds a job by its ID. + + :type job_id: str + :param str jobstore_alias: alias of a job store to look in + :return tuple[Job, str]: a tuple of job, jobstore alias (jobstore alias is None in case of + a pending job) + :raises JobLookupError: if no job by the given ID is found. + + """ + if self.state == STATE_STOPPED: + # Check if the job is among the pending jobs + for job, alias, replace_existing in self._pending_jobs: + if job.id == job_id: + return job, None + else: + # Look in all job stores + for alias, store in six.iteritems(self._jobstores): + if jobstore_alias in (None, alias): + job = store.lookup_job(job_id) + if job is not None: + return job, alias + + raise JobLookupError(job_id) + + def _dispatch_event(self, event): + """ + Dispatches the given event to interested listeners. + + :param SchedulerEvent event: the event to send + + """ + with self._listeners_lock: + listeners = tuple(self._listeners) + + for cb, mask in listeners: + if event.code & mask: + try: + cb(event) + except BaseException: + self._logger.exception('Error notifying listener') + + def _check_uwsgi(self): + """Check if we're running under uWSGI with threads disabled.""" + uwsgi_module = sys.modules.get('uwsgi') + if not getattr(uwsgi_module, 'has_threads', True): + raise RuntimeError('The scheduler seems to be running under uWSGI, but threads have ' + 'been disabled. You must run uWSGI with the --enable-threads ' + 'option for the scheduler to work.') + + def _real_add_job(self, job, jobstore_alias, replace_existing): + """ + :param Job job: the job to add + :param bool replace_existing: ``True`` to use update_job() in case the job already exists + in the store + + """ + # Fill in undefined values with defaults + replacements = {} + for key, value in six.iteritems(self._job_defaults): + if not hasattr(job, key): + replacements[key] = value + + # Calculate the next run time if there is none defined + if not hasattr(job, 'next_run_time'): + now = datetime.now(self.timezone) + replacements['next_run_time'] = job.trigger.get_next_fire_time(None, now) + + # Apply any replacements + job._modify(**replacements) + + # Add the job to the given job store + store = self._lookup_jobstore(jobstore_alias) + try: + store.add_job(job) + except ConflictingIdError: + if replace_existing: + store.update_job(job) + else: + raise + + # Mark the job as no longer pending + job._jobstore_alias = jobstore_alias + + # Notify listeners that a new job has been added + event = JobEvent(EVENT_JOB_ADDED, job.id, jobstore_alias) + self._dispatch_event(event) + + self._logger.info('Added job "%s" to job store "%s"', job.name, jobstore_alias) + + # Notify the scheduler about the new job + if self.state == STATE_RUNNING: + self.wakeup() + + def _create_plugin_instance(self, type_, alias, constructor_kwargs): + """Creates an instance of the given plugin type, loading the plugin first if necessary.""" + plugin_container, class_container, base_class = { + 'trigger': (self._trigger_plugins, self._trigger_classes, BaseTrigger), + 'jobstore': (self._jobstore_plugins, self._jobstore_classes, BaseJobStore), + 'executor': (self._executor_plugins, self._executor_classes, BaseExecutor) + }[type_] + + try: + plugin_cls = class_container[alias] + except KeyError: + if alias in plugin_container: + plugin_cls = class_container[alias] = plugin_container[alias].load() + if not issubclass(plugin_cls, base_class): + raise TypeError('The {0} entry point does not point to a {0} class'. + format(type_)) + else: + raise LookupError('No {0} by the name "{1}" was found'.format(type_, alias)) + + return plugin_cls(**constructor_kwargs) + + def _create_trigger(self, trigger, trigger_args): + if isinstance(trigger, BaseTrigger): + return trigger + elif trigger is None: + trigger = 'date' + elif not isinstance(trigger, six.string_types): + raise TypeError('Expected a trigger instance or string, got %s instead' % + trigger.__class__.__name__) + + # Use the scheduler's time zone if nothing else is specified + trigger_args.setdefault('timezone', self.timezone) + + # Instantiate the trigger class + return self._create_plugin_instance('trigger', trigger, trigger_args) + + def _create_lock(self): + """Creates a reentrant lock object.""" + return RLock() + + def _process_jobs(self): + """ + Iterates through jobs in every jobstore, starts jobs that are due and figures out how long + to wait for the next round. + + If the ``get_due_jobs()`` call raises an exception, a new wakeup is scheduled in at least + ``jobstore_retry_interval`` seconds. + + """ + if self.state == STATE_PAUSED: + self._logger.debug('Scheduler is paused -- not processing jobs') + return None + + self._logger.debug('Looking for jobs to run') + now = datetime.now(self.timezone) + next_wakeup_time = None + events = [] + + with self._jobstores_lock: + for jobstore_alias, jobstore in six.iteritems(self._jobstores): + try: + due_jobs = jobstore.get_due_jobs(now) + except Exception as e: + # Schedule a wakeup at least in jobstore_retry_interval seconds + self._logger.warning('Error getting due jobs from job store %r: %s', + jobstore_alias, e) + retry_wakeup_time = now + timedelta(seconds=self.jobstore_retry_interval) + if not next_wakeup_time or next_wakeup_time > retry_wakeup_time: + next_wakeup_time = retry_wakeup_time + + continue + + for job in due_jobs: + # Look up the job's executor + try: + executor = self._lookup_executor(job.executor) + except BaseException: + self._logger.error( + 'Executor lookup ("%s") failed for job "%s" -- removing it from the ' + 'job store', job.executor, job) + self.remove_job(job.id, jobstore_alias) + continue + + run_times = job._get_run_times(now) + run_times = run_times[-1:] if run_times and job.coalesce else run_times + if run_times: + try: + executor.submit_job(job, run_times) + except MaxInstancesReachedError: + self._logger.warning( + 'Execution of job "%s" skipped: maximum number of running ' + 'instances reached (%d)', job, job.max_instances) + event = JobSubmissionEvent(EVENT_JOB_MAX_INSTANCES, job.id, + jobstore_alias, run_times) + events.append(event) + except BaseException: + self._logger.exception('Error submitting job "%s" to executor "%s"', + job, job.executor) + else: + event = JobSubmissionEvent(EVENT_JOB_SUBMITTED, job.id, jobstore_alias, + run_times) + events.append(event) + + # Update the job if it has a next execution time. + # Otherwise remove it from the job store. + job_next_run = job.trigger.get_next_fire_time(run_times[-1], now) + if job_next_run: + job._modify(next_run_time=job_next_run) + jobstore.update_job(job) + else: + self.remove_job(job.id, jobstore_alias) + + # Set a new next wakeup time if there isn't one yet or + # the jobstore has an even earlier one + jobstore_next_run_time = jobstore.get_next_run_time() + if jobstore_next_run_time and (next_wakeup_time is None or + jobstore_next_run_time < next_wakeup_time): + next_wakeup_time = jobstore_next_run_time.astimezone(self.timezone) + + # Dispatch collected events + for event in events: + self._dispatch_event(event) + + # Determine the delay until this method should be called again + if self.state == STATE_PAUSED: + wait_seconds = None + self._logger.debug('Scheduler is paused; waiting until resume() is called') + elif next_wakeup_time is None: + wait_seconds = None + self._logger.debug('No jobs; waiting until a job is added') + else: + wait_seconds = min(max(timedelta_seconds(next_wakeup_time - now), 0), TIMEOUT_MAX) + self._logger.debug('Next wakeup is due at %s (in %f seconds)', next_wakeup_time, + wait_seconds) + + return wait_seconds diff --git a/telegramer/include/apscheduler/schedulers/blocking.py b/telegramer/include/apscheduler/schedulers/blocking.py new file mode 100644 index 0000000..4ecc9f6 --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/blocking.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import + +from threading import Event + +from apscheduler.schedulers.base import BaseScheduler, STATE_STOPPED +from apscheduler.util import TIMEOUT_MAX + + +class BlockingScheduler(BaseScheduler): + """ + A scheduler that runs in the foreground + (:meth:`~apscheduler.schedulers.base.BaseScheduler.start` will block). + """ + _event = None + + def start(self, *args, **kwargs): + if self._event is None or self._event.is_set(): + self._event = Event() + + super(BlockingScheduler, self).start(*args, **kwargs) + self._main_loop() + + def shutdown(self, wait=True): + super(BlockingScheduler, self).shutdown(wait) + self._event.set() + + def _main_loop(self): + wait_seconds = TIMEOUT_MAX + while self.state != STATE_STOPPED: + self._event.wait(wait_seconds) + self._event.clear() + wait_seconds = self._process_jobs() + + def wakeup(self): + self._event.set() diff --git a/telegramer/include/apscheduler/schedulers/gevent.py b/telegramer/include/apscheduler/schedulers/gevent.py new file mode 100644 index 0000000..d48ed74 --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/gevent.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import + +from apscheduler.schedulers.blocking import BlockingScheduler +from apscheduler.schedulers.base import BaseScheduler + +try: + from gevent.event import Event + from gevent.lock import RLock + import gevent +except ImportError: # pragma: nocover + raise ImportError('GeventScheduler requires gevent installed') + + +class GeventScheduler(BlockingScheduler): + """A scheduler that runs as a Gevent greenlet.""" + + _greenlet = None + + def start(self, *args, **kwargs): + self._event = Event() + BaseScheduler.start(self, *args, **kwargs) + self._greenlet = gevent.spawn(self._main_loop) + return self._greenlet + + def shutdown(self, *args, **kwargs): + super(GeventScheduler, self).shutdown(*args, **kwargs) + self._greenlet.join() + del self._greenlet + + def _create_lock(self): + return RLock() + + def _create_default_executor(self): + from apscheduler.executors.gevent import GeventExecutor + return GeventExecutor() diff --git a/telegramer/include/apscheduler/schedulers/qt.py b/telegramer/include/apscheduler/schedulers/qt.py new file mode 100644 index 0000000..600f6e6 --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/qt.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import + +from apscheduler.schedulers.base import BaseScheduler + +try: + from PyQt5.QtCore import QObject, QTimer +except (ImportError, RuntimeError): # pragma: nocover + try: + from PyQt4.QtCore import QObject, QTimer + except ImportError: + try: + from PySide6.QtCore import QObject, QTimer # noqa + except ImportError: + try: + from PySide2.QtCore import QObject, QTimer # noqa + except ImportError: + try: + from PySide.QtCore import QObject, QTimer # noqa + except ImportError: + raise ImportError('QtScheduler requires either PyQt5, PyQt4, PySide6, PySide2 ' + 'or PySide installed') + + +class QtScheduler(BaseScheduler): + """A scheduler that runs in a Qt event loop.""" + + _timer = None + + def shutdown(self, *args, **kwargs): + super(QtScheduler, self).shutdown(*args, **kwargs) + self._stop_timer() + + def _start_timer(self, wait_seconds): + self._stop_timer() + if wait_seconds is not None: + wait_time = min(wait_seconds * 1000, 2147483647) + self._timer = QTimer.singleShot(wait_time, self._process_jobs) + + def _stop_timer(self): + if self._timer: + if self._timer.isActive(): + self._timer.stop() + del self._timer + + def wakeup(self): + self._start_timer(0) + + def _process_jobs(self): + wait_seconds = super(QtScheduler, self)._process_jobs() + self._start_timer(wait_seconds) diff --git a/telegramer/include/apscheduler/schedulers/tornado.py b/telegramer/include/apscheduler/schedulers/tornado.py new file mode 100644 index 0000000..0a9171f --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/tornado.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import + +from datetime import timedelta +from functools import wraps + +from apscheduler.schedulers.base import BaseScheduler +from apscheduler.util import maybe_ref + +try: + from tornado.ioloop import IOLoop +except ImportError: # pragma: nocover + raise ImportError('TornadoScheduler requires tornado installed') + + +def run_in_ioloop(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + self._ioloop.add_callback(func, self, *args, **kwargs) + return wrapper + + +class TornadoScheduler(BaseScheduler): + """ + A scheduler that runs on a Tornado IOLoop. + + The default executor can run jobs based on native coroutines (``async def``). + + =========== =============================================================== + ``io_loop`` Tornado IOLoop instance to use (defaults to the global IO loop) + =========== =============================================================== + """ + + _ioloop = None + _timeout = None + + @run_in_ioloop + def shutdown(self, wait=True): + super(TornadoScheduler, self).shutdown(wait) + self._stop_timer() + + def _configure(self, config): + self._ioloop = maybe_ref(config.pop('io_loop', None)) or IOLoop.current() + super(TornadoScheduler, self)._configure(config) + + def _start_timer(self, wait_seconds): + self._stop_timer() + if wait_seconds is not None: + self._timeout = self._ioloop.add_timeout(timedelta(seconds=wait_seconds), self.wakeup) + + def _stop_timer(self): + if self._timeout: + self._ioloop.remove_timeout(self._timeout) + del self._timeout + + def _create_default_executor(self): + from apscheduler.executors.tornado import TornadoExecutor + return TornadoExecutor() + + @run_in_ioloop + def wakeup(self): + self._stop_timer() + wait_seconds = self._process_jobs() + self._start_timer(wait_seconds) diff --git a/telegramer/include/apscheduler/schedulers/twisted.py b/telegramer/include/apscheduler/schedulers/twisted.py new file mode 100644 index 0000000..6b43a84 --- /dev/null +++ b/telegramer/include/apscheduler/schedulers/twisted.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import + +from functools import wraps + +from apscheduler.schedulers.base import BaseScheduler +from apscheduler.util import maybe_ref + +try: + from twisted.internet import reactor as default_reactor +except ImportError: # pragma: nocover + raise ImportError('TwistedScheduler requires Twisted installed') + + +def run_in_reactor(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + self._reactor.callFromThread(func, self, *args, **kwargs) + return wrapper + + +class TwistedScheduler(BaseScheduler): + """ + A scheduler that runs on a Twisted reactor. + + Extra options: + + =========== ======================================================== + ``reactor`` Reactor instance to use (defaults to the global reactor) + =========== ======================================================== + """ + + _reactor = None + _delayedcall = None + + def _configure(self, config): + self._reactor = maybe_ref(config.pop('reactor', default_reactor)) + super(TwistedScheduler, self)._configure(config) + + @run_in_reactor + def shutdown(self, wait=True): + super(TwistedScheduler, self).shutdown(wait) + self._stop_timer() + + def _start_timer(self, wait_seconds): + self._stop_timer() + if wait_seconds is not None: + self._delayedcall = self._reactor.callLater(wait_seconds, self.wakeup) + + def _stop_timer(self): + if self._delayedcall and self._delayedcall.active(): + self._delayedcall.cancel() + del self._delayedcall + + @run_in_reactor + def wakeup(self): + self._stop_timer() + wait_seconds = self._process_jobs() + self._start_timer(wait_seconds) + + def _create_default_executor(self): + from apscheduler.executors.twisted import TwistedExecutor + return TwistedExecutor() diff --git a/telegramer/include/apscheduler/triggers/__init__.py b/telegramer/include/apscheduler/triggers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegramer/include/apscheduler/triggers/base.py b/telegramer/include/apscheduler/triggers/base.py new file mode 100644 index 0000000..55d010d --- /dev/null +++ b/telegramer/include/apscheduler/triggers/base.py @@ -0,0 +1,37 @@ +from abc import ABCMeta, abstractmethod +from datetime import timedelta +import random + +import six + + +class BaseTrigger(six.with_metaclass(ABCMeta)): + """Abstract base class that defines the interface that every trigger must implement.""" + + __slots__ = () + + @abstractmethod + def get_next_fire_time(self, previous_fire_time, now): + """ + Returns the next datetime to fire on, If no such datetime can be calculated, returns + ``None``. + + :param datetime.datetime previous_fire_time: the previous time the trigger was fired + :param datetime.datetime now: current datetime + """ + + def _apply_jitter(self, next_fire_time, jitter, now): + """ + Randomize ``next_fire_time`` by adding a random value (the jitter). + + :param datetime.datetime|None next_fire_time: next fire time without jitter applied. If + ``None``, returns ``None``. + :param int|None jitter: maximum number of seconds to add to ``next_fire_time`` + (if ``None`` or ``0``, returns ``next_fire_time``) + :param datetime.datetime now: current datetime + :return datetime.datetime|None: next fire time with a jitter. + """ + if next_fire_time is None or not jitter: + return next_fire_time + + return next_fire_time + timedelta(seconds=random.uniform(0, jitter)) diff --git a/telegramer/include/apscheduler/triggers/combining.py b/telegramer/include/apscheduler/triggers/combining.py new file mode 100644 index 0000000..bb90006 --- /dev/null +++ b/telegramer/include/apscheduler/triggers/combining.py @@ -0,0 +1,95 @@ +from apscheduler.triggers.base import BaseTrigger +from apscheduler.util import obj_to_ref, ref_to_obj + + +class BaseCombiningTrigger(BaseTrigger): + __slots__ = ('triggers', 'jitter') + + def __init__(self, triggers, jitter=None): + self.triggers = triggers + self.jitter = jitter + + def __getstate__(self): + return { + 'version': 1, + 'triggers': [(obj_to_ref(trigger.__class__), trigger.__getstate__()) + for trigger in self.triggers], + 'jitter': self.jitter + } + + def __setstate__(self, state): + if state.get('version', 1) > 1: + raise ValueError( + 'Got serialized data for version %s of %s, but only versions up to 1 can be ' + 'handled' % (state['version'], self.__class__.__name__)) + + self.jitter = state['jitter'] + self.triggers = [] + for clsref, state in state['triggers']: + cls = ref_to_obj(clsref) + trigger = cls.__new__(cls) + trigger.__setstate__(state) + self.triggers.append(trigger) + + def __repr__(self): + return '<{}({}{})>'.format(self.__class__.__name__, self.triggers, + ', jitter={}'.format(self.jitter) if self.jitter else '') + + +class AndTrigger(BaseCombiningTrigger): + """ + Always returns the earliest next fire time that all the given triggers can agree on. + The trigger is considered to be finished when any of the given triggers has finished its + schedule. + + Trigger alias: ``and`` + + :param list triggers: triggers to combine + :param int|None jitter: delay the job execution by ``jitter`` seconds at most + """ + + __slots__ = () + + def get_next_fire_time(self, previous_fire_time, now): + while True: + fire_times = [trigger.get_next_fire_time(previous_fire_time, now) + for trigger in self.triggers] + if None in fire_times: + return None + elif min(fire_times) == max(fire_times): + return self._apply_jitter(fire_times[0], self.jitter, now) + else: + now = max(fire_times) + + def __str__(self): + return 'and[{}]'.format(', '.join(str(trigger) for trigger in self.triggers)) + + +class OrTrigger(BaseCombiningTrigger): + """ + Always returns the earliest next fire time produced by any of the given triggers. + The trigger is considered finished when all the given triggers have finished their schedules. + + Trigger alias: ``or`` + + :param list triggers: triggers to combine + :param int|None jitter: delay the job execution by ``jitter`` seconds at most + + .. note:: Triggers that depends on the previous fire time, such as the interval trigger, may + seem to behave strangely since they are always passed the previous fire time produced by + any of the given triggers. + """ + + __slots__ = () + + def get_next_fire_time(self, previous_fire_time, now): + fire_times = [trigger.get_next_fire_time(previous_fire_time, now) + for trigger in self.triggers] + fire_times = [fire_time for fire_time in fire_times if fire_time is not None] + if fire_times: + return self._apply_jitter(min(fire_times), self.jitter, now) + else: + return None + + def __str__(self): + return 'or[{}]'.format(', '.join(str(trigger) for trigger in self.triggers)) diff --git a/telegramer/include/apscheduler/triggers/cron/__init__.py b/telegramer/include/apscheduler/triggers/cron/__init__.py new file mode 100644 index 0000000..b5389dd --- /dev/null +++ b/telegramer/include/apscheduler/triggers/cron/__init__.py @@ -0,0 +1,239 @@ +from datetime import datetime, timedelta + +from tzlocal import get_localzone +import six + +from apscheduler.triggers.base import BaseTrigger +from apscheduler.triggers.cron.fields import ( + BaseField, MonthField, WeekField, DayOfMonthField, DayOfWeekField, DEFAULT_VALUES) +from apscheduler.util import ( + datetime_ceil, convert_to_datetime, datetime_repr, astimezone, localize, normalize) + + +class CronTrigger(BaseTrigger): + """ + Triggers when current time matches all specified time constraints, + similarly to how the UNIX cron scheduler works. + + :param int|str year: 4-digit year + :param int|str month: month (1-12) + :param int|str day: day of month (1-31) + :param int|str week: ISO week (1-53) + :param int|str day_of_week: number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun) + :param int|str hour: hour (0-23) + :param int|str minute: minute (0-59) + :param int|str second: second (0-59) + :param datetime|str start_date: earliest possible date/time to trigger on (inclusive) + :param datetime|str end_date: latest possible date/time to trigger on (inclusive) + :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (defaults + to scheduler timezone) + :param int|None jitter: delay the job execution by ``jitter`` seconds at most + + .. note:: The first weekday is always **monday**. + """ + + FIELD_NAMES = ('year', 'month', 'day', 'week', 'day_of_week', 'hour', 'minute', 'second') + FIELDS_MAP = { + 'year': BaseField, + 'month': MonthField, + 'week': WeekField, + 'day': DayOfMonthField, + 'day_of_week': DayOfWeekField, + 'hour': BaseField, + 'minute': BaseField, + 'second': BaseField + } + + __slots__ = 'timezone', 'start_date', 'end_date', 'fields', 'jitter' + + def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None, hour=None, + minute=None, second=None, start_date=None, end_date=None, timezone=None, + jitter=None): + if timezone: + self.timezone = astimezone(timezone) + elif isinstance(start_date, datetime) and start_date.tzinfo: + self.timezone = start_date.tzinfo + elif isinstance(end_date, datetime) and end_date.tzinfo: + self.timezone = end_date.tzinfo + else: + self.timezone = get_localzone() + + self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date') + self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date') + + self.jitter = jitter + + values = dict((key, value) for (key, value) in six.iteritems(locals()) + if key in self.FIELD_NAMES and value is not None) + self.fields = [] + assign_defaults = False + for field_name in self.FIELD_NAMES: + if field_name in values: + exprs = values.pop(field_name) + is_default = False + assign_defaults = not values + elif assign_defaults: + exprs = DEFAULT_VALUES[field_name] + is_default = True + else: + exprs = '*' + is_default = True + + field_class = self.FIELDS_MAP[field_name] + field = field_class(field_name, exprs, is_default) + self.fields.append(field) + + @classmethod + def from_crontab(cls, expr, timezone=None): + """ + Create a :class:`~CronTrigger` from a standard crontab expression. + + See https://en.wikipedia.org/wiki/Cron for more information on the format accepted here. + + :param expr: minute, hour, day of month, month, day of week + :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations ( + defaults to scheduler timezone) + :return: a :class:`~CronTrigger` instance + + """ + values = expr.split() + if len(values) != 5: + raise ValueError('Wrong number of fields; got {}, expected 5'.format(len(values))) + + return cls(minute=values[0], hour=values[1], day=values[2], month=values[3], + day_of_week=values[4], timezone=timezone) + + def _increment_field_value(self, dateval, fieldnum): + """ + Increments the designated field and resets all less significant fields to their minimum + values. + + :type dateval: datetime + :type fieldnum: int + :return: a tuple containing the new date, and the number of the field that was actually + incremented + :rtype: tuple + """ + + values = {} + i = 0 + while i < len(self.fields): + field = self.fields[i] + if not field.REAL: + if i == fieldnum: + fieldnum -= 1 + i -= 1 + else: + i += 1 + continue + + if i < fieldnum: + values[field.name] = field.get_value(dateval) + i += 1 + elif i > fieldnum: + values[field.name] = field.get_min(dateval) + i += 1 + else: + value = field.get_value(dateval) + maxval = field.get_max(dateval) + if value == maxval: + fieldnum -= 1 + i -= 1 + else: + values[field.name] = value + 1 + i += 1 + + difference = datetime(**values) - dateval.replace(tzinfo=None) + return normalize(dateval + difference), fieldnum + + def _set_field_value(self, dateval, fieldnum, new_value): + values = {} + for i, field in enumerate(self.fields): + if field.REAL: + if i < fieldnum: + values[field.name] = field.get_value(dateval) + elif i > fieldnum: + values[field.name] = field.get_min(dateval) + else: + values[field.name] = new_value + + return localize(datetime(**values), self.timezone) + + def get_next_fire_time(self, previous_fire_time, now): + if previous_fire_time: + start_date = min(now, previous_fire_time + timedelta(microseconds=1)) + if start_date == previous_fire_time: + start_date += timedelta(microseconds=1) + else: + start_date = max(now, self.start_date) if self.start_date else now + + fieldnum = 0 + next_date = datetime_ceil(start_date).astimezone(self.timezone) + while 0 <= fieldnum < len(self.fields): + field = self.fields[fieldnum] + curr_value = field.get_value(next_date) + next_value = field.get_next_value(next_date) + + if next_value is None: + # No valid value was found + next_date, fieldnum = self._increment_field_value(next_date, fieldnum - 1) + elif next_value > curr_value: + # A valid, but higher than the starting value, was found + if field.REAL: + next_date = self._set_field_value(next_date, fieldnum, next_value) + fieldnum += 1 + else: + next_date, fieldnum = self._increment_field_value(next_date, fieldnum) + else: + # A valid value was found, no changes necessary + fieldnum += 1 + + # Return if the date has rolled past the end date + if self.end_date and next_date > self.end_date: + return None + + if fieldnum >= 0: + next_date = self._apply_jitter(next_date, self.jitter, now) + return min(next_date, self.end_date) if self.end_date else next_date + + def __getstate__(self): + return { + 'version': 2, + 'timezone': self.timezone, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'fields': self.fields, + 'jitter': self.jitter, + } + + def __setstate__(self, state): + # This is for compatibility with APScheduler 3.0.x + if isinstance(state, tuple): + state = state[1] + + if state.get('version', 1) > 2: + raise ValueError( + 'Got serialized data for version %s of %s, but only versions up to 2 can be ' + 'handled' % (state['version'], self.__class__.__name__)) + + self.timezone = state['timezone'] + self.start_date = state['start_date'] + self.end_date = state['end_date'] + self.fields = state['fields'] + self.jitter = state.get('jitter') + + def __str__(self): + options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default] + return 'cron[%s]' % (', '.join(options)) + + def __repr__(self): + options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default] + if self.start_date: + options.append("start_date=%r" % datetime_repr(self.start_date)) + if self.end_date: + options.append("end_date=%r" % datetime_repr(self.end_date)) + if self.jitter: + options.append('jitter=%s' % self.jitter) + + return "<%s (%s, timezone='%s')>" % ( + self.__class__.__name__, ', '.join(options), self.timezone) diff --git a/telegramer/include/apscheduler/triggers/cron/expressions.py b/telegramer/include/apscheduler/triggers/cron/expressions.py new file mode 100644 index 0000000..55a3716 --- /dev/null +++ b/telegramer/include/apscheduler/triggers/cron/expressions.py @@ -0,0 +1,251 @@ +"""This module contains the expressions applicable for CronTrigger's fields.""" + +from calendar import monthrange +import re + +from apscheduler.util import asint + +__all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression', + 'WeekdayPositionExpression', 'LastDayOfMonthExpression') + + +WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] +MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + + +class AllExpression(object): + value_re = re.compile(r'\*(?:/(?P\d+))?$') + + def __init__(self, step=None): + self.step = asint(step) + if self.step == 0: + raise ValueError('Increment must be higher than 0') + + def validate_range(self, field_name): + from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES + + value_range = MAX_VALUES[field_name] - MIN_VALUES[field_name] + if self.step and self.step > value_range: + raise ValueError('the step value ({}) is higher than the total range of the ' + 'expression ({})'.format(self.step, value_range)) + + def get_next_value(self, date, field): + start = field.get_value(date) + minval = field.get_min(date) + maxval = field.get_max(date) + start = max(start, minval) + + if not self.step: + next = start + else: + distance_to_next = (self.step - (start - minval)) % self.step + next = start + distance_to_next + + if next <= maxval: + return next + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.step == other.step + + def __str__(self): + if self.step: + return '*/%d' % self.step + return '*' + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self.step) + + +class RangeExpression(AllExpression): + value_re = re.compile( + r'(?P\d+)(?:-(?P\d+))?(?:/(?P\d+))?$') + + def __init__(self, first, last=None, step=None): + super(RangeExpression, self).__init__(step) + first = asint(first) + last = asint(last) + if last is None and step is None: + last = first + if last is not None and first > last: + raise ValueError('The minimum value in a range must not be higher than the maximum') + self.first = first + self.last = last + + def validate_range(self, field_name): + from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES + + super(RangeExpression, self).validate_range(field_name) + if self.first < MIN_VALUES[field_name]: + raise ValueError('the first value ({}) is lower than the minimum value ({})' + .format(self.first, MIN_VALUES[field_name])) + if self.last is not None and self.last > MAX_VALUES[field_name]: + raise ValueError('the last value ({}) is higher than the maximum value ({})' + .format(self.last, MAX_VALUES[field_name])) + value_range = (self.last or MAX_VALUES[field_name]) - self.first + if self.step and self.step > value_range: + raise ValueError('the step value ({}) is higher than the total range of the ' + 'expression ({})'.format(self.step, value_range)) + + def get_next_value(self, date, field): + startval = field.get_value(date) + minval = field.get_min(date) + maxval = field.get_max(date) + + # Apply range limits + minval = max(minval, self.first) + maxval = min(maxval, self.last) if self.last is not None else maxval + nextval = max(minval, startval) + + # Apply the step if defined + if self.step: + distance_to_next = (self.step - (nextval - minval)) % self.step + nextval += distance_to_next + + return nextval if nextval <= maxval else None + + def __eq__(self, other): + return (isinstance(other, self.__class__) and self.first == other.first and + self.last == other.last) + + def __str__(self): + if self.last != self.first and self.last is not None: + range = '%d-%d' % (self.first, self.last) + else: + range = str(self.first) + + if self.step: + return '%s/%d' % (range, self.step) + return range + + def __repr__(self): + args = [str(self.first)] + if self.last != self.first and self.last is not None or self.step: + args.append(str(self.last)) + if self.step: + args.append(str(self.step)) + return "%s(%s)" % (self.__class__.__name__, ', '.join(args)) + + +class MonthRangeExpression(RangeExpression): + value_re = re.compile(r'(?P[a-z]+)(?:-(?P[a-z]+))?', re.IGNORECASE) + + def __init__(self, first, last=None): + try: + first_num = MONTHS.index(first.lower()) + 1 + except ValueError: + raise ValueError('Invalid month name "%s"' % first) + + if last: + try: + last_num = MONTHS.index(last.lower()) + 1 + except ValueError: + raise ValueError('Invalid month name "%s"' % last) + else: + last_num = None + + super(MonthRangeExpression, self).__init__(first_num, last_num) + + def __str__(self): + if self.last != self.first and self.last is not None: + return '%s-%s' % (MONTHS[self.first - 1], MONTHS[self.last - 1]) + return MONTHS[self.first - 1] + + def __repr__(self): + args = ["'%s'" % MONTHS[self.first]] + if self.last != self.first and self.last is not None: + args.append("'%s'" % MONTHS[self.last - 1]) + return "%s(%s)" % (self.__class__.__name__, ', '.join(args)) + + +class WeekdayRangeExpression(RangeExpression): + value_re = re.compile(r'(?P[a-z]+)(?:-(?P[a-z]+))?', re.IGNORECASE) + + def __init__(self, first, last=None): + try: + first_num = WEEKDAYS.index(first.lower()) + except ValueError: + raise ValueError('Invalid weekday name "%s"' % first) + + if last: + try: + last_num = WEEKDAYS.index(last.lower()) + except ValueError: + raise ValueError('Invalid weekday name "%s"' % last) + else: + last_num = None + + super(WeekdayRangeExpression, self).__init__(first_num, last_num) + + def __str__(self): + if self.last != self.first and self.last is not None: + return '%s-%s' % (WEEKDAYS[self.first], WEEKDAYS[self.last]) + return WEEKDAYS[self.first] + + def __repr__(self): + args = ["'%s'" % WEEKDAYS[self.first]] + if self.last != self.first and self.last is not None: + args.append("'%s'" % WEEKDAYS[self.last]) + return "%s(%s)" % (self.__class__.__name__, ', '.join(args)) + + +class WeekdayPositionExpression(AllExpression): + options = ['1st', '2nd', '3rd', '4th', '5th', 'last'] + value_re = re.compile(r'(?P%s) +(?P(?:\d+|\w+))' % + '|'.join(options), re.IGNORECASE) + + def __init__(self, option_name, weekday_name): + super(WeekdayPositionExpression, self).__init__(None) + try: + self.option_num = self.options.index(option_name.lower()) + except ValueError: + raise ValueError('Invalid weekday position "%s"' % option_name) + + try: + self.weekday = WEEKDAYS.index(weekday_name.lower()) + except ValueError: + raise ValueError('Invalid weekday name "%s"' % weekday_name) + + def get_next_value(self, date, field): + # Figure out the weekday of the month's first day and the number of days in that month + first_day_wday, last_day = monthrange(date.year, date.month) + + # Calculate which day of the month is the first of the target weekdays + first_hit_day = self.weekday - first_day_wday + 1 + if first_hit_day <= 0: + first_hit_day += 7 + + # Calculate what day of the month the target weekday would be + if self.option_num < 5: + target_day = first_hit_day + self.option_num * 7 + else: + target_day = first_hit_day + ((last_day - first_hit_day) // 7) * 7 + + if target_day <= last_day and target_day >= date.day: + return target_day + + def __eq__(self, other): + return (super(WeekdayPositionExpression, self).__eq__(other) and + self.option_num == other.option_num and self.weekday == other.weekday) + + def __str__(self): + return '%s %s' % (self.options[self.option_num], WEEKDAYS[self.weekday]) + + def __repr__(self): + return "%s('%s', '%s')" % (self.__class__.__name__, self.options[self.option_num], + WEEKDAYS[self.weekday]) + + +class LastDayOfMonthExpression(AllExpression): + value_re = re.compile(r'last', re.IGNORECASE) + + def __init__(self): + super(LastDayOfMonthExpression, self).__init__(None) + + def get_next_value(self, date, field): + return monthrange(date.year, date.month)[1] + + def __str__(self): + return 'last' + + def __repr__(self): + return "%s()" % self.__class__.__name__ diff --git a/telegramer/include/apscheduler/triggers/cron/fields.py b/telegramer/include/apscheduler/triggers/cron/fields.py new file mode 100644 index 0000000..86d620c --- /dev/null +++ b/telegramer/include/apscheduler/triggers/cron/fields.py @@ -0,0 +1,111 @@ +"""Fields represent CronTrigger options which map to :class:`~datetime.datetime` fields.""" + +from calendar import monthrange +import re + +import six + +from apscheduler.triggers.cron.expressions import ( + AllExpression, RangeExpression, WeekdayPositionExpression, LastDayOfMonthExpression, + WeekdayRangeExpression, MonthRangeExpression) + + +__all__ = ('MIN_VALUES', 'MAX_VALUES', 'DEFAULT_VALUES', 'BaseField', 'WeekField', + 'DayOfMonthField', 'DayOfWeekField') + + +MIN_VALUES = {'year': 1970, 'month': 1, 'day': 1, 'week': 1, 'day_of_week': 0, 'hour': 0, + 'minute': 0, 'second': 0} +MAX_VALUES = {'year': 9999, 'month': 12, 'day': 31, 'week': 53, 'day_of_week': 6, 'hour': 23, + 'minute': 59, 'second': 59} +DEFAULT_VALUES = {'year': '*', 'month': 1, 'day': 1, 'week': '*', 'day_of_week': '*', 'hour': 0, + 'minute': 0, 'second': 0} +SEPARATOR = re.compile(' *, *') + + +class BaseField(object): + REAL = True + COMPILERS = [AllExpression, RangeExpression] + + def __init__(self, name, exprs, is_default=False): + self.name = name + self.is_default = is_default + self.compile_expressions(exprs) + + def get_min(self, dateval): + return MIN_VALUES[self.name] + + def get_max(self, dateval): + return MAX_VALUES[self.name] + + def get_value(self, dateval): + return getattr(dateval, self.name) + + def get_next_value(self, dateval): + smallest = None + for expr in self.expressions: + value = expr.get_next_value(dateval, self) + if smallest is None or (value is not None and value < smallest): + smallest = value + + return smallest + + def compile_expressions(self, exprs): + self.expressions = [] + + # Split a comma-separated expression list, if any + for expr in SEPARATOR.split(str(exprs).strip()): + self.compile_expression(expr) + + def compile_expression(self, expr): + for compiler in self.COMPILERS: + match = compiler.value_re.match(expr) + if match: + compiled_expr = compiler(**match.groupdict()) + + try: + compiled_expr.validate_range(self.name) + except ValueError as e: + exc = ValueError('Error validating expression {!r}: {}'.format(expr, e)) + six.raise_from(exc, None) + + self.expressions.append(compiled_expr) + return + + raise ValueError('Unrecognized expression "%s" for field "%s"' % (expr, self.name)) + + def __eq__(self, other): + return isinstance(self, self.__class__) and self.expressions == other.expressions + + def __str__(self): + expr_strings = (str(e) for e in self.expressions) + return ','.join(expr_strings) + + def __repr__(self): + return "%s('%s', '%s')" % (self.__class__.__name__, self.name, self) + + +class WeekField(BaseField): + REAL = False + + def get_value(self, dateval): + return dateval.isocalendar()[1] + + +class DayOfMonthField(BaseField): + COMPILERS = BaseField.COMPILERS + [WeekdayPositionExpression, LastDayOfMonthExpression] + + def get_max(self, dateval): + return monthrange(dateval.year, dateval.month)[1] + + +class DayOfWeekField(BaseField): + REAL = False + COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression] + + def get_value(self, dateval): + return dateval.weekday() + + +class MonthField(BaseField): + COMPILERS = BaseField.COMPILERS + [MonthRangeExpression] diff --git a/telegramer/include/apscheduler/triggers/date.py b/telegramer/include/apscheduler/triggers/date.py new file mode 100644 index 0000000..0768100 --- /dev/null +++ b/telegramer/include/apscheduler/triggers/date.py @@ -0,0 +1,51 @@ +from datetime import datetime + +from tzlocal import get_localzone + +from apscheduler.triggers.base import BaseTrigger +from apscheduler.util import convert_to_datetime, datetime_repr, astimezone + + +class DateTrigger(BaseTrigger): + """ + Triggers once on the given datetime. If ``run_date`` is left empty, current time is used. + + :param datetime|str run_date: the date/time to run the job at + :param datetime.tzinfo|str timezone: time zone for ``run_date`` if it doesn't have one already + """ + + __slots__ = 'run_date' + + def __init__(self, run_date=None, timezone=None): + timezone = astimezone(timezone) or get_localzone() + if run_date is not None: + self.run_date = convert_to_datetime(run_date, timezone, 'run_date') + else: + self.run_date = datetime.now(timezone) + + def get_next_fire_time(self, previous_fire_time, now): + return self.run_date if previous_fire_time is None else None + + def __getstate__(self): + return { + 'version': 1, + 'run_date': self.run_date + } + + def __setstate__(self, state): + # This is for compatibility with APScheduler 3.0.x + if isinstance(state, tuple): + state = state[1] + + if state.get('version', 1) > 1: + raise ValueError( + 'Got serialized data for version %s of %s, but only version 1 can be handled' % + (state['version'], self.__class__.__name__)) + + self.run_date = state['run_date'] + + def __str__(self): + return 'date[%s]' % datetime_repr(self.run_date) + + def __repr__(self): + return "<%s (run_date='%s')>" % (self.__class__.__name__, datetime_repr(self.run_date)) diff --git a/telegramer/include/apscheduler/triggers/interval.py b/telegramer/include/apscheduler/triggers/interval.py new file mode 100644 index 0000000..b0e2dbd --- /dev/null +++ b/telegramer/include/apscheduler/triggers/interval.py @@ -0,0 +1,108 @@ +from datetime import timedelta, datetime +from math import ceil + +from tzlocal import get_localzone + +from apscheduler.triggers.base import BaseTrigger +from apscheduler.util import ( + convert_to_datetime, normalize, timedelta_seconds, datetime_repr, + astimezone) + + +class IntervalTrigger(BaseTrigger): + """ + Triggers on specified intervals, starting on ``start_date`` if specified, ``datetime.now()`` + + interval otherwise. + + :param int weeks: number of weeks to wait + :param int days: number of days to wait + :param int hours: number of hours to wait + :param int minutes: number of minutes to wait + :param int seconds: number of seconds to wait + :param datetime|str start_date: starting point for the interval calculation + :param datetime|str end_date: latest possible date/time to trigger on + :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations + :param int|None jitter: delay the job execution by ``jitter`` seconds at most + """ + + __slots__ = 'timezone', 'start_date', 'end_date', 'interval', 'interval_length', 'jitter' + + def __init__(self, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None, + end_date=None, timezone=None, jitter=None): + self.interval = timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, + seconds=seconds) + self.interval_length = timedelta_seconds(self.interval) + if self.interval_length == 0: + self.interval = timedelta(seconds=1) + self.interval_length = 1 + + if timezone: + self.timezone = astimezone(timezone) + elif isinstance(start_date, datetime) and start_date.tzinfo: + self.timezone = start_date.tzinfo + elif isinstance(end_date, datetime) and end_date.tzinfo: + self.timezone = end_date.tzinfo + else: + self.timezone = get_localzone() + + start_date = start_date or (datetime.now(self.timezone) + self.interval) + self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date') + self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date') + + self.jitter = jitter + + def get_next_fire_time(self, previous_fire_time, now): + if previous_fire_time: + next_fire_time = previous_fire_time + self.interval + elif self.start_date > now: + next_fire_time = self.start_date + else: + timediff_seconds = timedelta_seconds(now - self.start_date) + next_interval_num = int(ceil(timediff_seconds / self.interval_length)) + next_fire_time = self.start_date + self.interval * next_interval_num + + if self.jitter is not None: + next_fire_time = self._apply_jitter(next_fire_time, self.jitter, now) + + if not self.end_date or next_fire_time <= self.end_date: + return normalize(next_fire_time) + + def __getstate__(self): + return { + 'version': 2, + 'timezone': self.timezone, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'interval': self.interval, + 'jitter': self.jitter, + } + + def __setstate__(self, state): + # This is for compatibility with APScheduler 3.0.x + if isinstance(state, tuple): + state = state[1] + + if state.get('version', 1) > 2: + raise ValueError( + 'Got serialized data for version %s of %s, but only versions up to 2 can be ' + 'handled' % (state['version'], self.__class__.__name__)) + + self.timezone = state['timezone'] + self.start_date = state['start_date'] + self.end_date = state['end_date'] + self.interval = state['interval'] + self.interval_length = timedelta_seconds(self.interval) + self.jitter = state.get('jitter') + + def __str__(self): + return 'interval[%s]' % str(self.interval) + + def __repr__(self): + options = ['interval=%r' % self.interval, 'start_date=%r' % datetime_repr(self.start_date)] + if self.end_date: + options.append("end_date=%r" % datetime_repr(self.end_date)) + if self.jitter: + options.append('jitter=%s' % self.jitter) + + return "<%s (%s, timezone='%s')>" % ( + self.__class__.__name__, ', '.join(options), self.timezone) diff --git a/telegramer/include/apscheduler/util.py b/telegramer/include/apscheduler/util.py new file mode 100644 index 0000000..d929a48 --- /dev/null +++ b/telegramer/include/apscheduler/util.py @@ -0,0 +1,438 @@ +"""This module contains several handy functions primarily meant for internal use.""" + +from __future__ import division + +from datetime import date, datetime, time, timedelta, tzinfo +from calendar import timegm +from functools import partial +from inspect import isclass, ismethod +import re +import sys + +from pytz import timezone, utc, FixedOffset +import six + +try: + from inspect import signature +except ImportError: # pragma: nocover + from funcsigs import signature + +try: + from threading import TIMEOUT_MAX +except ImportError: + TIMEOUT_MAX = 4294967 # Maximum value accepted by Event.wait() on Windows + +try: + from asyncio import iscoroutinefunction +except ImportError: + try: + from trollius import iscoroutinefunction + except ImportError: + def iscoroutinefunction(func): + return False + +__all__ = ('asint', 'asbool', 'astimezone', 'convert_to_datetime', 'datetime_to_utc_timestamp', + 'utc_timestamp_to_datetime', 'timedelta_seconds', 'datetime_ceil', 'get_callable_name', + 'obj_to_ref', 'ref_to_obj', 'maybe_ref', 'repr_escape', 'check_callable_args', + 'normalize', 'localize', 'TIMEOUT_MAX') + + +class _Undefined(object): + def __nonzero__(self): + return False + + def __bool__(self): + return False + + def __repr__(self): + return '' + + +undefined = _Undefined() #: a unique object that only signifies that no value is defined + + +def asint(text): + """ + Safely converts a string to an integer, returning ``None`` if the string is ``None``. + + :type text: str + :rtype: int + + """ + if text is not None: + return int(text) + + +def asbool(obj): + """ + Interprets an object as a boolean value. + + :rtype: bool + + """ + if isinstance(obj, str): + obj = obj.strip().lower() + if obj in ('true', 'yes', 'on', 'y', 't', '1'): + return True + if obj in ('false', 'no', 'off', 'n', 'f', '0'): + return False + raise ValueError('Unable to interpret value "%s" as boolean' % obj) + return bool(obj) + + +def astimezone(obj): + """ + Interprets an object as a timezone. + + :rtype: tzinfo + + """ + if isinstance(obj, six.string_types): + return timezone(obj) + if isinstance(obj, tzinfo): + if obj.tzname(None) == 'local': + raise ValueError( + 'Unable to determine the name of the local timezone -- you must explicitly ' + 'specify the name of the local timezone. Please refrain from using timezones like ' + 'EST to prevent problems with daylight saving time. Instead, use a locale based ' + 'timezone name (such as Europe/Helsinki).') + return obj + if obj is not None: + raise TypeError('Expected tzinfo, got %s instead' % obj.__class__.__name__) + + +_DATE_REGEX = re.compile( + r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' + r'(?:[ T](?P\d{1,2}):(?P\d{1,2}):(?P\d{1,2})' + r'(?:\.(?P\d{1,6}))?' + r'(?PZ|[+-]\d\d:\d\d)?)?$') + + +def convert_to_datetime(input, tz, arg_name): + """ + Converts the given object to a timezone aware datetime object. + + If a timezone aware datetime object is passed, it is returned unmodified. + If a native datetime object is passed, it is given the specified timezone. + If the input is a string, it is parsed as a datetime with the given timezone. + + Date strings are accepted in three different forms: date only (Y-m-d), date with time + (Y-m-d H:M:S) or with date+time with microseconds (Y-m-d H:M:S.micro). Additionally you can + override the time zone by giving a specific offset in the format specified by ISO 8601: + Z (UTC), +HH:MM or -HH:MM. + + :param str|datetime input: the datetime or string to convert to a timezone aware datetime + :param datetime.tzinfo tz: timezone to interpret ``input`` in + :param str arg_name: the name of the argument (used in an error message) + :rtype: datetime + + """ + if input is None: + return + elif isinstance(input, datetime): + datetime_ = input + elif isinstance(input, date): + datetime_ = datetime.combine(input, time()) + elif isinstance(input, six.string_types): + m = _DATE_REGEX.match(input) + if not m: + raise ValueError('Invalid date string') + + values = m.groupdict() + tzname = values.pop('timezone') + if tzname == 'Z': + tz = utc + elif tzname: + hours, minutes = (int(x) for x in tzname[1:].split(':')) + sign = 1 if tzname[0] == '+' else -1 + tz = FixedOffset(sign * (hours * 60 + minutes)) + + values = {k: int(v or 0) for k, v in values.items()} + datetime_ = datetime(**values) + else: + raise TypeError('Unsupported type for %s: %s' % (arg_name, input.__class__.__name__)) + + if datetime_.tzinfo is not None: + return datetime_ + if tz is None: + raise ValueError( + 'The "tz" argument must be specified if %s has no timezone information' % arg_name) + if isinstance(tz, six.string_types): + tz = timezone(tz) + + return localize(datetime_, tz) + + +def datetime_to_utc_timestamp(timeval): + """ + Converts a datetime instance to a timestamp. + + :type timeval: datetime + :rtype: float + + """ + if timeval is not None: + return timegm(timeval.utctimetuple()) + timeval.microsecond / 1000000 + + +def utc_timestamp_to_datetime(timestamp): + """ + Converts the given timestamp to a datetime instance. + + :type timestamp: float + :rtype: datetime + + """ + if timestamp is not None: + return datetime.fromtimestamp(timestamp, utc) + + +def timedelta_seconds(delta): + """ + Converts the given timedelta to seconds. + + :type delta: timedelta + :rtype: float + + """ + return delta.days * 24 * 60 * 60 + delta.seconds + \ + delta.microseconds / 1000000.0 + + +def datetime_ceil(dateval): + """ + Rounds the given datetime object upwards. + + :type dateval: datetime + + """ + if dateval.microsecond > 0: + return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond) + return dateval + + +def datetime_repr(dateval): + return dateval.strftime('%Y-%m-%d %H:%M:%S %Z') if dateval else 'None' + + +def get_callable_name(func): + """ + Returns the best available display name for the given function/callable. + + :rtype: str + + """ + # the easy case (on Python 3.3+) + if hasattr(func, '__qualname__'): + return func.__qualname__ + + # class methods, bound and unbound methods + f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None) + if f_self and hasattr(func, '__name__'): + f_class = f_self if isclass(f_self) else f_self.__class__ + else: + f_class = getattr(func, 'im_class', None) + + if f_class and hasattr(func, '__name__'): + return '%s.%s' % (f_class.__name__, func.__name__) + + # class or class instance + if hasattr(func, '__call__'): + # class + if hasattr(func, '__name__'): + return func.__name__ + + # instance of a class with a __call__ method + return func.__class__.__name__ + + raise TypeError('Unable to determine a name for %r -- maybe it is not a callable?' % func) + + +def obj_to_ref(obj): + """ + Returns the path to the given callable. + + :rtype: str + :raises TypeError: if the given object is not callable + :raises ValueError: if the given object is a :class:`~functools.partial`, lambda or a nested + function + + """ + if isinstance(obj, partial): + raise ValueError('Cannot create a reference to a partial()') + + name = get_callable_name(obj) + if '' in name: + raise ValueError('Cannot create a reference to a lambda') + if '' in name: + raise ValueError('Cannot create a reference to a nested function') + + if ismethod(obj): + if hasattr(obj, 'im_self') and obj.im_self: + # bound method + module = obj.im_self.__module__ + elif hasattr(obj, 'im_class') and obj.im_class: + # unbound method + module = obj.im_class.__module__ + else: + module = obj.__module__ + else: + module = obj.__module__ + return '%s:%s' % (module, name) + + +def ref_to_obj(ref): + """ + Returns the object pointed to by ``ref``. + + :type ref: str + + """ + if not isinstance(ref, six.string_types): + raise TypeError('References must be strings') + if ':' not in ref: + raise ValueError('Invalid reference') + + modulename, rest = ref.split(':', 1) + try: + obj = __import__(modulename, fromlist=[rest]) + except ImportError: + raise LookupError('Error resolving reference %s: could not import module' % ref) + + try: + for name in rest.split('.'): + obj = getattr(obj, name) + return obj + except Exception: + raise LookupError('Error resolving reference %s: error looking up object' % ref) + + +def maybe_ref(ref): + """ + Returns the object that the given reference points to, if it is indeed a reference. + If it is not a reference, the object is returned as-is. + + """ + if not isinstance(ref, str): + return ref + return ref_to_obj(ref) + + +if six.PY2: + def repr_escape(string): + if isinstance(string, six.text_type): + return string.encode('ascii', 'backslashreplace') + return string +else: + def repr_escape(string): + return string + + +def check_callable_args(func, args, kwargs): + """ + Ensures that the given callable can be called with the given arguments. + + :type args: tuple + :type kwargs: dict + + """ + pos_kwargs_conflicts = [] # parameters that have a match in both args and kwargs + positional_only_kwargs = [] # positional-only parameters that have a match in kwargs + unsatisfied_args = [] # parameters in signature that don't have a match in args or kwargs + unsatisfied_kwargs = [] # keyword-only arguments that don't have a match in kwargs + unmatched_args = list(args) # args that didn't match any of the parameters in the signature + # kwargs that didn't match any of the parameters in the signature + unmatched_kwargs = list(kwargs) + # indicates if the signature defines *args and **kwargs respectively + has_varargs = has_var_kwargs = False + + try: + if sys.version_info >= (3, 5): + sig = signature(func, follow_wrapped=False) + else: + sig = signature(func) + except ValueError: + # signature() doesn't work against every kind of callable + return + + for param in six.itervalues(sig.parameters): + if param.kind == param.POSITIONAL_OR_KEYWORD: + if param.name in unmatched_kwargs and unmatched_args: + pos_kwargs_conflicts.append(param.name) + elif unmatched_args: + del unmatched_args[0] + elif param.name in unmatched_kwargs: + unmatched_kwargs.remove(param.name) + elif param.default is param.empty: + unsatisfied_args.append(param.name) + elif param.kind == param.POSITIONAL_ONLY: + if unmatched_args: + del unmatched_args[0] + elif param.name in unmatched_kwargs: + unmatched_kwargs.remove(param.name) + positional_only_kwargs.append(param.name) + elif param.default is param.empty: + unsatisfied_args.append(param.name) + elif param.kind == param.KEYWORD_ONLY: + if param.name in unmatched_kwargs: + unmatched_kwargs.remove(param.name) + elif param.default is param.empty: + unsatisfied_kwargs.append(param.name) + elif param.kind == param.VAR_POSITIONAL: + has_varargs = True + elif param.kind == param.VAR_KEYWORD: + has_var_kwargs = True + + # Make sure there are no conflicts between args and kwargs + if pos_kwargs_conflicts: + raise ValueError('The following arguments are supplied in both args and kwargs: %s' % + ', '.join(pos_kwargs_conflicts)) + + # Check if keyword arguments are being fed to positional-only parameters + if positional_only_kwargs: + raise ValueError('The following arguments cannot be given as keyword arguments: %s' % + ', '.join(positional_only_kwargs)) + + # Check that the number of positional arguments minus the number of matched kwargs matches the + # argspec + if unsatisfied_args: + raise ValueError('The following arguments have not been supplied: %s' % + ', '.join(unsatisfied_args)) + + # Check that all keyword-only arguments have been supplied + if unsatisfied_kwargs: + raise ValueError( + 'The following keyword-only arguments have not been supplied in kwargs: %s' % + ', '.join(unsatisfied_kwargs)) + + # Check that the callable can accept the given number of positional arguments + if not has_varargs and unmatched_args: + raise ValueError( + 'The list of positional arguments is longer than the target callable can handle ' + '(allowed: %d, given in args: %d)' % (len(args) - len(unmatched_args), len(args))) + + # Check that the callable can accept the given keyword arguments + if not has_var_kwargs and unmatched_kwargs: + raise ValueError( + 'The target callable does not accept the following keyword arguments: %s' % + ', '.join(unmatched_kwargs)) + + +def iscoroutinefunction_partial(f): + while isinstance(f, partial): + f = f.func + + # The asyncio version of iscoroutinefunction includes testing for @coroutine + # decorations vs. the inspect version which does not. + return iscoroutinefunction(f) + + +def normalize(dt): + return datetime.fromtimestamp(dt.timestamp(), dt.tzinfo) + + +def localize(dt, tzinfo): + if hasattr(tzinfo, 'localize'): + return tzinfo.localize(dt) + + return normalize(dt.replace(tzinfo=tzinfo)) diff --git a/telegramer/include/cacert/cacert.pem b/telegramer/include/cacert/cacert.pem new file mode 100644 index 0000000..264923b --- /dev/null +++ b/telegramer/include/cacert/cacert.pem @@ -0,0 +1,3138 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Tue May 25 03:12:05 2021 GMT +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## +## Conversion done with mk-ca-bundle.pl version 1.28. +## SHA256: e292bd4e2d500c86df45b830d89417be5c42ee670408f1d2c454c63d8a782865 +## + + +GlobalSign Root CA +================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +GlobalSign Root CA - R2 +======================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 +ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp +s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN +S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL +TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C +ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i +YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN +BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp +9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu +01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 +9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +Entrust.net Premium 2048 Secure Server CA +========================================= +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ +KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy +T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT +J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e +nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +Baltimore CyberTrust Root +========================= +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE +ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li +ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC +SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs +dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME +uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB +UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C +G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 +XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr +l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI +VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB +BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh +cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 +hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa +Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H +RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +Comodo AAA Services root +======================== +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw +MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl +c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG +C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs +i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW +Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH +Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK +Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl +cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz +LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm +7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z +8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +QuoVadis Root CA +================ +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE +ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz +MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp +cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD +EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk +J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL +F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL +YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen +AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w +PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y +ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 +MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj +YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs +ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW +Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu +BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw +FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 +tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo +fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul +LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x +gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi +5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi +5nrQNiOKSnQ2+Q== +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx +ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 +XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk +lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB +lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy +lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt +66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn +wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh +D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy +BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie +J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud +DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU +a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv +Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 +UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm +VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK ++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW +IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 +WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X +f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II +4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 +VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +Security Communication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw +8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM +DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX +5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd +DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 +JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g +0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a +mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ +s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ +6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi +FL39vmwLAw== +-----END CERTIFICATE----- + +Sonera Class 2 Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG +U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw +NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh +IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 +/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT +dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG +f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P +tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH +nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT +XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt +0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI +cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph +Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx +EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH +llpwrN9M +-----END CERTIFICATE----- + +XRamp Global CA Root +==================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE +BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj +dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx +HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg +U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu +IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx +foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE +zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs +AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry +xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap +oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC +AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc +/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n +nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz +8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +Go Daddy Class 2 CA +=================== +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY +VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG +A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD +ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 +qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j +YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY +vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O +BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o +atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu +MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim +PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt +I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI +Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b +vZ8= +-----END CERTIFICATE----- + +Starfield Class 2 CA +==================== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc +U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo +MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG +A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG +SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY +bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ +JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm +epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN +F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF +MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f +hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo +bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs +afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM +PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD +KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 +QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +DST Root CA X3 +============== +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK +ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X +DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 +cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT +rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 +UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy +xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d +utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ +MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug +dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE +GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw +RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS +fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SwissSign Silver CA - G2 +======================== +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT +BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X +DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 +aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 +N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm ++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH +6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu +MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h +qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 +FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs +ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc +celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X +CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB +tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P +4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F +kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L +3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx +/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa +DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP +e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu +WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ +DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub +DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +Network Solutions Certificate Authority +======================================= +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG +EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr +IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx +MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx +jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT +aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT +crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc +/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB +AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv +bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA +A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q +4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ +GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD +ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +Cybertrust Global Root +====================== +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li +ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 +MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD +ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW +0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL +AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin +89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT +8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 +MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G +A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO +lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi +5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 +hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T +X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +======================================== +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Hongkong Post Root CA 1 +======================= +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT +DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx +NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n +IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 +ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr +auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh +qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY +V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV +HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i +h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio +l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei +IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps +T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT +c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== +-----END CERTIFICATE----- + +SecureSign RootCA11 +=================== +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi +SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS +b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw +KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 +cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL +TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO +wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq +g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP +O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA +bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX +t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh +OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r +bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ +Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 +y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 +lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH +DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA +bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx +ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx +51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk +R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP +T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f +Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl +osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR +crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR +saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD +KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi +6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +EC-ACC +====== +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE +BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w +ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD +VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE +CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT +BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 +MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt +SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl +Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh +cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK +w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT +ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 +HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a +E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw +0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD +VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 +Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l +dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ +lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa +Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe +l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 +E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D +5EI= +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2011 +======================================================= +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT +O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y +aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT +AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo +IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI +1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa +71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u +8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH +3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ +MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 +MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu +b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt +XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD +/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N +7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Trustis FPS Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290 +IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV +BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ +RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk +H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa +cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt +o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA +AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd +BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c +GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC +yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P +8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV +l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl +iB6XzCGcKQENZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +TeliaSonera Root CA v1 +====================== +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE +CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 +MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW +VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ +6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA +3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k +B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn +Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH +oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 +F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ +oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 +gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc +TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB +AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW +DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm +zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW +pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV +G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc +c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT +JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 +qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 +Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems +WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +E-Tugra Certification Authority +=============================== +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNVBAYTAlRSMQ8w +DQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamls +ZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMw +NTEyMDk0OFoXDTIzMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmEx +QDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxl +cmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQD +DB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEA4vU/kwVRHoViVF56C/UYB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vd +hQd2h8y/L5VMzH2nPbxHD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5K +CKpbknSFQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEoq1+g +ElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3Dk14opz8n8Y4e0ypQ +BaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcHfC425lAcP9tDJMW/hkd5s3kc91r0 +E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsutdEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gz +rt48Ue7LE3wBf4QOXVGUnhMMti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAq +jqFGOjGY5RH8zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUXU8u3Zg5mTPj5 +dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6Jyr+zE7S6E5UMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEG +MA0GCSqGSIb3DQEBCwUAA4ICAQAFNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAK +kEh47U6YA5n+KGCRHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jO +XKqYGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c77NCR807 +VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3+GbHeJAAFS6LrVE1Uweo +a2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WKvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCc +dlqMQ1DujjByTd//SffGqWfZbawCEeI6FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEV +KV0jq9BgoRJP3vQXzTLlyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gT +Dx4JnW2PAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpDy4Q0 +8ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8dNL/+I5c30jn6PQ0G +C7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl +OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV +MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF +JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +Staat der Nederlanden EV Root CA +================================ +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M +MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl +cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk +SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW +O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r +0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 +Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV +XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr +08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV +0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd +74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx +fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa +ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu +c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq +5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN +b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN +f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi +5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 +WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK +DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy +eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GB CA +=============================== +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG +EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw +MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds +b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX +scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP +rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk +9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o +Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg +GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD +dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 +VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui +HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +SZAFIR ROOT CA2 +=============== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV +BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ +BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD +VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q +qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK +DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE +2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ +ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi +ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC +AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 +O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 +oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul +4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 ++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +Certum Trusted Network CA 2 +=========================== +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE +BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 +bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y +ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ +TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB +IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 +7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o +CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b +Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p +uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 +GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ +9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB +Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye +hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM +BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI +hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW +Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA +L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo +clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM +pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb +w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo +J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm +ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX +is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 +zAYspsbiDrW5viSP +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2015 +======================================================= +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT +BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 +aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx +MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg +QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV +BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw +MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv +bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh +iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ +6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd +FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr +i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F +GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 +fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu +iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI +hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ +D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM +d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y +d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn +82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb +davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F +Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt +J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa +JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q +p/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions ECC RootCA 2015 +=========================================================== +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 +aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw +MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj +IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD +VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 +Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP +dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK +Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA +GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn +dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +ISRG Root X1 +============ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT +D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr +IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g +TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp +ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD +VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt +c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth +bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 +IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 +6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc +wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 +3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 +WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU +ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc +lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R +e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j +q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +GDCA TrustAUTH R5 ROOT +====================== +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw +BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD +DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow +YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs +AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p +OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr +pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ +9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ +xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM +R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ +D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 +oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx +9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 +H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 +6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd ++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ +HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD +F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ +8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv +/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT +aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +TrustCor RootCert CA-1 +====================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkx +MjMxMTcyMzE2WjCBpDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFu +YW1hIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUGA1UECwwe +VHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZUcnVzdENvciBSb290Q2Vy +dCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv463leLCJhJrMxnHQFgKq1mq +jQCj/IDHUHuO1CAmujIS2CNUSSUQIpidRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4 +pQa81QBeCQryJ3pS/C3Vseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0 +JEsq1pme9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CVEY4h +gLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorWhnAbJN7+KIor0Gqw +/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/DeOxCbeKyKsZn3MzUOcwHwYDVR0j +BBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwDQYJKoZIhvcNAQELBQADggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5 +mDo4Nvu7Zp5I/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZyonnMlo2HD6C +qFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djtsL1Ac59v2Z3kf9YKVmgenFK+P +3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdNzl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- + +TrustCor RootCert CA-2 +====================== +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNVBAYTAlBBMQ8w +DQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQwIgYDVQQKDBtUcnVzdENvciBT +eXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0 +eTEfMB0GA1UEAwwWVHJ1c3RDb3IgUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEy +MzExNzI2MzlaMIGkMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5h +bWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0 +IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnIG7CKqJiJJWQdsg4foDSq8Gb +ZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9Nk +RvRUqdw6VC0xK5mC8tkq1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1 +oYxOdqHp2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nKDOOb +XUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hapeaz6LMvYHL1cEksr1 +/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF3wP+TfSvPd9cW436cOGlfifHhi5q +jxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQP +eSghYA2FFn3XVDjxklb9tTNMg9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+Ctg +rKAmrhQhJ8Z3mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAdBgNVHQ4EFgQU +2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6UnrybPZx9mCAZ5YwwYrIwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/h +Osh80QA9z+LqBrWyOrsGS2h60COXdKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnp +kpfbsEZC89NiqpX+MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv +2wnL/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RXCI/hOWB3 +S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYaZH9bDTMJBzN7Bj8RpFxw +PIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dv +DDqPys/cA8GiCcjl/YBeyGBCARsaU1q7N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYU +RpFHmygk71dSTlxCnKr3Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANE +xdqtvArBAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp5KeX +RKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu1uwJ +-----END CERTIFICATE----- + +TrustCor ECA-1 +============== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxFzAVBgNVBAMMDlRydXN0Q29yIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3Mjgw +N1owgZwxCzAJBgNVBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5 +MSQwIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29y +IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3IgRUNBLTEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb3w9U73NjKYKtR8aja+3+XzP4Q1HpGjOR +MRegdMTUpwHmspI+ap3tDvl0mEDTPwOABoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23 +xFUfJ3zSCNV2HykVh0A53ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmc +p0yJF4OuowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/wZ0+ +fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZFZtS6mFjBAgMBAAGj +YzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAfBgNVHSMEGDAWgBREnkj1zG1I1KBL +f/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF +AAOCAQEABT41XBVwm8nHc2FvcivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u +/ukZMjgDfxT2AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50soIipX1TH0Xs +J5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BIWJZpTdwHjFGTot+fDz2LYLSC +jaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1WitJ/X5g== +-----END CERTIFICATE----- + +SSL.com Root Certification Authority RSA +======================================== +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM +BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x +MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw +MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM +LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C +Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 +P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge +oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp +k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z +fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ +gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 +UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 +1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s +bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr +dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf +ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl +u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq +erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj +MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ +vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI +Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y +wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI +WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +SSL.com Root Certification Authority ECC +======================================== +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv +BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy +MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO +BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ +8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR +hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT +jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW +e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z +5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority RSA R2 +============================================== +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w +DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u +MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD +VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh +hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w +cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO +Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ +B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh +CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim +9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto +RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm +JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 ++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp +qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 +++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx +Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G +guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz +OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 +CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq +lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR +rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 +hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX +9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority ECC +=========================================== +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy +BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw +MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM +LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy +3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O +BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe +5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ +N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm +m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +GlobalSign Root CA - R6 +======================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX +R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i +YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs +U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss +grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE +3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF +vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM +PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ +azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O +WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy +CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP +0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN +b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV +HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 +lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY +BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym +Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr +3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 +0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T +uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK +oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t +JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GC CA +=============================== +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD +SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo +MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa +Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL +ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr +VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab +NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E +AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk +AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +GTS Root R1 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx +9vaMf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7r +aKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnW +r4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqM +LnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly +4cpk9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr +06zqkUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om +3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNu +JLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEM +BQADggIBADiWCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 +d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6ZXPYfcX3v73sv +fuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZRgyFmxhE+885H7pwoHyXa/6xm +ld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9b +gsiG1eGZbYwE8na6SfZu6W0eX6DvJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq +4BjFbkerQUIpm/ZgDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWEr +tXvM+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyyF62ARPBo +pY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9SQ98POyDGCBDTtWTurQ0 +sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdwsE3PYJ/HQcu51OyLemGhmW/HGY0dVHLql +CFF1pkgl +-----END CERTIFICATE----- + +GTS Root R2 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTuk +k3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo +7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWI +m8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5Gm +dFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbu +ak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscsz +cTJGr61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73Vululycsl +aVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy +5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEM +BQADggIBALZp8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT +vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiTz9D2PGcDFWEJ ++YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiApJiS4wGWAqoC7o87xdFtCjMw +c3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvbpxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3Da +WsYDQvTtN6LwG1BUSw7YhN4ZKJmBR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5r +n/WkhLx3+WuXrD5RRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56Gtmwfu +Nmsk0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC5AwiWVIQ +7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiFizoHCBy69Y9Vmhh1fuXs +gWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLnyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ld +o/DUhgkC +-----END CERTIFICATE----- + +GTS Root R3 +=========== +-----BEGIN CERTIFICATE----- +MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUU +Rout736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24Cej +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP +0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFukfCPAlaUs3L6JbyO5o91lAFJekazInXJ0 +glMLfalAvWhgxeG4VDvBNhcl2MG9AjEAnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOa +KaqW04MjyaR7YbPMAuhd +-----END CERTIFICATE----- + +GTS Root R4 +=========== +-----BEGIN CERTIFICATE----- +MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa +6zzuhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqj +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV +2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0CMRw3J5QdCHojXohw0+WbhXRIjVhLfoI +N+4Zba3bssx9BzT1YBkstTTZbyACMANxsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11x +zPKwTdb+mciUqXWi4w== +-----END CERTIFICATE----- + +UCA Global G2 Root +================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x +NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU +cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT +oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV +8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS +h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o +LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ +R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe +KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa +4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc +OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 +8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo +5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A +Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 +yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX +c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo +jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk +bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x +ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn +RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== +-----END CERTIFICATE----- + +UCA Extended Validation Root +============================ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u +IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G +A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs +iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF +Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu +eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR +59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH +0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR +el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv +B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth +WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS +NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS +3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL +BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM +aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 +dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb ++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW +F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi +GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc +GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi +djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr +dhh2n1ax +-----END CERTIFICATE----- + +Certigna Root CA +================ +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE +BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ +MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda +MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz +MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX +stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz +KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 +JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 +XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq +4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej +wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ +lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI +jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ +/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy +dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h +LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl +cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt +OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP +TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq +7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 +4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd +8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS +6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY +tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS +aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde +E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +emSign Root CA - G1 +=================== +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx +ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk +aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN +LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 +cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW +DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ +6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH +hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 +vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q +NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q ++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih +U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +emSign ECC Root CA - G3 +======================= +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +emSign Root CA - C1 +=================== +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx +EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp +Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD +ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up +ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ +Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX +OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V +I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms +lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ +XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp +/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 +NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 +wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ +BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +emSign ECC Root CA - C3 +======================= +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG +A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF +Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD +ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd +6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 +SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA +B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA +MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU +ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +Hongkong Post Root CA 3 +======================= +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG +A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK +Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 +MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv +bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX +SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz +iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf +jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim +5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe +sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj +0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ +JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u +y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h ++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG +xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID +AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN +AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw +W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld +y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov ++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc +eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw +9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 +nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY +hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB +60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq +dBb9HxEGmpv0 +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G4 +========================================= +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu +bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 +dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT +AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D +umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV +3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds +8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ +e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 +ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X +xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV +7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW +Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n +MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q +jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht +7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK +YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt +jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ +m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW +RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA +JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G ++TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT +kcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- + +Microsoft ECC Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND +IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 +MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 +thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB +eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM ++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf +Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR +eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +Microsoft RSA Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg +UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw +NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml +7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e +S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 +1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ +dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F +yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS +MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr +lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ +0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ +ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og +6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 +dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk ++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex +/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy +AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW +ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE +7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT +c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D +5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +e-Szigno Root CA 2017 +===================== +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw +DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt +MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa +Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE +CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp +Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx +s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv +vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA +tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO +svxyqltZ+efcMQ== +-----END CERTIFICATE----- + +certSIGN Root CA G2 +=================== +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw +EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy +MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH +TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 +N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk +abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg +wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp +dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh +ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 +jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf +95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc +z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL +iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB +ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB +/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 +8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 +BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW +atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU +Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M +NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N +0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +Trustwave Global Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29 +zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf +LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq +stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o +WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+ +OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40 +Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE +uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm ++9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj +ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H +PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H +ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla +4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R +vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd +zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O +856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH +Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu +3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP +29FpHOTKyeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +Trustwave Global ECC P256 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1 +NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj +43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm +P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt +0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz +RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +Trustwave Global ECC P384 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4 +NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH +Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr +/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV +HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn +ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl +CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw== +-----END CERTIFICATE----- + +NAVER Global Root Certification Authority +========================================= +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG +A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD +DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4 +NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT +UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb +UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW ++j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7 +XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2 +aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4 +Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z +VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B +A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai +cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy +YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV +HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK +21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB +jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx +hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg +E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH +D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ +A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY +qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG +I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg +kpzNNIaRkPpkUZ3+/uul9XXeifdy +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM SERVIDORES SEGUROS +=================================== +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQswCQYDVQQGEwJF +UzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgwFgYDVQRhDA9WQVRFUy1RMjgy +NjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1SQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4 +MTIyMDA5MzczM1oXDTQzMTIyMDA5MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQt +UkNNMQ4wDAYDVQQLDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNB +QyBSQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LHsbI6GA60XYyzZl2hNPk2 +LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oKUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqG +SM49BAMDA2kAMGYCMQCuSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoD +zBOQn5ICMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJyv+c= +-----END CERTIFICATE----- + +GlobalSign Root R46 +=================== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv +b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX +BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es +CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/ +r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje +2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt +bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj +K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4 +12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on +ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls +eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9 +vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM +BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy +gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92 +CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm +OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq +JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye +qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz +nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7 +DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3 +QEUxeCp6 +-----END CERTIFICATE----- + +GlobalSign Root E46 +=================== +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYxCzAJBgNVBAYT +AkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3Qg +RTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNV +BAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkB +jtjqR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGddyXqBPCCj +QjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxCpCPtsad0kRL +gLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZk +vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+ +CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- diff --git a/telegramer/include/cachetools/__init__.py b/telegramer/include/cachetools/__init__.py new file mode 100644 index 0000000..03b6f25 --- /dev/null +++ b/telegramer/include/cachetools/__init__.py @@ -0,0 +1,718 @@ +"""Extensible memoizing collections and decorators.""" + +__all__ = ( + "Cache", + "FIFOCache", + "LFUCache", + "LRUCache", + "MRUCache", + "RRCache", + "TLRUCache", + "TTLCache", + "cached", + "cachedmethod", +) + +__version__ = "5.0.0" + +import collections +import collections.abc +import functools +import heapq +import random +import time + +from .keys import hashkey as _defaultkey + + +def _methodkey(_, *args, **kwargs): + return _defaultkey(*args, **kwargs) + + +class _DefaultSize: + + __slots__ = () + + def __getitem__(self, _): + return 1 + + def __setitem__(self, _, value): + assert value == 1 + + def pop(self, _): + return 1 + + +class Cache(collections.abc.MutableMapping): + """Mutable mapping to serve as a simple cache or cache base class.""" + + __marker = object() + + __size = _DefaultSize() + + def __init__(self, maxsize, getsizeof=None): + if getsizeof: + self.getsizeof = getsizeof + if self.getsizeof is not Cache.getsizeof: + self.__size = dict() + self.__data = dict() + self.__currsize = 0 + self.__maxsize = maxsize + + def __repr__(self): + return "%s(%s, maxsize=%r, currsize=%r)" % ( + self.__class__.__name__, + repr(self.__data), + self.__maxsize, + self.__currsize, + ) + + def __getitem__(self, key): + try: + return self.__data[key] + except KeyError: + return self.__missing__(key) + + def __setitem__(self, key, value): + maxsize = self.__maxsize + size = self.getsizeof(value) + if size > maxsize: + raise ValueError("value too large") + if key not in self.__data or self.__size[key] < size: + while self.__currsize + size > maxsize: + self.popitem() + if key in self.__data: + diffsize = size - self.__size[key] + else: + diffsize = size + self.__data[key] = value + self.__size[key] = size + self.__currsize += diffsize + + def __delitem__(self, key): + size = self.__size.pop(key) + del self.__data[key] + self.__currsize -= size + + def __contains__(self, key): + return key in self.__data + + def __missing__(self, key): + raise KeyError(key) + + def __iter__(self): + return iter(self.__data) + + def __len__(self): + return len(self.__data) + + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def pop(self, key, default=__marker): + if key in self: + value = self[key] + del self[key] + elif default is self.__marker: + raise KeyError(key) + else: + value = default + return value + + def setdefault(self, key, default=None): + if key in self: + value = self[key] + else: + self[key] = value = default + return value + + @property + def maxsize(self): + """The maximum size of the cache.""" + return self.__maxsize + + @property + def currsize(self): + """The current size of the cache.""" + return self.__currsize + + @staticmethod + def getsizeof(value): + """Return the size of a cache element's value.""" + return 1 + + +class FIFOCache(Cache): + """First In First Out (FIFO) cache implementation.""" + + def __init__(self, maxsize, getsizeof=None): + Cache.__init__(self, maxsize, getsizeof) + self.__order = collections.OrderedDict() + + def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): + cache_setitem(self, key, value) + try: + self.__order.move_to_end(key) + except KeyError: + self.__order[key] = None + + def __delitem__(self, key, cache_delitem=Cache.__delitem__): + cache_delitem(self, key) + del self.__order[key] + + def popitem(self): + """Remove and return the `(key, value)` pair first inserted.""" + try: + key = next(iter(self.__order)) + except StopIteration: + raise KeyError("%s is empty" % type(self).__name__) from None + else: + return (key, self.pop(key)) + + +class LFUCache(Cache): + """Least Frequently Used (LFU) cache implementation.""" + + def __init__(self, maxsize, getsizeof=None): + Cache.__init__(self, maxsize, getsizeof) + self.__counter = collections.Counter() + + def __getitem__(self, key, cache_getitem=Cache.__getitem__): + value = cache_getitem(self, key) + if key in self: # __missing__ may not store item + self.__counter[key] -= 1 + return value + + def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): + cache_setitem(self, key, value) + self.__counter[key] -= 1 + + def __delitem__(self, key, cache_delitem=Cache.__delitem__): + cache_delitem(self, key) + del self.__counter[key] + + def popitem(self): + """Remove and return the `(key, value)` pair least frequently used.""" + try: + ((key, _),) = self.__counter.most_common(1) + except ValueError: + raise KeyError("%s is empty" % type(self).__name__) from None + else: + return (key, self.pop(key)) + + +class LRUCache(Cache): + """Least Recently Used (LRU) cache implementation.""" + + def __init__(self, maxsize, getsizeof=None): + Cache.__init__(self, maxsize, getsizeof) + self.__order = collections.OrderedDict() + + def __getitem__(self, key, cache_getitem=Cache.__getitem__): + value = cache_getitem(self, key) + if key in self: # __missing__ may not store item + self.__update(key) + return value + + def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): + cache_setitem(self, key, value) + self.__update(key) + + def __delitem__(self, key, cache_delitem=Cache.__delitem__): + cache_delitem(self, key) + del self.__order[key] + + def popitem(self): + """Remove and return the `(key, value)` pair least recently used.""" + try: + key = next(iter(self.__order)) + except StopIteration: + raise KeyError("%s is empty" % type(self).__name__) from None + else: + return (key, self.pop(key)) + + def __update(self, key): + try: + self.__order.move_to_end(key) + except KeyError: + self.__order[key] = None + + +class MRUCache(Cache): + """Most Recently Used (MRU) cache implementation.""" + + def __init__(self, maxsize, getsizeof=None): + Cache.__init__(self, maxsize, getsizeof) + self.__order = collections.OrderedDict() + + def __getitem__(self, key, cache_getitem=Cache.__getitem__): + value = cache_getitem(self, key) + if key in self: # __missing__ may not store item + self.__update(key) + return value + + def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): + cache_setitem(self, key, value) + self.__update(key) + + def __delitem__(self, key, cache_delitem=Cache.__delitem__): + cache_delitem(self, key) + del self.__order[key] + + def popitem(self): + """Remove and return the `(key, value)` pair most recently used.""" + try: + key = next(iter(self.__order)) + except StopIteration: + raise KeyError("%s is empty" % type(self).__name__) from None + else: + return (key, self.pop(key)) + + def __update(self, key): + try: + self.__order.move_to_end(key, last=False) + except KeyError: + self.__order[key] = None + + +class RRCache(Cache): + """Random Replacement (RR) cache implementation.""" + + def __init__(self, maxsize, choice=random.choice, getsizeof=None): + Cache.__init__(self, maxsize, getsizeof) + self.__choice = choice + + @property + def choice(self): + """The `choice` function used by the cache.""" + return self.__choice + + def popitem(self): + """Remove and return a random `(key, value)` pair.""" + try: + key = self.__choice(list(self)) + except IndexError: + raise KeyError("%s is empty" % type(self).__name__) from None + else: + return (key, self.pop(key)) + + +class _TimedCache(Cache): + """Base class for time aware cache implementations.""" + + class _Timer: + def __init__(self, timer): + self.__timer = timer + self.__nesting = 0 + + def __call__(self): + if self.__nesting == 0: + return self.__timer() + else: + return self.__time + + def __enter__(self): + if self.__nesting == 0: + self.__time = time = self.__timer() + else: + time = self.__time + self.__nesting += 1 + return time + + def __exit__(self, *exc): + self.__nesting -= 1 + + def __reduce__(self): + return _TimedCache._Timer, (self.__timer,) + + def __getattr__(self, name): + return getattr(self.__timer, name) + + def __init__(self, maxsize, timer=time.monotonic, getsizeof=None): + Cache.__init__(self, maxsize, getsizeof) + self.__timer = _TimedCache._Timer(timer) + + def __repr__(self, cache_repr=Cache.__repr__): + with self.__timer as time: + self.expire(time) + return cache_repr(self) + + def __len__(self, cache_len=Cache.__len__): + with self.__timer as time: + self.expire(time) + return cache_len(self) + + @property + def currsize(self): + with self.__timer as time: + self.expire(time) + return super().currsize + + @property + def timer(self): + """The timer function used by the cache.""" + return self.__timer + + def clear(self): + with self.__timer as time: + self.expire(time) + Cache.clear(self) + + def get(self, *args, **kwargs): + with self.__timer: + return Cache.get(self, *args, **kwargs) + + def pop(self, *args, **kwargs): + with self.__timer: + return Cache.pop(self, *args, **kwargs) + + def setdefault(self, *args, **kwargs): + with self.__timer: + return Cache.setdefault(self, *args, **kwargs) + + +class TTLCache(_TimedCache): + """LRU Cache implementation with per-item time-to-live (TTL) value.""" + + class _Link: + + __slots__ = ("key", "expires", "next", "prev") + + def __init__(self, key=None, expires=None): + self.key = key + self.expires = expires + + def __reduce__(self): + return TTLCache._Link, (self.key, self.expires) + + def unlink(self): + next = self.next + prev = self.prev + prev.next = next + next.prev = prev + + def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None): + _TimedCache.__init__(self, maxsize, timer, getsizeof) + self.__root = root = TTLCache._Link() + root.prev = root.next = root + self.__links = collections.OrderedDict() + self.__ttl = ttl + + def __contains__(self, key): + try: + link = self.__links[key] # no reordering + except KeyError: + return False + else: + return self.timer() < link.expires + + def __getitem__(self, key, cache_getitem=Cache.__getitem__): + try: + link = self.__getlink(key) + except KeyError: + expired = False + else: + expired = not (self.timer() < link.expires) + if expired: + return self.__missing__(key) + else: + return cache_getitem(self, key) + + def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): + with self.timer as time: + self.expire(time) + cache_setitem(self, key, value) + try: + link = self.__getlink(key) + except KeyError: + self.__links[key] = link = TTLCache._Link(key) + else: + link.unlink() + link.expires = time + self.__ttl + link.next = root = self.__root + link.prev = prev = root.prev + prev.next = root.prev = link + + def __delitem__(self, key, cache_delitem=Cache.__delitem__): + cache_delitem(self, key) + link = self.__links.pop(key) + link.unlink() + if not (self.timer() < link.expires): + raise KeyError(key) + + def __iter__(self): + root = self.__root + curr = root.next + while curr is not root: + # "freeze" time for iterator access + with self.timer as time: + if time < curr.expires: + yield curr.key + curr = curr.next + + def __setstate__(self, state): + self.__dict__.update(state) + root = self.__root + root.prev = root.next = root + for link in sorted(self.__links.values(), key=lambda obj: obj.expires): + link.next = root + link.prev = prev = root.prev + prev.next = root.prev = link + self.expire(self.timer()) + + @property + def ttl(self): + """The time-to-live value of the cache's items.""" + return self.__ttl + + def expire(self, time=None): + """Remove expired items from the cache.""" + if time is None: + time = self.timer() + root = self.__root + curr = root.next + links = self.__links + cache_delitem = Cache.__delitem__ + while curr is not root and not (time < curr.expires): + cache_delitem(self, curr.key) + del links[curr.key] + next = curr.next + curr.unlink() + curr = next + + def popitem(self): + """Remove and return the `(key, value)` pair least recently used that + has not already expired. + + """ + with self.timer as time: + self.expire(time) + try: + key = next(iter(self.__links)) + except StopIteration: + raise KeyError("%s is empty" % type(self).__name__) from None + else: + return (key, self.pop(key)) + + def __getlink(self, key): + value = self.__links[key] + self.__links.move_to_end(key) + return value + + +class TLRUCache(_TimedCache): + """Time aware Least Recently Used (TLRU) cache implementation.""" + + @functools.total_ordering + class _Item: + + __slots__ = ("key", "expires", "removed") + + def __init__(self, key=None, expires=None): + self.key = key + self.expires = expires + self.removed = False + + def __lt__(self, other): + return self.expires < other.expires + + def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None): + _TimedCache.__init__(self, maxsize, timer, getsizeof) + self.__items = collections.OrderedDict() + self.__order = [] + self.__ttu = ttu + + def __contains__(self, key): + try: + item = self.__items[key] # no reordering + except KeyError: + return False + else: + return self.timer() < item.expires + + def __getitem__(self, key, cache_getitem=Cache.__getitem__): + try: + item = self.__getitem(key) + except KeyError: + expired = False + else: + expired = not (self.timer() < item.expires) + if expired: + return self.__missing__(key) + else: + return cache_getitem(self, key) + + def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): + with self.timer as time: + expires = self.__ttu(key, value, time) + if not (time < expires): + return # skip expired items + self.expire(time) + cache_setitem(self, key, value) + # removing an existing item would break the heap structure, so + # only mark it as removed for now + try: + self.__getitem(key).removed = True + except KeyError: + pass + self.__items[key] = item = TLRUCache._Item(key, expires) + heapq.heappush(self.__order, item) + + def __delitem__(self, key, cache_delitem=Cache.__delitem__): + with self.timer as time: + # no self.expire() for performance reasons, e.g. self.clear() [#67] + cache_delitem(self, key) + item = self.__items.pop(key) + item.removed = True + if not (time < item.expires): + raise KeyError(key) + + def __iter__(self): + for curr in self.__order: + # "freeze" time for iterator access + with self.timer as time: + if time < curr.expires and not curr.removed: + yield curr.key + + @property + def ttu(self): + """The local time-to-use function used by the cache.""" + return self.__ttu + + def expire(self, time=None): + """Remove expired items from the cache.""" + if time is None: + time = self.timer() + items = self.__items + order = self.__order + # clean up the heap if too many items are marked as removed + if len(order) > len(items) * 2: + self.__order = order = [item for item in order if not item.removed] + heapq.heapify(order) + cache_delitem = Cache.__delitem__ + while order and (order[0].removed or not (time < order[0].expires)): + item = heapq.heappop(order) + if not item.removed: + cache_delitem(self, item.key) + del items[item.key] + + def popitem(self): + """Remove and return the `(key, value)` pair least recently used that + has not already expired. + + """ + with self.timer as time: + self.expire(time) + try: + key = next(iter(self.__items)) + except StopIteration: + raise KeyError("%s is empty" % self.__class__.__name__) from None + else: + return (key, self.pop(key)) + + def __getitem(self, key): + value = self.__items[key] + self.__items.move_to_end(key) + return value + + +def cached(cache, key=_defaultkey, lock=None): + """Decorator to wrap a function with a memoizing callable that saves + results in a cache. + + """ + + def decorator(func): + if cache is None: + + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + elif lock is None: + + def wrapper(*args, **kwargs): + k = key(*args, **kwargs) + try: + return cache[k] + except KeyError: + pass # key not found + v = func(*args, **kwargs) + try: + cache[k] = v + except ValueError: + pass # value too large + return v + + else: + + def wrapper(*args, **kwargs): + k = key(*args, **kwargs) + try: + with lock: + return cache[k] + except KeyError: + pass # key not found + v = func(*args, **kwargs) + # in case of a race, prefer the item already in the cache + try: + with lock: + return cache.setdefault(k, v) + except ValueError: + return v # value too large + + return functools.update_wrapper(wrapper, func) + + return decorator + + +def cachedmethod(cache, key=_methodkey, lock=None): + """Decorator to wrap a class or instance method with a memoizing + callable that saves results in a cache. + + """ + + def decorator(method): + if lock is None: + + def wrapper(self, *args, **kwargs): + c = cache(self) + if c is None: + return method(self, *args, **kwargs) + k = key(self, *args, **kwargs) + try: + return c[k] + except KeyError: + pass # key not found + v = method(self, *args, **kwargs) + try: + c[k] = v + except ValueError: + pass # value too large + return v + + else: + + def wrapper(self, *args, **kwargs): + c = cache(self) + if c is None: + return method(self, *args, **kwargs) + k = key(self, *args, **kwargs) + try: + with lock(self): + return c[k] + except KeyError: + pass # key not found + v = method(self, *args, **kwargs) + # in case of a race, prefer the item already in the cache + try: + with lock(self): + return c.setdefault(k, v) + except ValueError: + return v # value too large + + return functools.update_wrapper(wrapper, method) + + return decorator diff --git a/telegramer/include/cachetools/func.py b/telegramer/include/cachetools/func.py new file mode 100644 index 0000000..01702c2 --- /dev/null +++ b/telegramer/include/cachetools/func.py @@ -0,0 +1,171 @@ +"""`functools.lru_cache` compatible memoizing function decorators.""" + +__all__ = ("fifo_cache", "lfu_cache", "lru_cache", "mru_cache", "rr_cache", "ttl_cache") + +import collections +import functools +import math +import random +import time + +try: + from threading import RLock +except ImportError: # pragma: no cover + from dummy_threading import RLock + +from . import FIFOCache, LFUCache, LRUCache, MRUCache, RRCache, TTLCache +from . import keys + + +_CacheInfo = collections.namedtuple( + "CacheInfo", ["hits", "misses", "maxsize", "currsize"] +) + + +class _UnboundCache(dict): + @property + def maxsize(self): + return None + + @property + def currsize(self): + return len(self) + + +class _UnboundTTLCache(TTLCache): + def __init__(self, ttl, timer): + TTLCache.__init__(self, math.inf, ttl, timer) + + @property + def maxsize(self): + return None + + +def _cache(cache, typed): + maxsize = cache.maxsize + + def decorator(func): + key = keys.typedkey if typed else keys.hashkey + lock = RLock() + stats = [0, 0] + + def wrapper(*args, **kwargs): + k = key(*args, **kwargs) + with lock: + try: + v = cache[k] + stats[0] += 1 + return v + except KeyError: + stats[1] += 1 + v = func(*args, **kwargs) + # in case of a race, prefer the item already in the cache + try: + with lock: + return cache.setdefault(k, v) + except ValueError: + return v # value too large + + def cache_info(): + with lock: + hits, misses = stats + maxsize = cache.maxsize + currsize = cache.currsize + return _CacheInfo(hits, misses, maxsize, currsize) + + def cache_clear(): + with lock: + try: + cache.clear() + finally: + stats[:] = [0, 0] + + wrapper.cache_info = cache_info + wrapper.cache_clear = cache_clear + wrapper.cache_parameters = lambda: {"maxsize": maxsize, "typed": typed} + functools.update_wrapper(wrapper, func) + return wrapper + + return decorator + + +def fifo_cache(maxsize=128, typed=False): + """Decorator to wrap a function with a memoizing callable that saves + up to `maxsize` results based on a First In First Out (FIFO) + algorithm. + + """ + if maxsize is None: + return _cache(_UnboundCache(), typed) + elif callable(maxsize): + return _cache(FIFOCache(128), typed)(maxsize) + else: + return _cache(FIFOCache(maxsize), typed) + + +def lfu_cache(maxsize=128, typed=False): + """Decorator to wrap a function with a memoizing callable that saves + up to `maxsize` results based on a Least Frequently Used (LFU) + algorithm. + + """ + if maxsize is None: + return _cache(_UnboundCache(), typed) + elif callable(maxsize): + return _cache(LFUCache(128), typed)(maxsize) + else: + return _cache(LFUCache(maxsize), typed) + + +def lru_cache(maxsize=128, typed=False): + """Decorator to wrap a function with a memoizing callable that saves + up to `maxsize` results based on a Least Recently Used (LRU) + algorithm. + + """ + if maxsize is None: + return _cache(_UnboundCache(), typed) + elif callable(maxsize): + return _cache(LRUCache(128), typed)(maxsize) + else: + return _cache(LRUCache(maxsize), typed) + + +def mru_cache(maxsize=128, typed=False): + """Decorator to wrap a function with a memoizing callable that saves + up to `maxsize` results based on a Most Recently Used (MRU) + algorithm. + """ + if maxsize is None: + return _cache(_UnboundCache(), typed) + elif callable(maxsize): + return _cache(MRUCache(128), typed)(maxsize) + else: + return _cache(MRUCache(maxsize), typed) + + +def rr_cache(maxsize=128, choice=random.choice, typed=False): + """Decorator to wrap a function with a memoizing callable that saves + up to `maxsize` results based on a Random Replacement (RR) + algorithm. + + """ + if maxsize is None: + return _cache(_UnboundCache(), typed) + elif callable(maxsize): + return _cache(RRCache(128, choice), typed)(maxsize) + else: + return _cache(RRCache(maxsize, choice), typed) + + +def ttl_cache(maxsize=128, ttl=600, timer=time.monotonic, typed=False): + """Decorator to wrap a function with a memoizing callable that saves + up to `maxsize` results based on a Least Recently Used (LRU) + algorithm with a per-item time-to-live (TTL) value. + """ + if maxsize is None: + return _cache(_UnboundTTLCache(ttl, timer), typed) + elif callable(maxsize): + return _cache(TTLCache(128, ttl, timer), typed)(maxsize) + else: + return _cache(TTLCache(maxsize, ttl, timer), typed) diff --git a/telegramer/include/cachetools/keys.py b/telegramer/include/cachetools/keys.py new file mode 100644 index 0000000..13630a4 --- /dev/null +++ b/telegramer/include/cachetools/keys.py @@ -0,0 +1,52 @@ +"""Key functions for memoizing decorators.""" + +__all__ = ("hashkey", "typedkey") + + +class _HashedTuple(tuple): + """A tuple that ensures that hash() will be called no more than once + per element, since cache decorators will hash the key multiple + times on a cache miss. See also _HashedSeq in the standard + library functools implementation. + + """ + + __hashvalue = None + + def __hash__(self, hash=tuple.__hash__): + hashvalue = self.__hashvalue + if hashvalue is None: + self.__hashvalue = hashvalue = hash(self) + return hashvalue + + def __add__(self, other, add=tuple.__add__): + return _HashedTuple(add(self, other)) + + def __radd__(self, other, add=tuple.__add__): + return _HashedTuple(add(other, self)) + + def __getstate__(self): + return {} + + +# used for separating keyword arguments; we do not use an object +# instance here so identity is preserved when pickling/unpickling +_kwmark = (_HashedTuple,) + + +def hashkey(*args, **kwargs): + """Return a cache key for the specified hashable arguments.""" + + if kwargs: + return _HashedTuple(args + sum(sorted(kwargs.items()), _kwmark)) + else: + return _HashedTuple(args) + + +def typedkey(*args, **kwargs): + """Return a typed cache key for the specified hashable arguments.""" + + key = hashkey(*args, **kwargs) + key += tuple(type(v) for v in args) + key += tuple(type(v) for _, v in sorted(kwargs.items())) + return key diff --git a/telegramer/include/pytz/__init__.py b/telegramer/include/pytz/__init__.py new file mode 100644 index 0000000..900e8ca --- /dev/null +++ b/telegramer/include/pytz/__init__.py @@ -0,0 +1,1559 @@ +''' +datetime.tzinfo timezone definitions generated from the +Olson timezone database: + + ftp://elsie.nci.nih.gov/pub/tz*.tar.gz + +See the datetime section of the Python Library Reference for information +on how to use these modules. +''' + +import sys +import datetime +import os.path + +from pytz.exceptions import AmbiguousTimeError +from pytz.exceptions import InvalidTimeError +from pytz.exceptions import NonExistentTimeError +from pytz.exceptions import UnknownTimeZoneError +from pytz.lazy import LazyDict, LazyList, LazySet # noqa +from pytz.tzinfo import unpickler, BaseTzInfo +from pytz.tzfile import build_tzinfo + + +# The IANA (nee Olson) database is updated several times a year. +OLSON_VERSION = '2022a' +VERSION = '2022.1' # pip compatible version number. +__version__ = VERSION + +OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling + +__all__ = [ + 'timezone', 'utc', 'country_timezones', 'country_names', + 'AmbiguousTimeError', 'InvalidTimeError', + 'NonExistentTimeError', 'UnknownTimeZoneError', + 'all_timezones', 'all_timezones_set', + 'common_timezones', 'common_timezones_set', + 'BaseTzInfo', 'FixedOffset', +] + + +if sys.version_info[0] > 2: # Python 3.x + + # Python 3.x doesn't have unicode(), making writing code + # for Python 2.3 and Python 3.x a pain. + unicode = str + + def ascii(s): + r""" + >>> ascii('Hello') + 'Hello' + >>> ascii('\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + UnicodeEncodeError: ... + """ + if type(s) == bytes: + s = s.decode('ASCII') + else: + s.encode('ASCII') # Raise an exception if not ASCII + return s # But the string - not a byte string. + +else: # Python 2.x + + def ascii(s): + r""" + >>> ascii('Hello') + 'Hello' + >>> ascii(u'Hello') + 'Hello' + >>> ascii(u'\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + UnicodeEncodeError: ... + """ + return s.encode('ASCII') + + +def open_resource(name): + """Open a resource from the zoneinfo subdir for reading. + + Uses the pkg_resources module if available and no standard file + found at the calculated location. + + It is possible to specify different location for zoneinfo + subdir by using the PYTZ_TZDATADIR environment variable. + """ + name_parts = name.lstrip('/').split('/') + for part in name_parts: + if part == os.path.pardir or os.path.sep in part: + raise ValueError('Bad path segment: %r' % part) + zoneinfo_dir = os.environ.get('PYTZ_TZDATADIR', None) + if zoneinfo_dir is not None: + filename = os.path.join(zoneinfo_dir, *name_parts) + else: + filename = os.path.join(os.path.dirname(__file__), + 'zoneinfo', *name_parts) + if not os.path.exists(filename): + # http://bugs.launchpad.net/bugs/383171 - we avoid using this + # unless absolutely necessary to help when a broken version of + # pkg_resources is installed. + try: + from pkg_resources import resource_stream + except ImportError: + resource_stream = None + + if resource_stream is not None: + return resource_stream(__name__, 'zoneinfo/' + name) + return open(filename, 'rb') + + +def resource_exists(name): + """Return true if the given resource exists""" + try: + if os.environ.get('PYTZ_SKIPEXISTSCHECK', ''): + # In "standard" distributions, we can assume that + # all the listed timezones are present. As an + # import-speed optimization, you can set the + # PYTZ_SKIPEXISTSCHECK flag to skip checking + # for the presence of the resource file on disk. + return True + open_resource(name).close() + return True + except IOError: + return False + + +_tzinfo_cache = {} + + +def timezone(zone): + r''' Return a datetime.tzinfo implementation for the given timezone + + >>> from datetime import datetime, timedelta + >>> utc = timezone('UTC') + >>> eastern = timezone('US/Eastern') + >>> eastern.zone + 'US/Eastern' + >>> timezone(unicode('US/Eastern')) is eastern + True + >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) + >>> loc_dt = utc_dt.astimezone(eastern) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> loc_dt.strftime(fmt) + '2002-10-27 01:00:00 EST (-0500)' + >>> (loc_dt - timedelta(minutes=10)).strftime(fmt) + '2002-10-27 00:50:00 EST (-0500)' + >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt) + '2002-10-27 01:50:00 EDT (-0400)' + >>> (loc_dt + timedelta(minutes=10)).strftime(fmt) + '2002-10-27 01:10:00 EST (-0500)' + + Raises UnknownTimeZoneError if passed an unknown zone. + + >>> try: + ... timezone('Asia/Shangri-La') + ... except UnknownTimeZoneError: + ... print('Unknown') + Unknown + + >>> try: + ... timezone(unicode('\N{TRADE MARK SIGN}')) + ... except UnknownTimeZoneError: + ... print('Unknown') + Unknown + + ''' + if zone is None: + raise UnknownTimeZoneError(None) + + if zone.upper() == 'UTC': + return utc + + try: + zone = ascii(zone) + except UnicodeEncodeError: + # All valid timezones are ASCII + raise UnknownTimeZoneError(zone) + + zone = _case_insensitive_zone_lookup(_unmunge_zone(zone)) + if zone not in _tzinfo_cache: + if zone in all_timezones_set: # noqa + fp = open_resource(zone) + try: + _tzinfo_cache[zone] = build_tzinfo(zone, fp) + finally: + fp.close() + else: + raise UnknownTimeZoneError(zone) + + return _tzinfo_cache[zone] + + +def _unmunge_zone(zone): + """Undo the time zone name munging done by older versions of pytz.""" + return zone.replace('_plus_', '+').replace('_minus_', '-') + + +_all_timezones_lower_to_standard = None + + +def _case_insensitive_zone_lookup(zone): + """case-insensitively matching timezone, else return zone unchanged""" + global _all_timezones_lower_to_standard + if _all_timezones_lower_to_standard is None: + _all_timezones_lower_to_standard = dict((tz.lower(), tz) for tz in all_timezones) # noqa + return _all_timezones_lower_to_standard.get(zone.lower()) or zone # noqa + + +ZERO = datetime.timedelta(0) +HOUR = datetime.timedelta(hours=1) + + +class UTC(BaseTzInfo): + """UTC + + Optimized UTC implementation. It unpickles using the single module global + instance defined beneath this class declaration. + """ + zone = "UTC" + + _utcoffset = ZERO + _dst = ZERO + _tzname = zone + + def fromutc(self, dt): + if dt.tzinfo is None: + return self.localize(dt) + return super(utc.__class__, self).fromutc(dt) + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + + def __reduce__(self): + return _UTC, () + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + def __repr__(self): + return "" + + def __str__(self): + return "UTC" + + +UTC = utc = UTC() # UTC is a singleton + + +def _UTC(): + """Factory function for utc unpickling. + + Makes sure that unpickling a utc instance always returns the same + module global. + + These examples belong in the UTC class above, but it is obscured; or in + the README.rst, but we are not depending on Python 2.4 so integrating + the README.rst examples with the unit tests is not trivial. + + >>> import datetime, pickle + >>> dt = datetime.datetime(2005, 3, 1, 14, 13, 21, tzinfo=utc) + >>> naive = dt.replace(tzinfo=None) + >>> p = pickle.dumps(dt, 1) + >>> naive_p = pickle.dumps(naive, 1) + >>> len(p) - len(naive_p) + 17 + >>> new = pickle.loads(p) + >>> new == dt + True + >>> new is dt + False + >>> new.tzinfo is dt.tzinfo + True + >>> utc is UTC is timezone('UTC') + True + >>> utc is timezone('GMT') + False + """ + return utc + + +_UTC.__safe_for_unpickling__ = True + + +def _p(*args): + """Factory function for unpickling pytz tzinfo instances. + + Just a wrapper around tzinfo.unpickler to save a few bytes in each pickle + by shortening the path. + """ + return unpickler(*args) + + +_p.__safe_for_unpickling__ = True + + +class _CountryTimezoneDict(LazyDict): + """Map ISO 3166 country code to a list of timezone names commonly used + in that country. + + iso3166_code is the two letter code used to identify the country. + + >>> def print_list(list_of_strings): + ... 'We use a helper so doctests work under Python 2.3 -> 3.x' + ... for s in list_of_strings: + ... print(s) + + >>> print_list(country_timezones['nz']) + Pacific/Auckland + Pacific/Chatham + >>> print_list(country_timezones['ch']) + Europe/Zurich + >>> print_list(country_timezones['CH']) + Europe/Zurich + >>> print_list(country_timezones[unicode('ch')]) + Europe/Zurich + >>> print_list(country_timezones['XXX']) + Traceback (most recent call last): + ... + KeyError: 'XXX' + + Previously, this information was exposed as a function rather than a + dictionary. This is still supported:: + + >>> print_list(country_timezones('nz')) + Pacific/Auckland + Pacific/Chatham + """ + def __call__(self, iso3166_code): + """Backwards compatibility.""" + return self[iso3166_code] + + def _fill(self): + data = {} + zone_tab = open_resource('zone.tab') + try: + for line in zone_tab: + line = line.decode('UTF-8') + if line.startswith('#'): + continue + code, coordinates, zone = line.split(None, 4)[:3] + if zone not in all_timezones_set: # noqa + continue + try: + data[code].append(zone) + except KeyError: + data[code] = [zone] + self.data = data + finally: + zone_tab.close() + + +country_timezones = _CountryTimezoneDict() + + +class _CountryNameDict(LazyDict): + '''Dictionary proving ISO3166 code -> English name. + + >>> print(country_names['au']) + Australia + ''' + def _fill(self): + data = {} + zone_tab = open_resource('iso3166.tab') + try: + for line in zone_tab.readlines(): + line = line.decode('UTF-8') + if line.startswith('#'): + continue + code, name = line.split(None, 1) + data[code] = name.strip() + self.data = data + finally: + zone_tab.close() + + +country_names = _CountryNameDict() + + +# Time-zone info based solely on fixed offsets + +class _FixedOffset(datetime.tzinfo): + + zone = None # to match the standard pytz API + + def __init__(self, minutes): + if abs(minutes) >= 1440: + raise ValueError("absolute offset is too large", minutes) + self._minutes = minutes + self._offset = datetime.timedelta(minutes=minutes) + + def utcoffset(self, dt): + return self._offset + + def __reduce__(self): + return FixedOffset, (self._minutes, ) + + def dst(self, dt): + return ZERO + + def tzname(self, dt): + return None + + def __repr__(self): + return 'pytz.FixedOffset(%d)' % self._minutes + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + +def FixedOffset(offset, _tzinfos={}): + """return a fixed-offset timezone based off a number of minutes. + + >>> one = FixedOffset(-330) + >>> one + pytz.FixedOffset(-330) + >>> str(one.utcoffset(datetime.datetime.now())) + '-1 day, 18:30:00' + >>> str(one.dst(datetime.datetime.now())) + '0:00:00' + + >>> two = FixedOffset(1380) + >>> two + pytz.FixedOffset(1380) + >>> str(two.utcoffset(datetime.datetime.now())) + '23:00:00' + >>> str(two.dst(datetime.datetime.now())) + '0:00:00' + + The datetime.timedelta must be between the range of -1 and 1 day, + non-inclusive. + + >>> FixedOffset(1440) + Traceback (most recent call last): + ... + ValueError: ('absolute offset is too large', 1440) + + >>> FixedOffset(-1440) + Traceback (most recent call last): + ... + ValueError: ('absolute offset is too large', -1440) + + An offset of 0 is special-cased to return UTC. + + >>> FixedOffset(0) is UTC + True + + There should always be only one instance of a FixedOffset per timedelta. + This should be true for multiple creation calls. + + >>> FixedOffset(-330) is one + True + >>> FixedOffset(1380) is two + True + + It should also be true for pickling. + + >>> import pickle + >>> pickle.loads(pickle.dumps(one)) is one + True + >>> pickle.loads(pickle.dumps(two)) is two + True + """ + if offset == 0: + return UTC + + info = _tzinfos.get(offset) + if info is None: + # We haven't seen this one before. we need to save it. + + # Use setdefault to avoid a race condition and make sure we have + # only one + info = _tzinfos.setdefault(offset, _FixedOffset(offset)) + + return info + + +FixedOffset.__safe_for_unpickling__ = True + + +def _test(): + import doctest + sys.path.insert(0, os.pardir) + import pytz + return doctest.testmod(pytz) + + +if __name__ == '__main__': + _test() +all_timezones = \ +['Africa/Abidjan', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Algiers', + 'Africa/Asmara', + 'Africa/Asmera', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/El_Aaiun', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Sao_Tome', + 'Africa/Timbuktu', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Anguilla', + 'America/Antigua', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/ComodRivadavia', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Aruba', + 'America/Asuncion', + 'America/Atikokan', + 'America/Atka', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Buenos_Aires', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Catamarca', + 'America/Cayenne', + 'America/Cayman', + 'America/Chicago', + 'America/Chihuahua', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Costa_Rica', + 'America/Creston', + 'America/Cuiaba', + 'America/Curacao', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Dominica', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Ensenada', + 'America/Fort_Nelson', + 'America/Fort_Wayne', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Godthab', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Indianapolis', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Jujuy', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Knox_IN', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Louisville', + 'America/Lower_Princes', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Mendoza', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/Montreal', + 'America/Montserrat', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Nuuk', + 'America/Ojinaga', + 'America/Panama', + 'America/Pangnirtung', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port-au-Prince', + 'America/Port_of_Spain', + 'America/Porto_Acre', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Rosario', + 'America/Santa_Isabel', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Shiprock', + 'America/Sitka', + 'America/St_Barthelemy', + 'America/St_Johns', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Thunder_Bay', + 'America/Tijuana', + 'America/Toronto', + 'America/Tortola', + 'America/Vancouver', + 'America/Virgin', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/McMurdo', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/South_Pole', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Ashkhabad', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Bahrain', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Brunei', + 'Asia/Calcutta', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Chongqing', + 'Asia/Chungking', + 'Asia/Colombo', + 'Asia/Dacca', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Harbin', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Istanbul', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kashgar', + 'Asia/Kathmandu', + 'Asia/Katmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Kuwait', + 'Asia/Macao', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Muscat', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qostanay', + 'Asia/Qyzylorda', + 'Asia/Rangoon', + 'Asia/Riyadh', + 'Asia/Saigon', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Tel_Aviv', + 'Asia/Thimbu', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ujung_Pandang', + 'Asia/Ulaanbaatar', + 'Asia/Ulan_Bator', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vientiane', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faeroe', + 'Atlantic/Faroe', + 'Atlantic/Jan_Mayen', + 'Atlantic/Madeira', + 'Atlantic/Reykjavik', + 'Atlantic/South_Georgia', + 'Atlantic/St_Helena', + 'Atlantic/Stanley', + 'Australia/ACT', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Canberra', + 'Australia/Currie', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/LHI', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/NSW', + 'Australia/North', + 'Australia/Perth', + 'Australia/Queensland', + 'Australia/South', + 'Australia/Sydney', + 'Australia/Tasmania', + 'Australia/Victoria', + 'Australia/West', + 'Australia/Yancowinna', + 'Brazil/Acre', + 'Brazil/DeNoronha', + 'Brazil/East', + 'Brazil/West', + 'CET', + 'CST6CDT', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Canada/Saskatchewan', + 'Canada/Yukon', + 'Chile/Continental', + 'Chile/EasterIsland', + 'Cuba', + 'EET', + 'EST', + 'EST5EDT', + 'Egypt', + 'Eire', + 'Etc/GMT', + 'Etc/GMT+0', + 'Etc/GMT+1', + 'Etc/GMT+10', + 'Etc/GMT+11', + 'Etc/GMT+12', + 'Etc/GMT+2', + 'Etc/GMT+3', + 'Etc/GMT+4', + 'Etc/GMT+5', + 'Etc/GMT+6', + 'Etc/GMT+7', + 'Etc/GMT+8', + 'Etc/GMT+9', + 'Etc/GMT-0', + 'Etc/GMT-1', + 'Etc/GMT-10', + 'Etc/GMT-11', + 'Etc/GMT-12', + 'Etc/GMT-13', + 'Etc/GMT-14', + 'Etc/GMT-2', + 'Etc/GMT-3', + 'Etc/GMT-4', + 'Etc/GMT-5', + 'Etc/GMT-6', + 'Etc/GMT-7', + 'Etc/GMT-8', + 'Etc/GMT-9', + 'Etc/GMT0', + 'Etc/Greenwich', + 'Etc/UCT', + 'Etc/UTC', + 'Etc/Universal', + 'Etc/Zulu', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belfast', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Busingen', + 'Europe/Chisinau', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Guernsey', + 'Europe/Helsinki', + 'Europe/Isle_of_Man', + 'Europe/Istanbul', + 'Europe/Jersey', + 'Europe/Kaliningrad', + 'Europe/Kiev', + 'Europe/Kirov', + 'Europe/Lisbon', + 'Europe/Ljubljana', + 'Europe/London', + 'Europe/Luxembourg', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Mariehamn', + 'Europe/Minsk', + 'Europe/Monaco', + 'Europe/Moscow', + 'Europe/Nicosia', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Skopje', + 'Europe/Sofia', + 'Europe/Stockholm', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Tiraspol', + 'Europe/Ulyanovsk', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'Europe/Zurich', + 'GB', + 'GB-Eire', + 'GMT', + 'GMT+0', + 'GMT-0', + 'GMT0', + 'Greenwich', + 'HST', + 'Hongkong', + 'Iceland', + 'Indian/Antananarivo', + 'Indian/Chagos', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Maldives', + 'Indian/Mauritius', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Iran', + 'Israel', + 'Jamaica', + 'Japan', + 'Kwajalein', + 'Libya', + 'MET', + 'MST', + 'MST7MDT', + 'Mexico/BajaNorte', + 'Mexico/BajaSur', + 'Mexico/General', + 'NZ', + 'NZ-CHAT', + 'Navajo', + 'PRC', + 'PST8PDT', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Chuuk', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Enderbury', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Funafuti', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Johnston', + 'Pacific/Kanton', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Marquesas', + 'Pacific/Midway', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Pohnpei', + 'Pacific/Ponape', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Saipan', + 'Pacific/Samoa', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + 'Pacific/Truk', + 'Pacific/Wake', + 'Pacific/Wallis', + 'Pacific/Yap', + 'Poland', + 'Portugal', + 'ROC', + 'ROK', + 'Singapore', + 'Turkey', + 'UCT', + 'US/Alaska', + 'US/Aleutian', + 'US/Arizona', + 'US/Central', + 'US/East-Indiana', + 'US/Eastern', + 'US/Hawaii', + 'US/Indiana-Starke', + 'US/Michigan', + 'US/Mountain', + 'US/Pacific', + 'US/Samoa', + 'UTC', + 'Universal', + 'W-SU', + 'WET', + 'Zulu'] +all_timezones = LazyList( + tz for tz in all_timezones if resource_exists(tz)) + +all_timezones_set = LazySet(all_timezones) +common_timezones = \ +['Africa/Abidjan', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Algiers', + 'Africa/Asmara', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/El_Aaiun', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Sao_Tome', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Anguilla', + 'America/Antigua', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Aruba', + 'America/Asuncion', + 'America/Atikokan', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Cayenne', + 'America/Cayman', + 'America/Chicago', + 'America/Chihuahua', + 'America/Costa_Rica', + 'America/Creston', + 'America/Cuiaba', + 'America/Curacao', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Dominica', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Fort_Nelson', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Lower_Princes', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/Montserrat', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Nuuk', + 'America/Ojinaga', + 'America/Panama', + 'America/Pangnirtung', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port-au-Prince', + 'America/Port_of_Spain', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Sitka', + 'America/St_Barthelemy', + 'America/St_Johns', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Thunder_Bay', + 'America/Tijuana', + 'America/Toronto', + 'America/Tortola', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/McMurdo', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Bahrain', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Brunei', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Colombo', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kathmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Kuwait', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Muscat', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qostanay', + 'Asia/Qyzylorda', + 'Asia/Riyadh', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ulaanbaatar', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vientiane', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faroe', + 'Atlantic/Madeira', + 'Atlantic/Reykjavik', + 'Atlantic/South_Georgia', + 'Atlantic/St_Helena', + 'Atlantic/Stanley', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/Perth', + 'Australia/Sydney', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Busingen', + 'Europe/Chisinau', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Guernsey', + 'Europe/Helsinki', + 'Europe/Isle_of_Man', + 'Europe/Istanbul', + 'Europe/Jersey', + 'Europe/Kaliningrad', + 'Europe/Kiev', + 'Europe/Kirov', + 'Europe/Lisbon', + 'Europe/Ljubljana', + 'Europe/London', + 'Europe/Luxembourg', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Mariehamn', + 'Europe/Minsk', + 'Europe/Monaco', + 'Europe/Moscow', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Skopje', + 'Europe/Sofia', + 'Europe/Stockholm', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Ulyanovsk', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'Europe/Zurich', + 'GMT', + 'Indian/Antananarivo', + 'Indian/Chagos', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Maldives', + 'Indian/Mauritius', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Chuuk', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Funafuti', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Kanton', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Marquesas', + 'Pacific/Midway', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Pohnpei', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Saipan', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + 'Pacific/Wake', + 'Pacific/Wallis', + 'US/Alaska', + 'US/Arizona', + 'US/Central', + 'US/Eastern', + 'US/Hawaii', + 'US/Mountain', + 'US/Pacific', + 'UTC'] +common_timezones = LazyList( + tz for tz in common_timezones if tz in all_timezones) + +common_timezones_set = LazySet(common_timezones) diff --git a/telegramer/include/pytz/exceptions.py b/telegramer/include/pytz/exceptions.py new file mode 100644 index 0000000..4b20bde --- /dev/null +++ b/telegramer/include/pytz/exceptions.py @@ -0,0 +1,59 @@ +''' +Custom exceptions raised by pytz. +''' + +__all__ = [ + 'UnknownTimeZoneError', 'InvalidTimeError', 'AmbiguousTimeError', + 'NonExistentTimeError', +] + + +class Error(Exception): + '''Base class for all exceptions raised by the pytz library''' + + +class UnknownTimeZoneError(KeyError, Error): + '''Exception raised when pytz is passed an unknown timezone. + + >>> isinstance(UnknownTimeZoneError(), LookupError) + True + + This class is actually a subclass of KeyError to provide backwards + compatibility with code relying on the undocumented behavior of earlier + pytz releases. + + >>> isinstance(UnknownTimeZoneError(), KeyError) + True + + And also a subclass of pytz.exceptions.Error, as are other pytz + exceptions. + + >>> isinstance(UnknownTimeZoneError(), Error) + True + + ''' + pass + + +class InvalidTimeError(Error): + '''Base class for invalid time exceptions.''' + + +class AmbiguousTimeError(InvalidTimeError): + '''Exception raised when attempting to create an ambiguous wallclock time. + + At the end of a DST transition period, a particular wallclock time will + occur twice (once before the clocks are set back, once after). Both + possibilities may be correct, unless further information is supplied. + + See DstTzInfo.normalize() for more info + ''' + + +class NonExistentTimeError(InvalidTimeError): + '''Exception raised when attempting to create a wallclock time that + cannot exist. + + At the start of a DST transition period, the wallclock time jumps forward. + The instants jumped over never occur. + ''' diff --git a/telegramer/include/pytz/lazy.py b/telegramer/include/pytz/lazy.py new file mode 100644 index 0000000..39344fc --- /dev/null +++ b/telegramer/include/pytz/lazy.py @@ -0,0 +1,172 @@ +from threading import RLock +try: + from collections.abc import Mapping as DictMixin +except ImportError: # Python < 3.3 + try: + from UserDict import DictMixin # Python 2 + except ImportError: # Python 3.0-3.3 + from collections import Mapping as DictMixin + + +# With lazy loading, we might end up with multiple threads triggering +# it at the same time. We need a lock. +_fill_lock = RLock() + + +class LazyDict(DictMixin): + """Dictionary populated on first use.""" + data = None + + def __getitem__(self, key): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return self.data[key.upper()] + + def __contains__(self, key): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return key in self.data + + def __iter__(self): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return iter(self.data) + + def __len__(self): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return len(self.data) + + def keys(self): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return self.data.keys() + + +class LazyList(list): + """List populated on first use.""" + + _props = [ + '__str__', '__repr__', '__unicode__', + '__hash__', '__sizeof__', '__cmp__', + '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', + 'append', 'count', 'index', 'extend', 'insert', 'pop', 'remove', + 'reverse', 'sort', '__add__', '__radd__', '__iadd__', '__mul__', + '__rmul__', '__imul__', '__contains__', '__len__', '__nonzero__', + '__getitem__', '__setitem__', '__delitem__', '__iter__', + '__reversed__', '__getslice__', '__setslice__', '__delslice__'] + + def __new__(cls, fill_iter=None): + + if fill_iter is None: + return list() + + # We need a new class as we will be dynamically messing with its + # methods. + class LazyList(list): + pass + + fill_iter = [fill_iter] + + def lazy(name): + def _lazy(self, *args, **kw): + _fill_lock.acquire() + try: + if len(fill_iter) > 0: + list.extend(self, fill_iter.pop()) + for method_name in cls._props: + delattr(LazyList, method_name) + finally: + _fill_lock.release() + return getattr(list, name)(self, *args, **kw) + return _lazy + + for name in cls._props: + setattr(LazyList, name, lazy(name)) + + new_list = LazyList() + return new_list + +# Not all versions of Python declare the same magic methods. +# Filter out properties that don't exist in this version of Python +# from the list. +LazyList._props = [prop for prop in LazyList._props if hasattr(list, prop)] + + +class LazySet(set): + """Set populated on first use.""" + + _props = ( + '__str__', '__repr__', '__unicode__', + '__hash__', '__sizeof__', '__cmp__', + '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', + '__contains__', '__len__', '__nonzero__', + '__getitem__', '__setitem__', '__delitem__', '__iter__', + '__sub__', '__and__', '__xor__', '__or__', + '__rsub__', '__rand__', '__rxor__', '__ror__', + '__isub__', '__iand__', '__ixor__', '__ior__', + 'add', 'clear', 'copy', 'difference', 'difference_update', + 'discard', 'intersection', 'intersection_update', 'isdisjoint', + 'issubset', 'issuperset', 'pop', 'remove', + 'symmetric_difference', 'symmetric_difference_update', + 'union', 'update') + + def __new__(cls, fill_iter=None): + + if fill_iter is None: + return set() + + class LazySet(set): + pass + + fill_iter = [fill_iter] + + def lazy(name): + def _lazy(self, *args, **kw): + _fill_lock.acquire() + try: + if len(fill_iter) > 0: + for i in fill_iter.pop(): + set.add(self, i) + for method_name in cls._props: + delattr(LazySet, method_name) + finally: + _fill_lock.release() + return getattr(set, name)(self, *args, **kw) + return _lazy + + for name in cls._props: + setattr(LazySet, name, lazy(name)) + + new_set = LazySet() + return new_set + +# Not all versions of Python declare the same magic methods. +# Filter out properties that don't exist in this version of Python +# from the list. +LazySet._props = [prop for prop in LazySet._props if hasattr(set, prop)] diff --git a/telegramer/include/pytz/reference.py b/telegramer/include/pytz/reference.py new file mode 100644 index 0000000..f765ca0 --- /dev/null +++ b/telegramer/include/pytz/reference.py @@ -0,0 +1,140 @@ +''' +Reference tzinfo implementations from the Python docs. +Used for testing against as they are only correct for the years +1987 to 2006. Do not use these for real code. +''' + +from datetime import tzinfo, timedelta, datetime +from pytz import HOUR, ZERO, UTC + +__all__ = [ + 'FixedOffset', + 'LocalTimezone', + 'USTimeZone', + 'Eastern', + 'Central', + 'Mountain', + 'Pacific', + 'UTC' +] + + +# A class building tzinfo objects for fixed-offset time zones. +# Note that FixedOffset(0, "UTC") is a different way to build a +# UTC tzinfo object. +class FixedOffset(tzinfo): + """Fixed offset in minutes east from UTC.""" + + def __init__(self, offset, name): + self.__offset = timedelta(minutes=offset) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return ZERO + + +import time as _time + +STDOFFSET = timedelta(seconds=-_time.timezone) +if _time.daylight: + DSTOFFSET = timedelta(seconds=-_time.altzone) +else: + DSTOFFSET = STDOFFSET + +DSTDIFF = DSTOFFSET - STDOFFSET + + +# A class capturing the platform's idea of local time. +class LocalTimezone(tzinfo): + + def utcoffset(self, dt): + if self._isdst(dt): + return DSTOFFSET + else: + return STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return _time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, -1) + stamp = _time.mktime(tt) + tt = _time.localtime(stamp) + return tt.tm_isdst > 0 + +Local = LocalTimezone() + + +def first_sunday_on_or_after(dt): + days_to_go = 6 - dt.weekday() + if days_to_go: + dt += timedelta(days_to_go) + return dt + + +# In the US, DST starts at 2am (standard time) on the first Sunday in April. +DSTSTART = datetime(1, 4, 1, 2) +# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct. +# which is the first Sunday on or after Oct 25. +DSTEND = datetime(1, 10, 25, 1) + + +# A complete implementation of current DST rules for major US time zones. +class USTimeZone(tzinfo): + + def __init__(self, hours, reprname, stdname, dstname): + self.stdoffset = timedelta(hours=hours) + self.reprname = reprname + self.stdname = stdname + self.dstname = dstname + + def __repr__(self): + return self.reprname + + def tzname(self, dt): + if self.dst(dt): + return self.dstname + else: + return self.stdname + + def utcoffset(self, dt): + return self.stdoffset + self.dst(dt) + + def dst(self, dt): + if dt is None or dt.tzinfo is None: + # An exception may be sensible here, in one or both cases. + # It depends on how you want to treat them. The default + # fromutc() implementation (called by the default astimezone() + # implementation) passes a datetime with dt.tzinfo is self. + return ZERO + assert dt.tzinfo is self + + # Find first Sunday in April & the last in October. + start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) + end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) + + # Can't compare naive to aware objects, so strip the timezone from + # dt first. + if start <= dt.replace(tzinfo=None) < end: + return HOUR + else: + return ZERO + +Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") +Central = USTimeZone(-6, "Central", "CST", "CDT") +Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") +Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") diff --git a/telegramer/include/pytz/tzfile.py b/telegramer/include/pytz/tzfile.py new file mode 100644 index 0000000..99e7448 --- /dev/null +++ b/telegramer/include/pytz/tzfile.py @@ -0,0 +1,133 @@ +''' +$Id: tzfile.py,v 1.8 2004/06/03 00:15:24 zenzen Exp $ +''' + +from datetime import datetime +from struct import unpack, calcsize + +from pytz.tzinfo import StaticTzInfo, DstTzInfo, memorized_ttinfo +from pytz.tzinfo import memorized_datetime, memorized_timedelta + + +def _byte_string(s): + """Cast a string or byte string to an ASCII byte string.""" + return s.encode('ASCII') + +_NULL = _byte_string('\0') + + +def _std_string(s): + """Cast a string or byte string to an ASCII string.""" + return str(s.decode('ASCII')) + + +def build_tzinfo(zone, fp): + head_fmt = '>4s c 15x 6l' + head_size = calcsize(head_fmt) + (magic, format, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, + typecnt, charcnt) = unpack(head_fmt, fp.read(head_size)) + + # Make sure it is a tzfile(5) file + assert magic == _byte_string('TZif'), 'Got magic %s' % repr(magic) + + # Read out the transition times, localtime indices and ttinfo structures. + data_fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict( + timecnt=timecnt, ttinfo='lBB' * typecnt, charcnt=charcnt) + data_size = calcsize(data_fmt) + data = unpack(data_fmt, fp.read(data_size)) + + # make sure we unpacked the right number of values + assert len(data) == 2 * timecnt + 3 * typecnt + 1 + transitions = [memorized_datetime(trans) + for trans in data[:timecnt]] + lindexes = list(data[timecnt:2 * timecnt]) + ttinfo_raw = data[2 * timecnt:-1] + tznames_raw = data[-1] + del data + + # Process ttinfo into separate structs + ttinfo = [] + tznames = {} + i = 0 + while i < len(ttinfo_raw): + # have we looked up this timezone name yet? + tzname_offset = ttinfo_raw[i + 2] + if tzname_offset not in tznames: + nul = tznames_raw.find(_NULL, tzname_offset) + if nul < 0: + nul = len(tznames_raw) + tznames[tzname_offset] = _std_string( + tznames_raw[tzname_offset:nul]) + ttinfo.append((ttinfo_raw[i], + bool(ttinfo_raw[i + 1]), + tznames[tzname_offset])) + i += 3 + + # Now build the timezone object + if len(ttinfo) == 1 or len(transitions) == 0: + ttinfo[0][0], ttinfo[0][2] + cls = type(zone, (StaticTzInfo,), dict( + zone=zone, + _utcoffset=memorized_timedelta(ttinfo[0][0]), + _tzname=ttinfo[0][2])) + else: + # Early dates use the first standard time ttinfo + i = 0 + while ttinfo[i][1]: + i += 1 + if ttinfo[i] == ttinfo[lindexes[0]]: + transitions[0] = datetime.min + else: + transitions.insert(0, datetime.min) + lindexes.insert(0, i) + + # calculate transition info + transition_info = [] + for i in range(len(transitions)): + inf = ttinfo[lindexes[i]] + utcoffset = inf[0] + if not inf[1]: + dst = 0 + else: + for j in range(i - 1, -1, -1): + prev_inf = ttinfo[lindexes[j]] + if not prev_inf[1]: + break + dst = inf[0] - prev_inf[0] # dst offset + + # Bad dst? Look further. DST > 24 hours happens when + # a timzone has moved across the international dateline. + if dst <= 0 or dst > 3600 * 3: + for j in range(i + 1, len(transitions)): + stdinf = ttinfo[lindexes[j]] + if not stdinf[1]: + dst = inf[0] - stdinf[0] + if dst > 0: + break # Found a useful std time. + + tzname = inf[2] + + # Round utcoffset and dst to the nearest minute or the + # datetime library will complain. Conversions to these timezones + # might be up to plus or minus 30 seconds out, but it is + # the best we can do. + utcoffset = int((utcoffset + 30) // 60) * 60 + dst = int((dst + 30) // 60) * 60 + transition_info.append(memorized_ttinfo(utcoffset, dst, tzname)) + + cls = type(zone, (DstTzInfo,), dict( + zone=zone, + _utc_transition_times=transitions, + _transition_info=transition_info)) + + return cls() + +if __name__ == '__main__': + import os.path + from pprint import pprint + base = os.path.join(os.path.dirname(__file__), 'zoneinfo') + tz = build_tzinfo('Australia/Melbourne', + open(os.path.join(base, 'Australia', 'Melbourne'), 'rb')) + tz = build_tzinfo('US/Eastern', + open(os.path.join(base, 'US', 'Eastern'), 'rb')) + pprint(tz._utc_transition_times) diff --git a/telegramer/include/pytz/tzinfo.py b/telegramer/include/pytz/tzinfo.py new file mode 100644 index 0000000..725978d --- /dev/null +++ b/telegramer/include/pytz/tzinfo.py @@ -0,0 +1,577 @@ +'''Base classes and helpers for building zone specific tzinfo classes''' + +from datetime import datetime, timedelta, tzinfo +from bisect import bisect_right +try: + set +except NameError: + from sets import Set as set + +import pytz +from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError + +__all__ = [] + +_timedelta_cache = {} + + +def memorized_timedelta(seconds): + '''Create only one instance of each distinct timedelta''' + try: + return _timedelta_cache[seconds] + except KeyError: + delta = timedelta(seconds=seconds) + _timedelta_cache[seconds] = delta + return delta + +_epoch = datetime.utcfromtimestamp(0) +_datetime_cache = {0: _epoch} + + +def memorized_datetime(seconds): + '''Create only one instance of each distinct datetime''' + try: + return _datetime_cache[seconds] + except KeyError: + # NB. We can't just do datetime.utcfromtimestamp(seconds) as this + # fails with negative values under Windows (Bug #90096) + dt = _epoch + timedelta(seconds=seconds) + _datetime_cache[seconds] = dt + return dt + +_ttinfo_cache = {} + + +def memorized_ttinfo(*args): + '''Create only one instance of each distinct tuple''' + try: + return _ttinfo_cache[args] + except KeyError: + ttinfo = ( + memorized_timedelta(args[0]), + memorized_timedelta(args[1]), + args[2] + ) + _ttinfo_cache[args] = ttinfo + return ttinfo + +_notime = memorized_timedelta(0) + + +def _to_seconds(td): + '''Convert a timedelta to seconds''' + return td.seconds + td.days * 24 * 60 * 60 + + +class BaseTzInfo(tzinfo): + # Overridden in subclass + _utcoffset = None + _tzname = None + zone = None + + def __str__(self): + return self.zone + + +class StaticTzInfo(BaseTzInfo): + '''A timezone that has a constant offset from UTC + + These timezones are rare, as most locations have changed their + offset at some point in their history + ''' + def fromutc(self, dt): + '''See datetime.tzinfo.fromutc''' + if dt.tzinfo is not None and dt.tzinfo is not self: + raise ValueError('fromutc: dt.tzinfo is not self') + return (dt + self._utcoffset).replace(tzinfo=self) + + def utcoffset(self, dt, is_dst=None): + '''See datetime.tzinfo.utcoffset + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return self._utcoffset + + def dst(self, dt, is_dst=None): + '''See datetime.tzinfo.dst + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return _notime + + def tzname(self, dt, is_dst=None): + '''See datetime.tzinfo.tzname + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return self._tzname + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime. + + This is normally a no-op, as StaticTzInfo timezones never have + ambiguous cases to correct: + + >>> from pytz import timezone + >>> gmt = timezone('GMT') + >>> isinstance(gmt, StaticTzInfo) + True + >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt) + >>> gmt.normalize(dt) is dt + True + + The supported method of converting between timezones is to use + datetime.astimezone(). Currently normalize() also works: + + >>> la = timezone('America/Los_Angeles') + >>> dt = la.localize(datetime(2011, 5, 7, 1, 2, 3)) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> gmt.normalize(dt).strftime(fmt) + '2011-05-07 08:02:03 GMT (+0000)' + ''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + def __repr__(self): + return '' % (self.zone,) + + def __reduce__(self): + # Special pickle to zone remains a singleton and to cope with + # database changes. + return pytz._p, (self.zone,) + + +class DstTzInfo(BaseTzInfo): + '''A timezone that has a variable offset from UTC + + The offset might change if daylight saving time comes into effect, + or at a point in history when the region decides to change their + timezone definition. + ''' + # Overridden in subclass + + # Sorted list of DST transition times, UTC + _utc_transition_times = None + + # [(utcoffset, dstoffset, tzname)] corresponding to + # _utc_transition_times entries + _transition_info = None + + zone = None + + # Set in __init__ + + _tzinfos = None + _dst = None # DST offset + + def __init__(self, _inf=None, _tzinfos=None): + if _inf: + self._tzinfos = _tzinfos + self._utcoffset, self._dst, self._tzname = _inf + else: + _tzinfos = {} + self._tzinfos = _tzinfos + self._utcoffset, self._dst, self._tzname = ( + self._transition_info[0]) + _tzinfos[self._transition_info[0]] = self + for inf in self._transition_info[1:]: + if inf not in _tzinfos: + _tzinfos[inf] = self.__class__(inf, _tzinfos) + + def fromutc(self, dt): + '''See datetime.tzinfo.fromutc''' + if (dt.tzinfo is not None and + getattr(dt.tzinfo, '_tzinfos', None) is not self._tzinfos): + raise ValueError('fromutc: dt.tzinfo is not self') + dt = dt.replace(tzinfo=None) + idx = max(0, bisect_right(self._utc_transition_times, dt) - 1) + inf = self._transition_info[idx] + return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf]) + + def normalize(self, dt): + '''Correct the timezone information on the given datetime + + If date arithmetic crosses DST boundaries, the tzinfo + is not magically adjusted. This method normalizes the + tzinfo to the correct one. + + To test, first we need to do some setup + + >>> from pytz import timezone + >>> utc = timezone('UTC') + >>> eastern = timezone('US/Eastern') + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + + We next create a datetime right on an end-of-DST transition point, + the instant when the wallclocks are wound back one hour. + + >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) + >>> loc_dt = utc_dt.astimezone(eastern) + >>> loc_dt.strftime(fmt) + '2002-10-27 01:00:00 EST (-0500)' + + Now, if we subtract a few minutes from it, note that the timezone + information has not changed. + + >>> before = loc_dt - timedelta(minutes=10) + >>> before.strftime(fmt) + '2002-10-27 00:50:00 EST (-0500)' + + But we can fix that by calling the normalize method + + >>> before = eastern.normalize(before) + >>> before.strftime(fmt) + '2002-10-27 01:50:00 EDT (-0400)' + + The supported method of converting between timezones is to use + datetime.astimezone(). Currently, normalize() also works: + + >>> th = timezone('Asia/Bangkok') + >>> am = timezone('Europe/Amsterdam') + >>> dt = th.localize(datetime(2011, 5, 7, 1, 2, 3)) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> am.normalize(dt).strftime(fmt) + '2011-05-06 20:02:03 CEST (+0200)' + ''' + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + + # Convert dt in localtime to UTC + offset = dt.tzinfo._utcoffset + dt = dt.replace(tzinfo=None) + dt = dt - offset + # convert it back, and return it + return self.fromutc(dt) + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time. + + This method should be used to construct localtimes, rather + than passing a tzinfo argument to a datetime constructor. + + is_dst is used to determine the correct timezone in the ambigous + period at the end of daylight saving time. + + >>> from pytz import timezone + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> amdam = timezone('Europe/Amsterdam') + >>> dt = datetime(2004, 10, 31, 2, 0, 0) + >>> loc_dt1 = amdam.localize(dt, is_dst=True) + >>> loc_dt2 = amdam.localize(dt, is_dst=False) + >>> loc_dt1.strftime(fmt) + '2004-10-31 02:00:00 CEST (+0200)' + >>> loc_dt2.strftime(fmt) + '2004-10-31 02:00:00 CET (+0100)' + >>> str(loc_dt2 - loc_dt1) + '1:00:00' + + Use is_dst=None to raise an AmbiguousTimeError for ambiguous + times at the end of daylight saving time + + >>> try: + ... loc_dt1 = amdam.localize(dt, is_dst=None) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + is_dst defaults to False + + >>> amdam.localize(dt) == amdam.localize(dt, False) + True + + is_dst is also used to determine the correct timezone in the + wallclock times jumped over at the start of daylight saving time. + + >>> pacific = timezone('US/Pacific') + >>> dt = datetime(2008, 3, 9, 2, 0, 0) + >>> ploc_dt1 = pacific.localize(dt, is_dst=True) + >>> ploc_dt2 = pacific.localize(dt, is_dst=False) + >>> ploc_dt1.strftime(fmt) + '2008-03-09 02:00:00 PDT (-0700)' + >>> ploc_dt2.strftime(fmt) + '2008-03-09 02:00:00 PST (-0800)' + >>> str(ploc_dt2 - ploc_dt1) + '1:00:00' + + Use is_dst=None to raise a NonExistentTimeError for these skipped + times. + + >>> try: + ... loc_dt1 = pacific.localize(dt, is_dst=None) + ... except NonExistentTimeError: + ... print('Non-existent') + Non-existent + ''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + + # Find the two best possibilities. + possible_loc_dt = set() + for delta in [timedelta(days=-1), timedelta(days=1)]: + loc_dt = dt + delta + idx = max(0, bisect_right( + self._utc_transition_times, loc_dt) - 1) + inf = self._transition_info[idx] + tzinfo = self._tzinfos[inf] + loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo)) + if loc_dt.replace(tzinfo=None) == dt: + possible_loc_dt.add(loc_dt) + + if len(possible_loc_dt) == 1: + return possible_loc_dt.pop() + + # If there are no possibly correct timezones, we are attempting + # to convert a time that never happened - the time period jumped + # during the start-of-DST transition period. + if len(possible_loc_dt) == 0: + # If we refuse to guess, raise an exception. + if is_dst is None: + raise NonExistentTimeError(dt) + + # If we are forcing the pre-DST side of the DST transition, we + # obtain the correct timezone by winding the clock forward a few + # hours. + elif is_dst: + return self.localize( + dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6) + + # If we are forcing the post-DST side of the DST transition, we + # obtain the correct timezone by winding the clock back. + else: + return self.localize( + dt - timedelta(hours=6), + is_dst=False) + timedelta(hours=6) + + # If we get this far, we have multiple possible timezones - this + # is an ambiguous case occuring during the end-of-DST transition. + + # If told to be strict, raise an exception since we have an + # ambiguous case + if is_dst is None: + raise AmbiguousTimeError(dt) + + # Filter out the possiblilities that don't match the requested + # is_dst + filtered_possible_loc_dt = [ + p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst + ] + + # Hopefully we only have one possibility left. Return it. + if len(filtered_possible_loc_dt) == 1: + return filtered_possible_loc_dt[0] + + if len(filtered_possible_loc_dt) == 0: + filtered_possible_loc_dt = list(possible_loc_dt) + + # If we get this far, we have in a wierd timezone transition + # where the clocks have been wound back but is_dst is the same + # in both (eg. Europe/Warsaw 1915 when they switched to CET). + # At this point, we just have to guess unless we allow more + # hints to be passed in (such as the UTC offset or abbreviation), + # but that is just getting silly. + # + # Choose the earliest (by UTC) applicable timezone if is_dst=True + # Choose the latest (by UTC) applicable timezone if is_dst=False + # i.e., behave like end-of-DST transition + dates = {} # utc -> local + for local_dt in filtered_possible_loc_dt: + utc_time = ( + local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset) + assert utc_time not in dates + dates[utc_time] = local_dt + return dates[[min, max][not is_dst](dates)] + + def utcoffset(self, dt, is_dst=None): + '''See datetime.tzinfo.utcoffset + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from pytz import timezone + >>> tz = timezone('America/St_Johns') + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> str(tz.utcoffset(ambiguous, is_dst=False)) + '-1 day, 20:30:00' + + >>> str(tz.utcoffset(ambiguous, is_dst=True)) + '-1 day, 21:30:00' + + >>> try: + ... tz.utcoffset(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + ''' + if dt is None: + return None + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._utcoffset + else: + return self._utcoffset + + def dst(self, dt, is_dst=None): + '''See datetime.tzinfo.dst + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from pytz import timezone + >>> tz = timezone('America/St_Johns') + + >>> normal = datetime(2009, 9, 1) + + >>> str(tz.dst(normal)) + '1:00:00' + >>> str(tz.dst(normal, is_dst=False)) + '1:00:00' + >>> str(tz.dst(normal, is_dst=True)) + '1:00:00' + + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> str(tz.dst(ambiguous, is_dst=False)) + '0:00:00' + >>> str(tz.dst(ambiguous, is_dst=True)) + '1:00:00' + >>> try: + ... tz.dst(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + ''' + if dt is None: + return None + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._dst + else: + return self._dst + + def tzname(self, dt, is_dst=None): + '''See datetime.tzinfo.tzname + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from pytz import timezone + >>> tz = timezone('America/St_Johns') + + >>> normal = datetime(2009, 9, 1) + + >>> tz.tzname(normal) + 'NDT' + >>> tz.tzname(normal, is_dst=False) + 'NDT' + >>> tz.tzname(normal, is_dst=True) + 'NDT' + + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> tz.tzname(ambiguous, is_dst=False) + 'NST' + >>> tz.tzname(ambiguous, is_dst=True) + 'NDT' + >>> try: + ... tz.tzname(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + ''' + if dt is None: + return self.zone + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._tzname + else: + return self._tzname + + def __repr__(self): + if self._dst: + dst = 'DST' + else: + dst = 'STD' + if self._utcoffset > _notime: + return '' % ( + self.zone, self._tzname, self._utcoffset, dst + ) + else: + return '' % ( + self.zone, self._tzname, self._utcoffset, dst + ) + + def __reduce__(self): + # Special pickle to zone remains a singleton and to cope with + # database changes. + return pytz._p, ( + self.zone, + _to_seconds(self._utcoffset), + _to_seconds(self._dst), + self._tzname + ) + + +def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None): + """Factory function for unpickling pytz tzinfo instances. + + This is shared for both StaticTzInfo and DstTzInfo instances, because + database changes could cause a zones implementation to switch between + these two base classes and we can't break pickles on a pytz version + upgrade. + """ + # Raises a KeyError if zone no longer exists, which should never happen + # and would be a bug. + tz = pytz.timezone(zone) + + # A StaticTzInfo - just return it + if utcoffset is None: + return tz + + # This pickle was created from a DstTzInfo. We need to + # determine which of the list of tzinfo instances for this zone + # to use in order to restore the state of any datetime instances using + # it correctly. + utcoffset = memorized_timedelta(utcoffset) + dstoffset = memorized_timedelta(dstoffset) + try: + return tz._tzinfos[(utcoffset, dstoffset, tzname)] + except KeyError: + # The particular state requested in this timezone no longer exists. + # This indicates a corrupt pickle, or the timezone database has been + # corrected violently enough to make this particular + # (utcoffset,dstoffset) no longer exist in the zone, or the + # abbreviation has been changed. + pass + + # See if we can find an entry differing only by tzname. Abbreviations + # get changed from the initial guess by the database maintainers to + # match reality when this information is discovered. + for localized_tz in tz._tzinfos.values(): + if (localized_tz._utcoffset == utcoffset and + localized_tz._dst == dstoffset): + return localized_tz + + # This (utcoffset, dstoffset) information has been removed from the + # zone. Add it back. This might occur when the database maintainers have + # corrected incorrect information. datetime instances using this + # incorrect information will continue to do so, exactly as they were + # before being pickled. This is purely an overly paranoid safety net - I + # doubt this will ever been needed in real life. + inf = (utcoffset, dstoffset, tzname) + tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos) + return tz._tzinfos[inf] diff --git a/telegramer/include/pytz/zoneinfo/Africa/Abidjan b/telegramer/include/pytz/zoneinfo/Africa/Abidjan new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Abidjan differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Accra b/telegramer/include/pytz/zoneinfo/Africa/Accra new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Accra differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Addis_Ababa b/telegramer/include/pytz/zoneinfo/Africa/Addis_Ababa new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Addis_Ababa differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Algiers b/telegramer/include/pytz/zoneinfo/Africa/Algiers new file mode 100644 index 0000000..6cfd8a1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Algiers differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Asmara b/telegramer/include/pytz/zoneinfo/Africa/Asmara new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Asmara differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Asmera b/telegramer/include/pytz/zoneinfo/Africa/Asmera new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Asmera differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bamako b/telegramer/include/pytz/zoneinfo/Africa/Bamako new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bamako differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bangui b/telegramer/include/pytz/zoneinfo/Africa/Bangui new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bangui differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Banjul b/telegramer/include/pytz/zoneinfo/Africa/Banjul new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Banjul differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bissau b/telegramer/include/pytz/zoneinfo/Africa/Bissau new file mode 100644 index 0000000..82ea5aa Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bissau differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Blantyre b/telegramer/include/pytz/zoneinfo/Africa/Blantyre new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Blantyre differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Brazzaville b/telegramer/include/pytz/zoneinfo/Africa/Brazzaville new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Brazzaville differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bujumbura b/telegramer/include/pytz/zoneinfo/Africa/Bujumbura new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bujumbura differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Cairo b/telegramer/include/pytz/zoneinfo/Africa/Cairo new file mode 100644 index 0000000..d3f8196 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Cairo differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Casablanca b/telegramer/include/pytz/zoneinfo/Africa/Casablanca new file mode 100644 index 0000000..17e0d1b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Casablanca differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Ceuta b/telegramer/include/pytz/zoneinfo/Africa/Ceuta new file mode 100644 index 0000000..850c8f0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Ceuta differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Conakry b/telegramer/include/pytz/zoneinfo/Africa/Conakry new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Conakry differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Dakar b/telegramer/include/pytz/zoneinfo/Africa/Dakar new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Dakar differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Dar_es_Salaam b/telegramer/include/pytz/zoneinfo/Africa/Dar_es_Salaam new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Dar_es_Salaam differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Djibouti b/telegramer/include/pytz/zoneinfo/Africa/Djibouti new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Djibouti differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Douala b/telegramer/include/pytz/zoneinfo/Africa/Douala new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Douala differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/El_Aaiun b/telegramer/include/pytz/zoneinfo/Africa/El_Aaiun new file mode 100644 index 0000000..64f1b76 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/El_Aaiun differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Freetown b/telegramer/include/pytz/zoneinfo/Africa/Freetown new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Freetown differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Gaborone b/telegramer/include/pytz/zoneinfo/Africa/Gaborone new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Gaborone differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Harare b/telegramer/include/pytz/zoneinfo/Africa/Harare new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Harare differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Johannesburg b/telegramer/include/pytz/zoneinfo/Africa/Johannesburg new file mode 100644 index 0000000..b1c425d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Johannesburg differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Juba b/telegramer/include/pytz/zoneinfo/Africa/Juba new file mode 100644 index 0000000..0648294 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Juba differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Kampala b/telegramer/include/pytz/zoneinfo/Africa/Kampala new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Kampala differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Khartoum b/telegramer/include/pytz/zoneinfo/Africa/Khartoum new file mode 100644 index 0000000..8ee8cb9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Khartoum differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Kigali b/telegramer/include/pytz/zoneinfo/Africa/Kigali new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Kigali differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Kinshasa b/telegramer/include/pytz/zoneinfo/Africa/Kinshasa new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Kinshasa differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lagos b/telegramer/include/pytz/zoneinfo/Africa/Lagos new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lagos differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Libreville b/telegramer/include/pytz/zoneinfo/Africa/Libreville new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Libreville differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lome b/telegramer/include/pytz/zoneinfo/Africa/Lome new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lome differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Luanda b/telegramer/include/pytz/zoneinfo/Africa/Luanda new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Luanda differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lubumbashi b/telegramer/include/pytz/zoneinfo/Africa/Lubumbashi new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lubumbashi differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lusaka b/telegramer/include/pytz/zoneinfo/Africa/Lusaka new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lusaka differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Malabo b/telegramer/include/pytz/zoneinfo/Africa/Malabo new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Malabo differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Maputo b/telegramer/include/pytz/zoneinfo/Africa/Maputo new file mode 100644 index 0000000..52753c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Maputo differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Maseru b/telegramer/include/pytz/zoneinfo/Africa/Maseru new file mode 100644 index 0000000..b1c425d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Maseru differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Mbabane b/telegramer/include/pytz/zoneinfo/Africa/Mbabane new file mode 100644 index 0000000..b1c425d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Mbabane differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Mogadishu b/telegramer/include/pytz/zoneinfo/Africa/Mogadishu new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Mogadishu differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Monrovia b/telegramer/include/pytz/zoneinfo/Africa/Monrovia new file mode 100644 index 0000000..6d68850 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Monrovia differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Nairobi b/telegramer/include/pytz/zoneinfo/Africa/Nairobi new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Nairobi differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Ndjamena b/telegramer/include/pytz/zoneinfo/Africa/Ndjamena new file mode 100644 index 0000000..a968845 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Ndjamena differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Niamey b/telegramer/include/pytz/zoneinfo/Africa/Niamey new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Niamey differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Nouakchott b/telegramer/include/pytz/zoneinfo/Africa/Nouakchott new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Nouakchott differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Ouagadougou b/telegramer/include/pytz/zoneinfo/Africa/Ouagadougou new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Ouagadougou differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Porto-Novo b/telegramer/include/pytz/zoneinfo/Africa/Porto-Novo new file mode 100644 index 0000000..afb6a4a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Porto-Novo differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Sao_Tome b/telegramer/include/pytz/zoneinfo/Africa/Sao_Tome new file mode 100644 index 0000000..59f3759 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Sao_Tome differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Timbuktu b/telegramer/include/pytz/zoneinfo/Africa/Timbuktu new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Timbuktu differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Tripoli b/telegramer/include/pytz/zoneinfo/Africa/Tripoli new file mode 100644 index 0000000..07b393b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Tripoli differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Tunis b/telegramer/include/pytz/zoneinfo/Africa/Tunis new file mode 100644 index 0000000..427fa56 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Tunis differ diff --git a/telegramer/include/pytz/zoneinfo/Africa/Windhoek b/telegramer/include/pytz/zoneinfo/Africa/Windhoek new file mode 100644 index 0000000..abecd13 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Windhoek differ diff --git a/telegramer/include/pytz/zoneinfo/America/Adak b/telegramer/include/pytz/zoneinfo/America/Adak new file mode 100644 index 0000000..4323649 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Adak differ diff --git a/telegramer/include/pytz/zoneinfo/America/Anchorage b/telegramer/include/pytz/zoneinfo/America/Anchorage new file mode 100644 index 0000000..9bbb2fd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Anchorage differ diff --git a/telegramer/include/pytz/zoneinfo/America/Anguilla b/telegramer/include/pytz/zoneinfo/America/Anguilla new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Anguilla differ diff --git a/telegramer/include/pytz/zoneinfo/America/Antigua b/telegramer/include/pytz/zoneinfo/America/Antigua new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Antigua differ diff --git a/telegramer/include/pytz/zoneinfo/America/Araguaina b/telegramer/include/pytz/zoneinfo/America/Araguaina new file mode 100644 index 0000000..49381b4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Araguaina differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Buenos_Aires b/telegramer/include/pytz/zoneinfo/America/Argentina/Buenos_Aires new file mode 100644 index 0000000..260f86a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Buenos_Aires differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Catamarca b/telegramer/include/pytz/zoneinfo/America/Argentina/Catamarca new file mode 100644 index 0000000..0ae222a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Catamarca differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/ComodRivadavia b/telegramer/include/pytz/zoneinfo/America/Argentina/ComodRivadavia new file mode 100644 index 0000000..0ae222a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/ComodRivadavia differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Cordoba b/telegramer/include/pytz/zoneinfo/America/Argentina/Cordoba new file mode 100644 index 0000000..da4c23a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Cordoba differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Jujuy b/telegramer/include/pytz/zoneinfo/America/Argentina/Jujuy new file mode 100644 index 0000000..604b856 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Jujuy differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/La_Rioja b/telegramer/include/pytz/zoneinfo/America/Argentina/La_Rioja new file mode 100644 index 0000000..2218e36 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/La_Rioja differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Mendoza b/telegramer/include/pytz/zoneinfo/America/Argentina/Mendoza new file mode 100644 index 0000000..f9e677f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Mendoza differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Rio_Gallegos b/telegramer/include/pytz/zoneinfo/America/Argentina/Rio_Gallegos new file mode 100644 index 0000000..c36587e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Rio_Gallegos differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Salta b/telegramer/include/pytz/zoneinfo/America/Argentina/Salta new file mode 100644 index 0000000..0e797f2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Salta differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/San_Juan b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Juan new file mode 100644 index 0000000..2698495 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Juan differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/San_Luis b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Luis new file mode 100644 index 0000000..fe50f62 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Luis differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Tucuman b/telegramer/include/pytz/zoneinfo/America/Argentina/Tucuman new file mode 100644 index 0000000..c954000 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Tucuman differ diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Ushuaia b/telegramer/include/pytz/zoneinfo/America/Argentina/Ushuaia new file mode 100644 index 0000000..3643628 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Ushuaia differ diff --git a/telegramer/include/pytz/zoneinfo/America/Aruba b/telegramer/include/pytz/zoneinfo/America/Aruba new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Aruba differ diff --git a/telegramer/include/pytz/zoneinfo/America/Asuncion b/telegramer/include/pytz/zoneinfo/America/Asuncion new file mode 100644 index 0000000..2f3bbda Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Asuncion differ diff --git a/telegramer/include/pytz/zoneinfo/America/Atikokan b/telegramer/include/pytz/zoneinfo/America/Atikokan new file mode 100644 index 0000000..9964b9a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Atikokan differ diff --git a/telegramer/include/pytz/zoneinfo/America/Atka b/telegramer/include/pytz/zoneinfo/America/Atka new file mode 100644 index 0000000..4323649 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Atka differ diff --git a/telegramer/include/pytz/zoneinfo/America/Bahia b/telegramer/include/pytz/zoneinfo/America/Bahia new file mode 100644 index 0000000..15808d3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Bahia differ diff --git a/telegramer/include/pytz/zoneinfo/America/Bahia_Banderas b/telegramer/include/pytz/zoneinfo/America/Bahia_Banderas new file mode 100644 index 0000000..896af3f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Bahia_Banderas differ diff --git a/telegramer/include/pytz/zoneinfo/America/Barbados b/telegramer/include/pytz/zoneinfo/America/Barbados new file mode 100644 index 0000000..00cd045 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Barbados differ diff --git a/telegramer/include/pytz/zoneinfo/America/Belem b/telegramer/include/pytz/zoneinfo/America/Belem new file mode 100644 index 0000000..60b5924 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Belem differ diff --git a/telegramer/include/pytz/zoneinfo/America/Belize b/telegramer/include/pytz/zoneinfo/America/Belize new file mode 100644 index 0000000..e6f5dfa Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Belize differ diff --git a/telegramer/include/pytz/zoneinfo/America/Blanc-Sablon b/telegramer/include/pytz/zoneinfo/America/Blanc-Sablon new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Blanc-Sablon differ diff --git a/telegramer/include/pytz/zoneinfo/America/Boa_Vista b/telegramer/include/pytz/zoneinfo/America/Boa_Vista new file mode 100644 index 0000000..978c331 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Boa_Vista differ diff --git a/telegramer/include/pytz/zoneinfo/America/Bogota b/telegramer/include/pytz/zoneinfo/America/Bogota new file mode 100644 index 0000000..b2647d7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Bogota differ diff --git a/telegramer/include/pytz/zoneinfo/America/Boise b/telegramer/include/pytz/zoneinfo/America/Boise new file mode 100644 index 0000000..f8d54e2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Boise differ diff --git a/telegramer/include/pytz/zoneinfo/America/Buenos_Aires b/telegramer/include/pytz/zoneinfo/America/Buenos_Aires new file mode 100644 index 0000000..260f86a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Buenos_Aires differ diff --git a/telegramer/include/pytz/zoneinfo/America/Cambridge_Bay b/telegramer/include/pytz/zoneinfo/America/Cambridge_Bay new file mode 100644 index 0000000..f8db4b6 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cambridge_Bay differ diff --git a/telegramer/include/pytz/zoneinfo/America/Campo_Grande b/telegramer/include/pytz/zoneinfo/America/Campo_Grande new file mode 100644 index 0000000..8120624 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Campo_Grande differ diff --git a/telegramer/include/pytz/zoneinfo/America/Cancun b/telegramer/include/pytz/zoneinfo/America/Cancun new file mode 100644 index 0000000..f907f0a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cancun differ diff --git a/telegramer/include/pytz/zoneinfo/America/Caracas b/telegramer/include/pytz/zoneinfo/America/Caracas new file mode 100644 index 0000000..eedf725 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Caracas differ diff --git a/telegramer/include/pytz/zoneinfo/America/Catamarca b/telegramer/include/pytz/zoneinfo/America/Catamarca new file mode 100644 index 0000000..0ae222a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Catamarca differ diff --git a/telegramer/include/pytz/zoneinfo/America/Cayenne b/telegramer/include/pytz/zoneinfo/America/Cayenne new file mode 100644 index 0000000..e5bc06f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cayenne differ diff --git a/telegramer/include/pytz/zoneinfo/America/Cayman b/telegramer/include/pytz/zoneinfo/America/Cayman new file mode 100644 index 0000000..9964b9a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cayman differ diff --git a/telegramer/include/pytz/zoneinfo/America/Chicago b/telegramer/include/pytz/zoneinfo/America/Chicago new file mode 100644 index 0000000..a5b1617 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Chicago differ diff --git a/telegramer/include/pytz/zoneinfo/America/Chihuahua b/telegramer/include/pytz/zoneinfo/America/Chihuahua new file mode 100644 index 0000000..8ed5f93 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Chihuahua differ diff --git a/telegramer/include/pytz/zoneinfo/America/Coral_Harbour b/telegramer/include/pytz/zoneinfo/America/Coral_Harbour new file mode 100644 index 0000000..9964b9a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Coral_Harbour differ diff --git a/telegramer/include/pytz/zoneinfo/America/Cordoba b/telegramer/include/pytz/zoneinfo/America/Cordoba new file mode 100644 index 0000000..da4c23a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cordoba differ diff --git a/telegramer/include/pytz/zoneinfo/America/Costa_Rica b/telegramer/include/pytz/zoneinfo/America/Costa_Rica new file mode 100644 index 0000000..37cb85e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Costa_Rica differ diff --git a/telegramer/include/pytz/zoneinfo/America/Creston b/telegramer/include/pytz/zoneinfo/America/Creston new file mode 100644 index 0000000..ac6bb0c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Creston differ diff --git a/telegramer/include/pytz/zoneinfo/America/Cuiaba b/telegramer/include/pytz/zoneinfo/America/Cuiaba new file mode 100644 index 0000000..9bea3d4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cuiaba differ diff --git a/telegramer/include/pytz/zoneinfo/America/Curacao b/telegramer/include/pytz/zoneinfo/America/Curacao new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Curacao differ diff --git a/telegramer/include/pytz/zoneinfo/America/Danmarkshavn b/telegramer/include/pytz/zoneinfo/America/Danmarkshavn new file mode 100644 index 0000000..9549adc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Danmarkshavn differ diff --git a/telegramer/include/pytz/zoneinfo/America/Dawson b/telegramer/include/pytz/zoneinfo/America/Dawson new file mode 100644 index 0000000..343b632 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Dawson differ diff --git a/telegramer/include/pytz/zoneinfo/America/Dawson_Creek b/telegramer/include/pytz/zoneinfo/America/Dawson_Creek new file mode 100644 index 0000000..db9e339 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Dawson_Creek differ diff --git a/telegramer/include/pytz/zoneinfo/America/Denver b/telegramer/include/pytz/zoneinfo/America/Denver new file mode 100644 index 0000000..5fbe26b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Denver differ diff --git a/telegramer/include/pytz/zoneinfo/America/Detroit b/telegramer/include/pytz/zoneinfo/America/Detroit new file mode 100644 index 0000000..e104faa Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Detroit differ diff --git a/telegramer/include/pytz/zoneinfo/America/Dominica b/telegramer/include/pytz/zoneinfo/America/Dominica new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Dominica differ diff --git a/telegramer/include/pytz/zoneinfo/America/Edmonton b/telegramer/include/pytz/zoneinfo/America/Edmonton new file mode 100644 index 0000000..cd78a6f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Edmonton differ diff --git a/telegramer/include/pytz/zoneinfo/America/Eirunepe b/telegramer/include/pytz/zoneinfo/America/Eirunepe new file mode 100644 index 0000000..39d6dae Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Eirunepe differ diff --git a/telegramer/include/pytz/zoneinfo/America/El_Salvador b/telegramer/include/pytz/zoneinfo/America/El_Salvador new file mode 100644 index 0000000..e2f2230 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/El_Salvador differ diff --git a/telegramer/include/pytz/zoneinfo/America/Ensenada b/telegramer/include/pytz/zoneinfo/America/Ensenada new file mode 100644 index 0000000..ada6bf7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Ensenada differ diff --git a/telegramer/include/pytz/zoneinfo/America/Fort_Nelson b/telegramer/include/pytz/zoneinfo/America/Fort_Nelson new file mode 100644 index 0000000..5a0b7f1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Fort_Nelson differ diff --git a/telegramer/include/pytz/zoneinfo/America/Fort_Wayne b/telegramer/include/pytz/zoneinfo/America/Fort_Wayne new file mode 100644 index 0000000..09511cc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Fort_Wayne differ diff --git a/telegramer/include/pytz/zoneinfo/America/Fortaleza b/telegramer/include/pytz/zoneinfo/America/Fortaleza new file mode 100644 index 0000000..be57dc2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Fortaleza differ diff --git a/telegramer/include/pytz/zoneinfo/America/Glace_Bay b/telegramer/include/pytz/zoneinfo/America/Glace_Bay new file mode 100644 index 0000000..48412a4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Glace_Bay differ diff --git a/telegramer/include/pytz/zoneinfo/America/Godthab b/telegramer/include/pytz/zoneinfo/America/Godthab new file mode 100644 index 0000000..0160308 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Godthab differ diff --git a/telegramer/include/pytz/zoneinfo/America/Goose_Bay b/telegramer/include/pytz/zoneinfo/America/Goose_Bay new file mode 100644 index 0000000..a3f2990 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Goose_Bay differ diff --git a/telegramer/include/pytz/zoneinfo/America/Grand_Turk b/telegramer/include/pytz/zoneinfo/America/Grand_Turk new file mode 100644 index 0000000..06da1a6 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Grand_Turk differ diff --git a/telegramer/include/pytz/zoneinfo/America/Grenada b/telegramer/include/pytz/zoneinfo/America/Grenada new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Grenada differ diff --git a/telegramer/include/pytz/zoneinfo/America/Guadeloupe b/telegramer/include/pytz/zoneinfo/America/Guadeloupe new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guadeloupe differ diff --git a/telegramer/include/pytz/zoneinfo/America/Guatemala b/telegramer/include/pytz/zoneinfo/America/Guatemala new file mode 100644 index 0000000..407138c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guatemala differ diff --git a/telegramer/include/pytz/zoneinfo/America/Guayaquil b/telegramer/include/pytz/zoneinfo/America/Guayaquil new file mode 100644 index 0000000..0559a7a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guayaquil differ diff --git a/telegramer/include/pytz/zoneinfo/America/Guyana b/telegramer/include/pytz/zoneinfo/America/Guyana new file mode 100644 index 0000000..7af58e5 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guyana differ diff --git a/telegramer/include/pytz/zoneinfo/America/Halifax b/telegramer/include/pytz/zoneinfo/America/Halifax new file mode 100644 index 0000000..756099a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Halifax differ diff --git a/telegramer/include/pytz/zoneinfo/America/Havana b/telegramer/include/pytz/zoneinfo/America/Havana new file mode 100644 index 0000000..b69ac45 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Havana differ diff --git a/telegramer/include/pytz/zoneinfo/America/Hermosillo b/telegramer/include/pytz/zoneinfo/America/Hermosillo new file mode 100644 index 0000000..791a9fa Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Hermosillo differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Indianapolis b/telegramer/include/pytz/zoneinfo/America/Indiana/Indianapolis new file mode 100644 index 0000000..09511cc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Indianapolis differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Knox b/telegramer/include/pytz/zoneinfo/America/Indiana/Knox new file mode 100644 index 0000000..fcd408d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Knox differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Marengo b/telegramer/include/pytz/zoneinfo/America/Indiana/Marengo new file mode 100644 index 0000000..1abf75e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Marengo differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Petersburg b/telegramer/include/pytz/zoneinfo/America/Indiana/Petersburg new file mode 100644 index 0000000..0133548 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Petersburg differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Tell_City b/telegramer/include/pytz/zoneinfo/America/Indiana/Tell_City new file mode 100644 index 0000000..7bbb653 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Tell_City differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Vevay b/telegramer/include/pytz/zoneinfo/America/Indiana/Vevay new file mode 100644 index 0000000..d236b7c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Vevay differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Vincennes b/telegramer/include/pytz/zoneinfo/America/Indiana/Vincennes new file mode 100644 index 0000000..c818929 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Vincennes differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Winamac b/telegramer/include/pytz/zoneinfo/America/Indiana/Winamac new file mode 100644 index 0000000..630935c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Winamac differ diff --git a/telegramer/include/pytz/zoneinfo/America/Indianapolis b/telegramer/include/pytz/zoneinfo/America/Indianapolis new file mode 100644 index 0000000..09511cc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indianapolis differ diff --git a/telegramer/include/pytz/zoneinfo/America/Inuvik b/telegramer/include/pytz/zoneinfo/America/Inuvik new file mode 100644 index 0000000..87bb355 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Inuvik differ diff --git a/telegramer/include/pytz/zoneinfo/America/Iqaluit b/telegramer/include/pytz/zoneinfo/America/Iqaluit new file mode 100644 index 0000000..c8138bd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Iqaluit differ diff --git a/telegramer/include/pytz/zoneinfo/America/Jamaica b/telegramer/include/pytz/zoneinfo/America/Jamaica new file mode 100644 index 0000000..2a9b7fd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Jamaica differ diff --git a/telegramer/include/pytz/zoneinfo/America/Jujuy b/telegramer/include/pytz/zoneinfo/America/Jujuy new file mode 100644 index 0000000..604b856 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Jujuy differ diff --git a/telegramer/include/pytz/zoneinfo/America/Juneau b/telegramer/include/pytz/zoneinfo/America/Juneau new file mode 100644 index 0000000..451f349 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Juneau differ diff --git a/telegramer/include/pytz/zoneinfo/America/Kentucky/Louisville b/telegramer/include/pytz/zoneinfo/America/Kentucky/Louisville new file mode 100644 index 0000000..177836e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Kentucky/Louisville differ diff --git a/telegramer/include/pytz/zoneinfo/America/Kentucky/Monticello b/telegramer/include/pytz/zoneinfo/America/Kentucky/Monticello new file mode 100644 index 0000000..438e3ea Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Kentucky/Monticello differ diff --git a/telegramer/include/pytz/zoneinfo/America/Knox_IN b/telegramer/include/pytz/zoneinfo/America/Knox_IN new file mode 100644 index 0000000..fcd408d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Knox_IN differ diff --git a/telegramer/include/pytz/zoneinfo/America/Kralendijk b/telegramer/include/pytz/zoneinfo/America/Kralendijk new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Kralendijk differ diff --git a/telegramer/include/pytz/zoneinfo/America/La_Paz b/telegramer/include/pytz/zoneinfo/America/La_Paz new file mode 100644 index 0000000..a101372 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/La_Paz differ diff --git a/telegramer/include/pytz/zoneinfo/America/Lima b/telegramer/include/pytz/zoneinfo/America/Lima new file mode 100644 index 0000000..3c6529b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Lima differ diff --git a/telegramer/include/pytz/zoneinfo/America/Los_Angeles b/telegramer/include/pytz/zoneinfo/America/Los_Angeles new file mode 100644 index 0000000..9dad4f4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Los_Angeles differ diff --git a/telegramer/include/pytz/zoneinfo/America/Louisville b/telegramer/include/pytz/zoneinfo/America/Louisville new file mode 100644 index 0000000..177836e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Louisville differ diff --git a/telegramer/include/pytz/zoneinfo/America/Lower_Princes b/telegramer/include/pytz/zoneinfo/America/Lower_Princes new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Lower_Princes differ diff --git a/telegramer/include/pytz/zoneinfo/America/Maceio b/telegramer/include/pytz/zoneinfo/America/Maceio new file mode 100644 index 0000000..bc8b951 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Maceio differ diff --git a/telegramer/include/pytz/zoneinfo/America/Managua b/telegramer/include/pytz/zoneinfo/America/Managua new file mode 100644 index 0000000..e0242bf Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Managua differ diff --git a/telegramer/include/pytz/zoneinfo/America/Manaus b/telegramer/include/pytz/zoneinfo/America/Manaus new file mode 100644 index 0000000..63d58f8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Manaus differ diff --git a/telegramer/include/pytz/zoneinfo/America/Marigot b/telegramer/include/pytz/zoneinfo/America/Marigot new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Marigot differ diff --git a/telegramer/include/pytz/zoneinfo/America/Martinique b/telegramer/include/pytz/zoneinfo/America/Martinique new file mode 100644 index 0000000..8df43dc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Martinique differ diff --git a/telegramer/include/pytz/zoneinfo/America/Matamoros b/telegramer/include/pytz/zoneinfo/America/Matamoros new file mode 100644 index 0000000..047968d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Matamoros differ diff --git a/telegramer/include/pytz/zoneinfo/America/Mazatlan b/telegramer/include/pytz/zoneinfo/America/Mazatlan new file mode 100644 index 0000000..e4a7857 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Mazatlan differ diff --git a/telegramer/include/pytz/zoneinfo/America/Mendoza b/telegramer/include/pytz/zoneinfo/America/Mendoza new file mode 100644 index 0000000..f9e677f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Mendoza differ diff --git a/telegramer/include/pytz/zoneinfo/America/Menominee b/telegramer/include/pytz/zoneinfo/America/Menominee new file mode 100644 index 0000000..3146138 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Menominee differ diff --git a/telegramer/include/pytz/zoneinfo/America/Merida b/telegramer/include/pytz/zoneinfo/America/Merida new file mode 100644 index 0000000..ea852da Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Merida differ diff --git a/telegramer/include/pytz/zoneinfo/America/Metlakatla b/telegramer/include/pytz/zoneinfo/America/Metlakatla new file mode 100644 index 0000000..1e94be3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Metlakatla differ diff --git a/telegramer/include/pytz/zoneinfo/America/Mexico_City b/telegramer/include/pytz/zoneinfo/America/Mexico_City new file mode 100644 index 0000000..e7fb6f2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Mexico_City differ diff --git a/telegramer/include/pytz/zoneinfo/America/Miquelon b/telegramer/include/pytz/zoneinfo/America/Miquelon new file mode 100644 index 0000000..b924b71 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Miquelon differ diff --git a/telegramer/include/pytz/zoneinfo/America/Moncton b/telegramer/include/pytz/zoneinfo/America/Moncton new file mode 100644 index 0000000..9df8d0f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Moncton differ diff --git a/telegramer/include/pytz/zoneinfo/America/Monterrey b/telegramer/include/pytz/zoneinfo/America/Monterrey new file mode 100644 index 0000000..a8928c8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Monterrey differ diff --git a/telegramer/include/pytz/zoneinfo/America/Montevideo b/telegramer/include/pytz/zoneinfo/America/Montevideo new file mode 100644 index 0000000..2f357bc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Montevideo differ diff --git a/telegramer/include/pytz/zoneinfo/America/Montreal b/telegramer/include/pytz/zoneinfo/America/Montreal new file mode 100644 index 0000000..6752c5b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Montreal differ diff --git a/telegramer/include/pytz/zoneinfo/America/Montserrat b/telegramer/include/pytz/zoneinfo/America/Montserrat new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Montserrat differ diff --git a/telegramer/include/pytz/zoneinfo/America/Nassau b/telegramer/include/pytz/zoneinfo/America/Nassau new file mode 100644 index 0000000..6752c5b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nassau differ diff --git a/telegramer/include/pytz/zoneinfo/America/New_York b/telegramer/include/pytz/zoneinfo/America/New_York new file mode 100644 index 0000000..2f75480 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/New_York differ diff --git a/telegramer/include/pytz/zoneinfo/America/Nipigon b/telegramer/include/pytz/zoneinfo/America/Nipigon new file mode 100644 index 0000000..f6a856e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nipigon differ diff --git a/telegramer/include/pytz/zoneinfo/America/Nome b/telegramer/include/pytz/zoneinfo/America/Nome new file mode 100644 index 0000000..10998df Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nome differ diff --git a/telegramer/include/pytz/zoneinfo/America/Noronha b/telegramer/include/pytz/zoneinfo/America/Noronha new file mode 100644 index 0000000..f140726 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Noronha differ diff --git a/telegramer/include/pytz/zoneinfo/America/North_Dakota/Beulah b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Beulah new file mode 100644 index 0000000..246345d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Beulah differ diff --git a/telegramer/include/pytz/zoneinfo/America/North_Dakota/Center b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Center new file mode 100644 index 0000000..1fa0703 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Center differ diff --git a/telegramer/include/pytz/zoneinfo/America/North_Dakota/New_Salem b/telegramer/include/pytz/zoneinfo/America/North_Dakota/New_Salem new file mode 100644 index 0000000..123f2ae Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/North_Dakota/New_Salem differ diff --git a/telegramer/include/pytz/zoneinfo/America/Nuuk b/telegramer/include/pytz/zoneinfo/America/Nuuk new file mode 100644 index 0000000..0160308 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nuuk differ diff --git a/telegramer/include/pytz/zoneinfo/America/Ojinaga b/telegramer/include/pytz/zoneinfo/America/Ojinaga new file mode 100644 index 0000000..fc4a03e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Ojinaga differ diff --git a/telegramer/include/pytz/zoneinfo/America/Panama b/telegramer/include/pytz/zoneinfo/America/Panama new file mode 100644 index 0000000..9964b9a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Panama differ diff --git a/telegramer/include/pytz/zoneinfo/America/Pangnirtung b/telegramer/include/pytz/zoneinfo/America/Pangnirtung new file mode 100644 index 0000000..3e4e0db Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Pangnirtung differ diff --git a/telegramer/include/pytz/zoneinfo/America/Paramaribo b/telegramer/include/pytz/zoneinfo/America/Paramaribo new file mode 100644 index 0000000..bc8a6ed Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Paramaribo differ diff --git a/telegramer/include/pytz/zoneinfo/America/Phoenix b/telegramer/include/pytz/zoneinfo/America/Phoenix new file mode 100644 index 0000000..ac6bb0c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Phoenix differ diff --git a/telegramer/include/pytz/zoneinfo/America/Port-au-Prince b/telegramer/include/pytz/zoneinfo/America/Port-au-Prince new file mode 100644 index 0000000..287f143 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Port-au-Prince differ diff --git a/telegramer/include/pytz/zoneinfo/America/Port_of_Spain b/telegramer/include/pytz/zoneinfo/America/Port_of_Spain new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Port_of_Spain differ diff --git a/telegramer/include/pytz/zoneinfo/America/Porto_Acre b/telegramer/include/pytz/zoneinfo/America/Porto_Acre new file mode 100644 index 0000000..a374cb4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Porto_Acre differ diff --git a/telegramer/include/pytz/zoneinfo/America/Porto_Velho b/telegramer/include/pytz/zoneinfo/America/Porto_Velho new file mode 100644 index 0000000..2e873a5 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Porto_Velho differ diff --git a/telegramer/include/pytz/zoneinfo/America/Puerto_Rico b/telegramer/include/pytz/zoneinfo/America/Puerto_Rico new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Puerto_Rico differ diff --git a/telegramer/include/pytz/zoneinfo/America/Punta_Arenas b/telegramer/include/pytz/zoneinfo/America/Punta_Arenas new file mode 100644 index 0000000..13bd1d9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Punta_Arenas differ diff --git a/telegramer/include/pytz/zoneinfo/America/Rainy_River b/telegramer/include/pytz/zoneinfo/America/Rainy_River new file mode 100644 index 0000000..ea66099 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rainy_River differ diff --git a/telegramer/include/pytz/zoneinfo/America/Rankin_Inlet b/telegramer/include/pytz/zoneinfo/America/Rankin_Inlet new file mode 100644 index 0000000..3a70587 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rankin_Inlet differ diff --git a/telegramer/include/pytz/zoneinfo/America/Recife b/telegramer/include/pytz/zoneinfo/America/Recife new file mode 100644 index 0000000..d7abb16 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Recife differ diff --git a/telegramer/include/pytz/zoneinfo/America/Regina b/telegramer/include/pytz/zoneinfo/America/Regina new file mode 100644 index 0000000..20c9c84 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Regina differ diff --git a/telegramer/include/pytz/zoneinfo/America/Resolute b/telegramer/include/pytz/zoneinfo/America/Resolute new file mode 100644 index 0000000..0a73b75 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Resolute differ diff --git a/telegramer/include/pytz/zoneinfo/America/Rio_Branco b/telegramer/include/pytz/zoneinfo/America/Rio_Branco new file mode 100644 index 0000000..a374cb4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rio_Branco differ diff --git a/telegramer/include/pytz/zoneinfo/America/Rosario b/telegramer/include/pytz/zoneinfo/America/Rosario new file mode 100644 index 0000000..da4c23a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rosario differ diff --git a/telegramer/include/pytz/zoneinfo/America/Santa_Isabel b/telegramer/include/pytz/zoneinfo/America/Santa_Isabel new file mode 100644 index 0000000..ada6bf7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santa_Isabel differ diff --git a/telegramer/include/pytz/zoneinfo/America/Santarem b/telegramer/include/pytz/zoneinfo/America/Santarem new file mode 100644 index 0000000..c28f360 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santarem differ diff --git a/telegramer/include/pytz/zoneinfo/America/Santiago b/telegramer/include/pytz/zoneinfo/America/Santiago new file mode 100644 index 0000000..aa29060 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santiago differ diff --git a/telegramer/include/pytz/zoneinfo/America/Santo_Domingo b/telegramer/include/pytz/zoneinfo/America/Santo_Domingo new file mode 100644 index 0000000..4fe36fd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santo_Domingo differ diff --git a/telegramer/include/pytz/zoneinfo/America/Sao_Paulo b/telegramer/include/pytz/zoneinfo/America/Sao_Paulo new file mode 100644 index 0000000..13ff083 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Sao_Paulo differ diff --git a/telegramer/include/pytz/zoneinfo/America/Scoresbysund b/telegramer/include/pytz/zoneinfo/America/Scoresbysund new file mode 100644 index 0000000..e20e9e1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Scoresbysund differ diff --git a/telegramer/include/pytz/zoneinfo/America/Shiprock b/telegramer/include/pytz/zoneinfo/America/Shiprock new file mode 100644 index 0000000..5fbe26b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Shiprock differ diff --git a/telegramer/include/pytz/zoneinfo/America/Sitka b/telegramer/include/pytz/zoneinfo/America/Sitka new file mode 100644 index 0000000..31f7061 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Sitka differ diff --git a/telegramer/include/pytz/zoneinfo/America/St_Barthelemy b/telegramer/include/pytz/zoneinfo/America/St_Barthelemy new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Barthelemy differ diff --git a/telegramer/include/pytz/zoneinfo/America/St_Johns b/telegramer/include/pytz/zoneinfo/America/St_Johns new file mode 100644 index 0000000..65a5b0c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Johns differ diff --git a/telegramer/include/pytz/zoneinfo/America/St_Kitts b/telegramer/include/pytz/zoneinfo/America/St_Kitts new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Kitts differ diff --git a/telegramer/include/pytz/zoneinfo/America/St_Lucia b/telegramer/include/pytz/zoneinfo/America/St_Lucia new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Lucia differ diff --git a/telegramer/include/pytz/zoneinfo/America/St_Thomas b/telegramer/include/pytz/zoneinfo/America/St_Thomas new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Thomas differ diff --git a/telegramer/include/pytz/zoneinfo/America/St_Vincent b/telegramer/include/pytz/zoneinfo/America/St_Vincent new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Vincent differ diff --git a/telegramer/include/pytz/zoneinfo/America/Swift_Current b/telegramer/include/pytz/zoneinfo/America/Swift_Current new file mode 100644 index 0000000..8e9ef25 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Swift_Current differ diff --git a/telegramer/include/pytz/zoneinfo/America/Tegucigalpa b/telegramer/include/pytz/zoneinfo/America/Tegucigalpa new file mode 100644 index 0000000..2adacb2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Tegucigalpa differ diff --git a/telegramer/include/pytz/zoneinfo/America/Thule b/telegramer/include/pytz/zoneinfo/America/Thule new file mode 100644 index 0000000..6f802f1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Thule differ diff --git a/telegramer/include/pytz/zoneinfo/America/Thunder_Bay b/telegramer/include/pytz/zoneinfo/America/Thunder_Bay new file mode 100644 index 0000000..e504c9a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Thunder_Bay differ diff --git a/telegramer/include/pytz/zoneinfo/America/Tijuana b/telegramer/include/pytz/zoneinfo/America/Tijuana new file mode 100644 index 0000000..ada6bf7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Tijuana differ diff --git a/telegramer/include/pytz/zoneinfo/America/Toronto b/telegramer/include/pytz/zoneinfo/America/Toronto new file mode 100644 index 0000000..6752c5b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Toronto differ diff --git a/telegramer/include/pytz/zoneinfo/America/Tortola b/telegramer/include/pytz/zoneinfo/America/Tortola new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Tortola differ diff --git a/telegramer/include/pytz/zoneinfo/America/Vancouver b/telegramer/include/pytz/zoneinfo/America/Vancouver new file mode 100644 index 0000000..bb60cbc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Vancouver differ diff --git a/telegramer/include/pytz/zoneinfo/America/Virgin b/telegramer/include/pytz/zoneinfo/America/Virgin new file mode 100644 index 0000000..a662a57 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Virgin differ diff --git a/telegramer/include/pytz/zoneinfo/America/Whitehorse b/telegramer/include/pytz/zoneinfo/America/Whitehorse new file mode 100644 index 0000000..9ee229c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Whitehorse differ diff --git a/telegramer/include/pytz/zoneinfo/America/Winnipeg b/telegramer/include/pytz/zoneinfo/America/Winnipeg new file mode 100644 index 0000000..ac40299 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Winnipeg differ diff --git a/telegramer/include/pytz/zoneinfo/America/Yakutat b/telegramer/include/pytz/zoneinfo/America/Yakutat new file mode 100644 index 0000000..da209f9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Yakutat differ diff --git a/telegramer/include/pytz/zoneinfo/America/Yellowknife b/telegramer/include/pytz/zoneinfo/America/Yellowknife new file mode 100644 index 0000000..e6afa39 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Yellowknife differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Casey b/telegramer/include/pytz/zoneinfo/Antarctica/Casey new file mode 100644 index 0000000..cbcbe4e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Casey differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Davis b/telegramer/include/pytz/zoneinfo/Antarctica/Davis new file mode 100644 index 0000000..916f2c2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Davis differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/DumontDUrville b/telegramer/include/pytz/zoneinfo/Antarctica/DumontDUrville new file mode 100644 index 0000000..920ad27 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/DumontDUrville differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Macquarie b/telegramer/include/pytz/zoneinfo/Antarctica/Macquarie new file mode 100644 index 0000000..9e7cc68 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Macquarie differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Mawson b/telegramer/include/pytz/zoneinfo/Antarctica/Mawson new file mode 100644 index 0000000..b32e7fd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Mawson differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/McMurdo b/telegramer/include/pytz/zoneinfo/Antarctica/McMurdo new file mode 100644 index 0000000..6575fdc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/McMurdo differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Palmer b/telegramer/include/pytz/zoneinfo/Antarctica/Palmer new file mode 100644 index 0000000..3dd85f8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Palmer differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Rothera b/telegramer/include/pytz/zoneinfo/Antarctica/Rothera new file mode 100644 index 0000000..8b2430a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Rothera differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/South_Pole b/telegramer/include/pytz/zoneinfo/Antarctica/South_Pole new file mode 100644 index 0000000..6575fdc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/South_Pole differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Syowa b/telegramer/include/pytz/zoneinfo/Antarctica/Syowa new file mode 100644 index 0000000..2aea25f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Syowa differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Troll b/telegramer/include/pytz/zoneinfo/Antarctica/Troll new file mode 100644 index 0000000..5e565da Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Troll differ diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Vostok b/telegramer/include/pytz/zoneinfo/Antarctica/Vostok new file mode 100644 index 0000000..7283053 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Vostok differ diff --git a/telegramer/include/pytz/zoneinfo/Arctic/Longyearbyen b/telegramer/include/pytz/zoneinfo/Arctic/Longyearbyen new file mode 100644 index 0000000..15a34c3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Arctic/Longyearbyen differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Aden b/telegramer/include/pytz/zoneinfo/Asia/Aden new file mode 100644 index 0000000..2aea25f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Aden differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Almaty b/telegramer/include/pytz/zoneinfo/Asia/Almaty new file mode 100644 index 0000000..a4b0077 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Almaty differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Amman b/telegramer/include/pytz/zoneinfo/Asia/Amman new file mode 100644 index 0000000..5dcf7e0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Amman differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Anadyr b/telegramer/include/pytz/zoneinfo/Asia/Anadyr new file mode 100644 index 0000000..6ed8b7c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Anadyr differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Aqtau b/telegramer/include/pytz/zoneinfo/Asia/Aqtau new file mode 100644 index 0000000..e2d0f91 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Aqtau differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Aqtobe b/telegramer/include/pytz/zoneinfo/Asia/Aqtobe new file mode 100644 index 0000000..06f0a13 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Aqtobe differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ashgabat b/telegramer/include/pytz/zoneinfo/Asia/Ashgabat new file mode 100644 index 0000000..73891af Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ashgabat differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ashkhabad b/telegramer/include/pytz/zoneinfo/Asia/Ashkhabad new file mode 100644 index 0000000..73891af Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ashkhabad differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Atyrau b/telegramer/include/pytz/zoneinfo/Asia/Atyrau new file mode 100644 index 0000000..8b5153e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Atyrau differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Baghdad b/telegramer/include/pytz/zoneinfo/Asia/Baghdad new file mode 100644 index 0000000..f7162ed Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Baghdad differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Bahrain b/telegramer/include/pytz/zoneinfo/Asia/Bahrain new file mode 100644 index 0000000..63188b2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Bahrain differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Baku b/telegramer/include/pytz/zoneinfo/Asia/Baku new file mode 100644 index 0000000..a0de74b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Baku differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Bangkok b/telegramer/include/pytz/zoneinfo/Asia/Bangkok new file mode 100644 index 0000000..c292ac5 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Bangkok differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Barnaul b/telegramer/include/pytz/zoneinfo/Asia/Barnaul new file mode 100644 index 0000000..759592a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Barnaul differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Beirut b/telegramer/include/pytz/zoneinfo/Asia/Beirut new file mode 100644 index 0000000..fb266ed Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Beirut differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Bishkek b/telegramer/include/pytz/zoneinfo/Asia/Bishkek new file mode 100644 index 0000000..f6e20dd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Bishkek differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Brunei b/telegramer/include/pytz/zoneinfo/Asia/Brunei new file mode 100644 index 0000000..3dab0ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Brunei differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Calcutta b/telegramer/include/pytz/zoneinfo/Asia/Calcutta new file mode 100644 index 0000000..0014046 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Calcutta differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Chita b/telegramer/include/pytz/zoneinfo/Asia/Chita new file mode 100644 index 0000000..c4149c0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Chita differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Choibalsan b/telegramer/include/pytz/zoneinfo/Asia/Choibalsan new file mode 100644 index 0000000..e48daa8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Choibalsan differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Chongqing b/telegramer/include/pytz/zoneinfo/Asia/Chongqing new file mode 100644 index 0000000..91f6f8b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Chongqing differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Chungking b/telegramer/include/pytz/zoneinfo/Asia/Chungking new file mode 100644 index 0000000..91f6f8b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Chungking differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Colombo b/telegramer/include/pytz/zoneinfo/Asia/Colombo new file mode 100644 index 0000000..62c64d8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Colombo differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dacca b/telegramer/include/pytz/zoneinfo/Asia/Dacca new file mode 100644 index 0000000..b11c928 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dacca differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Damascus b/telegramer/include/pytz/zoneinfo/Asia/Damascus new file mode 100644 index 0000000..d9104a7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Damascus differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dhaka b/telegramer/include/pytz/zoneinfo/Asia/Dhaka new file mode 100644 index 0000000..b11c928 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dhaka differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dili b/telegramer/include/pytz/zoneinfo/Asia/Dili new file mode 100644 index 0000000..30943bb Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dili differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dubai b/telegramer/include/pytz/zoneinfo/Asia/Dubai new file mode 100644 index 0000000..fc0a589 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dubai differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dushanbe b/telegramer/include/pytz/zoneinfo/Asia/Dushanbe new file mode 100644 index 0000000..82d85b8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dushanbe differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Famagusta b/telegramer/include/pytz/zoneinfo/Asia/Famagusta new file mode 100644 index 0000000..653b146 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Famagusta differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Gaza b/telegramer/include/pytz/zoneinfo/Asia/Gaza new file mode 100644 index 0000000..266981a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Gaza differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Harbin b/telegramer/include/pytz/zoneinfo/Asia/Harbin new file mode 100644 index 0000000..91f6f8b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Harbin differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Hebron b/telegramer/include/pytz/zoneinfo/Asia/Hebron new file mode 100644 index 0000000..0078bf0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Hebron differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ho_Chi_Minh b/telegramer/include/pytz/zoneinfo/Asia/Ho_Chi_Minh new file mode 100644 index 0000000..e2934e3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ho_Chi_Minh differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Hong_Kong b/telegramer/include/pytz/zoneinfo/Asia/Hong_Kong new file mode 100644 index 0000000..23d0375 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Hong_Kong differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Hovd b/telegramer/include/pytz/zoneinfo/Asia/Hovd new file mode 100644 index 0000000..4cb800a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Hovd differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Irkutsk b/telegramer/include/pytz/zoneinfo/Asia/Irkutsk new file mode 100644 index 0000000..4dcbbb7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Irkutsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Istanbul b/telegramer/include/pytz/zoneinfo/Asia/Istanbul new file mode 100644 index 0000000..508446b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Istanbul differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Jakarta b/telegramer/include/pytz/zoneinfo/Asia/Jakarta new file mode 100644 index 0000000..5baa3a8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Jakarta differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Jayapura b/telegramer/include/pytz/zoneinfo/Asia/Jayapura new file mode 100644 index 0000000..3002c82 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Jayapura differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Jerusalem b/telegramer/include/pytz/zoneinfo/Asia/Jerusalem new file mode 100644 index 0000000..1ebd066 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Jerusalem differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kabul b/telegramer/include/pytz/zoneinfo/Asia/Kabul new file mode 100644 index 0000000..d19b9bd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kabul differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kamchatka b/telegramer/include/pytz/zoneinfo/Asia/Kamchatka new file mode 100644 index 0000000..3e80b4e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kamchatka differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Karachi b/telegramer/include/pytz/zoneinfo/Asia/Karachi new file mode 100644 index 0000000..ba65c0e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Karachi differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kashgar b/telegramer/include/pytz/zoneinfo/Asia/Kashgar new file mode 100644 index 0000000..faa14d9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kashgar differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kathmandu b/telegramer/include/pytz/zoneinfo/Asia/Kathmandu new file mode 100644 index 0000000..a5d5107 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kathmandu differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Katmandu b/telegramer/include/pytz/zoneinfo/Asia/Katmandu new file mode 100644 index 0000000..a5d5107 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Katmandu differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Khandyga b/telegramer/include/pytz/zoneinfo/Asia/Khandyga new file mode 100644 index 0000000..72bea64 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Khandyga differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kolkata b/telegramer/include/pytz/zoneinfo/Asia/Kolkata new file mode 100644 index 0000000..0014046 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kolkata differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Krasnoyarsk b/telegramer/include/pytz/zoneinfo/Asia/Krasnoyarsk new file mode 100644 index 0000000..30c6f16 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Krasnoyarsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kuala_Lumpur b/telegramer/include/pytz/zoneinfo/Asia/Kuala_Lumpur new file mode 100644 index 0000000..612b01e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kuala_Lumpur differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kuching b/telegramer/include/pytz/zoneinfo/Asia/Kuching new file mode 100644 index 0000000..c86750c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kuching differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kuwait b/telegramer/include/pytz/zoneinfo/Asia/Kuwait new file mode 100644 index 0000000..2aea25f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kuwait differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Macao b/telegramer/include/pytz/zoneinfo/Asia/Macao new file mode 100644 index 0000000..cac6506 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Macao differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Macau b/telegramer/include/pytz/zoneinfo/Asia/Macau new file mode 100644 index 0000000..cac6506 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Macau differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Magadan b/telegramer/include/pytz/zoneinfo/Asia/Magadan new file mode 100644 index 0000000..b4fcac1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Magadan differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Makassar b/telegramer/include/pytz/zoneinfo/Asia/Makassar new file mode 100644 index 0000000..556ba86 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Makassar differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Manila b/telegramer/include/pytz/zoneinfo/Asia/Manila new file mode 100644 index 0000000..f4f4b04 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Manila differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Muscat b/telegramer/include/pytz/zoneinfo/Asia/Muscat new file mode 100644 index 0000000..fc0a589 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Muscat differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Nicosia b/telegramer/include/pytz/zoneinfo/Asia/Nicosia new file mode 100644 index 0000000..f7f10ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Nicosia differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Novokuznetsk b/telegramer/include/pytz/zoneinfo/Asia/Novokuznetsk new file mode 100644 index 0000000..d983276 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Novokuznetsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Novosibirsk b/telegramer/include/pytz/zoneinfo/Asia/Novosibirsk new file mode 100644 index 0000000..e0ee5fc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Novosibirsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Omsk b/telegramer/include/pytz/zoneinfo/Asia/Omsk new file mode 100644 index 0000000..b29b769 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Omsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Oral b/telegramer/include/pytz/zoneinfo/Asia/Oral new file mode 100644 index 0000000..ad1f9ca Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Oral differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Phnom_Penh b/telegramer/include/pytz/zoneinfo/Asia/Phnom_Penh new file mode 100644 index 0000000..c292ac5 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Phnom_Penh differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Pontianak b/telegramer/include/pytz/zoneinfo/Asia/Pontianak new file mode 100644 index 0000000..12ce24c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Pontianak differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Pyongyang b/telegramer/include/pytz/zoneinfo/Asia/Pyongyang new file mode 100644 index 0000000..7ad7e0b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Pyongyang differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Qatar b/telegramer/include/pytz/zoneinfo/Asia/Qatar new file mode 100644 index 0000000..63188b2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Qatar differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Qostanay b/telegramer/include/pytz/zoneinfo/Asia/Qostanay new file mode 100644 index 0000000..73b9d96 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Qostanay differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Qyzylorda b/telegramer/include/pytz/zoneinfo/Asia/Qyzylorda new file mode 100644 index 0000000..c2fe4c1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Qyzylorda differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Rangoon b/telegramer/include/pytz/zoneinfo/Asia/Rangoon new file mode 100644 index 0000000..dd77395 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Rangoon differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Riyadh b/telegramer/include/pytz/zoneinfo/Asia/Riyadh new file mode 100644 index 0000000..2aea25f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Riyadh differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Saigon b/telegramer/include/pytz/zoneinfo/Asia/Saigon new file mode 100644 index 0000000..e2934e3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Saigon differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Sakhalin b/telegramer/include/pytz/zoneinfo/Asia/Sakhalin new file mode 100644 index 0000000..485459c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Sakhalin differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Samarkand b/telegramer/include/pytz/zoneinfo/Asia/Samarkand new file mode 100644 index 0000000..030d47c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Samarkand differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Seoul b/telegramer/include/pytz/zoneinfo/Asia/Seoul new file mode 100644 index 0000000..96199e7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Seoul differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Shanghai b/telegramer/include/pytz/zoneinfo/Asia/Shanghai new file mode 100644 index 0000000..91f6f8b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Shanghai differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Singapore b/telegramer/include/pytz/zoneinfo/Asia/Singapore new file mode 100644 index 0000000..2364b21 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Singapore differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Srednekolymsk b/telegramer/include/pytz/zoneinfo/Asia/Srednekolymsk new file mode 100644 index 0000000..261a983 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Srednekolymsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Taipei b/telegramer/include/pytz/zoneinfo/Asia/Taipei new file mode 100644 index 0000000..24c4344 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Taipei differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tashkent b/telegramer/include/pytz/zoneinfo/Asia/Tashkent new file mode 100644 index 0000000..32a9d7d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tashkent differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tbilisi b/telegramer/include/pytz/zoneinfo/Asia/Tbilisi new file mode 100644 index 0000000..b608d79 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tbilisi differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tehran b/telegramer/include/pytz/zoneinfo/Asia/Tehran new file mode 100644 index 0000000..8cec5ad Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tehran differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tel_Aviv b/telegramer/include/pytz/zoneinfo/Asia/Tel_Aviv new file mode 100644 index 0000000..1ebd066 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tel_Aviv differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Thimbu b/telegramer/include/pytz/zoneinfo/Asia/Thimbu new file mode 100644 index 0000000..fe409c7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Thimbu differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Thimphu b/telegramer/include/pytz/zoneinfo/Asia/Thimphu new file mode 100644 index 0000000..fe409c7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Thimphu differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tokyo b/telegramer/include/pytz/zoneinfo/Asia/Tokyo new file mode 100644 index 0000000..26f4d34 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tokyo differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tomsk b/telegramer/include/pytz/zoneinfo/Asia/Tomsk new file mode 100644 index 0000000..670e2ad Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tomsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ujung_Pandang b/telegramer/include/pytz/zoneinfo/Asia/Ujung_Pandang new file mode 100644 index 0000000..556ba86 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ujung_Pandang differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ulaanbaatar b/telegramer/include/pytz/zoneinfo/Asia/Ulaanbaatar new file mode 100644 index 0000000..2e20cc3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ulaanbaatar differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ulan_Bator b/telegramer/include/pytz/zoneinfo/Asia/Ulan_Bator new file mode 100644 index 0000000..2e20cc3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ulan_Bator differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Urumqi b/telegramer/include/pytz/zoneinfo/Asia/Urumqi new file mode 100644 index 0000000..faa14d9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Urumqi differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ust-Nera b/telegramer/include/pytz/zoneinfo/Asia/Ust-Nera new file mode 100644 index 0000000..9e4a78f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ust-Nera differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Vientiane b/telegramer/include/pytz/zoneinfo/Asia/Vientiane new file mode 100644 index 0000000..c292ac5 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Vientiane differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Vladivostok b/telegramer/include/pytz/zoneinfo/Asia/Vladivostok new file mode 100644 index 0000000..8ab253c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Vladivostok differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yakutsk b/telegramer/include/pytz/zoneinfo/Asia/Yakutsk new file mode 100644 index 0000000..c815e99 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yakutsk differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yangon b/telegramer/include/pytz/zoneinfo/Asia/Yangon new file mode 100644 index 0000000..dd77395 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yangon differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yekaterinburg b/telegramer/include/pytz/zoneinfo/Asia/Yekaterinburg new file mode 100644 index 0000000..6958d7e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yekaterinburg differ diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yerevan b/telegramer/include/pytz/zoneinfo/Asia/Yerevan new file mode 100644 index 0000000..250bfe0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yerevan differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Azores b/telegramer/include/pytz/zoneinfo/Atlantic/Azores new file mode 100644 index 0000000..00a564f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Azores differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Bermuda b/telegramer/include/pytz/zoneinfo/Atlantic/Bermuda new file mode 100644 index 0000000..527524e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Bermuda differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Canary b/telegramer/include/pytz/zoneinfo/Atlantic/Canary new file mode 100644 index 0000000..f319215 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Canary differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Cape_Verde b/telegramer/include/pytz/zoneinfo/Atlantic/Cape_Verde new file mode 100644 index 0000000..e2a49d2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Cape_Verde differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Faeroe b/telegramer/include/pytz/zoneinfo/Atlantic/Faeroe new file mode 100644 index 0000000..4dab7ef Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Faeroe differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Faroe b/telegramer/include/pytz/zoneinfo/Atlantic/Faroe new file mode 100644 index 0000000..4dab7ef Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Faroe differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Jan_Mayen b/telegramer/include/pytz/zoneinfo/Atlantic/Jan_Mayen new file mode 100644 index 0000000..15a34c3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Jan_Mayen differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Madeira b/telegramer/include/pytz/zoneinfo/Atlantic/Madeira new file mode 100644 index 0000000..7ddcd88 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Madeira differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Reykjavik b/telegramer/include/pytz/zoneinfo/Atlantic/Reykjavik new file mode 100644 index 0000000..10e0fc8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Reykjavik differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/South_Georgia b/telegramer/include/pytz/zoneinfo/Atlantic/South_Georgia new file mode 100644 index 0000000..4466608 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/South_Georgia differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/St_Helena b/telegramer/include/pytz/zoneinfo/Atlantic/St_Helena new file mode 100644 index 0000000..28b32ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/St_Helena differ diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Stanley b/telegramer/include/pytz/zoneinfo/Atlantic/Stanley new file mode 100644 index 0000000..88077f1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Stanley differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/ACT b/telegramer/include/pytz/zoneinfo/Australia/ACT new file mode 100644 index 0000000..0aea4c3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/ACT differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Adelaide b/telegramer/include/pytz/zoneinfo/Australia/Adelaide new file mode 100644 index 0000000..f5dedca Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Adelaide differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Brisbane b/telegramer/include/pytz/zoneinfo/Australia/Brisbane new file mode 100644 index 0000000..7ff9949 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Brisbane differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Broken_Hill b/telegramer/include/pytz/zoneinfo/Australia/Broken_Hill new file mode 100644 index 0000000..698c76e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Broken_Hill differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Canberra b/telegramer/include/pytz/zoneinfo/Australia/Canberra new file mode 100644 index 0000000..0aea4c3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Canberra differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Currie b/telegramer/include/pytz/zoneinfo/Australia/Currie new file mode 100644 index 0000000..3adb8e1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Currie differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Darwin b/telegramer/include/pytz/zoneinfo/Australia/Darwin new file mode 100644 index 0000000..74a3087 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Darwin differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Eucla b/telegramer/include/pytz/zoneinfo/Australia/Eucla new file mode 100644 index 0000000..3bf1171 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Eucla differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Hobart b/telegramer/include/pytz/zoneinfo/Australia/Hobart new file mode 100644 index 0000000..3adb8e1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Hobart differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/LHI b/telegramer/include/pytz/zoneinfo/Australia/LHI new file mode 100644 index 0000000..9e04a80 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/LHI differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Lindeman b/telegramer/include/pytz/zoneinfo/Australia/Lindeman new file mode 100644 index 0000000..4ee1825 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Lindeman differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Lord_Howe b/telegramer/include/pytz/zoneinfo/Australia/Lord_Howe new file mode 100644 index 0000000..9e04a80 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Lord_Howe differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Melbourne b/telegramer/include/pytz/zoneinfo/Australia/Melbourne new file mode 100644 index 0000000..ee903f4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Melbourne differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/NSW b/telegramer/include/pytz/zoneinfo/Australia/NSW new file mode 100644 index 0000000..0aea4c3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/NSW differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/North b/telegramer/include/pytz/zoneinfo/Australia/North new file mode 100644 index 0000000..74a3087 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/North differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Perth b/telegramer/include/pytz/zoneinfo/Australia/Perth new file mode 100644 index 0000000..f8ddbdf Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Perth differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Queensland b/telegramer/include/pytz/zoneinfo/Australia/Queensland new file mode 100644 index 0000000..7ff9949 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Queensland differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/South b/telegramer/include/pytz/zoneinfo/Australia/South new file mode 100644 index 0000000..f5dedca Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/South differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Sydney b/telegramer/include/pytz/zoneinfo/Australia/Sydney new file mode 100644 index 0000000..0aea4c3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Sydney differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Tasmania b/telegramer/include/pytz/zoneinfo/Australia/Tasmania new file mode 100644 index 0000000..3adb8e1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Tasmania differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Victoria b/telegramer/include/pytz/zoneinfo/Australia/Victoria new file mode 100644 index 0000000..ee903f4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Victoria differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/West b/telegramer/include/pytz/zoneinfo/Australia/West new file mode 100644 index 0000000..f8ddbdf Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/West differ diff --git a/telegramer/include/pytz/zoneinfo/Australia/Yancowinna b/telegramer/include/pytz/zoneinfo/Australia/Yancowinna new file mode 100644 index 0000000..698c76e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Yancowinna differ diff --git a/telegramer/include/pytz/zoneinfo/Brazil/Acre b/telegramer/include/pytz/zoneinfo/Brazil/Acre new file mode 100644 index 0000000..a374cb4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/Acre differ diff --git a/telegramer/include/pytz/zoneinfo/Brazil/DeNoronha b/telegramer/include/pytz/zoneinfo/Brazil/DeNoronha new file mode 100644 index 0000000..f140726 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/DeNoronha differ diff --git a/telegramer/include/pytz/zoneinfo/Brazil/East b/telegramer/include/pytz/zoneinfo/Brazil/East new file mode 100644 index 0000000..13ff083 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/East differ diff --git a/telegramer/include/pytz/zoneinfo/Brazil/West b/telegramer/include/pytz/zoneinfo/Brazil/West new file mode 100644 index 0000000..63d58f8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/West differ diff --git a/telegramer/include/pytz/zoneinfo/CET b/telegramer/include/pytz/zoneinfo/CET new file mode 100644 index 0000000..122e934 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/CET differ diff --git a/telegramer/include/pytz/zoneinfo/CST6CDT b/telegramer/include/pytz/zoneinfo/CST6CDT new file mode 100644 index 0000000..ca67929 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/CST6CDT differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Atlantic b/telegramer/include/pytz/zoneinfo/Canada/Atlantic new file mode 100644 index 0000000..756099a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Atlantic differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Central b/telegramer/include/pytz/zoneinfo/Canada/Central new file mode 100644 index 0000000..ac40299 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Central differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Eastern b/telegramer/include/pytz/zoneinfo/Canada/Eastern new file mode 100644 index 0000000..6752c5b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Eastern differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Mountain b/telegramer/include/pytz/zoneinfo/Canada/Mountain new file mode 100644 index 0000000..cd78a6f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Mountain differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Newfoundland b/telegramer/include/pytz/zoneinfo/Canada/Newfoundland new file mode 100644 index 0000000..65a5b0c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Newfoundland differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Pacific b/telegramer/include/pytz/zoneinfo/Canada/Pacific new file mode 100644 index 0000000..bb60cbc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Pacific differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Saskatchewan b/telegramer/include/pytz/zoneinfo/Canada/Saskatchewan new file mode 100644 index 0000000..20c9c84 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Saskatchewan differ diff --git a/telegramer/include/pytz/zoneinfo/Canada/Yukon b/telegramer/include/pytz/zoneinfo/Canada/Yukon new file mode 100644 index 0000000..9ee229c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Yukon differ diff --git a/telegramer/include/pytz/zoneinfo/Chile/Continental b/telegramer/include/pytz/zoneinfo/Chile/Continental new file mode 100644 index 0000000..aa29060 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Chile/Continental differ diff --git a/telegramer/include/pytz/zoneinfo/Chile/EasterIsland b/telegramer/include/pytz/zoneinfo/Chile/EasterIsland new file mode 100644 index 0000000..cae3744 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Chile/EasterIsland differ diff --git a/telegramer/include/pytz/zoneinfo/Cuba b/telegramer/include/pytz/zoneinfo/Cuba new file mode 100644 index 0000000..b69ac45 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Cuba differ diff --git a/telegramer/include/pytz/zoneinfo/EET b/telegramer/include/pytz/zoneinfo/EET new file mode 100644 index 0000000..cbdb71d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/EET differ diff --git a/telegramer/include/pytz/zoneinfo/EST b/telegramer/include/pytz/zoneinfo/EST new file mode 100644 index 0000000..21ebc00 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/EST differ diff --git a/telegramer/include/pytz/zoneinfo/EST5EDT b/telegramer/include/pytz/zoneinfo/EST5EDT new file mode 100644 index 0000000..9bce500 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/EST5EDT differ diff --git a/telegramer/include/pytz/zoneinfo/Egypt b/telegramer/include/pytz/zoneinfo/Egypt new file mode 100644 index 0000000..d3f8196 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Egypt differ diff --git a/telegramer/include/pytz/zoneinfo/Eire b/telegramer/include/pytz/zoneinfo/Eire new file mode 100644 index 0000000..1d99490 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Eire differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT b/telegramer/include/pytz/zoneinfo/Etc/GMT new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+0 b/telegramer/include/pytz/zoneinfo/Etc/GMT+0 new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+0 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+1 b/telegramer/include/pytz/zoneinfo/Etc/GMT+1 new file mode 100644 index 0000000..4dab6f9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+1 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+10 b/telegramer/include/pytz/zoneinfo/Etc/GMT+10 new file mode 100644 index 0000000..c749290 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+10 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+11 b/telegramer/include/pytz/zoneinfo/Etc/GMT+11 new file mode 100644 index 0000000..d969982 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+11 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+12 b/telegramer/include/pytz/zoneinfo/Etc/GMT+12 new file mode 100644 index 0000000..cdeec90 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+12 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+2 b/telegramer/include/pytz/zoneinfo/Etc/GMT+2 new file mode 100644 index 0000000..fbd2a94 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+2 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+3 b/telegramer/include/pytz/zoneinfo/Etc/GMT+3 new file mode 100644 index 0000000..ee246ef Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+3 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+4 b/telegramer/include/pytz/zoneinfo/Etc/GMT+4 new file mode 100644 index 0000000..5a25ff2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+4 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+5 b/telegramer/include/pytz/zoneinfo/Etc/GMT+5 new file mode 100644 index 0000000..c0b745f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+5 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+6 b/telegramer/include/pytz/zoneinfo/Etc/GMT+6 new file mode 100644 index 0000000..06e777d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+6 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+7 b/telegramer/include/pytz/zoneinfo/Etc/GMT+7 new file mode 100644 index 0000000..4e0b53a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+7 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+8 b/telegramer/include/pytz/zoneinfo/Etc/GMT+8 new file mode 100644 index 0000000..714b0c5 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+8 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+9 b/telegramer/include/pytz/zoneinfo/Etc/GMT+9 new file mode 100644 index 0000000..78b9daa Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+9 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-0 b/telegramer/include/pytz/zoneinfo/Etc/GMT-0 new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-0 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-1 b/telegramer/include/pytz/zoneinfo/Etc/GMT-1 new file mode 100644 index 0000000..a838beb Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-1 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-10 b/telegramer/include/pytz/zoneinfo/Etc/GMT-10 new file mode 100644 index 0000000..68ff77d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-10 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-11 b/telegramer/include/pytz/zoneinfo/Etc/GMT-11 new file mode 100644 index 0000000..66af5a4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-11 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-12 b/telegramer/include/pytz/zoneinfo/Etc/GMT-12 new file mode 100644 index 0000000..17ba505 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-12 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-13 b/telegramer/include/pytz/zoneinfo/Etc/GMT-13 new file mode 100644 index 0000000..5f3706c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-13 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-14 b/telegramer/include/pytz/zoneinfo/Etc/GMT-14 new file mode 100644 index 0000000..7e9f9c4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-14 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-2 b/telegramer/include/pytz/zoneinfo/Etc/GMT-2 new file mode 100644 index 0000000..fcef6d9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-2 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-3 b/telegramer/include/pytz/zoneinfo/Etc/GMT-3 new file mode 100644 index 0000000..27973bc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-3 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-4 b/telegramer/include/pytz/zoneinfo/Etc/GMT-4 new file mode 100644 index 0000000..1efd841 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-4 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-5 b/telegramer/include/pytz/zoneinfo/Etc/GMT-5 new file mode 100644 index 0000000..1f76184 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-5 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-6 b/telegramer/include/pytz/zoneinfo/Etc/GMT-6 new file mode 100644 index 0000000..952681e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-6 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-7 b/telegramer/include/pytz/zoneinfo/Etc/GMT-7 new file mode 100644 index 0000000..cefc912 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-7 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-8 b/telegramer/include/pytz/zoneinfo/Etc/GMT-8 new file mode 100644 index 0000000..afb093d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-8 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-9 b/telegramer/include/pytz/zoneinfo/Etc/GMT-9 new file mode 100644 index 0000000..9265fb7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-9 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT0 b/telegramer/include/pytz/zoneinfo/Etc/GMT0 new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT0 differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/Greenwich b/telegramer/include/pytz/zoneinfo/Etc/Greenwich new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/Greenwich differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/UCT b/telegramer/include/pytz/zoneinfo/Etc/UCT new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/UCT differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/UTC b/telegramer/include/pytz/zoneinfo/Etc/UTC new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/UTC differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/Universal b/telegramer/include/pytz/zoneinfo/Etc/Universal new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/Universal differ diff --git a/telegramer/include/pytz/zoneinfo/Etc/Zulu b/telegramer/include/pytz/zoneinfo/Etc/Zulu new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/Zulu differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Amsterdam b/telegramer/include/pytz/zoneinfo/Europe/Amsterdam new file mode 100644 index 0000000..c3ff07b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Amsterdam differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Andorra b/telegramer/include/pytz/zoneinfo/Europe/Andorra new file mode 100644 index 0000000..5962550 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Andorra differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Astrakhan b/telegramer/include/pytz/zoneinfo/Europe/Astrakhan new file mode 100644 index 0000000..73a4d01 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Astrakhan differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Athens b/telegramer/include/pytz/zoneinfo/Europe/Athens new file mode 100644 index 0000000..9f3a067 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Athens differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Belfast b/telegramer/include/pytz/zoneinfo/Europe/Belfast new file mode 100644 index 0000000..ac02a81 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Belfast differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Belgrade b/telegramer/include/pytz/zoneinfo/Europe/Belgrade new file mode 100644 index 0000000..27de456 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Belgrade differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Berlin b/telegramer/include/pytz/zoneinfo/Europe/Berlin new file mode 100644 index 0000000..7f6d958 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Berlin differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Bratislava b/telegramer/include/pytz/zoneinfo/Europe/Bratislava new file mode 100644 index 0000000..ce8f433 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Bratislava differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Brussels b/telegramer/include/pytz/zoneinfo/Europe/Brussels new file mode 100644 index 0000000..40d7124 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Brussels differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Bucharest b/telegramer/include/pytz/zoneinfo/Europe/Bucharest new file mode 100644 index 0000000..4303b90 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Bucharest differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Budapest b/telegramer/include/pytz/zoneinfo/Europe/Budapest new file mode 100644 index 0000000..b76c873 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Budapest differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Busingen b/telegramer/include/pytz/zoneinfo/Europe/Busingen new file mode 100644 index 0000000..ad6cf59 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Busingen differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Chisinau b/telegramer/include/pytz/zoneinfo/Europe/Chisinau new file mode 100644 index 0000000..5ee23fe Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Chisinau differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Copenhagen b/telegramer/include/pytz/zoneinfo/Europe/Copenhagen new file mode 100644 index 0000000..776be6e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Copenhagen differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Dublin b/telegramer/include/pytz/zoneinfo/Europe/Dublin new file mode 100644 index 0000000..1d99490 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Dublin differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Gibraltar b/telegramer/include/pytz/zoneinfo/Europe/Gibraltar new file mode 100644 index 0000000..117aadb Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Gibraltar differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Guernsey b/telegramer/include/pytz/zoneinfo/Europe/Guernsey new file mode 100644 index 0000000..ac02a81 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Guernsey differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Helsinki b/telegramer/include/pytz/zoneinfo/Europe/Helsinki new file mode 100644 index 0000000..b4f8f9c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Helsinki differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Isle_of_Man b/telegramer/include/pytz/zoneinfo/Europe/Isle_of_Man new file mode 100644 index 0000000..ac02a81 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Isle_of_Man differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Istanbul b/telegramer/include/pytz/zoneinfo/Europe/Istanbul new file mode 100644 index 0000000..508446b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Istanbul differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Jersey b/telegramer/include/pytz/zoneinfo/Europe/Jersey new file mode 100644 index 0000000..ac02a81 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Jersey differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Kaliningrad b/telegramer/include/pytz/zoneinfo/Europe/Kaliningrad new file mode 100644 index 0000000..cc99bea Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Kaliningrad differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Kiev b/telegramer/include/pytz/zoneinfo/Europe/Kiev new file mode 100644 index 0000000..52efea8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Kiev differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Kirov b/telegramer/include/pytz/zoneinfo/Europe/Kirov new file mode 100644 index 0000000..a3b5320 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Kirov differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Lisbon b/telegramer/include/pytz/zoneinfo/Europe/Lisbon new file mode 100644 index 0000000..55f0193 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Lisbon differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Ljubljana b/telegramer/include/pytz/zoneinfo/Europe/Ljubljana new file mode 100644 index 0000000..27de456 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Ljubljana differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/London b/telegramer/include/pytz/zoneinfo/Europe/London new file mode 100644 index 0000000..ac02a81 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/London differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Luxembourg b/telegramer/include/pytz/zoneinfo/Europe/Luxembourg new file mode 100644 index 0000000..c4ca733 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Luxembourg differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Madrid b/telegramer/include/pytz/zoneinfo/Europe/Madrid new file mode 100644 index 0000000..16f6420 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Madrid differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Malta b/telegramer/include/pytz/zoneinfo/Europe/Malta new file mode 100644 index 0000000..bf2452d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Malta differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Mariehamn b/telegramer/include/pytz/zoneinfo/Europe/Mariehamn new file mode 100644 index 0000000..b4f8f9c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Mariehamn differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Minsk b/telegramer/include/pytz/zoneinfo/Europe/Minsk new file mode 100644 index 0000000..453306c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Minsk differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Monaco b/telegramer/include/pytz/zoneinfo/Europe/Monaco new file mode 100644 index 0000000..adbe45d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Monaco differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Moscow b/telegramer/include/pytz/zoneinfo/Europe/Moscow new file mode 100644 index 0000000..ddb3f4e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Moscow differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Nicosia b/telegramer/include/pytz/zoneinfo/Europe/Nicosia new file mode 100644 index 0000000..f7f10ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Nicosia differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Oslo b/telegramer/include/pytz/zoneinfo/Europe/Oslo new file mode 100644 index 0000000..15a34c3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Oslo differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Paris b/telegramer/include/pytz/zoneinfo/Europe/Paris new file mode 100644 index 0000000..7d366c6 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Paris differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Podgorica b/telegramer/include/pytz/zoneinfo/Europe/Podgorica new file mode 100644 index 0000000..27de456 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Podgorica differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Prague b/telegramer/include/pytz/zoneinfo/Europe/Prague new file mode 100644 index 0000000..ce8f433 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Prague differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Riga b/telegramer/include/pytz/zoneinfo/Europe/Riga new file mode 100644 index 0000000..8db477d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Riga differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Rome b/telegramer/include/pytz/zoneinfo/Europe/Rome new file mode 100644 index 0000000..ac4c163 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Rome differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Samara b/telegramer/include/pytz/zoneinfo/Europe/Samara new file mode 100644 index 0000000..97d5dd9 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Samara differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/San_Marino b/telegramer/include/pytz/zoneinfo/Europe/San_Marino new file mode 100644 index 0000000..ac4c163 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/San_Marino differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Sarajevo b/telegramer/include/pytz/zoneinfo/Europe/Sarajevo new file mode 100644 index 0000000..27de456 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Sarajevo differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Saratov b/telegramer/include/pytz/zoneinfo/Europe/Saratov new file mode 100644 index 0000000..8fd5f6d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Saratov differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Simferopol b/telegramer/include/pytz/zoneinfo/Europe/Simferopol new file mode 100644 index 0000000..29107a0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Simferopol differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Skopje b/telegramer/include/pytz/zoneinfo/Europe/Skopje new file mode 100644 index 0000000..27de456 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Skopje differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Sofia b/telegramer/include/pytz/zoneinfo/Europe/Sofia new file mode 100644 index 0000000..0e4d879 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Sofia differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Stockholm b/telegramer/include/pytz/zoneinfo/Europe/Stockholm new file mode 100644 index 0000000..f3e0c7f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Stockholm differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Tallinn b/telegramer/include/pytz/zoneinfo/Europe/Tallinn new file mode 100644 index 0000000..b5acca3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Tallinn differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Tirane b/telegramer/include/pytz/zoneinfo/Europe/Tirane new file mode 100644 index 0000000..0b86017 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Tirane differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Tiraspol b/telegramer/include/pytz/zoneinfo/Europe/Tiraspol new file mode 100644 index 0000000..5ee23fe Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Tiraspol differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Ulyanovsk b/telegramer/include/pytz/zoneinfo/Europe/Ulyanovsk new file mode 100644 index 0000000..7b61bdc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Ulyanovsk differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Uzhgorod b/telegramer/include/pytz/zoneinfo/Europe/Uzhgorod new file mode 100644 index 0000000..0eb2150 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Uzhgorod differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vaduz b/telegramer/include/pytz/zoneinfo/Europe/Vaduz new file mode 100644 index 0000000..ad6cf59 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vaduz differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vatican b/telegramer/include/pytz/zoneinfo/Europe/Vatican new file mode 100644 index 0000000..ac4c163 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vatican differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vienna b/telegramer/include/pytz/zoneinfo/Europe/Vienna new file mode 100644 index 0000000..3582bb1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vienna differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vilnius b/telegramer/include/pytz/zoneinfo/Europe/Vilnius new file mode 100644 index 0000000..7abd63f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vilnius differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Volgograd b/telegramer/include/pytz/zoneinfo/Europe/Volgograd new file mode 100644 index 0000000..11739ac Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Volgograd differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Warsaw b/telegramer/include/pytz/zoneinfo/Europe/Warsaw new file mode 100644 index 0000000..e33cf67 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Warsaw differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Zagreb b/telegramer/include/pytz/zoneinfo/Europe/Zagreb new file mode 100644 index 0000000..27de456 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Zagreb differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Zaporozhye b/telegramer/include/pytz/zoneinfo/Europe/Zaporozhye new file mode 100644 index 0000000..f0406c1 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Zaporozhye differ diff --git a/telegramer/include/pytz/zoneinfo/Europe/Zurich b/telegramer/include/pytz/zoneinfo/Europe/Zurich new file mode 100644 index 0000000..ad6cf59 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Zurich differ diff --git a/telegramer/include/pytz/zoneinfo/Factory b/telegramer/include/pytz/zoneinfo/Factory new file mode 100644 index 0000000..60aa2a0 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Factory differ diff --git a/telegramer/include/pytz/zoneinfo/GB b/telegramer/include/pytz/zoneinfo/GB new file mode 100644 index 0000000..ac02a81 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GB differ diff --git a/telegramer/include/pytz/zoneinfo/GB-Eire b/telegramer/include/pytz/zoneinfo/GB-Eire new file mode 100644 index 0000000..ac02a81 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GB-Eire differ diff --git a/telegramer/include/pytz/zoneinfo/GMT b/telegramer/include/pytz/zoneinfo/GMT new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT differ diff --git a/telegramer/include/pytz/zoneinfo/GMT+0 b/telegramer/include/pytz/zoneinfo/GMT+0 new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT+0 differ diff --git a/telegramer/include/pytz/zoneinfo/GMT-0 b/telegramer/include/pytz/zoneinfo/GMT-0 new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT-0 differ diff --git a/telegramer/include/pytz/zoneinfo/GMT0 b/telegramer/include/pytz/zoneinfo/GMT0 new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT0 differ diff --git a/telegramer/include/pytz/zoneinfo/Greenwich b/telegramer/include/pytz/zoneinfo/Greenwich new file mode 100644 index 0000000..c634746 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Greenwich differ diff --git a/telegramer/include/pytz/zoneinfo/HST b/telegramer/include/pytz/zoneinfo/HST new file mode 100644 index 0000000..cccd45e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/HST differ diff --git a/telegramer/include/pytz/zoneinfo/Hongkong b/telegramer/include/pytz/zoneinfo/Hongkong new file mode 100644 index 0000000..23d0375 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Hongkong differ diff --git a/telegramer/include/pytz/zoneinfo/Iceland b/telegramer/include/pytz/zoneinfo/Iceland new file mode 100644 index 0000000..10e0fc8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Iceland differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Antananarivo b/telegramer/include/pytz/zoneinfo/Indian/Antananarivo new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Antananarivo differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Chagos b/telegramer/include/pytz/zoneinfo/Indian/Chagos new file mode 100644 index 0000000..93d6dda Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Chagos differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Christmas b/telegramer/include/pytz/zoneinfo/Indian/Christmas new file mode 100644 index 0000000..d18c381 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Christmas differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Cocos b/telegramer/include/pytz/zoneinfo/Indian/Cocos new file mode 100644 index 0000000..f8116e7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Cocos differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Comoro b/telegramer/include/pytz/zoneinfo/Indian/Comoro new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Comoro differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Kerguelen b/telegramer/include/pytz/zoneinfo/Indian/Kerguelen new file mode 100644 index 0000000..cde4cf7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Kerguelen differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Mahe b/telegramer/include/pytz/zoneinfo/Indian/Mahe new file mode 100644 index 0000000..208f938 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Mahe differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Maldives b/telegramer/include/pytz/zoneinfo/Indian/Maldives new file mode 100644 index 0000000..7c839cf Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Maldives differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Mauritius b/telegramer/include/pytz/zoneinfo/Indian/Mauritius new file mode 100644 index 0000000..17f2616 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Mauritius differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Mayotte b/telegramer/include/pytz/zoneinfo/Indian/Mayotte new file mode 100644 index 0000000..9dcfc19 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Mayotte differ diff --git a/telegramer/include/pytz/zoneinfo/Indian/Reunion b/telegramer/include/pytz/zoneinfo/Indian/Reunion new file mode 100644 index 0000000..dfe0831 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Reunion differ diff --git a/telegramer/include/pytz/zoneinfo/Iran b/telegramer/include/pytz/zoneinfo/Iran new file mode 100644 index 0000000..8cec5ad Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Iran differ diff --git a/telegramer/include/pytz/zoneinfo/Israel b/telegramer/include/pytz/zoneinfo/Israel new file mode 100644 index 0000000..1ebd066 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Israel differ diff --git a/telegramer/include/pytz/zoneinfo/Jamaica b/telegramer/include/pytz/zoneinfo/Jamaica new file mode 100644 index 0000000..2a9b7fd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Jamaica differ diff --git a/telegramer/include/pytz/zoneinfo/Japan b/telegramer/include/pytz/zoneinfo/Japan new file mode 100644 index 0000000..26f4d34 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Japan differ diff --git a/telegramer/include/pytz/zoneinfo/Kwajalein b/telegramer/include/pytz/zoneinfo/Kwajalein new file mode 100644 index 0000000..1a7975f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Kwajalein differ diff --git a/telegramer/include/pytz/zoneinfo/Libya b/telegramer/include/pytz/zoneinfo/Libya new file mode 100644 index 0000000..07b393b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Libya differ diff --git a/telegramer/include/pytz/zoneinfo/MET b/telegramer/include/pytz/zoneinfo/MET new file mode 100644 index 0000000..4a826bb Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/MET differ diff --git a/telegramer/include/pytz/zoneinfo/MST b/telegramer/include/pytz/zoneinfo/MST new file mode 100644 index 0000000..c93a58e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/MST differ diff --git a/telegramer/include/pytz/zoneinfo/MST7MDT b/telegramer/include/pytz/zoneinfo/MST7MDT new file mode 100644 index 0000000..4506a6e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/MST7MDT differ diff --git a/telegramer/include/pytz/zoneinfo/Mexico/BajaNorte b/telegramer/include/pytz/zoneinfo/Mexico/BajaNorte new file mode 100644 index 0000000..ada6bf7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Mexico/BajaNorte differ diff --git a/telegramer/include/pytz/zoneinfo/Mexico/BajaSur b/telegramer/include/pytz/zoneinfo/Mexico/BajaSur new file mode 100644 index 0000000..e4a7857 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Mexico/BajaSur differ diff --git a/telegramer/include/pytz/zoneinfo/Mexico/General b/telegramer/include/pytz/zoneinfo/Mexico/General new file mode 100644 index 0000000..e7fb6f2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Mexico/General differ diff --git a/telegramer/include/pytz/zoneinfo/NZ b/telegramer/include/pytz/zoneinfo/NZ new file mode 100644 index 0000000..6575fdc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/NZ differ diff --git a/telegramer/include/pytz/zoneinfo/NZ-CHAT b/telegramer/include/pytz/zoneinfo/NZ-CHAT new file mode 100644 index 0000000..c004109 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/NZ-CHAT differ diff --git a/telegramer/include/pytz/zoneinfo/Navajo b/telegramer/include/pytz/zoneinfo/Navajo new file mode 100644 index 0000000..5fbe26b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Navajo differ diff --git a/telegramer/include/pytz/zoneinfo/PRC b/telegramer/include/pytz/zoneinfo/PRC new file mode 100644 index 0000000..91f6f8b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/PRC differ diff --git a/telegramer/include/pytz/zoneinfo/PST8PDT b/telegramer/include/pytz/zoneinfo/PST8PDT new file mode 100644 index 0000000..99d246b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/PST8PDT differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Apia b/telegramer/include/pytz/zoneinfo/Pacific/Apia new file mode 100644 index 0000000..999c367 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Apia differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Auckland b/telegramer/include/pytz/zoneinfo/Pacific/Auckland new file mode 100644 index 0000000..6575fdc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Auckland differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Bougainville b/telegramer/include/pytz/zoneinfo/Pacific/Bougainville new file mode 100644 index 0000000..2892d26 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Bougainville differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Chatham b/telegramer/include/pytz/zoneinfo/Pacific/Chatham new file mode 100644 index 0000000..c004109 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Chatham differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Chuuk b/telegramer/include/pytz/zoneinfo/Pacific/Chuuk new file mode 100644 index 0000000..07c84b7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Chuuk differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Easter b/telegramer/include/pytz/zoneinfo/Pacific/Easter new file mode 100644 index 0000000..cae3744 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Easter differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Efate b/telegramer/include/pytz/zoneinfo/Pacific/Efate new file mode 100644 index 0000000..d8d4093 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Efate differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Enderbury b/telegramer/include/pytz/zoneinfo/Pacific/Enderbury new file mode 100644 index 0000000..39b786e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Enderbury differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Fakaofo b/telegramer/include/pytz/zoneinfo/Pacific/Fakaofo new file mode 100644 index 0000000..e40307f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Fakaofo differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Fiji b/telegramer/include/pytz/zoneinfo/Pacific/Fiji new file mode 100644 index 0000000..af07ac8 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Fiji differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Funafuti b/telegramer/include/pytz/zoneinfo/Pacific/Funafuti new file mode 100644 index 0000000..ea72863 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Funafuti differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Galapagos b/telegramer/include/pytz/zoneinfo/Pacific/Galapagos new file mode 100644 index 0000000..31f0921 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Galapagos differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Gambier b/telegramer/include/pytz/zoneinfo/Pacific/Gambier new file mode 100644 index 0000000..e1fc3da Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Gambier differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Guadalcanal b/telegramer/include/pytz/zoneinfo/Pacific/Guadalcanal new file mode 100644 index 0000000..7e9d10a Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Guadalcanal differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Guam b/telegramer/include/pytz/zoneinfo/Pacific/Guam new file mode 100644 index 0000000..66490d2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Guam differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Honolulu b/telegramer/include/pytz/zoneinfo/Pacific/Honolulu new file mode 100644 index 0000000..c7cd060 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Honolulu differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Johnston b/telegramer/include/pytz/zoneinfo/Pacific/Johnston new file mode 100644 index 0000000..c7cd060 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Johnston differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kanton b/telegramer/include/pytz/zoneinfo/Pacific/Kanton new file mode 100644 index 0000000..39b786e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kanton differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kiritimati b/telegramer/include/pytz/zoneinfo/Pacific/Kiritimati new file mode 100644 index 0000000..7cae0cb Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kiritimati differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kosrae b/telegramer/include/pytz/zoneinfo/Pacific/Kosrae new file mode 100644 index 0000000..a584aae Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kosrae differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kwajalein b/telegramer/include/pytz/zoneinfo/Pacific/Kwajalein new file mode 100644 index 0000000..1a7975f Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kwajalein differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Majuro b/telegramer/include/pytz/zoneinfo/Pacific/Majuro new file mode 100644 index 0000000..9ef8374 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Majuro differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Marquesas b/telegramer/include/pytz/zoneinfo/Pacific/Marquesas new file mode 100644 index 0000000..74d6792 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Marquesas differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Midway b/telegramer/include/pytz/zoneinfo/Pacific/Midway new file mode 100644 index 0000000..cb56709 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Midway differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Nauru b/telegramer/include/pytz/zoneinfo/Pacific/Nauru new file mode 100644 index 0000000..acec042 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Nauru differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Niue b/telegramer/include/pytz/zoneinfo/Pacific/Niue new file mode 100644 index 0000000..89117b3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Niue differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Norfolk b/telegramer/include/pytz/zoneinfo/Pacific/Norfolk new file mode 100644 index 0000000..53c1aad Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Norfolk differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Noumea b/telegramer/include/pytz/zoneinfo/Pacific/Noumea new file mode 100644 index 0000000..931a1a3 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Noumea differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Pago_Pago b/telegramer/include/pytz/zoneinfo/Pacific/Pago_Pago new file mode 100644 index 0000000..cb56709 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Pago_Pago differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Palau b/telegramer/include/pytz/zoneinfo/Pacific/Palau new file mode 100644 index 0000000..146b351 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Palau differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Pitcairn b/telegramer/include/pytz/zoneinfo/Pacific/Pitcairn new file mode 100644 index 0000000..ef91b06 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Pitcairn differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Pohnpei b/telegramer/include/pytz/zoneinfo/Pacific/Pohnpei new file mode 100644 index 0000000..c298ddd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Pohnpei differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Ponape b/telegramer/include/pytz/zoneinfo/Pacific/Ponape new file mode 100644 index 0000000..c298ddd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Ponape differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Port_Moresby b/telegramer/include/pytz/zoneinfo/Pacific/Port_Moresby new file mode 100644 index 0000000..920ad27 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Port_Moresby differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Rarotonga b/telegramer/include/pytz/zoneinfo/Pacific/Rarotonga new file mode 100644 index 0000000..eea37ab Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Rarotonga differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Saipan b/telegramer/include/pytz/zoneinfo/Pacific/Saipan new file mode 100644 index 0000000..66490d2 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Saipan differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Samoa b/telegramer/include/pytz/zoneinfo/Pacific/Samoa new file mode 100644 index 0000000..cb56709 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Samoa differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Tahiti b/telegramer/include/pytz/zoneinfo/Pacific/Tahiti new file mode 100644 index 0000000..442b8eb Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Tahiti differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Tarawa b/telegramer/include/pytz/zoneinfo/Pacific/Tarawa new file mode 100644 index 0000000..3db6c75 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Tarawa differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Tongatapu b/telegramer/include/pytz/zoneinfo/Pacific/Tongatapu new file mode 100644 index 0000000..c2e5999 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Tongatapu differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Truk b/telegramer/include/pytz/zoneinfo/Pacific/Truk new file mode 100644 index 0000000..07c84b7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Truk differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Wake b/telegramer/include/pytz/zoneinfo/Pacific/Wake new file mode 100644 index 0000000..c9e3106 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Wake differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Wallis b/telegramer/include/pytz/zoneinfo/Pacific/Wallis new file mode 100644 index 0000000..b35344b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Wallis differ diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Yap b/telegramer/include/pytz/zoneinfo/Pacific/Yap new file mode 100644 index 0000000..07c84b7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Yap differ diff --git a/telegramer/include/pytz/zoneinfo/Poland b/telegramer/include/pytz/zoneinfo/Poland new file mode 100644 index 0000000..e33cf67 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Poland differ diff --git a/telegramer/include/pytz/zoneinfo/Portugal b/telegramer/include/pytz/zoneinfo/Portugal new file mode 100644 index 0000000..55f0193 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Portugal differ diff --git a/telegramer/include/pytz/zoneinfo/ROC b/telegramer/include/pytz/zoneinfo/ROC new file mode 100644 index 0000000..24c4344 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/ROC differ diff --git a/telegramer/include/pytz/zoneinfo/ROK b/telegramer/include/pytz/zoneinfo/ROK new file mode 100644 index 0000000..96199e7 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/ROK differ diff --git a/telegramer/include/pytz/zoneinfo/Singapore b/telegramer/include/pytz/zoneinfo/Singapore new file mode 100644 index 0000000..2364b21 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Singapore differ diff --git a/telegramer/include/pytz/zoneinfo/Turkey b/telegramer/include/pytz/zoneinfo/Turkey new file mode 100644 index 0000000..508446b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Turkey differ diff --git a/telegramer/include/pytz/zoneinfo/UCT b/telegramer/include/pytz/zoneinfo/UCT new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/UCT differ diff --git a/telegramer/include/pytz/zoneinfo/US/Alaska b/telegramer/include/pytz/zoneinfo/US/Alaska new file mode 100644 index 0000000..9bbb2fd Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Alaska differ diff --git a/telegramer/include/pytz/zoneinfo/US/Aleutian b/telegramer/include/pytz/zoneinfo/US/Aleutian new file mode 100644 index 0000000..4323649 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Aleutian differ diff --git a/telegramer/include/pytz/zoneinfo/US/Arizona b/telegramer/include/pytz/zoneinfo/US/Arizona new file mode 100644 index 0000000..ac6bb0c Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Arizona differ diff --git a/telegramer/include/pytz/zoneinfo/US/Central b/telegramer/include/pytz/zoneinfo/US/Central new file mode 100644 index 0000000..a5b1617 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Central differ diff --git a/telegramer/include/pytz/zoneinfo/US/East-Indiana b/telegramer/include/pytz/zoneinfo/US/East-Indiana new file mode 100644 index 0000000..09511cc Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/East-Indiana differ diff --git a/telegramer/include/pytz/zoneinfo/US/Eastern b/telegramer/include/pytz/zoneinfo/US/Eastern new file mode 100644 index 0000000..2f75480 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Eastern differ diff --git a/telegramer/include/pytz/zoneinfo/US/Hawaii b/telegramer/include/pytz/zoneinfo/US/Hawaii new file mode 100644 index 0000000..c7cd060 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Hawaii differ diff --git a/telegramer/include/pytz/zoneinfo/US/Indiana-Starke b/telegramer/include/pytz/zoneinfo/US/Indiana-Starke new file mode 100644 index 0000000..fcd408d Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Indiana-Starke differ diff --git a/telegramer/include/pytz/zoneinfo/US/Michigan b/telegramer/include/pytz/zoneinfo/US/Michigan new file mode 100644 index 0000000..e104faa Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Michigan differ diff --git a/telegramer/include/pytz/zoneinfo/US/Mountain b/telegramer/include/pytz/zoneinfo/US/Mountain new file mode 100644 index 0000000..5fbe26b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Mountain differ diff --git a/telegramer/include/pytz/zoneinfo/US/Pacific b/telegramer/include/pytz/zoneinfo/US/Pacific new file mode 100644 index 0000000..9dad4f4 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Pacific differ diff --git a/telegramer/include/pytz/zoneinfo/US/Samoa b/telegramer/include/pytz/zoneinfo/US/Samoa new file mode 100644 index 0000000..cb56709 Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Samoa differ diff --git a/telegramer/include/pytz/zoneinfo/UTC b/telegramer/include/pytz/zoneinfo/UTC new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/UTC differ diff --git a/telegramer/include/pytz/zoneinfo/Universal b/telegramer/include/pytz/zoneinfo/Universal new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Universal differ diff --git a/telegramer/include/pytz/zoneinfo/W-SU b/telegramer/include/pytz/zoneinfo/W-SU new file mode 100644 index 0000000..ddb3f4e Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/W-SU differ diff --git a/telegramer/include/pytz/zoneinfo/WET b/telegramer/include/pytz/zoneinfo/WET new file mode 100644 index 0000000..c27390b Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/WET differ diff --git a/telegramer/include/pytz/zoneinfo/Zulu b/telegramer/include/pytz/zoneinfo/Zulu new file mode 100644 index 0000000..91558be Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Zulu differ diff --git a/telegramer/include/pytz/zoneinfo/iso3166.tab b/telegramer/include/pytz/zoneinfo/iso3166.tab new file mode 100644 index 0000000..a4ff61a --- /dev/null +++ b/telegramer/include/pytz/zoneinfo/iso3166.tab @@ -0,0 +1,274 @@ +# ISO 3166 alpha-2 country codes +# +# This file is in the public domain, so clarified as of +# 2009-05-17 by Arthur David Olson. +# +# From Paul Eggert (2015-05-02): +# This file contains a table of two-letter country codes. Columns are +# separated by a single tab. Lines beginning with '#' are comments. +# All text uses UTF-8 encoding. The columns of the table are as follows: +# +# 1. ISO 3166-1 alpha-2 country code, current as of +# ISO 3166-1 N976 (2018-11-06). See: Updates on ISO 3166-1 +# https://isotc.iso.org/livelink/livelink/Open/16944257 +# 2. The usual English name for the coded region, +# chosen so that alphabetic sorting of subsets produces helpful lists. +# This is not the same as the English name in the ISO 3166 tables. +# +# The table is sorted by country code. +# +# This table is intended as an aid for users, to help them select time +# zone data appropriate for their practical needs. It is not intended +# to take or endorse any position on legal or territorial claims. +# +#country- +#code name of country, territory, area, or subdivision +AD Andorra +AE United Arab Emirates +AF Afghanistan +AG Antigua & Barbuda +AI Anguilla +AL Albania +AM Armenia +AO Angola +AQ Antarctica +AR Argentina +AS Samoa (American) +AT Austria +AU Australia +AW Aruba +AX Åland Islands +AZ Azerbaijan +BA Bosnia & Herzegovina +BB Barbados +BD Bangladesh +BE Belgium +BF Burkina Faso +BG Bulgaria +BH Bahrain +BI Burundi +BJ Benin +BL St Barthelemy +BM Bermuda +BN Brunei +BO Bolivia +BQ Caribbean NL +BR Brazil +BS Bahamas +BT Bhutan +BV Bouvet Island +BW Botswana +BY Belarus +BZ Belize +CA Canada +CC Cocos (Keeling) Islands +CD Congo (Dem. Rep.) +CF Central African Rep. +CG Congo (Rep.) +CH Switzerland +CI Côte d'Ivoire +CK Cook Islands +CL Chile +CM Cameroon +CN China +CO Colombia +CR Costa Rica +CU Cuba +CV Cape Verde +CW Curaçao +CX Christmas Island +CY Cyprus +CZ Czech Republic +DE Germany +DJ Djibouti +DK Denmark +DM Dominica +DO Dominican Republic +DZ Algeria +EC Ecuador +EE Estonia +EG Egypt +EH Western Sahara +ER Eritrea +ES Spain +ET Ethiopia +FI Finland +FJ Fiji +FK Falkland Islands +FM Micronesia +FO Faroe Islands +FR France +GA Gabon +GB Britain (UK) +GD Grenada +GE Georgia +GF French Guiana +GG Guernsey +GH Ghana +GI Gibraltar +GL Greenland +GM Gambia +GN Guinea +GP Guadeloupe +GQ Equatorial Guinea +GR Greece +GS South Georgia & the South Sandwich Islands +GT Guatemala +GU Guam +GW Guinea-Bissau +GY Guyana +HK Hong Kong +HM Heard Island & McDonald Islands +HN Honduras +HR Croatia +HT Haiti +HU Hungary +ID Indonesia +IE Ireland +IL Israel +IM Isle of Man +IN India +IO British Indian Ocean Territory +IQ Iraq +IR Iran +IS Iceland +IT Italy +JE Jersey +JM Jamaica +JO Jordan +JP Japan +KE Kenya +KG Kyrgyzstan +KH Cambodia +KI Kiribati +KM Comoros +KN St Kitts & Nevis +KP Korea (North) +KR Korea (South) +KW Kuwait +KY Cayman Islands +KZ Kazakhstan +LA Laos +LB Lebanon +LC St Lucia +LI Liechtenstein +LK Sri Lanka +LR Liberia +LS Lesotho +LT Lithuania +LU Luxembourg +LV Latvia +LY Libya +MA Morocco +MC Monaco +MD Moldova +ME Montenegro +MF St Martin (French) +MG Madagascar +MH Marshall Islands +MK North Macedonia +ML Mali +MM Myanmar (Burma) +MN Mongolia +MO Macau +MP Northern Mariana Islands +MQ Martinique +MR Mauritania +MS Montserrat +MT Malta +MU Mauritius +MV Maldives +MW Malawi +MX Mexico +MY Malaysia +MZ Mozambique +NA Namibia +NC New Caledonia +NE Niger +NF Norfolk Island +NG Nigeria +NI Nicaragua +NL Netherlands +NO Norway +NP Nepal +NR Nauru +NU Niue +NZ New Zealand +OM Oman +PA Panama +PE Peru +PF French Polynesia +PG Papua New Guinea +PH Philippines +PK Pakistan +PL Poland +PM St Pierre & Miquelon +PN Pitcairn +PR Puerto Rico +PS Palestine +PT Portugal +PW Palau +PY Paraguay +QA Qatar +RE Réunion +RO Romania +RS Serbia +RU Russia +RW Rwanda +SA Saudi Arabia +SB Solomon Islands +SC Seychelles +SD Sudan +SE Sweden +SG Singapore +SH St Helena +SI Slovenia +SJ Svalbard & Jan Mayen +SK Slovakia +SL Sierra Leone +SM San Marino +SN Senegal +SO Somalia +SR Suriname +SS South Sudan +ST Sao Tome & Principe +SV El Salvador +SX St Maarten (Dutch) +SY Syria +SZ Eswatini (Swaziland) +TC Turks & Caicos Is +TD Chad +TF French Southern & Antarctic Lands +TG Togo +TH Thailand +TJ Tajikistan +TK Tokelau +TL East Timor +TM Turkmenistan +TN Tunisia +TO Tonga +TR Turkey +TT Trinidad & Tobago +TV Tuvalu +TW Taiwan +TZ Tanzania +UA Ukraine +UG Uganda +UM US minor outlying islands +US United States +UY Uruguay +UZ Uzbekistan +VA Vatican City +VC St Vincent +VE Venezuela +VG Virgin Islands (UK) +VI Virgin Islands (US) +VN Vietnam +VU Vanuatu +WF Wallis & Futuna +WS Samoa (western) +YE Yemen +YT Mayotte +ZA South Africa +ZM Zambia +ZW Zimbabwe diff --git a/telegramer/include/pytz/zoneinfo/leapseconds b/telegramer/include/pytz/zoneinfo/leapseconds new file mode 100644 index 0000000..ffa5eb8 --- /dev/null +++ b/telegramer/include/pytz/zoneinfo/leapseconds @@ -0,0 +1,82 @@ +# Allowance for leap seconds added to each time zone file. + +# This file is in the public domain. + +# This file is generated automatically from the data in the public-domain +# NIST format leap-seconds.list file, which can be copied from +# +# or . +# The NIST file is used instead of its IERS upstream counterpart +# +# because under US law the NIST file is public domain +# whereas the IERS file's copyright and license status is unclear. +# For more about leap-seconds.list, please see +# The NTP Timescale and Leap Seconds +# . + +# The rules for leap seconds are specified in Annex 1 (Time scales) of: +# Standard-frequency and time-signal emissions. +# International Telecommunication Union - Radiocommunication Sector +# (ITU-R) Recommendation TF.460-6 (02/2002) +# . +# The International Earth Rotation and Reference Systems Service (IERS) +# periodically uses leap seconds to keep UTC to within 0.9 s of UT1 +# (a proxy for Earth's angle in space as measured by astronomers) +# and publishes leap second data in a copyrighted file +# . +# See: Levine J. Coordinated Universal Time and the leap second. +# URSI Radio Sci Bull. 2016;89(4):30-6. doi:10.23919/URSIRSB.2016.7909995 +# . + +# There were no leap seconds before 1972, as no official mechanism +# accounted for the discrepancy between atomic time (TAI) and the earth's +# rotation. The first ("1 Jan 1972") data line in leap-seconds.list +# does not denote a leap second; it denotes the start of the current definition +# of UTC. + +# All leap-seconds are Stationary (S) at the given UTC time. +# The correction (+ or -) is made at the given time, so in the unlikely +# event of a negative leap second, a line would look like this: +# Leap YEAR MON DAY 23:59:59 - S +# Typical lines look like this: +# Leap YEAR MON DAY 23:59:60 + S +Leap 1972 Jun 30 23:59:60 + S +Leap 1972 Dec 31 23:59:60 + S +Leap 1973 Dec 31 23:59:60 + S +Leap 1974 Dec 31 23:59:60 + S +Leap 1975 Dec 31 23:59:60 + S +Leap 1976 Dec 31 23:59:60 + S +Leap 1977 Dec 31 23:59:60 + S +Leap 1978 Dec 31 23:59:60 + S +Leap 1979 Dec 31 23:59:60 + S +Leap 1981 Jun 30 23:59:60 + S +Leap 1982 Jun 30 23:59:60 + S +Leap 1983 Jun 30 23:59:60 + S +Leap 1985 Jun 30 23:59:60 + S +Leap 1987 Dec 31 23:59:60 + S +Leap 1989 Dec 31 23:59:60 + S +Leap 1990 Dec 31 23:59:60 + S +Leap 1992 Jun 30 23:59:60 + S +Leap 1993 Jun 30 23:59:60 + S +Leap 1994 Jun 30 23:59:60 + S +Leap 1995 Dec 31 23:59:60 + S +Leap 1997 Jun 30 23:59:60 + S +Leap 1998 Dec 31 23:59:60 + S +Leap 2005 Dec 31 23:59:60 + S +Leap 2008 Dec 31 23:59:60 + S +Leap 2012 Jun 30 23:59:60 + S +Leap 2015 Jun 30 23:59:60 + S +Leap 2016 Dec 31 23:59:60 + S + +# UTC timestamp when this leap second list expires. +# Any additional leap seconds will come after this. +# This Expires line is commented out for now, +# so that pre-2020a zic implementations do not reject this file. +#Expires 2022 Dec 28 00:00:00 + +# POSIX timestamps for the data in this file: +#updated 1467936000 (2016-07-08 00:00:00 UTC) +#expires 1672185600 (2022-12-28 00:00:00 UTC) + +# Updated through IERS Bulletin C63 +# File expires on: 28 December 2022 diff --git a/telegramer/include/pytz/zoneinfo/tzdata.zi b/telegramer/include/pytz/zoneinfo/tzdata.zi new file mode 100644 index 0000000..e21fc92 --- /dev/null +++ b/telegramer/include/pytz/zoneinfo/tzdata.zi @@ -0,0 +1,4437 @@ +# version unknown-dirty +# This zic input file is in the public domain. +R d 1916 o - Jun 14 23s 1 S +R d 1916 1919 - O Su>=1 23s 0 - +R d 1917 o - Mar 24 23s 1 S +R d 1918 o - Mar 9 23s 1 S +R d 1919 o - Mar 1 23s 1 S +R d 1920 o - F 14 23s 1 S +R d 1920 o - O 23 23s 0 - +R d 1921 o - Mar 14 23s 1 S +R d 1921 o - Jun 21 23s 0 - +R d 1939 o - S 11 23s 1 S +R d 1939 o - N 19 1 0 - +R d 1944 1945 - Ap M>=1 2 1 S +R d 1944 o - O 8 2 0 - +R d 1945 o - S 16 1 0 - +R d 1971 o - Ap 25 23s 1 S +R d 1971 o - S 26 23s 0 - +R d 1977 o - May 6 0 1 S +R d 1977 o - O 21 0 0 - +R d 1978 o - Mar 24 1 1 S +R d 1978 o - S 22 3 0 - +R d 1980 o - Ap 25 0 1 S +R d 1980 o - O 31 2 0 - +Z Africa/Algiers 0:12:12 - LMT 1891 Mar 16 +0:9:21 - PMT 1911 Mar 11 +0 d WE%sT 1940 F 25 2 +1 d CE%sT 1946 O 7 +0 - WET 1956 Ja 29 +1 - CET 1963 Ap 14 +0 d WE%sT 1977 O 21 +1 d CE%sT 1979 O 26 +0 d WE%sT 1981 May +1 - CET +Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u +-2 - -02 1942 S +-2 1 -01 1945 O 15 +-2 - -02 1975 N 25 2 +-1 - -01 +Z Africa/Ndjamena 1:0:12 - LMT 1912 +1 - WAT 1979 O 14 +1 1 WAST 1980 Mar 8 +1 - WAT +Z Africa/Abidjan -0:16:8 - LMT 1912 +0 - GMT +L Africa/Abidjan Africa/Accra +L Africa/Abidjan Africa/Bamako +L Africa/Abidjan Africa/Banjul +L Africa/Abidjan Africa/Conakry +L Africa/Abidjan Africa/Dakar +L Africa/Abidjan Africa/Freetown +L Africa/Abidjan Africa/Lome +L Africa/Abidjan Africa/Nouakchott +L Africa/Abidjan Africa/Ouagadougou +L Africa/Abidjan Atlantic/St_Helena +R K 1940 o - Jul 15 0 1 S +R K 1940 o - O 1 0 0 - +R K 1941 o - Ap 15 0 1 S +R K 1941 o - S 16 0 0 - +R K 1942 1944 - Ap 1 0 1 S +R K 1942 o - O 27 0 0 - +R K 1943 1945 - N 1 0 0 - +R K 1945 o - Ap 16 0 1 S +R K 1957 o - May 10 0 1 S +R K 1957 1958 - O 1 0 0 - +R K 1958 o - May 1 0 1 S +R K 1959 1981 - May 1 1 1 S +R K 1959 1965 - S 30 3 0 - +R K 1966 1994 - O 1 3 0 - +R K 1982 o - Jul 25 1 1 S +R K 1983 o - Jul 12 1 1 S +R K 1984 1988 - May 1 1 1 S +R K 1989 o - May 6 1 1 S +R K 1990 1994 - May 1 1 1 S +R K 1995 2010 - Ap lastF 0s 1 S +R K 1995 2005 - S lastTh 24 0 - +R K 2006 o - S 21 24 0 - +R K 2007 o - S Th>=1 24 0 - +R K 2008 o - Au lastTh 24 0 - +R K 2009 o - Au 20 24 0 - +R K 2010 o - Au 10 24 0 - +R K 2010 o - S 9 24 1 S +R K 2010 o - S lastTh 24 0 - +R K 2014 o - May 15 24 1 S +R K 2014 o - Jun 26 24 0 - +R K 2014 o - Jul 31 24 1 S +R K 2014 o - S lastTh 24 0 - +Z Africa/Cairo 2:5:9 - LMT 1900 O +2 K EE%sT +Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u +-1 - -01 1975 +0 - GMT +Z Africa/Nairobi 2:27:16 - LMT 1908 May +2:30 - +0230 1928 Jun 30 24 +3 - EAT 1930 Ja 4 24 +2:30 - +0230 1936 D 31 24 +2:45 - +0245 1942 Jul 31 24 +3 - EAT +L Africa/Nairobi Africa/Addis_Ababa +L Africa/Nairobi Africa/Asmara +L Africa/Nairobi Africa/Dar_es_Salaam +L Africa/Nairobi Africa/Djibouti +L Africa/Nairobi Africa/Kampala +L Africa/Nairobi Africa/Mogadishu +L Africa/Nairobi Indian/Antananarivo +L Africa/Nairobi Indian/Comoro +L Africa/Nairobi Indian/Mayotte +Z Africa/Monrovia -0:43:8 - LMT 1882 +-0:43:8 - MMT 1919 Mar +-0:44:30 - MMT 1972 Ja 7 +0 - GMT +R L 1951 o - O 14 2 1 S +R L 1952 o - Ja 1 0 0 - +R L 1953 o - O 9 2 1 S +R L 1954 o - Ja 1 0 0 - +R L 1955 o - S 30 0 1 S +R L 1956 o - Ja 1 0 0 - +R L 1982 1984 - Ap 1 0 1 S +R L 1982 1985 - O 1 0 0 - +R L 1985 o - Ap 6 0 1 S +R L 1986 o - Ap 4 0 1 S +R L 1986 o - O 3 0 0 - +R L 1987 1989 - Ap 1 0 1 S +R L 1987 1989 - O 1 0 0 - +R L 1997 o - Ap 4 0 1 S +R L 1997 o - O 4 0 0 - +R L 2013 o - Mar lastF 1 1 S +R L 2013 o - O lastF 2 0 - +Z Africa/Tripoli 0:52:44 - LMT 1920 +1 L CE%sT 1959 +2 - EET 1982 +1 L CE%sT 1990 May 4 +2 - EET 1996 S 30 +1 L CE%sT 1997 O 4 +2 - EET 2012 N 10 2 +1 L CE%sT 2013 O 25 2 +2 - EET +R MU 1982 o - O 10 0 1 - +R MU 1983 o - Mar 21 0 0 - +R MU 2008 o - O lastSu 2 1 - +R MU 2009 o - Mar lastSu 2 0 - +Z Indian/Mauritius 3:50 - LMT 1907 +4 MU +04/+05 +R M 1939 o - S 12 0 1 - +R M 1939 o - N 19 0 0 - +R M 1940 o - F 25 0 1 - +R M 1945 o - N 18 0 0 - +R M 1950 o - Jun 11 0 1 - +R M 1950 o - O 29 0 0 - +R M 1967 o - Jun 3 12 1 - +R M 1967 o - O 1 0 0 - +R M 1974 o - Jun 24 0 1 - +R M 1974 o - S 1 0 0 - +R M 1976 1977 - May 1 0 1 - +R M 1976 o - Au 1 0 0 - +R M 1977 o - S 28 0 0 - +R M 1978 o - Jun 1 0 1 - +R M 1978 o - Au 4 0 0 - +R M 2008 o - Jun 1 0 1 - +R M 2008 o - S 1 0 0 - +R M 2009 o - Jun 1 0 1 - +R M 2009 o - Au 21 0 0 - +R M 2010 o - May 2 0 1 - +R M 2010 o - Au 8 0 0 - +R M 2011 o - Ap 3 0 1 - +R M 2011 o - Jul 31 0 0 - +R M 2012 2013 - Ap lastSu 2 1 - +R M 2012 o - Jul 20 3 0 - +R M 2012 o - Au 20 2 1 - +R M 2012 o - S 30 3 0 - +R M 2013 o - Jul 7 3 0 - +R M 2013 o - Au 10 2 1 - +R M 2013 2018 - O lastSu 3 0 - +R M 2014 2018 - Mar lastSu 2 1 - +R M 2014 o - Jun 28 3 0 - +R M 2014 o - Au 2 2 1 - +R M 2015 o - Jun 14 3 0 - +R M 2015 o - Jul 19 2 1 - +R M 2016 o - Jun 5 3 0 - +R M 2016 o - Jul 10 2 1 - +R M 2017 o - May 21 3 0 - +R M 2017 o - Jul 2 2 1 - +R M 2018 o - May 13 3 0 - +R M 2018 o - Jun 17 2 1 - +R M 2019 o - May 5 3 -1 - +R M 2019 o - Jun 9 2 0 - +R M 2020 o - Ap 19 3 -1 - +R M 2020 o - May 31 2 0 - +R M 2021 o - Ap 11 3 -1 - +R M 2021 o - May 16 2 0 - +R M 2022 o - Mar 27 3 -1 - +R M 2022 o - May 8 2 0 - +R M 2023 o - Mar 19 3 -1 - +R M 2023 o - Ap 30 2 0 - +R M 2024 o - Mar 10 3 -1 - +R M 2024 o - Ap 14 2 0 - +R M 2025 o - F 23 3 -1 - +R M 2025 o - Ap 6 2 0 - +R M 2026 o - F 15 3 -1 - +R M 2026 o - Mar 22 2 0 - +R M 2027 o - F 7 3 -1 - +R M 2027 o - Mar 14 2 0 - +R M 2028 o - Ja 23 3 -1 - +R M 2028 o - Mar 5 2 0 - +R M 2029 o - Ja 14 3 -1 - +R M 2029 o - F 18 2 0 - +R M 2029 o - D 30 3 -1 - +R M 2030 o - F 10 2 0 - +R M 2030 o - D 22 3 -1 - +R M 2031 o - F 2 2 0 - +R M 2031 o - D 14 3 -1 - +R M 2032 o - Ja 18 2 0 - +R M 2032 o - N 28 3 -1 - +R M 2033 o - Ja 9 2 0 - +R M 2033 o - N 20 3 -1 - +R M 2033 o - D 25 2 0 - +R M 2034 o - N 5 3 -1 - +R M 2034 o - D 17 2 0 - +R M 2035 o - O 28 3 -1 - +R M 2035 o - D 9 2 0 - +R M 2036 o - O 19 3 -1 - +R M 2036 o - N 23 2 0 - +R M 2037 o - O 4 3 -1 - +R M 2037 o - N 15 2 0 - +R M 2038 o - S 26 3 -1 - +R M 2038 o - N 7 2 0 - +R M 2039 o - S 18 3 -1 - +R M 2039 o - O 23 2 0 - +R M 2040 o - S 2 3 -1 - +R M 2040 o - O 14 2 0 - +R M 2041 o - Au 25 3 -1 - +R M 2041 o - S 29 2 0 - +R M 2042 o - Au 10 3 -1 - +R M 2042 o - S 21 2 0 - +R M 2043 o - Au 2 3 -1 - +R M 2043 o - S 13 2 0 - +R M 2044 o - Jul 24 3 -1 - +R M 2044 o - Au 28 2 0 - +R M 2045 o - Jul 9 3 -1 - +R M 2045 o - Au 20 2 0 - +R M 2046 o - Jul 1 3 -1 - +R M 2046 o - Au 12 2 0 - +R M 2047 o - Jun 23 3 -1 - +R M 2047 o - Jul 28 2 0 - +R M 2048 o - Jun 7 3 -1 - +R M 2048 o - Jul 19 2 0 - +R M 2049 o - May 30 3 -1 - +R M 2049 o - Jul 4 2 0 - +R M 2050 o - May 15 3 -1 - +R M 2050 o - Jun 26 2 0 - +R M 2051 o - May 7 3 -1 - +R M 2051 o - Jun 18 2 0 - +R M 2052 o - Ap 28 3 -1 - +R M 2052 o - Jun 2 2 0 - +R M 2053 o - Ap 13 3 -1 - +R M 2053 o - May 25 2 0 - +R M 2054 o - Ap 5 3 -1 - +R M 2054 o - May 17 2 0 - +R M 2055 o - Mar 28 3 -1 - +R M 2055 o - May 2 2 0 - +R M 2056 o - Mar 12 3 -1 - +R M 2056 o - Ap 23 2 0 - +R M 2057 o - Mar 4 3 -1 - +R M 2057 o - Ap 8 2 0 - +R M 2058 o - F 17 3 -1 - +R M 2058 o - Mar 31 2 0 - +R M 2059 o - F 9 3 -1 - +R M 2059 o - Mar 23 2 0 - +R M 2060 o - F 1 3 -1 - +R M 2060 o - Mar 7 2 0 - +R M 2061 o - Ja 16 3 -1 - +R M 2061 o - F 27 2 0 - +R M 2062 o - Ja 8 3 -1 - +R M 2062 o - F 19 2 0 - +R M 2062 o - D 31 3 -1 - +R M 2063 o - F 4 2 0 - +R M 2063 o - D 16 3 -1 - +R M 2064 o - Ja 27 2 0 - +R M 2064 o - D 7 3 -1 - +R M 2065 o - Ja 11 2 0 - +R M 2065 o - N 22 3 -1 - +R M 2066 o - Ja 3 2 0 - +R M 2066 o - N 14 3 -1 - +R M 2066 o - D 26 2 0 - +R M 2067 o - N 6 3 -1 - +R M 2067 o - D 11 2 0 - +R M 2068 o - O 21 3 -1 - +R M 2068 o - D 2 2 0 - +R M 2069 o - O 13 3 -1 - +R M 2069 o - N 24 2 0 - +R M 2070 o - O 5 3 -1 - +R M 2070 o - N 9 2 0 - +R M 2071 o - S 20 3 -1 - +R M 2071 o - N 1 2 0 - +R M 2072 o - S 11 3 -1 - +R M 2072 o - O 16 2 0 - +R M 2073 o - Au 27 3 -1 - +R M 2073 o - O 8 2 0 - +R M 2074 o - Au 19 3 -1 - +R M 2074 o - S 30 2 0 - +R M 2075 o - Au 11 3 -1 - +R M 2075 o - S 15 2 0 - +R M 2076 o - Jul 26 3 -1 - +R M 2076 o - S 6 2 0 - +R M 2077 o - Jul 18 3 -1 - +R M 2077 o - Au 29 2 0 - +R M 2078 o - Jul 10 3 -1 - +R M 2078 o - Au 14 2 0 - +R M 2079 o - Jun 25 3 -1 - +R M 2079 o - Au 6 2 0 - +R M 2080 o - Jun 16 3 -1 - +R M 2080 o - Jul 21 2 0 - +R M 2081 o - Jun 1 3 -1 - +R M 2081 o - Jul 13 2 0 - +R M 2082 o - May 24 3 -1 - +R M 2082 o - Jul 5 2 0 - +R M 2083 o - May 16 3 -1 - +R M 2083 o - Jun 20 2 0 - +R M 2084 o - Ap 30 3 -1 - +R M 2084 o - Jun 11 2 0 - +R M 2085 o - Ap 22 3 -1 - +R M 2085 o - Jun 3 2 0 - +R M 2086 o - Ap 14 3 -1 - +R M 2086 o - May 19 2 0 - +R M 2087 o - Mar 30 3 -1 - +R M 2087 o - May 11 2 0 - +Z Africa/Casablanca -0:30:20 - LMT 1913 O 26 +0 M +00/+01 1984 Mar 16 +1 - +01 1986 +0 M +00/+01 2018 O 28 3 +1 M +01/+00 +Z Africa/El_Aaiun -0:52:48 - LMT 1934 +-1 - -01 1976 Ap 14 +0 M +00/+01 2018 O 28 3 +1 M +01/+00 +Z Africa/Maputo 2:10:20 - LMT 1903 Mar +2 - CAT +L Africa/Maputo Africa/Blantyre +L Africa/Maputo Africa/Bujumbura +L Africa/Maputo Africa/Gaborone +L Africa/Maputo Africa/Harare +L Africa/Maputo Africa/Kigali +L Africa/Maputo Africa/Lubumbashi +L Africa/Maputo Africa/Lusaka +R NA 1994 o - Mar 21 0 -1 WAT +R NA 1994 2017 - S Su>=1 2 0 CAT +R NA 1995 2017 - Ap Su>=1 2 -1 WAT +Z Africa/Windhoek 1:8:24 - LMT 1892 F 8 +1:30 - +0130 1903 Mar +2 - SAST 1942 S 20 2 +2 1 SAST 1943 Mar 21 2 +2 - SAST 1990 Mar 21 +2 NA %s +Z Africa/Lagos 0:13:35 - LMT 1905 Jul +0 - GMT 1908 Jul +0:13:35 - LMT 1914 +0:30 - +0030 1919 S +1 - WAT +L Africa/Lagos Africa/Bangui +L Africa/Lagos Africa/Brazzaville +L Africa/Lagos Africa/Douala +L Africa/Lagos Africa/Kinshasa +L Africa/Lagos Africa/Libreville +L Africa/Lagos Africa/Luanda +L Africa/Lagos Africa/Malabo +L Africa/Lagos Africa/Niamey +L Africa/Lagos Africa/Porto-Novo +Z Indian/Reunion 3:41:52 - LMT 1911 Jun +4 - +04 +Z Africa/Sao_Tome 0:26:56 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 - GMT 2018 Ja 1 1 +1 - WAT 2019 Ja 1 2 +0 - GMT +Z Indian/Mahe 3:41:48 - LMT 1907 +4 - +04 +R SA 1942 1943 - S Su>=15 2 1 - +R SA 1943 1944 - Mar Su>=15 2 0 - +Z Africa/Johannesburg 1:52 - LMT 1892 F 8 +1:30 - SAST 1903 Mar +2 SA SAST +L Africa/Johannesburg Africa/Maseru +L Africa/Johannesburg Africa/Mbabane +R SD 1970 o - May 1 0 1 S +R SD 1970 1985 - O 15 0 0 - +R SD 1971 o - Ap 30 0 1 S +R SD 1972 1985 - Ap lastSu 0 1 S +Z Africa/Khartoum 2:10:8 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT 2017 N +2 - CAT +Z Africa/Juba 2:6:28 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT 2021 F +2 - CAT +R n 1939 o - Ap 15 23s 1 S +R n 1939 o - N 18 23s 0 - +R n 1940 o - F 25 23s 1 S +R n 1941 o - O 6 0 0 - +R n 1942 o - Mar 9 0 1 S +R n 1942 o - N 2 3 0 - +R n 1943 o - Mar 29 2 1 S +R n 1943 o - Ap 17 2 0 - +R n 1943 o - Ap 25 2 1 S +R n 1943 o - O 4 2 0 - +R n 1944 1945 - Ap M>=1 2 1 S +R n 1944 o - O 8 0 0 - +R n 1945 o - S 16 0 0 - +R n 1977 o - Ap 30 0s 1 S +R n 1977 o - S 24 0s 0 - +R n 1978 o - May 1 0s 1 S +R n 1978 o - O 1 0s 0 - +R n 1988 o - Jun 1 0s 1 S +R n 1988 1990 - S lastSu 0s 0 - +R n 1989 o - Mar 26 0s 1 S +R n 1990 o - May 1 0s 1 S +R n 2005 o - May 1 0s 1 S +R n 2005 o - S 30 1s 0 - +R n 2006 2008 - Mar lastSu 2s 1 S +R n 2006 2008 - O lastSu 2s 0 - +Z Africa/Tunis 0:40:44 - LMT 1881 May 12 +0:9:21 - PMT 1911 Mar 11 +1 n CE%sT +Z Antarctica/Casey 0 - -00 1969 +8 - +08 2009 O 18 2 +11 - +11 2010 Mar 5 2 +8 - +08 2011 O 28 2 +11 - +11 2012 F 21 17u +8 - +08 2016 O 22 +11 - +11 2018 Mar 11 4 +8 - +08 2018 O 7 4 +11 - +11 2019 Mar 17 3 +8 - +08 2019 O 4 3 +11 - +11 2020 Mar 8 3 +8 - +08 2020 O 4 0:1 +11 - +11 +Z Antarctica/Davis 0 - -00 1957 Ja 13 +7 - +07 1964 N +0 - -00 1969 F +7 - +07 2009 O 18 2 +5 - +05 2010 Mar 10 20u +7 - +07 2011 O 28 2 +5 - +05 2012 F 21 20u +7 - +07 +Z Antarctica/Mawson 0 - -00 1954 F 13 +6 - +06 2009 O 18 2 +5 - +05 +Z Indian/Kerguelen 0 - -00 1950 +5 - +05 +R Tr 2005 ma - Mar lastSu 1u 2 +02 +R Tr 2004 ma - O lastSu 1u 0 +00 +Z Antarctica/Troll 0 - -00 2005 F 12 +0 Tr %s +Z Antarctica/Vostok 0 - -00 1957 D 16 +6 - +06 +Z Antarctica/Rothera 0 - -00 1976 D +-3 - -03 +Z Asia/Kabul 4:36:48 - LMT 1890 +4 - +04 1945 +4:30 - +0430 +R AM 2011 o - Mar lastSu 2s 1 - +R AM 2011 o - O lastSu 2s 0 - +Z Asia/Yerevan 2:58 - LMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1995 S 24 2s +4 - +04 1997 +4 R +04/+05 2011 +4 AM +04/+05 +R AZ 1997 2015 - Mar lastSu 4 1 - +R AZ 1997 2015 - O lastSu 5 0 - +Z Asia/Baku 3:19:24 - LMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1992 S lastSu 2s +4 - +04 1996 +4 E +04/+05 1997 +4 AZ +04/+05 +R BD 2009 o - Jun 19 23 1 - +R BD 2009 o - D 31 24 0 - +Z Asia/Dhaka 6:1:40 - LMT 1890 +5:53:20 - HMT 1941 O +6:30 - +0630 1942 May 15 +5:30 - +0530 1942 S +6:30 - +0630 1951 S 30 +6 - +06 2009 +6 BD +06/+07 +Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15 +5:30 - +0530 1987 O +6 - +06 +Z Indian/Chagos 4:49:40 - LMT 1907 +5 - +05 1996 +6 - +06 +Z Asia/Brunei 7:39:40 - LMT 1926 Mar +7:30 - +0730 1933 +8 - +08 +Z Asia/Yangon 6:24:47 - LMT 1880 +6:24:47 - RMT 1920 +6:30 - +0630 1942 May +9 - +09 1945 May 3 +6:30 - +0630 +R Sh 1919 o - Ap 12 24 1 D +R Sh 1919 o - S 30 24 0 S +R Sh 1940 o - Jun 1 0 1 D +R Sh 1940 o - O 12 24 0 S +R Sh 1941 o - Mar 15 0 1 D +R Sh 1941 o - N 1 24 0 S +R Sh 1942 o - Ja 31 0 1 D +R Sh 1945 o - S 1 24 0 S +R Sh 1946 o - May 15 0 1 D +R Sh 1946 o - S 30 24 0 S +R Sh 1947 o - Ap 15 0 1 D +R Sh 1947 o - O 31 24 0 S +R Sh 1948 1949 - May 1 0 1 D +R Sh 1948 1949 - S 30 24 0 S +R CN 1986 o - May 4 2 1 D +R CN 1986 1991 - S Su>=11 2 0 S +R CN 1987 1991 - Ap Su>=11 2 1 D +Z Asia/Shanghai 8:5:43 - LMT 1901 +8 Sh C%sT 1949 May 28 +8 CN C%sT +Z Asia/Urumqi 5:50:20 - LMT 1928 +6 - +06 +R HK 1946 o - Ap 21 0 1 S +R HK 1946 o - D 1 3:30s 0 - +R HK 1947 o - Ap 13 3:30s 1 S +R HK 1947 o - N 30 3:30s 0 - +R HK 1948 o - May 2 3:30s 1 S +R HK 1948 1952 - O Su>=28 3:30s 0 - +R HK 1949 1953 - Ap Su>=1 3:30 1 S +R HK 1953 1964 - O Su>=31 3:30 0 - +R HK 1954 1964 - Mar Su>=18 3:30 1 S +R HK 1965 1976 - Ap Su>=16 3:30 1 S +R HK 1965 1976 - O Su>=16 3:30 0 - +R HK 1973 o - D 30 3:30 1 S +R HK 1979 o - May 13 3:30 1 S +R HK 1979 o - O 21 3:30 0 - +Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 30 0:36:42 +8 - HKT 1941 Jun 15 3 +8 1 HKST 1941 O 1 4 +8 0:30 HKWT 1941 D 25 +9 - JST 1945 N 18 2 +8 HK HK%sT +R f 1946 o - May 15 0 1 D +R f 1946 o - O 1 0 0 S +R f 1947 o - Ap 15 0 1 D +R f 1947 o - N 1 0 0 S +R f 1948 1951 - May 1 0 1 D +R f 1948 1951 - O 1 0 0 S +R f 1952 o - Mar 1 0 1 D +R f 1952 1954 - N 1 0 0 S +R f 1953 1959 - Ap 1 0 1 D +R f 1955 1961 - O 1 0 0 S +R f 1960 1961 - Jun 1 0 1 D +R f 1974 1975 - Ap 1 0 1 D +R f 1974 1975 - O 1 0 0 S +R f 1979 o - Jul 1 0 1 D +R f 1979 o - O 1 0 0 S +Z Asia/Taipei 8:6 - LMT 1896 +8 - CST 1937 O +9 - JST 1945 S 21 1 +8 f C%sT +R _ 1942 1943 - Ap 30 23 1 - +R _ 1942 o - N 17 23 0 - +R _ 1943 o - S 30 23 0 S +R _ 1946 o - Ap 30 23s 1 D +R _ 1946 o - S 30 23s 0 S +R _ 1947 o - Ap 19 23s 1 D +R _ 1947 o - N 30 23s 0 S +R _ 1948 o - May 2 23s 1 D +R _ 1948 o - O 31 23s 0 S +R _ 1949 1950 - Ap Sa>=1 23s 1 D +R _ 1949 1950 - O lastSa 23s 0 S +R _ 1951 o - Mar 31 23s 1 D +R _ 1951 o - O 28 23s 0 S +R _ 1952 1953 - Ap Sa>=1 23s 1 D +R _ 1952 o - N 1 23s 0 S +R _ 1953 1954 - O lastSa 23s 0 S +R _ 1954 1956 - Mar Sa>=17 23s 1 D +R _ 1955 o - N 5 23s 0 S +R _ 1956 1964 - N Su>=1 3:30 0 S +R _ 1957 1964 - Mar Su>=18 3:30 1 D +R _ 1965 1973 - Ap Su>=16 3:30 1 D +R _ 1965 1966 - O Su>=16 2:30 0 S +R _ 1967 1976 - O Su>=16 3:30 0 S +R _ 1973 o - D 30 3:30 1 D +R _ 1975 1976 - Ap Su>=16 3:30 1 D +R _ 1979 o - May 13 3:30 1 D +R _ 1979 o - O Su>=16 3:30 0 S +Z Asia/Macau 7:34:10 - LMT 1904 O 30 +8 - CST 1941 D 21 23 +9 _ +09/+10 1945 S 30 24 +8 _ C%sT +R CY 1975 o - Ap 13 0 1 S +R CY 1975 o - O 12 0 0 - +R CY 1976 o - May 15 0 1 S +R CY 1976 o - O 11 0 0 - +R CY 1977 1980 - Ap Su>=1 0 1 S +R CY 1977 o - S 25 0 0 - +R CY 1978 o - O 2 0 0 - +R CY 1979 1997 - S lastSu 0 0 - +R CY 1981 1998 - Mar lastSu 0 1 S +Z Asia/Nicosia 2:13:28 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT +Z Asia/Famagusta 2:15:48 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT 2016 S 8 +3 - +03 2017 O 29 1u +2 E EE%sT +L Asia/Nicosia Europe/Nicosia +Z Asia/Tbilisi 2:59:11 - LMT 1880 +2:59:11 - TBMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1992 +3 e +03/+04 1994 S lastSu +4 e +04/+05 1996 O lastSu +4 1 +05 1997 Mar lastSu +4 e +04/+05 2004 Jun 27 +3 R +03/+04 2005 Mar lastSu 2 +4 - +04 +Z Asia/Dili 8:22:20 - LMT 1912 +8 - +08 1942 F 21 23 +9 - +09 1976 May 3 +8 - +08 2000 S 17 +9 - +09 +Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28 +5:53:20 - HMT 1870 +5:21:10 - MMT 1906 +5:30 - IST 1941 O +5:30 1 +0630 1942 May 15 +5:30 - IST 1942 S +5:30 1 +0630 1945 O 15 +5:30 - IST +Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10 +7:7:12 - BMT 1923 D 31 23:47:12 +7:20 - +0720 1932 N +7:30 - +0730 1942 Mar 23 +9 - +09 1945 S 23 +7:30 - +0730 1948 May +8 - +08 1950 May +7:30 - +0730 1964 +7 - WIB +Z Asia/Pontianak 7:17:20 - LMT 1908 May +7:17:20 - PMT 1932 N +7:30 - +0730 1942 Ja 29 +9 - +09 1945 S 23 +7:30 - +0730 1948 May +8 - +08 1950 May +7:30 - +0730 1964 +8 - WITA 1988 +7 - WIB +Z Asia/Makassar 7:57:36 - LMT 1920 +7:57:36 - MMT 1932 N +8 - +08 1942 F 9 +9 - +09 1945 S 23 +8 - WITA +Z Asia/Jayapura 9:22:48 - LMT 1932 N +9 - +09 1944 S +9:30 - +0930 1964 +9 - WIT +R i 1978 1980 - Mar 20 24 1 - +R i 1978 o - O 20 24 0 - +R i 1979 o - S 18 24 0 - +R i 1980 o - S 22 24 0 - +R i 1991 o - May 2 24 1 - +R i 1992 1995 - Mar 21 24 1 - +R i 1991 1995 - S 21 24 0 - +R i 1996 o - Mar 20 24 1 - +R i 1996 o - S 20 24 0 - +R i 1997 1999 - Mar 21 24 1 - +R i 1997 1999 - S 21 24 0 - +R i 2000 o - Mar 20 24 1 - +R i 2000 o - S 20 24 0 - +R i 2001 2003 - Mar 21 24 1 - +R i 2001 2003 - S 21 24 0 - +R i 2004 o - Mar 20 24 1 - +R i 2004 o - S 20 24 0 - +R i 2005 o - Mar 21 24 1 - +R i 2005 o - S 21 24 0 - +R i 2008 o - Mar 20 24 1 - +R i 2008 o - S 20 24 0 - +R i 2009 2011 - Mar 21 24 1 - +R i 2009 2011 - S 21 24 0 - +R i 2012 o - Mar 20 24 1 - +R i 2012 o - S 20 24 0 - +R i 2013 2015 - Mar 21 24 1 - +R i 2013 2015 - S 21 24 0 - +R i 2016 o - Mar 20 24 1 - +R i 2016 o - S 20 24 0 - +R i 2017 2019 - Mar 21 24 1 - +R i 2017 2019 - S 21 24 0 - +R i 2020 o - Mar 20 24 1 - +R i 2020 o - S 20 24 0 - +R i 2021 2023 - Mar 21 24 1 - +R i 2021 2023 - S 21 24 0 - +R i 2024 o - Mar 20 24 1 - +R i 2024 o - S 20 24 0 - +R i 2025 2027 - Mar 21 24 1 - +R i 2025 2027 - S 21 24 0 - +R i 2028 2029 - Mar 20 24 1 - +R i 2028 2029 - S 20 24 0 - +R i 2030 2031 - Mar 21 24 1 - +R i 2030 2031 - S 21 24 0 - +R i 2032 2033 - Mar 20 24 1 - +R i 2032 2033 - S 20 24 0 - +R i 2034 2035 - Mar 21 24 1 - +R i 2034 2035 - S 21 24 0 - +R i 2036 2037 - Mar 20 24 1 - +R i 2036 2037 - S 20 24 0 - +R i 2038 2039 - Mar 21 24 1 - +R i 2038 2039 - S 21 24 0 - +R i 2040 2041 - Mar 20 24 1 - +R i 2040 2041 - S 20 24 0 - +R i 2042 2043 - Mar 21 24 1 - +R i 2042 2043 - S 21 24 0 - +R i 2044 2045 - Mar 20 24 1 - +R i 2044 2045 - S 20 24 0 - +R i 2046 2047 - Mar 21 24 1 - +R i 2046 2047 - S 21 24 0 - +R i 2048 2049 - Mar 20 24 1 - +R i 2048 2049 - S 20 24 0 - +R i 2050 2051 - Mar 21 24 1 - +R i 2050 2051 - S 21 24 0 - +R i 2052 2053 - Mar 20 24 1 - +R i 2052 2053 - S 20 24 0 - +R i 2054 2055 - Mar 21 24 1 - +R i 2054 2055 - S 21 24 0 - +R i 2056 2057 - Mar 20 24 1 - +R i 2056 2057 - S 20 24 0 - +R i 2058 2059 - Mar 21 24 1 - +R i 2058 2059 - S 21 24 0 - +R i 2060 2062 - Mar 20 24 1 - +R i 2060 2062 - S 20 24 0 - +R i 2063 o - Mar 21 24 1 - +R i 2063 o - S 21 24 0 - +R i 2064 2066 - Mar 20 24 1 - +R i 2064 2066 - S 20 24 0 - +R i 2067 o - Mar 21 24 1 - +R i 2067 o - S 21 24 0 - +R i 2068 2070 - Mar 20 24 1 - +R i 2068 2070 - S 20 24 0 - +R i 2071 o - Mar 21 24 1 - +R i 2071 o - S 21 24 0 - +R i 2072 2074 - Mar 20 24 1 - +R i 2072 2074 - S 20 24 0 - +R i 2075 o - Mar 21 24 1 - +R i 2075 o - S 21 24 0 - +R i 2076 2078 - Mar 20 24 1 - +R i 2076 2078 - S 20 24 0 - +R i 2079 o - Mar 21 24 1 - +R i 2079 o - S 21 24 0 - +R i 2080 2082 - Mar 20 24 1 - +R i 2080 2082 - S 20 24 0 - +R i 2083 o - Mar 21 24 1 - +R i 2083 o - S 21 24 0 - +R i 2084 2086 - Mar 20 24 1 - +R i 2084 2086 - S 20 24 0 - +R i 2087 o - Mar 21 24 1 - +R i 2087 o - S 21 24 0 - +R i 2088 ma - Mar 20 24 1 - +R i 2088 ma - S 20 24 0 - +Z Asia/Tehran 3:25:44 - LMT 1916 +3:25:44 - TMT 1946 +3:30 - +0330 1977 N +4 i +04/+05 1979 +3:30 i +0330/+0430 +R IQ 1982 o - May 1 0 1 - +R IQ 1982 1984 - O 1 0 0 - +R IQ 1983 o - Mar 31 0 1 - +R IQ 1984 1985 - Ap 1 0 1 - +R IQ 1985 1990 - S lastSu 1s 0 - +R IQ 1986 1990 - Mar lastSu 1s 1 - +R IQ 1991 2007 - Ap 1 3s 1 - +R IQ 1991 2007 - O 1 3s 0 - +Z Asia/Baghdad 2:57:40 - LMT 1890 +2:57:36 - BMT 1918 +3 - +03 1982 May +3 IQ +03/+04 +R Z 1940 o - May 31 24u 1 D +R Z 1940 o - S 30 24u 0 S +R Z 1940 o - N 16 24u 1 D +R Z 1942 1946 - O 31 24u 0 S +R Z 1943 1944 - Mar 31 24u 1 D +R Z 1945 1946 - Ap 15 24u 1 D +R Z 1948 o - May 22 24u 2 DD +R Z 1948 o - Au 31 24u 1 D +R Z 1948 1949 - O 31 24u 0 S +R Z 1949 o - Ap 30 24u 1 D +R Z 1950 o - Ap 15 24u 1 D +R Z 1950 o - S 14 24u 0 S +R Z 1951 o - Mar 31 24u 1 D +R Z 1951 o - N 10 24u 0 S +R Z 1952 o - Ap 19 24u 1 D +R Z 1952 o - O 18 24u 0 S +R Z 1953 o - Ap 11 24u 1 D +R Z 1953 o - S 12 24u 0 S +R Z 1954 o - Jun 12 24u 1 D +R Z 1954 o - S 11 24u 0 S +R Z 1955 o - Jun 11 24u 1 D +R Z 1955 o - S 10 24u 0 S +R Z 1956 o - Jun 2 24u 1 D +R Z 1956 o - S 29 24u 0 S +R Z 1957 o - Ap 27 24u 1 D +R Z 1957 o - S 21 24u 0 S +R Z 1974 o - Jul 6 24 1 D +R Z 1974 o - O 12 24 0 S +R Z 1975 o - Ap 19 24 1 D +R Z 1975 o - Au 30 24 0 S +R Z 1980 o - Au 2 24s 1 D +R Z 1980 o - S 13 24s 0 S +R Z 1984 o - May 5 24s 1 D +R Z 1984 o - Au 25 24s 0 S +R Z 1985 o - Ap 13 24 1 D +R Z 1985 o - Au 31 24 0 S +R Z 1986 o - May 17 24 1 D +R Z 1986 o - S 6 24 0 S +R Z 1987 o - Ap 14 24 1 D +R Z 1987 o - S 12 24 0 S +R Z 1988 o - Ap 9 24 1 D +R Z 1988 o - S 3 24 0 S +R Z 1989 o - Ap 29 24 1 D +R Z 1989 o - S 2 24 0 S +R Z 1990 o - Mar 24 24 1 D +R Z 1990 o - Au 25 24 0 S +R Z 1991 o - Mar 23 24 1 D +R Z 1991 o - Au 31 24 0 S +R Z 1992 o - Mar 28 24 1 D +R Z 1992 o - S 5 24 0 S +R Z 1993 o - Ap 2 0 1 D +R Z 1993 o - S 5 0 0 S +R Z 1994 o - Ap 1 0 1 D +R Z 1994 o - Au 28 0 0 S +R Z 1995 o - Mar 31 0 1 D +R Z 1995 o - S 3 0 0 S +R Z 1996 o - Mar 14 24 1 D +R Z 1996 o - S 15 24 0 S +R Z 1997 o - Mar 20 24 1 D +R Z 1997 o - S 13 24 0 S +R Z 1998 o - Mar 20 0 1 D +R Z 1998 o - S 6 0 0 S +R Z 1999 o - Ap 2 2 1 D +R Z 1999 o - S 3 2 0 S +R Z 2000 o - Ap 14 2 1 D +R Z 2000 o - O 6 1 0 S +R Z 2001 o - Ap 9 1 1 D +R Z 2001 o - S 24 1 0 S +R Z 2002 o - Mar 29 1 1 D +R Z 2002 o - O 7 1 0 S +R Z 2003 o - Mar 28 1 1 D +R Z 2003 o - O 3 1 0 S +R Z 2004 o - Ap 7 1 1 D +R Z 2004 o - S 22 1 0 S +R Z 2005 2012 - Ap F<=1 2 1 D +R Z 2005 o - O 9 2 0 S +R Z 2006 o - O 1 2 0 S +R Z 2007 o - S 16 2 0 S +R Z 2008 o - O 5 2 0 S +R Z 2009 o - S 27 2 0 S +R Z 2010 o - S 12 2 0 S +R Z 2011 o - O 2 2 0 S +R Z 2012 o - S 23 2 0 S +R Z 2013 ma - Mar F>=23 2 1 D +R Z 2013 ma - O lastSu 2 0 S +Z Asia/Jerusalem 2:20:54 - LMT 1880 +2:20:40 - JMT 1918 +2 Z I%sT +R JP 1948 o - May Sa>=1 24 1 D +R JP 1948 1951 - S Sa>=8 25 0 S +R JP 1949 o - Ap Sa>=1 24 1 D +R JP 1950 1951 - May Sa>=1 24 1 D +Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u +9 JP J%sT +R J 1973 o - Jun 6 0 1 S +R J 1973 1975 - O 1 0 0 - +R J 1974 1977 - May 1 0 1 S +R J 1976 o - N 1 0 0 - +R J 1977 o - O 1 0 0 - +R J 1978 o - Ap 30 0 1 S +R J 1978 o - S 30 0 0 - +R J 1985 o - Ap 1 0 1 S +R J 1985 o - O 1 0 0 - +R J 1986 1988 - Ap F>=1 0 1 S +R J 1986 1990 - O F>=1 0 0 - +R J 1989 o - May 8 0 1 S +R J 1990 o - Ap 27 0 1 S +R J 1991 o - Ap 17 0 1 S +R J 1991 o - S 27 0 0 - +R J 1992 o - Ap 10 0 1 S +R J 1992 1993 - O F>=1 0 0 - +R J 1993 1998 - Ap F>=1 0 1 S +R J 1994 o - S F>=15 0 0 - +R J 1995 1998 - S F>=15 0s 0 - +R J 1999 o - Jul 1 0s 1 S +R J 1999 2002 - S lastF 0s 0 - +R J 2000 2001 - Mar lastTh 0s 1 S +R J 2002 2012 - Mar lastTh 24 1 S +R J 2003 o - O 24 0s 0 - +R J 2004 o - O 15 0s 0 - +R J 2005 o - S lastF 0s 0 - +R J 2006 2011 - O lastF 0s 0 - +R J 2013 o - D 20 0 0 - +R J 2014 2021 - Mar lastTh 24 1 S +R J 2014 ma - O lastF 0s 0 - +R J 2022 ma - F lastTh 24 1 S +Z Asia/Amman 2:23:44 - LMT 1931 +2 J EE%sT +Z Asia/Almaty 5:7:48 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 2004 O 31 2s +6 - +06 +Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1991 S 29 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 1992 Mar 29 2s +5 R +05/+06 2004 O 31 2s +6 - +06 2018 D 21 +5 - +05 +Z Asia/Qostanay 4:14:28 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2004 O 31 2s +6 - +06 +Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2004 O 31 2s +5 - +05 +Z Asia/Aqtau 3:21:4 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1994 S 25 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +Z Asia/Atyrau 3:27:44 - LMT 1924 May 2 +3 - +03 1930 Jun 21 +5 - +05 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1999 Mar 28 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +Z Asia/Oral 3:25:24 - LMT 1924 May 2 +3 - +03 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1989 Mar 26 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1992 Mar 29 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +R KG 1992 1996 - Ap Su>=7 0s 1 - +R KG 1992 1996 - S lastSu 0 0 - +R KG 1997 2005 - Mar lastSu 2:30 1 - +R KG 1997 2004 - O lastSu 2:30 0 - +Z Asia/Bishkek 4:58:24 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1991 Au 31 2 +5 KG +05/+06 2005 Au 12 +6 - +06 +R KR 1948 o - Jun 1 0 1 D +R KR 1948 o - S 12 24 0 S +R KR 1949 o - Ap 3 0 1 D +R KR 1949 1951 - S Sa>=7 24 0 S +R KR 1950 o - Ap 1 0 1 D +R KR 1951 o - May 6 0 1 D +R KR 1955 o - May 5 0 1 D +R KR 1955 o - S 8 24 0 S +R KR 1956 o - May 20 0 1 D +R KR 1956 o - S 29 24 0 S +R KR 1957 1960 - May Su>=1 0 1 D +R KR 1957 1960 - S Sa>=17 24 0 S +R KR 1987 1988 - May Su>=8 2 1 D +R KR 1987 1988 - O Su>=8 3 0 S +Z Asia/Seoul 8:27:52 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 S 8 +9 KR K%sT 1954 Mar 21 +8:30 KR K%sT 1961 Au 10 +9 KR K%sT +Z Asia/Pyongyang 8:23 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 Au 24 +9 - KST 2015 Au 15 +8:30 - KST 2018 May 4 23:30 +9 - KST +R l 1920 o - Mar 28 0 1 S +R l 1920 o - O 25 0 0 - +R l 1921 o - Ap 3 0 1 S +R l 1921 o - O 3 0 0 - +R l 1922 o - Mar 26 0 1 S +R l 1922 o - O 8 0 0 - +R l 1923 o - Ap 22 0 1 S +R l 1923 o - S 16 0 0 - +R l 1957 1961 - May 1 0 1 S +R l 1957 1961 - O 1 0 0 - +R l 1972 o - Jun 22 0 1 S +R l 1972 1977 - O 1 0 0 - +R l 1973 1977 - May 1 0 1 S +R l 1978 o - Ap 30 0 1 S +R l 1978 o - S 30 0 0 - +R l 1984 1987 - May 1 0 1 S +R l 1984 1991 - O 16 0 0 - +R l 1988 o - Jun 1 0 1 S +R l 1989 o - May 10 0 1 S +R l 1990 1992 - May 1 0 1 S +R l 1992 o - O 4 0 0 - +R l 1993 ma - Mar lastSu 0 1 S +R l 1993 1998 - S lastSu 0 0 - +R l 1999 ma - O lastSu 0 0 - +Z Asia/Beirut 2:22 - LMT 1880 +2 l EE%sT +R NB 1935 1941 - S 14 0 0:20 - +R NB 1935 1941 - D 14 0 0 - +Z Asia/Kuala_Lumpur 6:46:46 - LMT 1901 +6:55:25 - SMT 1905 Jun +7 - +07 1933 +7 0:20 +0720 1936 +7:20 - +0720 1941 S +7:30 - +0730 1942 F 16 +9 - +09 1945 S 12 +7:30 - +0730 1982 +8 - +08 +Z Asia/Kuching 7:21:20 - LMT 1926 Mar +7:30 - +0730 1933 +8 NB +08/+0820 1942 F 16 +9 - +09 1945 S 12 +8 - +08 +Z Indian/Maldives 4:54 - LMT 1880 +4:54 - MMT 1960 +5 - +05 +R X 1983 1984 - Ap 1 0 1 - +R X 1983 o - O 1 0 0 - +R X 1985 1998 - Mar lastSu 0 1 - +R X 1984 1998 - S lastSu 0 0 - +R X 2001 o - Ap lastSa 2 1 - +R X 2001 2006 - S lastSa 2 0 - +R X 2002 2006 - Mar lastSa 2 1 - +R X 2015 2016 - Mar lastSa 2 1 - +R X 2015 2016 - S lastSa 0 0 - +Z Asia/Hovd 6:6:36 - LMT 1905 Au +6 - +06 1978 +7 X +07/+08 +Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au +7 - +07 1978 +8 X +08/+09 +Z Asia/Choibalsan 7:38 - LMT 1905 Au +7 - +07 1978 +8 - +08 1983 Ap +9 X +09/+10 2008 Mar 31 +8 X +08/+09 +Z Asia/Kathmandu 5:41:16 - LMT 1920 +5:30 - +0530 1986 +5:45 - +0545 +R PK 2002 o - Ap Su>=2 0 1 S +R PK 2002 o - O Su>=2 0 0 - +R PK 2008 o - Jun 1 0 1 S +R PK 2008 2009 - N 1 0 0 - +R PK 2009 o - Ap 15 0 1 S +Z Asia/Karachi 4:28:12 - LMT 1907 +5:30 - +0530 1942 S +5:30 1 +0630 1945 O 15 +5:30 - +0530 1951 S 30 +5 - +05 1971 Mar 26 +5 PK PK%sT +R P 1999 2005 - Ap F>=15 0 1 S +R P 1999 2003 - O F>=15 0 0 - +R P 2004 o - O 1 1 0 - +R P 2005 o - O 4 2 0 - +R P 2006 2007 - Ap 1 0 1 S +R P 2006 o - S 22 0 0 - +R P 2007 o - S 13 2 0 - +R P 2008 2009 - Mar lastF 0 1 S +R P 2008 o - S 1 0 0 - +R P 2009 o - S 4 1 0 - +R P 2010 o - Mar 26 0 1 S +R P 2010 o - Au 11 0 0 - +R P 2011 o - Ap 1 0:1 1 S +R P 2011 o - Au 1 0 0 - +R P 2011 o - Au 30 0 1 S +R P 2011 o - S 30 0 0 - +R P 2012 2014 - Mar lastTh 24 1 S +R P 2012 o - S 21 1 0 - +R P 2013 o - S 27 0 0 - +R P 2014 o - O 24 0 0 - +R P 2015 o - Mar 28 0 1 S +R P 2015 o - O 23 1 0 - +R P 2016 2018 - Mar Sa>=24 1 1 S +R P 2016 2018 - O Sa>=24 1 0 - +R P 2019 o - Mar 29 0 1 S +R P 2019 o - O Sa>=24 0 0 - +R P 2020 2021 - Mar Sa>=24 0 1 S +R P 2020 o - O 24 1 0 - +R P 2021 ma - O F>=23 1 0 - +R P 2022 ma - Mar Su>=25 0 1 S +Z Asia/Gaza 2:17:52 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT 2008 Au 29 +2 - EET 2008 S +2 P EE%sT 2010 +2 - EET 2010 Mar 27 0:1 +2 P EE%sT 2011 Au +2 - EET 2012 +2 P EE%sT +Z Asia/Hebron 2:20:23 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT +R PH 1936 o - N 1 0 1 D +R PH 1937 o - F 1 0 0 S +R PH 1954 o - Ap 12 0 1 D +R PH 1954 o - Jul 1 0 0 S +R PH 1978 o - Mar 22 0 1 D +R PH 1978 o - S 21 0 0 S +Z Asia/Manila -15:56 - LMT 1844 D 31 +8:4 - LMT 1899 May 11 +8 PH P%sT 1942 May +9 - JST 1944 N +8 PH P%sT +Z Asia/Qatar 3:26:8 - LMT 1920 +4 - +04 1972 Jun +3 - +03 +L Asia/Qatar Asia/Bahrain +Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14 +3 - +03 +L Asia/Riyadh Antarctica/Syowa +L Asia/Riyadh Asia/Aden +L Asia/Riyadh Asia/Kuwait +Z Asia/Singapore 6:55:25 - LMT 1901 +6:55:25 - SMT 1905 Jun +7 - +07 1933 +7 0:20 +0720 1936 +7:20 - +0720 1941 S +7:30 - +0730 1942 F 16 +9 - +09 1945 S 12 +7:30 - +0730 1982 +8 - +08 +Z Asia/Colombo 5:19:24 - LMT 1880 +5:19:32 - MMT 1906 +5:30 - +0530 1942 Ja 5 +5:30 0:30 +06 1942 S +5:30 1 +0630 1945 O 16 2 +5:30 - +0530 1996 May 25 +6:30 - +0630 1996 O 26 0:30 +6 - +06 2006 Ap 15 0:30 +5:30 - +0530 +R S 1920 1923 - Ap Su>=15 2 1 S +R S 1920 1923 - O Su>=1 2 0 - +R S 1962 o - Ap 29 2 1 S +R S 1962 o - O 1 2 0 - +R S 1963 1965 - May 1 2 1 S +R S 1963 o - S 30 2 0 - +R S 1964 o - O 1 2 0 - +R S 1965 o - S 30 2 0 - +R S 1966 o - Ap 24 2 1 S +R S 1966 1976 - O 1 2 0 - +R S 1967 1978 - May 1 2 1 S +R S 1977 1978 - S 1 2 0 - +R S 1983 1984 - Ap 9 2 1 S +R S 1983 1984 - O 1 2 0 - +R S 1986 o - F 16 2 1 S +R S 1986 o - O 9 2 0 - +R S 1987 o - Mar 1 2 1 S +R S 1987 1988 - O 31 2 0 - +R S 1988 o - Mar 15 2 1 S +R S 1989 o - Mar 31 2 1 S +R S 1989 o - O 1 2 0 - +R S 1990 o - Ap 1 2 1 S +R S 1990 o - S 30 2 0 - +R S 1991 o - Ap 1 0 1 S +R S 1991 1992 - O 1 0 0 - +R S 1992 o - Ap 8 0 1 S +R S 1993 o - Mar 26 0 1 S +R S 1993 o - S 25 0 0 - +R S 1994 1996 - Ap 1 0 1 S +R S 1994 2005 - O 1 0 0 - +R S 1997 1998 - Mar lastM 0 1 S +R S 1999 2006 - Ap 1 0 1 S +R S 2006 o - S 22 0 0 - +R S 2007 o - Mar lastF 0 1 S +R S 2007 o - N F>=1 0 0 - +R S 2008 o - Ap F>=1 0 1 S +R S 2008 o - N 1 0 0 - +R S 2009 o - Mar lastF 0 1 S +R S 2010 2011 - Ap F>=1 0 1 S +R S 2012 ma - Mar lastF 0 1 S +R S 2009 ma - O lastF 0 0 - +Z Asia/Damascus 2:25:12 - LMT 1920 +2 S EE%sT +Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 1 +05/+06 1991 S 9 2s +5 - +05 +Z Asia/Bangkok 6:42:4 - LMT 1880 +6:42:4 - BMT 1920 Ap +7 - +07 +L Asia/Bangkok Asia/Phnom_Penh +L Asia/Bangkok Asia/Vientiane +Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 R +05/+06 1991 Mar 31 2 +4 R +04/+05 1992 Ja 19 2 +5 - +05 +Z Asia/Dubai 3:41:12 - LMT 1920 +4 - +04 +L Asia/Dubai Asia/Muscat +Z Asia/Samarkand 4:27:53 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1992 +5 - +05 +Z Asia/Tashkent 4:37:11 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2 +5 R +05/+06 1992 +5 - +05 +Z Asia/Ho_Chi_Minh 7:6:40 - LMT 1906 Jul +7:6:30 - PLMT 1911 May +7 - +07 1942 D 31 23 +8 - +08 1945 Mar 14 23 +9 - +09 1945 S 2 +7 - +07 1947 Ap +8 - +08 1955 Jul +7 - +07 1959 D 31 23 +8 - +08 1975 Jun 13 +7 - +07 +R AU 1917 o - Ja 1 2s 1 D +R AU 1917 o - Mar lastSu 2s 0 S +R AU 1942 o - Ja 1 2s 1 D +R AU 1942 o - Mar lastSu 2s 0 S +R AU 1942 o - S 27 2s 1 D +R AU 1943 1944 - Mar lastSu 2s 0 S +R AU 1943 o - O 3 2s 1 D +Z Australia/Darwin 8:43:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT +R AW 1974 o - O lastSu 2s 1 D +R AW 1975 o - Mar Su>=1 2s 0 S +R AW 1983 o - O lastSu 2s 1 D +R AW 1984 o - Mar Su>=1 2s 0 S +R AW 1991 o - N 17 2s 1 D +R AW 1992 o - Mar Su>=1 2s 0 S +R AW 2006 o - D 3 2s 1 D +R AW 2007 2009 - Mar lastSu 2s 0 S +R AW 2007 2008 - O lastSu 2s 1 D +Z Australia/Perth 7:43:24 - LMT 1895 D +8 AU AW%sT 1943 Jul +8 AW AW%sT +Z Australia/Eucla 8:35:28 - LMT 1895 D +8:45 AU +0845/+0945 1943 Jul +8:45 AW +0845/+0945 +R AQ 1971 o - O lastSu 2s 1 D +R AQ 1972 o - F lastSu 2s 0 S +R AQ 1989 1991 - O lastSu 2s 1 D +R AQ 1990 1992 - Mar Su>=1 2s 0 S +R Ho 1992 1993 - O lastSu 2s 1 D +R Ho 1993 1994 - Mar Su>=1 2s 0 S +Z Australia/Brisbane 10:12:8 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT +Z Australia/Lindeman 9:55:56 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT 1992 Jul +10 Ho AE%sT +R AS 1971 1985 - O lastSu 2s 1 D +R AS 1986 o - O 19 2s 1 D +R AS 1987 2007 - O lastSu 2s 1 D +R AS 1972 o - F 27 2s 0 S +R AS 1973 1985 - Mar Su>=1 2s 0 S +R AS 1986 1990 - Mar Su>=15 2s 0 S +R AS 1991 o - Mar 3 2s 0 S +R AS 1992 o - Mar 22 2s 0 S +R AS 1993 o - Mar 7 2s 0 S +R AS 1994 o - Mar 20 2s 0 S +R AS 1995 2005 - Mar lastSu 2s 0 S +R AS 2006 o - Ap 2 2s 0 S +R AS 2007 o - Mar lastSu 2s 0 S +R AS 2008 ma - Ap Su>=1 2s 0 S +R AS 2008 ma - O Su>=1 2s 1 D +Z Australia/Adelaide 9:14:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AS AC%sT +R AT 1916 o - O Su>=1 2s 1 D +R AT 1917 o - Mar lastSu 2s 0 S +R AT 1917 1918 - O Su>=22 2s 1 D +R AT 1918 1919 - Mar Su>=1 2s 0 S +R AT 1967 o - O Su>=1 2s 1 D +R AT 1968 o - Mar Su>=29 2s 0 S +R AT 1968 1985 - O lastSu 2s 1 D +R AT 1969 1971 - Mar Su>=8 2s 0 S +R AT 1972 o - F lastSu 2s 0 S +R AT 1973 1981 - Mar Su>=1 2s 0 S +R AT 1982 1983 - Mar lastSu 2s 0 S +R AT 1984 1986 - Mar Su>=1 2s 0 S +R AT 1986 o - O Su>=15 2s 1 D +R AT 1987 1990 - Mar Su>=15 2s 0 S +R AT 1987 o - O Su>=22 2s 1 D +R AT 1988 1990 - O lastSu 2s 1 D +R AT 1991 1999 - O Su>=1 2s 1 D +R AT 1991 2005 - Mar lastSu 2s 0 S +R AT 2000 o - Au lastSu 2s 1 D +R AT 2001 ma - O Su>=1 2s 1 D +R AT 2006 o - Ap Su>=1 2s 0 S +R AT 2007 o - Mar lastSu 2s 0 S +R AT 2008 ma - Ap Su>=1 2s 0 S +Z Australia/Hobart 9:49:16 - LMT 1895 S +10 AT AE%sT 1919 O 24 +10 AU AE%sT 1967 +10 AT AE%sT +R AV 1971 1985 - O lastSu 2s 1 D +R AV 1972 o - F lastSu 2s 0 S +R AV 1973 1985 - Mar Su>=1 2s 0 S +R AV 1986 1990 - Mar Su>=15 2s 0 S +R AV 1986 1987 - O Su>=15 2s 1 D +R AV 1988 1999 - O lastSu 2s 1 D +R AV 1991 1994 - Mar Su>=1 2s 0 S +R AV 1995 2005 - Mar lastSu 2s 0 S +R AV 2000 o - Au lastSu 2s 1 D +R AV 2001 2007 - O lastSu 2s 1 D +R AV 2006 o - Ap Su>=1 2s 0 S +R AV 2007 o - Mar lastSu 2s 0 S +R AV 2008 ma - Ap Su>=1 2s 0 S +R AV 2008 ma - O Su>=1 2s 1 D +Z Australia/Melbourne 9:39:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AV AE%sT +R AN 1971 1985 - O lastSu 2s 1 D +R AN 1972 o - F 27 2s 0 S +R AN 1973 1981 - Mar Su>=1 2s 0 S +R AN 1982 o - Ap Su>=1 2s 0 S +R AN 1983 1985 - Mar Su>=1 2s 0 S +R AN 1986 1989 - Mar Su>=15 2s 0 S +R AN 1986 o - O 19 2s 1 D +R AN 1987 1999 - O lastSu 2s 1 D +R AN 1990 1995 - Mar Su>=1 2s 0 S +R AN 1996 2005 - Mar lastSu 2s 0 S +R AN 2000 o - Au lastSu 2s 1 D +R AN 2001 2007 - O lastSu 2s 1 D +R AN 2006 o - Ap Su>=1 2s 0 S +R AN 2007 o - Mar lastSu 2s 0 S +R AN 2008 ma - Ap Su>=1 2s 0 S +R AN 2008 ma - O Su>=1 2s 1 D +Z Australia/Sydney 10:4:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AN AE%sT +Z Australia/Broken_Hill 9:25:48 - LMT 1895 F +10 - AEST 1896 Au 23 +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AN AC%sT 2000 +9:30 AS AC%sT +R LH 1981 1984 - O lastSu 2 1 - +R LH 1982 1985 - Mar Su>=1 2 0 - +R LH 1985 o - O lastSu 2 0:30 - +R LH 1986 1989 - Mar Su>=15 2 0 - +R LH 1986 o - O 19 2 0:30 - +R LH 1987 1999 - O lastSu 2 0:30 - +R LH 1990 1995 - Mar Su>=1 2 0 - +R LH 1996 2005 - Mar lastSu 2 0 - +R LH 2000 o - Au lastSu 2 0:30 - +R LH 2001 2007 - O lastSu 2 0:30 - +R LH 2006 o - Ap Su>=1 2 0 - +R LH 2007 o - Mar lastSu 2 0 - +R LH 2008 ma - Ap Su>=1 2 0 - +R LH 2008 ma - O Su>=1 2 0:30 - +Z Australia/Lord_Howe 10:36:20 - LMT 1895 F +10 - AEST 1981 Mar +10:30 LH +1030/+1130 1985 Jul +10:30 LH +1030/+11 +Z Antarctica/Macquarie 0 - -00 1899 N +10 - AEST 1916 O 1 2 +10 1 AEDT 1917 F +10 AU AE%sT 1919 Ap 1 0s +0 - -00 1948 Mar 25 +10 AU AE%sT 1967 +10 AT AE%sT 2010 +10 1 AEDT 2011 +10 AT AE%sT +Z Indian/Christmas 7:2:52 - LMT 1895 F +7 - +07 +Z Indian/Cocos 6:27:40 - LMT 1900 +6:30 - +0630 +R FJ 1998 1999 - N Su>=1 2 1 - +R FJ 1999 2000 - F lastSu 3 0 - +R FJ 2009 o - N 29 2 1 - +R FJ 2010 o - Mar lastSu 3 0 - +R FJ 2010 2013 - O Su>=21 2 1 - +R FJ 2011 o - Mar Su>=1 3 0 - +R FJ 2012 2013 - Ja Su>=18 3 0 - +R FJ 2014 o - Ja Su>=18 2 0 - +R FJ 2014 2018 - N Su>=1 2 1 - +R FJ 2015 2021 - Ja Su>=12 3 0 - +R FJ 2019 o - N Su>=8 2 1 - +R FJ 2020 o - D 20 2 1 - +R FJ 2022 ma - N Su>=8 2 1 - +R FJ 2023 ma - Ja Su>=12 3 0 - +Z Pacific/Fiji 11:55:44 - LMT 1915 O 26 +12 FJ +12/+13 +Z Pacific/Gambier -8:59:48 - LMT 1912 O +-9 - -09 +Z Pacific/Marquesas -9:18 - LMT 1912 O +-9:30 - -0930 +Z Pacific/Tahiti -9:58:16 - LMT 1912 O +-10 - -10 +R Gu 1959 o - Jun 27 2 1 D +R Gu 1961 o - Ja 29 2 0 S +R Gu 1967 o - S 1 2 1 D +R Gu 1969 o - Ja 26 0:1 0 S +R Gu 1969 o - Jun 22 2 1 D +R Gu 1969 o - Au 31 2 0 S +R Gu 1970 1971 - Ap lastSu 2 1 D +R Gu 1970 1971 - S Su>=1 2 0 S +R Gu 1973 o - D 16 2 1 D +R Gu 1974 o - F 24 2 0 S +R Gu 1976 o - May 26 2 1 D +R Gu 1976 o - Au 22 2:1 0 S +R Gu 1977 o - Ap 24 2 1 D +R Gu 1977 o - Au 28 2 0 S +Z Pacific/Guam -14:21 - LMT 1844 D 31 +9:39 - LMT 1901 +10 - GST 1941 D 10 +9 - +09 1944 Jul 31 +10 Gu G%sT 2000 D 23 +10 - ChST +L Pacific/Guam Pacific/Saipan +Z Pacific/Tarawa 11:32:4 - LMT 1901 +12 - +12 +Z Pacific/Kanton 0 - -00 1937 Au 31 +-12 - -12 1979 O +-11 - -11 1994 D 31 +13 - +13 +Z Pacific/Kiritimati -10:29:20 - LMT 1901 +-10:40 - -1040 1979 O +-10 - -10 1994 D 31 +14 - +14 +Z Pacific/Majuro 11:24:48 - LMT 1901 +11 - +11 1914 O +9 - +09 1919 F +11 - +11 1937 +10 - +10 1941 Ap +9 - +09 1944 Ja 30 +11 - +11 1969 O +12 - +12 +Z Pacific/Kwajalein 11:9:20 - LMT 1901 +11 - +11 1937 +10 - +10 1941 Ap +9 - +09 1944 F 6 +11 - +11 1969 O +-12 - -12 1993 Au 20 24 +12 - +12 +Z Pacific/Chuuk -13:52:52 - LMT 1844 D 31 +10:7:8 - LMT 1901 +10 - +10 1914 O +9 - +09 1919 F +10 - +10 1941 Ap +9 - +09 1945 Au +10 - +10 +Z Pacific/Pohnpei -13:27:8 - LMT 1844 D 31 +10:32:52 - LMT 1901 +11 - +11 1914 O +9 - +09 1919 F +11 - +11 1937 +10 - +10 1941 Ap +9 - +09 1945 Au +11 - +11 +Z Pacific/Kosrae -13:8:4 - LMT 1844 D 31 +10:51:56 - LMT 1901 +11 - +11 1914 O +9 - +09 1919 F +11 - +11 1937 +10 - +10 1941 Ap +9 - +09 1945 Au +11 - +11 1969 O +12 - +12 1999 +11 - +11 +Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15 +11:30 - +1130 1942 Au 29 +9 - +09 1945 S 8 +11:30 - +1130 1979 F 10 2 +12 - +12 +R NC 1977 1978 - D Su>=1 0 1 - +R NC 1978 1979 - F 27 0 0 - +R NC 1996 o - D 1 2s 1 - +R NC 1997 o - Mar 2 2s 0 - +Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13 +11 NC +11/+12 +R NZ 1927 o - N 6 2 1 S +R NZ 1928 o - Mar 4 2 0 M +R NZ 1928 1933 - O Su>=8 2 0:30 S +R NZ 1929 1933 - Mar Su>=15 2 0 M +R NZ 1934 1940 - Ap lastSu 2 0 M +R NZ 1934 1940 - S lastSu 2 0:30 S +R NZ 1946 o - Ja 1 0 0 S +R NZ 1974 o - N Su>=1 2s 1 D +R k 1974 o - N Su>=1 2:45s 1 - +R NZ 1975 o - F lastSu 2s 0 S +R k 1975 o - F lastSu 2:45s 0 - +R NZ 1975 1988 - O lastSu 2s 1 D +R k 1975 1988 - O lastSu 2:45s 1 - +R NZ 1976 1989 - Mar Su>=1 2s 0 S +R k 1976 1989 - Mar Su>=1 2:45s 0 - +R NZ 1989 o - O Su>=8 2s 1 D +R k 1989 o - O Su>=8 2:45s 1 - +R NZ 1990 2006 - O Su>=1 2s 1 D +R k 1990 2006 - O Su>=1 2:45s 1 - +R NZ 1990 2007 - Mar Su>=15 2s 0 S +R k 1990 2007 - Mar Su>=15 2:45s 0 - +R NZ 2007 ma - S lastSu 2s 1 D +R k 2007 ma - S lastSu 2:45s 1 - +R NZ 2008 ma - Ap Su>=1 2s 0 S +R k 2008 ma - Ap Su>=1 2:45s 0 - +Z Pacific/Auckland 11:39:4 - LMT 1868 N 2 +11:30 NZ NZ%sT 1946 +12 NZ NZ%sT +Z Pacific/Chatham 12:13:48 - LMT 1868 N 2 +12:15 - +1215 1946 +12:45 k +1245/+1345 +L Pacific/Auckland Antarctica/McMurdo +R CK 1978 o - N 12 0 0:30 - +R CK 1979 1991 - Mar Su>=1 0 0 - +R CK 1979 1990 - O lastSu 0 0:30 - +Z Pacific/Rarotonga 13:20:56 - LMT 1899 D 26 +-10:39:4 - LMT 1952 O 16 +-10:30 - -1030 1978 N 12 +-10 CK -10/-0930 +Z Pacific/Niue -11:19:40 - LMT 1952 O 16 +-11:20 - -1120 1964 Jul +-11 - -11 +Z Pacific/Norfolk 11:11:52 - LMT 1901 +11:12 - +1112 1951 +11:30 - +1130 1974 O 27 2s +11:30 1 +1230 1975 Mar 2 2s +11:30 - +1130 2015 O 4 2s +11 - +11 2019 Jul +11 AN +11/+12 +Z Pacific/Palau -15:2:4 - LMT 1844 D 31 +8:57:56 - LMT 1901 +9 - +09 +Z Pacific/Port_Moresby 9:48:40 - LMT 1880 +9:48:32 - PMMT 1895 +10 - +10 +L Pacific/Port_Moresby Antarctica/DumontDUrville +Z Pacific/Bougainville 10:22:16 - LMT 1880 +9:48:32 - PMMT 1895 +10 - +10 1942 Jul +9 - +09 1945 Au 21 +10 - +10 2014 D 28 2 +11 - +11 +Z Pacific/Pitcairn -8:40:20 - LMT 1901 +-8:30 - -0830 1998 Ap 27 +-8 - -08 +Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5 +-11:22:48 - LMT 1911 +-11 - SST +L Pacific/Pago_Pago Pacific/Midway +R WS 2010 o - S lastSu 0 1 - +R WS 2011 o - Ap Sa>=1 4 0 - +R WS 2011 o - S lastSa 3 1 - +R WS 2012 2021 - Ap Su>=1 4 0 - +R WS 2012 2020 - S lastSu 3 1 - +Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5 +-11:26:56 - LMT 1911 +-11:30 - -1130 1950 +-11 WS -11/-10 2011 D 29 24 +13 WS +13/+14 +Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O +11 - +11 +Z Pacific/Fakaofo -11:24:56 - LMT 1901 +-11 - -11 2011 D 30 +13 - +13 +R TO 1999 o - O 7 2s 1 - +R TO 2000 o - Mar 19 2s 0 - +R TO 2000 2001 - N Su>=1 2 1 - +R TO 2001 2002 - Ja lastSu 2 0 - +R TO 2016 o - N Su>=1 2 1 - +R TO 2017 o - Ja Su>=15 3 0 - +Z Pacific/Tongatapu 12:19:12 - LMT 1945 S 10 +12:20 - +1220 1961 +13 - +13 1999 +13 TO +13/+14 +Z Pacific/Funafuti 11:56:52 - LMT 1901 +12 - +12 +Z Pacific/Wake 11:6:28 - LMT 1901 +12 - +12 +R VU 1973 o - D 22 12u 1 - +R VU 1974 o - Mar 30 12u 0 - +R VU 1983 1991 - S Sa>=22 24 1 - +R VU 1984 1991 - Mar Sa>=22 24 0 - +R VU 1992 1993 - Ja Sa>=22 24 0 - +R VU 1992 o - O Sa>=22 24 1 - +Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13 +11 VU +11/+12 +Z Pacific/Wallis 12:15:20 - LMT 1901 +12 - +12 +R G 1916 o - May 21 2s 1 BST +R G 1916 o - O 1 2s 0 GMT +R G 1917 o - Ap 8 2s 1 BST +R G 1917 o - S 17 2s 0 GMT +R G 1918 o - Mar 24 2s 1 BST +R G 1918 o - S 30 2s 0 GMT +R G 1919 o - Mar 30 2s 1 BST +R G 1919 o - S 29 2s 0 GMT +R G 1920 o - Mar 28 2s 1 BST +R G 1920 o - O 25 2s 0 GMT +R G 1921 o - Ap 3 2s 1 BST +R G 1921 o - O 3 2s 0 GMT +R G 1922 o - Mar 26 2s 1 BST +R G 1922 o - O 8 2s 0 GMT +R G 1923 o - Ap Su>=16 2s 1 BST +R G 1923 1924 - S Su>=16 2s 0 GMT +R G 1924 o - Ap Su>=9 2s 1 BST +R G 1925 1926 - Ap Su>=16 2s 1 BST +R G 1925 1938 - O Su>=2 2s 0 GMT +R G 1927 o - Ap Su>=9 2s 1 BST +R G 1928 1929 - Ap Su>=16 2s 1 BST +R G 1930 o - Ap Su>=9 2s 1 BST +R G 1931 1932 - Ap Su>=16 2s 1 BST +R G 1933 o - Ap Su>=9 2s 1 BST +R G 1934 o - Ap Su>=16 2s 1 BST +R G 1935 o - Ap Su>=9 2s 1 BST +R G 1936 1937 - Ap Su>=16 2s 1 BST +R G 1938 o - Ap Su>=9 2s 1 BST +R G 1939 o - Ap Su>=16 2s 1 BST +R G 1939 o - N Su>=16 2s 0 GMT +R G 1940 o - F Su>=23 2s 1 BST +R G 1941 o - May Su>=2 1s 2 BDST +R G 1941 1943 - Au Su>=9 1s 1 BST +R G 1942 1944 - Ap Su>=2 1s 2 BDST +R G 1944 o - S Su>=16 1s 1 BST +R G 1945 o - Ap M>=2 1s 2 BDST +R G 1945 o - Jul Su>=9 1s 1 BST +R G 1945 1946 - O Su>=2 2s 0 GMT +R G 1946 o - Ap Su>=9 2s 1 BST +R G 1947 o - Mar 16 2s 1 BST +R G 1947 o - Ap 13 1s 2 BDST +R G 1947 o - Au 10 1s 1 BST +R G 1947 o - N 2 2s 0 GMT +R G 1948 o - Mar 14 2s 1 BST +R G 1948 o - O 31 2s 0 GMT +R G 1949 o - Ap 3 2s 1 BST +R G 1949 o - O 30 2s 0 GMT +R G 1950 1952 - Ap Su>=14 2s 1 BST +R G 1950 1952 - O Su>=21 2s 0 GMT +R G 1953 o - Ap Su>=16 2s 1 BST +R G 1953 1960 - O Su>=2 2s 0 GMT +R G 1954 o - Ap Su>=9 2s 1 BST +R G 1955 1956 - Ap Su>=16 2s 1 BST +R G 1957 o - Ap Su>=9 2s 1 BST +R G 1958 1959 - Ap Su>=16 2s 1 BST +R G 1960 o - Ap Su>=9 2s 1 BST +R G 1961 1963 - Mar lastSu 2s 1 BST +R G 1961 1968 - O Su>=23 2s 0 GMT +R G 1964 1967 - Mar Su>=19 2s 1 BST +R G 1968 o - F 18 2s 1 BST +R G 1972 1980 - Mar Su>=16 2s 1 BST +R G 1972 1980 - O Su>=23 2s 0 GMT +R G 1981 1995 - Mar lastSu 1u 1 BST +R G 1981 1989 - O Su>=23 1u 0 GMT +R G 1990 1995 - O Su>=22 1u 0 GMT +Z Europe/London -0:1:15 - LMT 1847 D 1 0s +0 G %s 1968 O 27 +1 - BST 1971 O 31 2u +0 G %s 1996 +0 E GMT/BST +L Europe/London Europe/Jersey +L Europe/London Europe/Guernsey +L Europe/London Europe/Isle_of_Man +R IE 1971 o - O 31 2u -1 - +R IE 1972 1980 - Mar Su>=16 2u 0 - +R IE 1972 1980 - O Su>=23 2u -1 - +R IE 1981 ma - Mar lastSu 1u 0 - +R IE 1981 1989 - O Su>=23 1u -1 - +R IE 1990 1995 - O Su>=22 1u -1 - +R IE 1996 ma - O lastSu 1u -1 - +Z Europe/Dublin -0:25 - LMT 1880 Au 2 +-0:25:21 - DMT 1916 May 21 2s +-0:25:21 1 IST 1916 O 1 2s +0 G %s 1921 D 6 +0 G GMT/IST 1940 F 25 2s +0 1 IST 1946 O 6 2s +0 - GMT 1947 Mar 16 2s +0 1 IST 1947 N 2 2s +0 - GMT 1948 Ap 18 2s +0 G GMT/IST 1968 O 27 +1 IE IST/GMT +R E 1977 1980 - Ap Su>=1 1u 1 S +R E 1977 o - S lastSu 1u 0 - +R E 1978 o - O 1 1u 0 - +R E 1979 1995 - S lastSu 1u 0 - +R E 1981 ma - Mar lastSu 1u 1 S +R E 1996 ma - O lastSu 1u 0 - +R W- 1977 1980 - Ap Su>=1 1s 1 S +R W- 1977 o - S lastSu 1s 0 - +R W- 1978 o - O 1 1s 0 - +R W- 1979 1995 - S lastSu 1s 0 - +R W- 1981 ma - Mar lastSu 1s 1 S +R W- 1996 ma - O lastSu 1s 0 - +R c 1916 o - Ap 30 23 1 S +R c 1916 o - O 1 1 0 - +R c 1917 1918 - Ap M>=15 2s 1 S +R c 1917 1918 - S M>=15 2s 0 - +R c 1940 o - Ap 1 2s 1 S +R c 1942 o - N 2 2s 0 - +R c 1943 o - Mar 29 2s 1 S +R c 1943 o - O 4 2s 0 - +R c 1944 1945 - Ap M>=1 2s 1 S +R c 1944 o - O 2 2s 0 - +R c 1945 o - S 16 2s 0 - +R c 1977 1980 - Ap Su>=1 2s 1 S +R c 1977 o - S lastSu 2s 0 - +R c 1978 o - O 1 2s 0 - +R c 1979 1995 - S lastSu 2s 0 - +R c 1981 ma - Mar lastSu 2s 1 S +R c 1996 ma - O lastSu 2s 0 - +R e 1977 1980 - Ap Su>=1 0 1 S +R e 1977 o - S lastSu 0 0 - +R e 1978 o - O 1 0 0 - +R e 1979 1995 - S lastSu 0 0 - +R e 1981 ma - Mar lastSu 0 1 S +R e 1996 ma - O lastSu 0 0 - +R R 1917 o - Jul 1 23 1 MST +R R 1917 o - D 28 0 0 MMT +R R 1918 o - May 31 22 2 MDST +R R 1918 o - S 16 1 1 MST +R R 1919 o - May 31 23 2 MDST +R R 1919 o - Jul 1 0u 1 MSD +R R 1919 o - Au 16 0 0 MSK +R R 1921 o - F 14 23 1 MSD +R R 1921 o - Mar 20 23 2 +05 +R R 1921 o - S 1 0 1 MSD +R R 1921 o - O 1 0 0 - +R R 1981 1984 - Ap 1 0 1 S +R R 1981 1983 - O 1 0 0 - +R R 1984 1995 - S lastSu 2s 0 - +R R 1985 2010 - Mar lastSu 2s 1 S +R R 1996 2010 - O lastSu 2s 0 - +Z WET 0 E WE%sT +Z CET 1 c CE%sT +Z MET 1 c ME%sT +Z EET 2 E EE%sT +R q 1940 o - Jun 16 0 1 S +R q 1942 o - N 2 3 0 - +R q 1943 o - Mar 29 2 1 S +R q 1943 o - Ap 10 3 0 - +R q 1974 o - May 4 0 1 S +R q 1974 o - O 2 0 0 - +R q 1975 o - May 1 0 1 S +R q 1975 o - O 2 0 0 - +R q 1976 o - May 2 0 1 S +R q 1976 o - O 3 0 0 - +R q 1977 o - May 8 0 1 S +R q 1977 o - O 2 0 0 - +R q 1978 o - May 6 0 1 S +R q 1978 o - O 1 0 0 - +R q 1979 o - May 5 0 1 S +R q 1979 o - S 30 0 0 - +R q 1980 o - May 3 0 1 S +R q 1980 o - O 4 0 0 - +R q 1981 o - Ap 26 0 1 S +R q 1981 o - S 27 0 0 - +R q 1982 o - May 2 0 1 S +R q 1982 o - O 3 0 0 - +R q 1983 o - Ap 18 0 1 S +R q 1983 o - O 1 0 0 - +R q 1984 o - Ap 1 0 1 S +Z Europe/Tirane 1:19:20 - LMT 1914 +1 - CET 1940 Jun 16 +1 q CE%sT 1984 Jul +1 E CE%sT +Z Europe/Andorra 0:6:4 - LMT 1901 +0 - WET 1946 S 30 +1 - CET 1985 Mar 31 2 +1 E CE%sT +R a 1920 o - Ap 5 2s 1 S +R a 1920 o - S 13 2s 0 - +R a 1946 o - Ap 14 2s 1 S +R a 1946 o - O 7 2s 0 - +R a 1947 1948 - O Su>=1 2s 0 - +R a 1947 o - Ap 6 2s 1 S +R a 1948 o - Ap 18 2s 1 S +R a 1980 o - Ap 6 0 1 S +R a 1980 o - S 28 0 0 - +Z Europe/Vienna 1:5:21 - LMT 1893 Ap +1 c CE%sT 1920 +1 a CE%sT 1940 Ap 1 2s +1 c CE%sT 1945 Ap 2 2s +1 1 CEST 1945 Ap 12 2s +1 - CET 1946 +1 a CE%sT 1981 +1 E CE%sT +Z Europe/Minsk 1:50:16 - LMT 1880 +1:50 - MMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 Jun 28 +1 c CE%sT 1944 Jul 3 +3 R MSK/MSD 1990 +3 - MSK 1991 Mar 31 2s +2 R EE%sT 2011 Mar 27 2s +3 - +03 +R b 1918 o - Mar 9 0s 1 S +R b 1918 1919 - O Sa>=1 23s 0 - +R b 1919 o - Mar 1 23s 1 S +R b 1920 o - F 14 23s 1 S +R b 1920 o - O 23 23s 0 - +R b 1921 o - Mar 14 23s 1 S +R b 1921 o - O 25 23s 0 - +R b 1922 o - Mar 25 23s 1 S +R b 1922 1927 - O Sa>=1 23s 0 - +R b 1923 o - Ap 21 23s 1 S +R b 1924 o - Mar 29 23s 1 S +R b 1925 o - Ap 4 23s 1 S +R b 1926 o - Ap 17 23s 1 S +R b 1927 o - Ap 9 23s 1 S +R b 1928 o - Ap 14 23s 1 S +R b 1928 1938 - O Su>=2 2s 0 - +R b 1929 o - Ap 21 2s 1 S +R b 1930 o - Ap 13 2s 1 S +R b 1931 o - Ap 19 2s 1 S +R b 1932 o - Ap 3 2s 1 S +R b 1933 o - Mar 26 2s 1 S +R b 1934 o - Ap 8 2s 1 S +R b 1935 o - Mar 31 2s 1 S +R b 1936 o - Ap 19 2s 1 S +R b 1937 o - Ap 4 2s 1 S +R b 1938 o - Mar 27 2s 1 S +R b 1939 o - Ap 16 2s 1 S +R b 1939 o - N 19 2s 0 - +R b 1940 o - F 25 2s 1 S +R b 1944 o - S 17 2s 0 - +R b 1945 o - Ap 2 2s 1 S +R b 1945 o - S 16 2s 0 - +R b 1946 o - May 19 2s 1 S +R b 1946 o - O 7 2s 0 - +Z Europe/Brussels 0:17:30 - LMT 1880 +0:17:30 - BMT 1892 May 1 0:17:30 +0 - WET 1914 N 8 +1 - CET 1916 May +1 c CE%sT 1918 N 11 11u +0 b WE%sT 1940 May 20 2s +1 c CE%sT 1944 S 3 +1 b CE%sT 1977 +1 E CE%sT +R BG 1979 o - Mar 31 23 1 S +R BG 1979 o - O 1 1 0 - +R BG 1980 1982 - Ap Sa>=1 23 1 S +R BG 1980 o - S 29 1 0 - +R BG 1981 o - S 27 2 0 - +Z Europe/Sofia 1:33:16 - LMT 1880 +1:56:56 - IMT 1894 N 30 +2 - EET 1942 N 2 3 +1 c CE%sT 1945 +1 - CET 1945 Ap 2 3 +2 - EET 1979 Mar 31 23 +2 BG EE%sT 1982 S 26 3 +2 c EE%sT 1991 +2 e EE%sT 1997 +2 E EE%sT +R CZ 1945 o - Ap M>=1 2s 1 S +R CZ 1945 o - O 1 2s 0 - +R CZ 1946 o - May 6 2s 1 S +R CZ 1946 1949 - O Su>=1 2s 0 - +R CZ 1947 1948 - Ap Su>=15 2s 1 S +R CZ 1949 o - Ap 9 2s 1 S +Z Europe/Prague 0:57:44 - LMT 1850 +0:57:44 - PMT 1891 O +1 c CE%sT 1945 May 9 +1 CZ CE%sT 1946 D 1 3 +1 -1 GMT 1947 F 23 2 +1 CZ CE%sT 1979 +1 E CE%sT +R D 1916 o - May 14 23 1 S +R D 1916 o - S 30 23 0 - +R D 1940 o - May 15 0 1 S +R D 1945 o - Ap 2 2s 1 S +R D 1945 o - Au 15 2s 0 - +R D 1946 o - May 1 2s 1 S +R D 1946 o - S 1 2s 0 - +R D 1947 o - May 4 2s 1 S +R D 1947 o - Au 10 2s 0 - +R D 1948 o - May 9 2s 1 S +R D 1948 o - Au 8 2s 0 - +Z Europe/Copenhagen 0:50:20 - LMT 1890 +0:50:20 - CMT 1894 +1 D CE%sT 1942 N 2 2s +1 c CE%sT 1945 Ap 2 2 +1 D CE%sT 1980 +1 E CE%sT +Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11 +0 - WET 1981 +0 E WE%sT +R Th 1991 1992 - Mar lastSu 2 1 D +R Th 1991 1992 - S lastSu 2 0 S +R Th 1993 2006 - Ap Su>=1 2 1 D +R Th 1993 2006 - O lastSu 2 0 S +R Th 2007 ma - Mar Su>=8 2 1 D +R Th 2007 ma - N Su>=1 2 0 S +Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28 +-3 - -03 1980 Ap 6 2 +-3 E -03/-02 1996 +0 - GMT +Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28 +-2 - -02 1980 Ap 6 2 +-2 c -02/-01 1981 Mar 29 +-1 E -01/+00 +Z America/Nuuk -3:26:56 - LMT 1916 Jul 28 +-3 - -03 1980 Ap 6 2 +-3 E -03/-02 +Z America/Thule -4:35:8 - LMT 1916 Jul 28 +-4 Th A%sT +Z Europe/Tallinn 1:39 - LMT 1880 +1:39 - TMT 1918 F +1 c CE%sT 1919 Jul +1:39 - TMT 1921 May +2 - EET 1940 Au 6 +3 - MSK 1941 S 15 +1 c CE%sT 1944 S 22 +3 R MSK/MSD 1989 Mar 26 2s +2 1 EEST 1989 S 24 2s +2 c EE%sT 1998 S 22 +2 E EE%sT 1999 O 31 4 +2 - EET 2002 F 21 +2 E EE%sT +R FI 1942 o - Ap 2 24 1 S +R FI 1942 o - O 4 1 0 - +R FI 1981 1982 - Mar lastSu 2 1 S +R FI 1981 1982 - S lastSu 3 0 - +Z Europe/Helsinki 1:39:49 - LMT 1878 May 31 +1:39:49 - HMT 1921 May +2 FI EE%sT 1983 +2 E EE%sT +L Europe/Helsinki Europe/Mariehamn +R F 1916 o - Jun 14 23s 1 S +R F 1916 1919 - O Su>=1 23s 0 - +R F 1917 o - Mar 24 23s 1 S +R F 1918 o - Mar 9 23s 1 S +R F 1919 o - Mar 1 23s 1 S +R F 1920 o - F 14 23s 1 S +R F 1920 o - O 23 23s 0 - +R F 1921 o - Mar 14 23s 1 S +R F 1921 o - O 25 23s 0 - +R F 1922 o - Mar 25 23s 1 S +R F 1922 1938 - O Sa>=1 23s 0 - +R F 1923 o - May 26 23s 1 S +R F 1924 o - Mar 29 23s 1 S +R F 1925 o - Ap 4 23s 1 S +R F 1926 o - Ap 17 23s 1 S +R F 1927 o - Ap 9 23s 1 S +R F 1928 o - Ap 14 23s 1 S +R F 1929 o - Ap 20 23s 1 S +R F 1930 o - Ap 12 23s 1 S +R F 1931 o - Ap 18 23s 1 S +R F 1932 o - Ap 2 23s 1 S +R F 1933 o - Mar 25 23s 1 S +R F 1934 o - Ap 7 23s 1 S +R F 1935 o - Mar 30 23s 1 S +R F 1936 o - Ap 18 23s 1 S +R F 1937 o - Ap 3 23s 1 S +R F 1938 o - Mar 26 23s 1 S +R F 1939 o - Ap 15 23s 1 S +R F 1939 o - N 18 23s 0 - +R F 1940 o - F 25 2 1 S +R F 1941 o - May 5 0 2 M +R F 1941 o - O 6 0 1 S +R F 1942 o - Mar 9 0 2 M +R F 1942 o - N 2 3 1 S +R F 1943 o - Mar 29 2 2 M +R F 1943 o - O 4 3 1 S +R F 1944 o - Ap 3 2 2 M +R F 1944 o - O 8 1 1 S +R F 1945 o - Ap 2 2 2 M +R F 1945 o - S 16 3 0 - +R F 1976 o - Mar 28 1 1 S +R F 1976 o - S 26 1 0 - +Z Europe/Paris 0:9:21 - LMT 1891 Mar 16 +0:9:21 - PMT 1911 Mar 11 +0 F WE%sT 1940 Jun 14 23 +1 c CE%sT 1944 Au 25 +0 F WE%sT 1945 S 16 3 +1 F CE%sT 1977 +1 E CE%sT +R DE 1946 o - Ap 14 2s 1 S +R DE 1946 o - O 7 2s 0 - +R DE 1947 1949 - O Su>=1 2s 0 - +R DE 1947 o - Ap 6 3s 1 S +R DE 1947 o - May 11 2s 2 M +R DE 1947 o - Jun 29 3 1 S +R DE 1948 o - Ap 18 2s 1 S +R DE 1949 o - Ap 10 2s 1 S +R So 1945 o - May 24 2 2 M +R So 1945 o - S 24 3 1 S +R So 1945 o - N 18 2s 0 - +Z Europe/Berlin 0:53:28 - LMT 1893 Ap +1 c CE%sT 1945 May 24 2 +1 So CE%sT 1946 +1 DE CE%sT 1980 +1 E CE%sT +L Europe/Zurich Europe/Busingen +Z Europe/Gibraltar -0:21:24 - LMT 1880 Au 2 0s +0 G %s 1957 Ap 14 2 +1 - CET 1982 +1 E CE%sT +R g 1932 o - Jul 7 0 1 S +R g 1932 o - S 1 0 0 - +R g 1941 o - Ap 7 0 1 S +R g 1942 o - N 2 3 0 - +R g 1943 o - Mar 30 0 1 S +R g 1943 o - O 4 0 0 - +R g 1952 o - Jul 1 0 1 S +R g 1952 o - N 2 0 0 - +R g 1975 o - Ap 12 0s 1 S +R g 1975 o - N 26 0s 0 - +R g 1976 o - Ap 11 2s 1 S +R g 1976 o - O 10 2s 0 - +R g 1977 1978 - Ap Su>=1 2s 1 S +R g 1977 o - S 26 2s 0 - +R g 1978 o - S 24 4 0 - +R g 1979 o - Ap 1 9 1 S +R g 1979 o - S 29 2 0 - +R g 1980 o - Ap 1 0 1 S +R g 1980 o - S 28 0 0 - +Z Europe/Athens 1:34:52 - LMT 1895 S 14 +1:34:52 - AMT 1916 Jul 28 0:1 +2 g EE%sT 1941 Ap 30 +1 g CE%sT 1944 Ap 4 +2 g EE%sT 1981 +2 E EE%sT +R h 1918 1919 - Ap 15 2 1 S +R h 1918 1920 - S M>=15 3 0 - +R h 1920 o - Ap 5 2 1 S +R h 1945 o - May 1 23 1 S +R h 1945 o - N 1 1 0 - +R h 1946 o - Mar 31 2s 1 S +R h 1946 o - O 7 2 0 - +R h 1947 1949 - Ap Su>=4 2s 1 S +R h 1947 1949 - O Su>=1 2s 0 - +R h 1954 o - May 23 0 1 S +R h 1954 o - O 3 0 0 - +R h 1955 o - May 22 2 1 S +R h 1955 o - O 2 3 0 - +R h 1956 1957 - Jun Su>=1 2 1 S +R h 1956 1957 - S lastSu 3 0 - +R h 1980 o - Ap 6 0 1 S +R h 1980 o - S 28 1 0 - +R h 1981 1983 - Mar lastSu 0 1 S +R h 1981 1983 - S lastSu 1 0 - +Z Europe/Budapest 1:16:20 - LMT 1890 N +1 c CE%sT 1918 +1 h CE%sT 1941 Ap 7 23 +1 c CE%sT 1945 +1 h CE%sT 1984 +1 E CE%sT +R w 1917 1919 - F 19 23 1 - +R w 1917 o - O 21 1 0 - +R w 1918 1919 - N 16 1 0 - +R w 1921 o - Mar 19 23 1 - +R w 1921 o - Jun 23 1 0 - +R w 1939 o - Ap 29 23 1 - +R w 1939 o - O 29 2 0 - +R w 1940 o - F 25 2 1 - +R w 1940 1941 - N Su>=2 1s 0 - +R w 1941 1942 - Mar Su>=2 1s 1 - +R w 1943 1946 - Mar Su>=1 1s 1 - +R w 1942 1948 - O Su>=22 1s 0 - +R w 1947 1967 - Ap Su>=1 1s 1 - +R w 1949 o - O 30 1s 0 - +R w 1950 1966 - O Su>=22 1s 0 - +R w 1967 o - O 29 1s 0 - +Z Atlantic/Reykjavik -1:28 - LMT 1908 +-1 w -01/+00 1968 Ap 7 1s +0 - GMT +R I 1916 o - Jun 3 24 1 S +R I 1916 1917 - S 30 24 0 - +R I 1917 o - Mar 31 24 1 S +R I 1918 o - Mar 9 24 1 S +R I 1918 o - O 6 24 0 - +R I 1919 o - Mar 1 24 1 S +R I 1919 o - O 4 24 0 - +R I 1920 o - Mar 20 24 1 S +R I 1920 o - S 18 24 0 - +R I 1940 o - Jun 14 24 1 S +R I 1942 o - N 2 2s 0 - +R I 1943 o - Mar 29 2s 1 S +R I 1943 o - O 4 2s 0 - +R I 1944 o - Ap 2 2s 1 S +R I 1944 o - S 17 2s 0 - +R I 1945 o - Ap 2 2 1 S +R I 1945 o - S 15 1 0 - +R I 1946 o - Mar 17 2s 1 S +R I 1946 o - O 6 2s 0 - +R I 1947 o - Mar 16 0s 1 S +R I 1947 o - O 5 0s 0 - +R I 1948 o - F 29 2s 1 S +R I 1948 o - O 3 2s 0 - +R I 1966 1968 - May Su>=22 0s 1 S +R I 1966 o - S 24 24 0 - +R I 1967 1969 - S Su>=22 0s 0 - +R I 1969 o - Jun 1 0s 1 S +R I 1970 o - May 31 0s 1 S +R I 1970 o - S lastSu 0s 0 - +R I 1971 1972 - May Su>=22 0s 1 S +R I 1971 o - S lastSu 0s 0 - +R I 1972 o - O 1 0s 0 - +R I 1973 o - Jun 3 0s 1 S +R I 1973 1974 - S lastSu 0s 0 - +R I 1974 o - May 26 0s 1 S +R I 1975 o - Jun 1 0s 1 S +R I 1975 1977 - S lastSu 0s 0 - +R I 1976 o - May 30 0s 1 S +R I 1977 1979 - May Su>=22 0s 1 S +R I 1978 o - O 1 0s 0 - +R I 1979 o - S 30 0s 0 - +Z Europe/Rome 0:49:56 - LMT 1866 D 12 +0:49:56 - RMT 1893 O 31 23:49:56 +1 I CE%sT 1943 S 10 +1 c CE%sT 1944 Jun 4 +1 I CE%sT 1980 +1 E CE%sT +L Europe/Rome Europe/Vatican +L Europe/Rome Europe/San_Marino +R LV 1989 1996 - Mar lastSu 2s 1 S +R LV 1989 1996 - S lastSu 2s 0 - +Z Europe/Riga 1:36:34 - LMT 1880 +1:36:34 - RMT 1918 Ap 15 2 +1:36:34 1 LST 1918 S 16 3 +1:36:34 - RMT 1919 Ap 1 2 +1:36:34 1 LST 1919 May 22 3 +1:36:34 - RMT 1926 May 11 +2 - EET 1940 Au 5 +3 - MSK 1941 Jul +1 c CE%sT 1944 O 13 +3 R MSK/MSD 1989 Mar lastSu 2s +2 1 EEST 1989 S lastSu 2s +2 LV EE%sT 1997 Ja 21 +2 E EE%sT 2000 F 29 +2 - EET 2001 Ja 2 +2 E EE%sT +L Europe/Zurich Europe/Vaduz +Z Europe/Vilnius 1:41:16 - LMT 1880 +1:24 - WMT 1917 +1:35:36 - KMT 1919 O 10 +1 - CET 1920 Jul 12 +2 - EET 1920 O 9 +1 - CET 1940 Au 3 +3 - MSK 1941 Jun 24 +1 c CE%sT 1944 Au +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 1991 S 29 2s +2 c EE%sT 1998 +2 - EET 1998 Mar 29 1u +1 E CE%sT 1999 O 31 1u +2 - EET 2003 +2 E EE%sT +R LX 1916 o - May 14 23 1 S +R LX 1916 o - O 1 1 0 - +R LX 1917 o - Ap 28 23 1 S +R LX 1917 o - S 17 1 0 - +R LX 1918 o - Ap M>=15 2s 1 S +R LX 1918 o - S M>=15 2s 0 - +R LX 1919 o - Mar 1 23 1 S +R LX 1919 o - O 5 3 0 - +R LX 1920 o - F 14 23 1 S +R LX 1920 o - O 24 2 0 - +R LX 1921 o - Mar 14 23 1 S +R LX 1921 o - O 26 2 0 - +R LX 1922 o - Mar 25 23 1 S +R LX 1922 o - O Su>=2 1 0 - +R LX 1923 o - Ap 21 23 1 S +R LX 1923 o - O Su>=2 2 0 - +R LX 1924 o - Mar 29 23 1 S +R LX 1924 1928 - O Su>=2 1 0 - +R LX 1925 o - Ap 5 23 1 S +R LX 1926 o - Ap 17 23 1 S +R LX 1927 o - Ap 9 23 1 S +R LX 1928 o - Ap 14 23 1 S +R LX 1929 o - Ap 20 23 1 S +Z Europe/Luxembourg 0:24:36 - LMT 1904 Jun +1 LX CE%sT 1918 N 25 +0 LX WE%sT 1929 O 6 2s +0 b WE%sT 1940 May 14 3 +1 c WE%sT 1944 S 18 3 +1 b CE%sT 1977 +1 E CE%sT +R MT 1973 o - Mar 31 0s 1 S +R MT 1973 o - S 29 0s 0 - +R MT 1974 o - Ap 21 0s 1 S +R MT 1974 o - S 16 0s 0 - +R MT 1975 1979 - Ap Su>=15 2 1 S +R MT 1975 1980 - S Su>=15 2 0 - +R MT 1980 o - Mar 31 2 1 S +Z Europe/Malta 0:58:4 - LMT 1893 N 2 0s +1 I CE%sT 1973 Mar 31 +1 MT CE%sT 1981 +1 E CE%sT +R MD 1997 ma - Mar lastSu 2 1 S +R MD 1997 ma - O lastSu 3 0 - +Z Europe/Chisinau 1:55:20 - LMT 1880 +1:55 - CMT 1918 F 15 +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1940 Au 15 +2 1 EEST 1941 Jul 17 +1 c CE%sT 1944 Au 24 +3 R MSK/MSD 1990 May 6 2 +2 R EE%sT 1992 +2 e EE%sT 1997 +2 MD EE%sT +Z Europe/Monaco 0:29:32 - LMT 1892 Jun +0:9:21 - PMT 1911 Mar 29 +0 F WE%sT 1945 S 16 3 +1 F CE%sT 1977 +1 E CE%sT +R N 1916 o - May 1 0 1 NST +R N 1916 o - O 1 0 0 AMT +R N 1917 o - Ap 16 2s 1 NST +R N 1917 o - S 17 2s 0 AMT +R N 1918 1921 - Ap M>=1 2s 1 NST +R N 1918 1921 - S lastM 2s 0 AMT +R N 1922 o - Mar lastSu 2s 1 NST +R N 1922 1936 - O Su>=2 2s 0 AMT +R N 1923 o - Jun F>=1 2s 1 NST +R N 1924 o - Mar lastSu 2s 1 NST +R N 1925 o - Jun F>=1 2s 1 NST +R N 1926 1931 - May 15 2s 1 NST +R N 1932 o - May 22 2s 1 NST +R N 1933 1936 - May 15 2s 1 NST +R N 1937 o - May 22 2s 1 NST +R N 1937 o - Jul 1 0 1 S +R N 1937 1939 - O Su>=2 2s 0 - +R N 1938 1939 - May 15 2s 1 S +R N 1945 o - Ap 2 2s 1 S +R N 1945 o - S 16 2s 0 - +Z Europe/Amsterdam 0:19:32 - LMT 1835 +0:19:32 N %s 1937 Jul +0:20 N +0020/+0120 1940 May 16 +1 c CE%sT 1945 Ap 2 2 +1 N CE%sT 1977 +1 E CE%sT +R NO 1916 o - May 22 1 1 S +R NO 1916 o - S 30 0 0 - +R NO 1945 o - Ap 2 2s 1 S +R NO 1945 o - O 1 2s 0 - +R NO 1959 1964 - Mar Su>=15 2s 1 S +R NO 1959 1965 - S Su>=15 2s 0 - +R NO 1965 o - Ap 25 2s 1 S +Z Europe/Oslo 0:43 - LMT 1895 +1 NO CE%sT 1940 Au 10 23 +1 c CE%sT 1945 Ap 2 2 +1 NO CE%sT 1980 +1 E CE%sT +L Europe/Oslo Arctic/Longyearbyen +R O 1918 1919 - S 16 2s 0 - +R O 1919 o - Ap 15 2s 1 S +R O 1944 o - Ap 3 2s 1 S +R O 1944 o - O 4 2 0 - +R O 1945 o - Ap 29 0 1 S +R O 1945 o - N 1 0 0 - +R O 1946 o - Ap 14 0s 1 S +R O 1946 o - O 7 2s 0 - +R O 1947 o - May 4 2s 1 S +R O 1947 1949 - O Su>=1 2s 0 - +R O 1948 o - Ap 18 2s 1 S +R O 1949 o - Ap 10 2s 1 S +R O 1957 o - Jun 2 1s 1 S +R O 1957 1958 - S lastSu 1s 0 - +R O 1958 o - Mar 30 1s 1 S +R O 1959 o - May 31 1s 1 S +R O 1959 1961 - O Su>=1 1s 0 - +R O 1960 o - Ap 3 1s 1 S +R O 1961 1964 - May lastSu 1s 1 S +R O 1962 1964 - S lastSu 1s 0 - +Z Europe/Warsaw 1:24 - LMT 1880 +1:24 - WMT 1915 Au 5 +1 c CE%sT 1918 S 16 3 +2 O EE%sT 1922 Jun +1 O CE%sT 1940 Jun 23 2 +1 c CE%sT 1944 O +1 O CE%sT 1977 +1 W- CE%sT 1988 +1 E CE%sT +R p 1916 o - Jun 17 23 1 S +R p 1916 o - N 1 1 0 - +R p 1917 o - F 28 23s 1 S +R p 1917 1921 - O 14 23s 0 - +R p 1918 o - Mar 1 23s 1 S +R p 1919 o - F 28 23s 1 S +R p 1920 o - F 29 23s 1 S +R p 1921 o - F 28 23s 1 S +R p 1924 o - Ap 16 23s 1 S +R p 1924 o - O 14 23s 0 - +R p 1926 o - Ap 17 23s 1 S +R p 1926 1929 - O Sa>=1 23s 0 - +R p 1927 o - Ap 9 23s 1 S +R p 1928 o - Ap 14 23s 1 S +R p 1929 o - Ap 20 23s 1 S +R p 1931 o - Ap 18 23s 1 S +R p 1931 1932 - O Sa>=1 23s 0 - +R p 1932 o - Ap 2 23s 1 S +R p 1934 o - Ap 7 23s 1 S +R p 1934 1938 - O Sa>=1 23s 0 - +R p 1935 o - Mar 30 23s 1 S +R p 1936 o - Ap 18 23s 1 S +R p 1937 o - Ap 3 23s 1 S +R p 1938 o - Mar 26 23s 1 S +R p 1939 o - Ap 15 23s 1 S +R p 1939 o - N 18 23s 0 - +R p 1940 o - F 24 23s 1 S +R p 1940 1941 - O 5 23s 0 - +R p 1941 o - Ap 5 23s 1 S +R p 1942 1945 - Mar Sa>=8 23s 1 S +R p 1942 o - Ap 25 22s 2 M +R p 1942 o - Au 15 22s 1 S +R p 1942 1945 - O Sa>=24 23s 0 - +R p 1943 o - Ap 17 22s 2 M +R p 1943 1945 - Au Sa>=25 22s 1 S +R p 1944 1945 - Ap Sa>=21 22s 2 M +R p 1946 o - Ap Sa>=1 23s 1 S +R p 1946 o - O Sa>=1 23s 0 - +R p 1947 1965 - Ap Su>=1 2s 1 S +R p 1947 1965 - O Su>=1 2s 0 - +R p 1977 o - Mar 27 0s 1 S +R p 1977 o - S 25 0s 0 - +R p 1978 1979 - Ap Su>=1 0s 1 S +R p 1978 o - O 1 0s 0 - +R p 1979 1982 - S lastSu 1s 0 - +R p 1980 o - Mar lastSu 0s 1 S +R p 1981 1982 - Mar lastSu 1s 1 S +R p 1983 o - Mar lastSu 2s 1 S +Z Europe/Lisbon -0:36:45 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 p WE%sT 1966 Ap 3 2 +1 - CET 1976 S 26 1 +0 p WE%sT 1983 S 25 1s +0 W- WE%sT 1992 S 27 1s +1 E CE%sT 1996 Mar 31 1u +0 E WE%sT +Z Atlantic/Azores -1:42:40 - LMT 1884 +-1:54:32 - HMT 1912 Ja 1 2u +-2 p -02/-01 1942 Ap 25 22s +-2 p +00 1942 Au 15 22s +-2 p -02/-01 1943 Ap 17 22s +-2 p +00 1943 Au 28 22s +-2 p -02/-01 1944 Ap 22 22s +-2 p +00 1944 Au 26 22s +-2 p -02/-01 1945 Ap 21 22s +-2 p +00 1945 Au 25 22s +-2 p -02/-01 1966 Ap 3 2 +-1 p -01/+00 1983 S 25 1s +-1 W- -01/+00 1992 S 27 1s +0 E WE%sT 1993 Mar 28 1u +-1 E -01/+00 +Z Atlantic/Madeira -1:7:36 - LMT 1884 +-1:7:36 - FMT 1912 Ja 1 1u +-1 p -01/+00 1942 Ap 25 22s +-1 p +01 1942 Au 15 22s +-1 p -01/+00 1943 Ap 17 22s +-1 p +01 1943 Au 28 22s +-1 p -01/+00 1944 Ap 22 22s +-1 p +01 1944 Au 26 22s +-1 p -01/+00 1945 Ap 21 22s +-1 p +01 1945 Au 25 22s +-1 p -01/+00 1966 Ap 3 2 +0 p WE%sT 1983 S 25 1s +0 E WE%sT +R z 1932 o - May 21 0s 1 S +R z 1932 1939 - O Su>=1 0s 0 - +R z 1933 1939 - Ap Su>=2 0s 1 S +R z 1979 o - May 27 0 1 S +R z 1979 o - S lastSu 0 0 - +R z 1980 o - Ap 5 23 1 S +R z 1980 o - S lastSu 1 0 - +R z 1991 1993 - Mar lastSu 0s 1 S +R z 1991 1993 - S lastSu 0s 0 - +Z Europe/Bucharest 1:44:24 - LMT 1891 O +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1981 Mar 29 2s +2 c EE%sT 1991 +2 z EE%sT 1994 +2 e EE%sT 1997 +2 E EE%sT +Z Europe/Kaliningrad 1:22 - LMT 1893 Ap +1 c CE%sT 1945 Ap 10 +2 O EE%sT 1946 Ap 7 +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 2011 Mar 27 2s +3 - +03 2014 O 26 2s +2 - EET +Z Europe/Moscow 2:30:17 - LMT 1880 +2:30:17 - MMT 1916 Jul 3 +2:31:19 R %s 1919 Jul 1 0u +3 R %s 1921 O +3 R MSK/MSD 1922 O +2 - EET 1930 Jun 21 +3 R MSK/MSD 1991 Mar 31 2s +2 R EE%sT 1992 Ja 19 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Simferopol 2:16:24 - LMT 1880 +2:16 - SMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 N +1 c CE%sT 1944 Ap 13 +3 R MSK/MSD 1990 +3 - MSK 1990 Jul 1 2 +2 - EET 1992 Mar 20 +2 c EE%sT 1994 May +3 e MSK/MSD 1996 Mar 31 0s +3 1 MSD 1996 O 27 3s +3 R MSK/MSD 1997 +3 - MSK 1997 Mar lastSu 1u +2 E EE%sT 2014 Mar 30 2 +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Astrakhan 3:12:12 - LMT 1924 May +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 Mar 27 2s +4 - +04 +Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3 +3 - +03 1930 Jun 21 +4 - +04 1961 N 11 +4 R +04/+05 1988 Mar 27 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2018 O 28 2s +4 - +04 2020 D 27 2s +3 - +03 +Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1988 Mar 27 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 D 4 2s +4 - +04 +Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 +Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 - +04 1935 Ja 27 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +2 R +02/+03 1991 S 29 2s +3 - +03 1991 O 20 3 +4 R +04/+05 2010 Mar 28 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 +Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +2 R +02/+03 1992 Ja 19 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 Mar 27 2s +4 - +04 +Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3 +3:45:5 - PMT 1919 Jul 15 4 +4 - +04 1930 Jun 21 +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2011 Mar 27 2s +6 - +06 2014 O 26 2s +5 - +05 +Z Asia/Omsk 4:53:30 - LMT 1919 N 14 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 +Z Asia/Barnaul 5:35 - LMT 1919 D 10 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 1995 May 28 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 Mar 27 2s +7 - +07 +Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 1993 May 23 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 Jul 24 2s +7 - +07 +Z Asia/Tomsk 5:39:51 - LMT 1919 D 22 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2002 May 1 3 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 May 29 2s +7 - +07 +Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2010 Mar 28 2s +6 R +06/+07 2011 Mar 27 2s +7 - +07 +Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2011 Mar 27 2s +8 - +08 2014 O 26 2s +7 - +07 +Z Asia/Irkutsk 6:57:5 - LMT 1880 +6:57:5 - IMT 1920 Ja 25 +7 - +07 1930 Jun 21 +8 R +08/+09 1991 Mar 31 2s +7 R +07/+08 1992 Ja 19 2s +8 R +08/+09 2011 Mar 27 2s +9 - +09 2014 O 26 2s +8 - +08 +Z Asia/Chita 7:33:52 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2011 Mar 27 2s +10 - +10 2014 O 26 2s +8 - +08 2016 Mar 27 2 +9 - +09 +Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2011 Mar 27 2s +10 - +10 2014 O 26 2s +9 - +09 +Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15 +9 - +09 1930 Jun 21 +10 R +10/+11 1991 Mar 31 2s +9 R +09/+10 1992 Ja 19 2s +10 R +10/+11 2011 Mar 27 2s +11 - +11 2014 O 26 2s +10 - +10 +Z Asia/Khandyga 9:2:13 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2004 +10 R +10/+11 2011 Mar 27 2s +11 - +11 2011 S 13 0s +10 - +10 2014 O 26 2s +9 - +09 +Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23 +9 - +09 1945 Au 25 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 1997 Mar lastSu 2s +10 R +10/+11 2011 Mar 27 2s +11 - +11 2014 O 26 2s +10 - +10 2016 Mar 27 2s +11 - +11 +Z Asia/Magadan 10:3:12 - LMT 1924 May 2 +10 - +10 1930 Jun 21 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2014 O 26 2s +10 - +10 2016 Ap 24 2s +11 - +11 +Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2 +10 - +10 1930 Jun 21 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2014 O 26 2s +11 - +11 +Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1981 Ap +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2011 S 13 0s +11 - +11 2014 O 26 2s +10 - +10 +Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10 +11 - +11 1930 Jun 21 +12 R +12/+13 1991 Mar 31 2s +11 R +11/+12 1992 Ja 19 2s +12 R +12/+13 2010 Mar 28 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 +Z Asia/Anadyr 11:49:56 - LMT 1924 May 2 +12 - +12 1930 Jun 21 +13 R +13/+14 1982 Ap 1 0s +12 R +12/+13 1991 Mar 31 2s +11 R +11/+12 1992 Ja 19 2s +12 R +12/+13 2010 Mar 28 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 +Z Europe/Belgrade 1:22 - LMT 1884 +1 - CET 1941 Ap 18 23 +1 c CE%sT 1945 +1 - CET 1945 May 8 2s +1 1 CEST 1945 S 16 2s +1 - CET 1982 N 27 +1 E CE%sT +L Europe/Belgrade Europe/Ljubljana +L Europe/Belgrade Europe/Podgorica +L Europe/Belgrade Europe/Sarajevo +L Europe/Belgrade Europe/Skopje +L Europe/Belgrade Europe/Zagreb +L Europe/Prague Europe/Bratislava +R s 1918 o - Ap 15 23 1 S +R s 1918 1919 - O 6 24s 0 - +R s 1919 o - Ap 6 23 1 S +R s 1924 o - Ap 16 23 1 S +R s 1924 o - O 4 24s 0 - +R s 1926 o - Ap 17 23 1 S +R s 1926 1929 - O Sa>=1 24s 0 - +R s 1927 o - Ap 9 23 1 S +R s 1928 o - Ap 15 0 1 S +R s 1929 o - Ap 20 23 1 S +R s 1937 o - Jun 16 23 1 S +R s 1937 o - O 2 24s 0 - +R s 1938 o - Ap 2 23 1 S +R s 1938 o - Ap 30 23 2 M +R s 1938 o - O 2 24 1 S +R s 1939 o - O 7 24s 0 - +R s 1942 o - May 2 23 1 S +R s 1942 o - S 1 1 0 - +R s 1943 1946 - Ap Sa>=13 23 1 S +R s 1943 1944 - O Su>=1 1 0 - +R s 1945 1946 - S lastSu 1 0 - +R s 1949 o - Ap 30 23 1 S +R s 1949 o - O 2 1 0 - +R s 1974 1975 - Ap Sa>=12 23 1 S +R s 1974 1975 - O Su>=1 1 0 - +R s 1976 o - Mar 27 23 1 S +R s 1976 1977 - S lastSu 1 0 - +R s 1977 o - Ap 2 23 1 S +R s 1978 o - Ap 2 2s 1 S +R s 1978 o - O 1 2s 0 - +R Sp 1967 o - Jun 3 12 1 S +R Sp 1967 o - O 1 0 0 - +R Sp 1974 o - Jun 24 0 1 S +R Sp 1974 o - S 1 0 0 - +R Sp 1976 1977 - May 1 0 1 S +R Sp 1976 o - Au 1 0 0 - +R Sp 1977 o - S 28 0 0 - +R Sp 1978 o - Jun 1 0 1 S +R Sp 1978 o - Au 4 0 0 - +Z Europe/Madrid -0:14:44 - LMT 1900 D 31 23:45:16 +0 s WE%sT 1940 Mar 16 23 +1 s CE%sT 1979 +1 E CE%sT +Z Africa/Ceuta -0:21:16 - LMT 1900 D 31 23:38:44 +0 - WET 1918 May 6 23 +0 1 WEST 1918 O 7 23 +0 - WET 1924 +0 s WE%sT 1929 +0 - WET 1967 +0 Sp WE%sT 1984 Mar 16 +1 - CET 1986 +1 E CE%sT +Z Atlantic/Canary -1:1:36 - LMT 1922 Mar +-1 - -01 1946 S 30 1 +0 - WET 1980 Ap 6 0s +0 1 WEST 1980 S 28 1u +0 E WE%sT +Z Europe/Stockholm 1:12:12 - LMT 1879 +1:0:14 - SET 1900 +1 - CET 1916 May 14 23 +1 1 CEST 1916 O 1 1 +1 - CET 1980 +1 E CE%sT +R CH 1941 1942 - May M>=1 1 1 S +R CH 1941 1942 - O M>=1 2 0 - +Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16 +0:29:46 - BMT 1894 Jun +1 CH CE%sT 1981 +1 E CE%sT +R T 1916 o - May 1 0 1 S +R T 1916 o - O 1 0 0 - +R T 1920 o - Mar 28 0 1 S +R T 1920 o - O 25 0 0 - +R T 1921 o - Ap 3 0 1 S +R T 1921 o - O 3 0 0 - +R T 1922 o - Mar 26 0 1 S +R T 1922 o - O 8 0 0 - +R T 1924 o - May 13 0 1 S +R T 1924 1925 - O 1 0 0 - +R T 1925 o - May 1 0 1 S +R T 1940 o - Jul 1 0 1 S +R T 1940 o - O 6 0 0 - +R T 1940 o - D 1 0 1 S +R T 1941 o - S 21 0 0 - +R T 1942 o - Ap 1 0 1 S +R T 1945 o - O 8 0 0 - +R T 1946 o - Jun 1 0 1 S +R T 1946 o - O 1 0 0 - +R T 1947 1948 - Ap Su>=16 0 1 S +R T 1947 1951 - O Su>=2 0 0 - +R T 1949 o - Ap 10 0 1 S +R T 1950 o - Ap 16 0 1 S +R T 1951 o - Ap 22 0 1 S +R T 1962 o - Jul 15 0 1 S +R T 1963 o - O 30 0 0 - +R T 1964 o - May 15 0 1 S +R T 1964 o - O 1 0 0 - +R T 1973 o - Jun 3 1 1 S +R T 1973 1976 - O Su>=31 2 0 - +R T 1974 o - Mar 31 2 1 S +R T 1975 o - Mar 22 2 1 S +R T 1976 o - Mar 21 2 1 S +R T 1977 1978 - Ap Su>=1 2 1 S +R T 1977 1978 - O Su>=15 2 0 - +R T 1978 o - Jun 29 0 0 - +R T 1983 o - Jul 31 2 1 S +R T 1983 o - O 2 2 0 - +R T 1985 o - Ap 20 1s 1 S +R T 1985 o - S 28 1s 0 - +R T 1986 1993 - Mar lastSu 1s 1 S +R T 1986 1995 - S lastSu 1s 0 - +R T 1994 o - Mar 20 1s 1 S +R T 1995 2006 - Mar lastSu 1s 1 S +R T 1996 2006 - O lastSu 1s 0 - +Z Europe/Istanbul 1:55:52 - LMT 1880 +1:56:56 - IMT 1910 O +2 T EE%sT 1978 Jun 29 +3 T +03/+04 1984 N 1 2 +2 T EE%sT 2007 +2 E EE%sT 2011 Mar 27 1u +2 - EET 2011 Mar 28 1u +2 E EE%sT 2014 Mar 30 1u +2 - EET 2014 Mar 31 1u +2 E EE%sT 2015 O 25 1u +2 1 EEST 2015 N 8 1u +2 E EE%sT 2016 S 7 +3 - +03 +L Europe/Istanbul Asia/Istanbul +Z Europe/Kiev 2:2:4 - LMT 1880 +2:2:4 - KMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 S 20 +1 c CE%sT 1943 N 6 +3 R MSK/MSD 1990 Jul 1 2 +2 1 EEST 1991 S 29 3 +2 c EE%sT 1996 May 13 +2 E EE%sT +Z Europe/Uzhgorod 1:29:12 - LMT 1890 O +1 - CET 1940 +1 c CE%sT 1944 O +1 1 CEST 1944 O 26 +1 - CET 1945 Jun 29 +3 R MSK/MSD 1990 +3 - MSK 1990 Jul 1 2 +1 - CET 1991 Mar 31 3 +2 - EET 1992 Mar 20 +2 c EE%sT 1996 May 13 +2 E EE%sT +Z Europe/Zaporozhye 2:20:40 - LMT 1880 +2:20 - +0220 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 Au 25 +1 c CE%sT 1943 O 25 +3 R MSK/MSD 1991 Mar 31 2 +2 e EE%sT 1992 Mar 20 +2 c EE%sT 1996 May 13 +2 E EE%sT +R u 1918 1919 - Mar lastSu 2 1 D +R u 1918 1919 - O lastSu 2 0 S +R u 1942 o - F 9 2 1 W +R u 1945 o - Au 14 23u 1 P +R u 1945 o - S 30 2 0 S +R u 1967 2006 - O lastSu 2 0 S +R u 1967 1973 - Ap lastSu 2 1 D +R u 1974 o - Ja 6 2 1 D +R u 1975 o - F lastSu 2 1 D +R u 1976 1986 - Ap lastSu 2 1 D +R u 1987 2006 - Ap Su>=1 2 1 D +R u 2007 ma - Mar Su>=8 2 1 D +R u 2007 ma - N Su>=1 2 0 S +Z EST -5 - EST +Z MST -7 - MST +Z HST -10 - HST +Z EST5EDT -5 u E%sT +Z CST6CDT -6 u C%sT +Z MST7MDT -7 u M%sT +Z PST8PDT -8 u P%sT +R NY 1920 o - Mar lastSu 2 1 D +R NY 1920 o - O lastSu 2 0 S +R NY 1921 1966 - Ap lastSu 2 1 D +R NY 1921 1954 - S lastSu 2 0 S +R NY 1955 1966 - O lastSu 2 0 S +Z America/New_York -4:56:2 - LMT 1883 N 18 12:3:58 +-5 u E%sT 1920 +-5 NY E%sT 1942 +-5 u E%sT 1946 +-5 NY E%sT 1967 +-5 u E%sT +R Ch 1920 o - Jun 13 2 1 D +R Ch 1920 1921 - O lastSu 2 0 S +R Ch 1921 o - Mar lastSu 2 1 D +R Ch 1922 1966 - Ap lastSu 2 1 D +R Ch 1922 1954 - S lastSu 2 0 S +R Ch 1955 1966 - O lastSu 2 0 S +Z America/Chicago -5:50:36 - LMT 1883 N 18 12:9:24 +-6 u C%sT 1920 +-6 Ch C%sT 1936 Mar 1 2 +-5 - EST 1936 N 15 2 +-6 Ch C%sT 1942 +-6 u C%sT 1946 +-6 Ch C%sT 1967 +-6 u C%sT +Z America/North_Dakota/Center -6:45:12 - LMT 1883 N 18 12:14:48 +-7 u M%sT 1992 O 25 2 +-6 u C%sT +Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 12:14:21 +-7 u M%sT 2003 O 26 2 +-6 u C%sT +Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 12:12:53 +-7 u M%sT 2010 N 7 2 +-6 u C%sT +R De 1920 1921 - Mar lastSu 2 1 D +R De 1920 o - O lastSu 2 0 S +R De 1921 o - May 22 2 0 S +R De 1965 1966 - Ap lastSu 2 1 D +R De 1965 1966 - O lastSu 2 0 S +Z America/Denver -6:59:56 - LMT 1883 N 18 12:0:4 +-7 u M%sT 1920 +-7 De M%sT 1942 +-7 u M%sT 1946 +-7 De M%sT 1967 +-7 u M%sT +R CA 1948 o - Mar 14 2:1 1 D +R CA 1949 o - Ja 1 2 0 S +R CA 1950 1966 - Ap lastSu 1 1 D +R CA 1950 1961 - S lastSu 2 0 S +R CA 1962 1966 - O lastSu 2 0 S +Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 12:7:2 +-8 u P%sT 1946 +-8 CA P%sT 1967 +-8 u P%sT +Z America/Juneau 15:2:19 - LMT 1867 O 19 15:33:32 +-8:57:41 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1980 Ap 27 2 +-9 u Y%sT 1980 O 26 2 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30 +-9:1:13 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55 +-8:46:18 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-8 - PST 2015 N 1 2 +-9 u AK%sT 2018 N 4 2 +-8 - PST 2019 Ja 20 2 +-9 u AK%sT +Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18 +-9:18:55 - LMT 1900 Au 20 12 +-9 - YST 1942 +-9 u Y%sT 1946 +-9 - YST 1969 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37 +-9:59:36 - LMT 1900 Au 20 12 +-10 - AST 1942 +-10 u A%sT 1967 Ap +-10 - AHST 1969 +-10 u AH%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35 +-11:1:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Adak 12:13:22 - LMT 1867 O 19 12:44:35 +-11:46:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-10 u AH%sT 1983 N 30 +-10 u H%sT +Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12 +-10:30 - HST 1933 Ap 30 2 +-10:30 1 HDT 1933 May 21 12 +-10:30 u H%sT 1947 Jun 8 2 +-10 - HST +Z America/Phoenix -7:28:18 - LMT 1883 N 18 11:31:42 +-7 u M%sT 1944 Ja 1 0:1 +-7 - MST 1944 Ap 1 0:1 +-7 u M%sT 1944 O 1 0:1 +-7 - MST 1967 +-7 u M%sT 1968 Mar 21 +-7 - MST +L America/Phoenix America/Creston +Z America/Boise -7:44:49 - LMT 1883 N 18 12:15:11 +-8 u P%sT 1923 May 13 2 +-7 u M%sT 1974 +-7 - MST 1974 F 3 2 +-7 u M%sT +R In 1941 o - Jun 22 2 1 D +R In 1941 1954 - S lastSu 2 0 S +R In 1946 1954 - Ap lastSu 2 1 D +Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 12:15:22 +-6 u C%sT 1920 +-6 In C%sT 1942 +-6 u C%sT 1946 +-6 In C%sT 1955 Ap 24 2 +-5 - EST 1957 S 29 2 +-6 - CST 1958 Ap 27 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 +-5 u E%sT +R Ma 1951 o - Ap lastSu 2 1 D +R Ma 1951 o - S lastSu 2 0 S +R Ma 1954 1960 - Ap lastSu 2 1 D +R Ma 1954 1960 - S lastSu 2 0 S +Z America/Indiana/Marengo -5:45:23 - LMT 1883 N 18 12:14:37 +-6 u C%sT 1951 +-6 Ma C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT 1976 +-5 - EST 2006 +-5 u E%sT +R V 1946 o - Ap lastSu 2 1 D +R V 1946 o - S lastSu 2 0 S +R V 1953 1954 - Ap lastSu 2 1 D +R V 1953 1959 - S lastSu 2 0 S +R V 1955 o - May 1 0 1 D +R V 1956 1963 - Ap lastSu 2 1 D +R V 1960 o - O lastSu 2 0 S +R V 1961 o - S lastSu 2 0 S +R V 1962 1963 - O lastSu 2 0 S +Z America/Indiana/Vincennes -5:50:7 - LMT 1883 N 18 12:9:53 +-6 u C%sT 1946 +-6 V C%sT 1964 Ap 26 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +R Pe 1955 o - May 1 0 1 D +R Pe 1955 1960 - S lastSu 2 0 S +R Pe 1956 1963 - Ap lastSu 2 1 D +R Pe 1961 1963 - O lastSu 2 0 S +Z America/Indiana/Tell_City -5:47:3 - LMT 1883 N 18 12:12:57 +-6 u C%sT 1946 +-6 Pe C%sT 1964 Ap 26 2 +-5 - EST 1967 O 29 2 +-6 u C%sT 1969 Ap 27 2 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +R Pi 1955 o - May 1 0 1 D +R Pi 1955 1960 - S lastSu 2 0 S +R Pi 1956 1964 - Ap lastSu 2 1 D +R Pi 1961 1964 - O lastSu 2 0 S +Z America/Indiana/Petersburg -5:49:7 - LMT 1883 N 18 12:10:53 +-6 u C%sT 1955 +-6 Pi C%sT 1965 Ap 25 2 +-5 - EST 1966 O 30 2 +-6 u C%sT 1977 O 30 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +R St 1947 1961 - Ap lastSu 2 1 D +R St 1947 1954 - S lastSu 2 0 S +R St 1955 1956 - O lastSu 2 0 S +R St 1957 1958 - S lastSu 2 0 S +R St 1959 1961 - O lastSu 2 0 S +Z America/Indiana/Knox -5:46:30 - LMT 1883 N 18 12:13:30 +-6 u C%sT 1947 +-6 St C%sT 1962 Ap 29 2 +-5 - EST 1963 O 27 2 +-6 u C%sT 1991 O 27 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +R Pu 1946 1960 - Ap lastSu 2 1 D +R Pu 1946 1954 - S lastSu 2 0 S +R Pu 1955 1956 - O lastSu 2 0 S +R Pu 1957 1960 - S lastSu 2 0 S +Z America/Indiana/Winamac -5:46:25 - LMT 1883 N 18 12:13:35 +-6 u C%sT 1946 +-6 Pu C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 Mar 11 2 +-5 u E%sT +Z America/Indiana/Vevay -5:40:16 - LMT 1883 N 18 12:19:44 +-6 u C%sT 1954 Ap 25 2 +-5 - EST 1969 +-5 u E%sT 1973 +-5 - EST 2006 +-5 u E%sT +R v 1921 o - May 1 2 1 D +R v 1921 o - S 1 2 0 S +R v 1941 o - Ap lastSu 2 1 D +R v 1941 o - S lastSu 2 0 S +R v 1946 o - Ap lastSu 0:1 1 D +R v 1946 o - Jun 2 2 0 S +R v 1950 1961 - Ap lastSu 2 1 D +R v 1950 1955 - S lastSu 2 0 S +R v 1956 1961 - O lastSu 2 0 S +Z America/Kentucky/Louisville -5:43:2 - LMT 1883 N 18 12:16:58 +-6 u C%sT 1921 +-6 v C%sT 1942 +-6 u C%sT 1946 +-6 v C%sT 1961 Jul 23 2 +-5 - EST 1968 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT +Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 12:20:36 +-6 u C%sT 1946 +-6 - CST 1968 +-6 u C%sT 2000 O 29 2 +-5 u E%sT +R Dt 1948 o - Ap lastSu 2 1 D +R Dt 1948 o - S lastSu 2 0 S +Z America/Detroit -5:32:11 - LMT 1905 +-6 - CST 1915 May 15 2 +-5 - EST 1942 +-5 u E%sT 1946 +-5 Dt E%sT 1967 Jun 14 0:1 +-5 u E%sT 1969 +-5 - EST 1973 +-5 u E%sT 1975 +-5 - EST 1975 Ap 27 2 +-5 u E%sT +R Me 1946 o - Ap lastSu 2 1 D +R Me 1946 o - S lastSu 2 0 S +R Me 1966 o - Ap lastSu 2 1 D +R Me 1966 o - O lastSu 2 0 S +Z America/Menominee -5:50:27 - LMT 1885 S 18 12 +-6 u C%sT 1946 +-6 Me C%sT 1969 Ap 27 2 +-5 - EST 1973 Ap 29 2 +-6 u C%sT +R C 1918 o - Ap 14 2 1 D +R C 1918 o - O 27 2 0 S +R C 1942 o - F 9 2 1 W +R C 1945 o - Au 14 23u 1 P +R C 1945 o - S 30 2 0 S +R C 1974 1986 - Ap lastSu 2 1 D +R C 1974 2006 - O lastSu 2 0 S +R C 1987 2006 - Ap Su>=1 2 1 D +R C 2007 ma - Mar Su>=8 2 1 D +R C 2007 ma - N Su>=1 2 0 S +R j 1917 o - Ap 8 2 1 D +R j 1917 o - S 17 2 0 S +R j 1919 o - May 5 23 1 D +R j 1919 o - Au 12 23 0 S +R j 1920 1935 - May Su>=1 23 1 D +R j 1920 1935 - O lastSu 23 0 S +R j 1936 1941 - May M>=9 0 1 D +R j 1936 1941 - O M>=2 0 0 S +R j 1946 1950 - May Su>=8 2 1 D +R j 1946 1950 - O Su>=2 2 0 S +R j 1951 1986 - Ap lastSu 2 1 D +R j 1951 1959 - S lastSu 2 0 S +R j 1960 1986 - O lastSu 2 0 S +R j 1987 o - Ap Su>=1 0:1 1 D +R j 1987 2006 - O lastSu 0:1 0 S +R j 1988 o - Ap Su>=1 0:1 2 DD +R j 1989 2006 - Ap Su>=1 0:1 1 D +R j 2007 2011 - Mar Su>=8 0:1 1 D +R j 2007 2010 - N Su>=1 0:1 0 S +Z America/St_Johns -3:30:52 - LMT 1884 +-3:30:52 j N%sT 1918 +-3:30:52 C N%sT 1919 +-3:30:52 j N%sT 1935 Mar 30 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 2011 N +-3:30 C N%sT +Z America/Goose_Bay -4:1:40 - LMT 1884 +-3:30:52 - NST 1918 +-3:30:52 C N%sT 1919 +-3:30:52 - NST 1935 Mar 30 +-3:30 - NST 1936 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 1966 Mar 15 2 +-4 j A%sT 2011 N +-4 C A%sT +R H 1916 o - Ap 1 0 1 D +R H 1916 o - O 1 0 0 S +R H 1920 o - May 9 0 1 D +R H 1920 o - Au 29 0 0 S +R H 1921 o - May 6 0 1 D +R H 1921 1922 - S 5 0 0 S +R H 1922 o - Ap 30 0 1 D +R H 1923 1925 - May Su>=1 0 1 D +R H 1923 o - S 4 0 0 S +R H 1924 o - S 15 0 0 S +R H 1925 o - S 28 0 0 S +R H 1926 o - May 16 0 1 D +R H 1926 o - S 13 0 0 S +R H 1927 o - May 1 0 1 D +R H 1927 o - S 26 0 0 S +R H 1928 1931 - May Su>=8 0 1 D +R H 1928 o - S 9 0 0 S +R H 1929 o - S 3 0 0 S +R H 1930 o - S 15 0 0 S +R H 1931 1932 - S M>=24 0 0 S +R H 1932 o - May 1 0 1 D +R H 1933 o - Ap 30 0 1 D +R H 1933 o - O 2 0 0 S +R H 1934 o - May 20 0 1 D +R H 1934 o - S 16 0 0 S +R H 1935 o - Jun 2 0 1 D +R H 1935 o - S 30 0 0 S +R H 1936 o - Jun 1 0 1 D +R H 1936 o - S 14 0 0 S +R H 1937 1938 - May Su>=1 0 1 D +R H 1937 1941 - S M>=24 0 0 S +R H 1939 o - May 28 0 1 D +R H 1940 1941 - May Su>=1 0 1 D +R H 1946 1949 - Ap lastSu 2 1 D +R H 1946 1949 - S lastSu 2 0 S +R H 1951 1954 - Ap lastSu 2 1 D +R H 1951 1954 - S lastSu 2 0 S +R H 1956 1959 - Ap lastSu 2 1 D +R H 1956 1959 - S lastSu 2 0 S +R H 1962 1973 - Ap lastSu 2 1 D +R H 1962 1973 - O lastSu 2 0 S +Z America/Halifax -4:14:24 - LMT 1902 Jun 15 +-4 H A%sT 1918 +-4 C A%sT 1919 +-4 H A%sT 1942 F 9 2s +-4 C A%sT 1946 +-4 H A%sT 1974 +-4 C A%sT +Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15 +-4 C A%sT 1953 +-4 H A%sT 1954 +-4 - AST 1972 +-4 H A%sT 1974 +-4 C A%sT +R o 1933 1935 - Jun Su>=8 1 1 D +R o 1933 1935 - S Su>=8 1 0 S +R o 1936 1938 - Jun Su>=1 1 1 D +R o 1936 1938 - S Su>=1 1 0 S +R o 1939 o - May 27 1 1 D +R o 1939 1941 - S Sa>=21 1 0 S +R o 1940 o - May 19 1 1 D +R o 1941 o - May 4 1 1 D +R o 1946 1972 - Ap lastSu 2 1 D +R o 1946 1956 - S lastSu 2 0 S +R o 1957 1972 - O lastSu 2 0 S +R o 1993 2006 - Ap Su>=1 0:1 1 D +R o 1993 2006 - O lastSu 0:1 0 S +Z America/Moncton -4:19:8 - LMT 1883 D 9 +-5 - EST 1902 Jun 15 +-4 C A%sT 1933 +-4 o A%sT 1942 +-4 C A%sT 1946 +-4 o A%sT 1973 +-4 C A%sT 1993 +-4 o A%sT 2007 +-4 C A%sT +R t 1919 o - Mar 30 23:30 1 D +R t 1919 o - O 26 0 0 S +R t 1920 o - May 2 2 1 D +R t 1920 o - S 26 0 0 S +R t 1921 o - May 15 2 1 D +R t 1921 o - S 15 2 0 S +R t 1922 1923 - May Su>=8 2 1 D +R t 1922 1926 - S Su>=15 2 0 S +R t 1924 1927 - May Su>=1 2 1 D +R t 1927 1937 - S Su>=25 2 0 S +R t 1928 1937 - Ap Su>=25 2 1 D +R t 1938 1940 - Ap lastSu 2 1 D +R t 1938 1939 - S lastSu 2 0 S +R t 1945 1946 - S lastSu 2 0 S +R t 1946 o - Ap lastSu 2 1 D +R t 1947 1949 - Ap lastSu 0 1 D +R t 1947 1948 - S lastSu 0 0 S +R t 1949 o - N lastSu 0 0 S +R t 1950 1973 - Ap lastSu 2 1 D +R t 1950 o - N lastSu 2 0 S +R t 1951 1956 - S lastSu 2 0 S +R t 1957 1973 - O lastSu 2 0 S +Z America/Toronto -5:17:32 - LMT 1895 +-5 C E%sT 1919 +-5 t E%sT 1942 F 9 2s +-5 C E%sT 1946 +-5 t E%sT 1974 +-5 C E%sT +L America/Toronto America/Nassau +Z America/Thunder_Bay -5:57 - LMT 1895 +-6 - CST 1910 +-5 - EST 1942 +-5 C E%sT 1970 +-5 t E%sT 1973 +-5 - EST 1974 +-5 C E%sT +Z America/Nipigon -5:53:4 - LMT 1895 +-5 C E%sT 1940 S 29 +-5 1 EDT 1942 F 9 2s +-5 C E%sT +Z America/Rainy_River -6:18:16 - LMT 1895 +-6 C C%sT 1940 S 29 +-6 1 CDT 1942 F 9 2s +-6 C C%sT +R W 1916 o - Ap 23 0 1 D +R W 1916 o - S 17 0 0 S +R W 1918 o - Ap 14 2 1 D +R W 1918 o - O 27 2 0 S +R W 1937 o - May 16 2 1 D +R W 1937 o - S 26 2 0 S +R W 1942 o - F 9 2 1 W +R W 1945 o - Au 14 23u 1 P +R W 1945 o - S lastSu 2 0 S +R W 1946 o - May 12 2 1 D +R W 1946 o - O 13 2 0 S +R W 1947 1949 - Ap lastSu 2 1 D +R W 1947 1949 - S lastSu 2 0 S +R W 1950 o - May 1 2 1 D +R W 1950 o - S 30 2 0 S +R W 1951 1960 - Ap lastSu 2 1 D +R W 1951 1958 - S lastSu 2 0 S +R W 1959 o - O lastSu 2 0 S +R W 1960 o - S lastSu 2 0 S +R W 1963 o - Ap lastSu 2 1 D +R W 1963 o - S 22 2 0 S +R W 1966 1986 - Ap lastSu 2s 1 D +R W 1966 2005 - O lastSu 2s 0 S +R W 1987 2005 - Ap Su>=1 2s 1 D +Z America/Winnipeg -6:28:36 - LMT 1887 Jul 16 +-6 W C%sT 2006 +-6 C C%sT +R r 1918 o - Ap 14 2 1 D +R r 1918 o - O 27 2 0 S +R r 1930 1934 - May Su>=1 0 1 D +R r 1930 1934 - O Su>=1 0 0 S +R r 1937 1941 - Ap Su>=8 0 1 D +R r 1937 o - O Su>=8 0 0 S +R r 1938 o - O Su>=1 0 0 S +R r 1939 1941 - O Su>=8 0 0 S +R r 1942 o - F 9 2 1 W +R r 1945 o - Au 14 23u 1 P +R r 1945 o - S lastSu 2 0 S +R r 1946 o - Ap Su>=8 2 1 D +R r 1946 o - O Su>=8 2 0 S +R r 1947 1957 - Ap lastSu 2 1 D +R r 1947 1957 - S lastSu 2 0 S +R r 1959 o - Ap lastSu 2 1 D +R r 1959 o - O lastSu 2 0 S +R Sw 1957 o - Ap lastSu 2 1 D +R Sw 1957 o - O lastSu 2 0 S +R Sw 1959 1961 - Ap lastSu 2 1 D +R Sw 1959 o - O lastSu 2 0 S +R Sw 1960 1961 - S lastSu 2 0 S +Z America/Regina -6:58:36 - LMT 1905 S +-7 r M%sT 1960 Ap lastSu 2 +-6 - CST +Z America/Swift_Current -7:11:20 - LMT 1905 S +-7 C M%sT 1946 Ap lastSu 2 +-7 r M%sT 1950 +-7 Sw M%sT 1972 Ap lastSu 2 +-6 - CST +R Ed 1918 1919 - Ap Su>=8 2 1 D +R Ed 1918 o - O 27 2 0 S +R Ed 1919 o - May 27 2 0 S +R Ed 1920 1923 - Ap lastSu 2 1 D +R Ed 1920 o - O lastSu 2 0 S +R Ed 1921 1923 - S lastSu 2 0 S +R Ed 1942 o - F 9 2 1 W +R Ed 1945 o - Au 14 23u 1 P +R Ed 1945 o - S lastSu 2 0 S +R Ed 1947 o - Ap lastSu 2 1 D +R Ed 1947 o - S lastSu 2 0 S +R Ed 1972 1986 - Ap lastSu 2 1 D +R Ed 1972 2006 - O lastSu 2 0 S +Z America/Edmonton -7:33:52 - LMT 1906 S +-7 Ed M%sT 1987 +-7 C M%sT +R Va 1918 o - Ap 14 2 1 D +R Va 1918 o - O 27 2 0 S +R Va 1942 o - F 9 2 1 W +R Va 1945 o - Au 14 23u 1 P +R Va 1945 o - S 30 2 0 S +R Va 1946 1986 - Ap lastSu 2 1 D +R Va 1946 o - S 29 2 0 S +R Va 1947 1961 - S lastSu 2 0 S +R Va 1962 2006 - O lastSu 2 0 S +Z America/Vancouver -8:12:28 - LMT 1884 +-8 Va P%sT 1987 +-8 C P%sT +Z America/Dawson_Creek -8:0:56 - LMT 1884 +-8 C P%sT 1947 +-8 Va P%sT 1972 Au 30 2 +-7 - MST +Z America/Fort_Nelson -8:10:47 - LMT 1884 +-8 Va P%sT 1946 +-8 - PST 1947 +-8 Va P%sT 1987 +-8 C P%sT 2015 Mar 8 2 +-7 - MST +R Y 1918 o - Ap 14 2 1 D +R Y 1918 o - O 27 2 0 S +R Y 1919 o - May 25 2 1 D +R Y 1919 o - N 1 0 0 S +R Y 1942 o - F 9 2 1 W +R Y 1945 o - Au 14 23u 1 P +R Y 1945 o - S 30 2 0 S +R Y 1965 o - Ap lastSu 0 2 DD +R Y 1965 o - O lastSu 2 0 S +R Y 1980 1986 - Ap lastSu 2 1 D +R Y 1980 2006 - O lastSu 2 0 S +R Y 1987 2006 - Ap Su>=1 2 1 D +Z America/Pangnirtung 0 - -00 1921 +-4 Y A%sT 1995 Ap Su>=1 2 +-5 C E%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 C E%sT +Z America/Iqaluit 0 - -00 1942 Au +-5 Y E%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 C E%sT +Z America/Resolute 0 - -00 1947 Au 31 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT 2006 O 29 2 +-5 - EST 2007 Mar 11 3 +-6 C C%sT +Z America/Rankin_Inlet 0 - -00 1957 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT +Z America/Cambridge_Bay 0 - -00 1920 +-7 Y M%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 - EST 2000 N 5 +-6 - CST 2001 Ap 1 3 +-7 C M%sT +Z America/Yellowknife 0 - -00 1935 +-7 Y M%sT 1980 +-7 C M%sT +Z America/Inuvik 0 - -00 1953 +-8 Y P%sT 1979 Ap lastSu 2 +-7 Y M%sT 1980 +-7 C M%sT +Z America/Whitehorse -9:0:12 - LMT 1900 Au 20 +-9 Y Y%sT 1967 May 28 +-8 Y P%sT 1980 +-8 C P%sT 2020 N +-7 - MST +Z America/Dawson -9:17:40 - LMT 1900 Au 20 +-9 Y Y%sT 1973 O 28 +-8 Y P%sT 1980 +-8 C P%sT 2020 N +-7 - MST +R m 1939 o - F 5 0 1 D +R m 1939 o - Jun 25 0 0 S +R m 1940 o - D 9 0 1 D +R m 1941 o - Ap 1 0 0 S +R m 1943 o - D 16 0 1 W +R m 1944 o - May 1 0 0 S +R m 1950 o - F 12 0 1 D +R m 1950 o - Jul 30 0 0 S +R m 1996 2000 - Ap Su>=1 2 1 D +R m 1996 2000 - O lastSu 2 0 S +R m 2001 o - May Su>=1 2 1 D +R m 2001 o - S lastSu 2 0 S +R m 2002 ma - Ap Su>=1 2 1 D +R m 2002 ma - O lastSu 2 0 S +Z America/Cancun -5:47:4 - LMT 1922 Ja 1 0:12:56 +-6 - CST 1981 D 23 +-5 m E%sT 1998 Au 2 2 +-6 m C%sT 2015 F 1 2 +-5 - EST +Z America/Merida -5:58:28 - LMT 1922 Ja 1 0:1:32 +-6 - CST 1981 D 23 +-5 - EST 1982 D 2 +-6 m C%sT +Z America/Matamoros -6:40 - LMT 1921 D 31 23:20 +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT 2010 +-6 u C%sT +Z America/Monterrey -6:41:16 - LMT 1921 D 31 23:18:44 +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT +Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 0:23:24 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 m C%sT 2001 S 30 2 +-6 - CST 2002 F 20 +-6 m C%sT +Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 0:2:20 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT 2010 +-7 u M%sT +Z America/Chihuahua -7:4:20 - LMT 1921 D 31 23:55:40 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT +Z America/Hermosillo -7:23:52 - LMT 1921 D 31 23:36:8 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT 1999 +-7 - MST +Z America/Mazatlan -7:5:40 - LMT 1921 D 31 23:54:20 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT +Z America/Bahia_Banderas -7:1 - LMT 1921 D 31 23:59 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT 2010 Ap 4 2 +-6 m C%sT +Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 0:11:56 +-7 - MST 1924 +-8 - PST 1927 Jun 10 23 +-7 - MST 1930 N 15 +-8 - PST 1931 Ap +-8 1 PDT 1931 S 30 +-8 - PST 1942 Ap 24 +-8 1 PWT 1945 Au 14 23u +-8 1 PPT 1945 N 12 +-8 - PST 1948 Ap 5 +-8 1 PDT 1949 Ja 14 +-8 - PST 1954 +-8 CA P%sT 1961 +-8 - PST 1976 +-8 u P%sT 1996 +-8 m P%sT 2001 +-8 u P%sT 2002 F 20 +-8 m P%sT 2010 +-8 u P%sT +R BB 1942 o - Ap 19 5u 1 D +R BB 1942 o - Au 31 6u 0 S +R BB 1943 o - May 2 5u 1 D +R BB 1943 o - S 5 6u 0 S +R BB 1944 o - Ap 10 5u 0:30 - +R BB 1944 o - S 10 6u 0 S +R BB 1977 o - Jun 12 2 1 D +R BB 1977 1978 - O Su>=1 2 0 S +R BB 1978 1980 - Ap Su>=15 2 1 D +R BB 1979 o - S 30 2 0 S +R BB 1980 o - S 25 2 0 S +Z America/Barbados -3:58:29 - LMT 1911 Au 28 +-4 BB A%sT 1944 +-4 BB AST/-0330 1945 +-4 BB A%sT +R BZ 1918 1941 - O Sa>=1 24 0:30 -0530 +R BZ 1919 1942 - F Sa>=8 24 0 CST +R BZ 1942 o - Jun 27 24 1 CWT +R BZ 1945 o - Au 14 23u 1 CPT +R BZ 1945 o - D 15 24 0 CST +R BZ 1947 1967 - O Sa>=1 24 0:30 -0530 +R BZ 1948 1968 - F Sa>=8 24 0 CST +R BZ 1973 o - D 5 0 1 CDT +R BZ 1974 o - F 9 0 0 CST +R BZ 1982 o - D 18 0 1 CDT +R BZ 1983 o - F 12 0 0 CST +Z America/Belize -5:52:48 - LMT 1912 Ap +-6 BZ %s +R Be 1917 o - Ap 5 24 1 - +R Be 1917 o - S 30 24 0 - +R Be 1918 o - Ap 13 24 1 - +R Be 1918 o - S 15 24 0 S +R Be 1942 o - Ja 11 2 1 D +R Be 1942 o - O 18 2 0 S +R Be 1943 o - Mar 21 2 1 D +R Be 1943 o - O 31 2 0 S +R Be 1944 1945 - Mar Su>=8 2 1 D +R Be 1944 1945 - N Su>=1 2 0 S +R Be 1947 o - May Su>=15 2 1 D +R Be 1947 o - S Su>=8 2 0 S +R Be 1948 1952 - May Su>=22 2 1 D +R Be 1948 1952 - S Su>=1 2 0 S +R Be 1956 o - May Su>=22 2 1 D +R Be 1956 o - O lastSu 2 0 S +Z Atlantic/Bermuda -4:19:18 - LMT 1890 +-4:19:18 Be BMT/BST 1930 Ja 1 2 +-4 Be A%sT 1974 Ap 28 2 +-4 C A%sT 1976 +-4 u A%sT +R CR 1979 1980 - F lastSu 0 1 D +R CR 1979 1980 - Jun Su>=1 0 0 S +R CR 1991 1992 - Ja Sa>=15 0 1 D +R CR 1991 o - Jul 1 0 0 S +R CR 1992 o - Mar 15 0 0 S +Z America/Costa_Rica -5:36:13 - LMT 1890 +-5:36:13 - SJMT 1921 Ja 15 +-6 CR C%sT +R Q 1928 o - Jun 10 0 1 D +R Q 1928 o - O 10 0 0 S +R Q 1940 1942 - Jun Su>=1 0 1 D +R Q 1940 1942 - S Su>=1 0 0 S +R Q 1945 1946 - Jun Su>=1 0 1 D +R Q 1945 1946 - S Su>=1 0 0 S +R Q 1965 o - Jun 1 0 1 D +R Q 1965 o - S 30 0 0 S +R Q 1966 o - May 29 0 1 D +R Q 1966 o - O 2 0 0 S +R Q 1967 o - Ap 8 0 1 D +R Q 1967 1968 - S Su>=8 0 0 S +R Q 1968 o - Ap 14 0 1 D +R Q 1969 1977 - Ap lastSu 0 1 D +R Q 1969 1971 - O lastSu 0 0 S +R Q 1972 1974 - O 8 0 0 S +R Q 1975 1977 - O lastSu 0 0 S +R Q 1978 o - May 7 0 1 D +R Q 1978 1990 - O Su>=8 0 0 S +R Q 1979 1980 - Mar Su>=15 0 1 D +R Q 1981 1985 - May Su>=5 0 1 D +R Q 1986 1989 - Mar Su>=14 0 1 D +R Q 1990 1997 - Ap Su>=1 0 1 D +R Q 1991 1995 - O Su>=8 0s 0 S +R Q 1996 o - O 6 0s 0 S +R Q 1997 o - O 12 0s 0 S +R Q 1998 1999 - Mar lastSu 0s 1 D +R Q 1998 2003 - O lastSu 0s 0 S +R Q 2000 2003 - Ap Su>=1 0s 1 D +R Q 2004 o - Mar lastSu 0s 1 D +R Q 2006 2010 - O lastSu 0s 0 S +R Q 2007 o - Mar Su>=8 0s 1 D +R Q 2008 o - Mar Su>=15 0s 1 D +R Q 2009 2010 - Mar Su>=8 0s 1 D +R Q 2011 o - Mar Su>=15 0s 1 D +R Q 2011 o - N 13 0s 0 S +R Q 2012 o - Ap 1 0s 1 D +R Q 2012 ma - N Su>=1 0s 0 S +R Q 2013 ma - Mar Su>=8 0s 1 D +Z America/Havana -5:29:28 - LMT 1890 +-5:29:36 - HMT 1925 Jul 19 12 +-5 Q C%sT +R DO 1966 o - O 30 0 1 EDT +R DO 1967 o - F 28 0 0 EST +R DO 1969 1973 - O lastSu 0 0:30 -0430 +R DO 1970 o - F 21 0 0 EST +R DO 1971 o - Ja 20 0 0 EST +R DO 1972 1974 - Ja 21 0 0 EST +Z America/Santo_Domingo -4:39:36 - LMT 1890 +-4:40 - SDMT 1933 Ap 1 12 +-5 DO %s 1974 O 27 +-4 - AST 2000 O 29 2 +-5 u E%sT 2000 D 3 1 +-4 - AST +R SV 1987 1988 - May Su>=1 0 1 D +R SV 1987 1988 - S lastSu 0 0 S +Z America/El_Salvador -5:56:48 - LMT 1921 +-6 SV C%sT +R GT 1973 o - N 25 0 1 D +R GT 1974 o - F 24 0 0 S +R GT 1983 o - May 21 0 1 D +R GT 1983 o - S 22 0 0 S +R GT 1991 o - Mar 23 0 1 D +R GT 1991 o - S 7 0 0 S +R GT 2006 o - Ap 30 0 1 D +R GT 2006 o - O 1 0 0 S +Z America/Guatemala -6:2:4 - LMT 1918 O 5 +-6 GT C%sT +R HT 1983 o - May 8 0 1 D +R HT 1984 1987 - Ap lastSu 0 1 D +R HT 1983 1987 - O lastSu 0 0 S +R HT 1988 1997 - Ap Su>=1 1s 1 D +R HT 1988 1997 - O lastSu 1s 0 S +R HT 2005 2006 - Ap Su>=1 0 1 D +R HT 2005 2006 - O lastSu 0 0 S +R HT 2012 2015 - Mar Su>=8 2 1 D +R HT 2012 2015 - N Su>=1 2 0 S +R HT 2017 ma - Mar Su>=8 2 1 D +R HT 2017 ma - N Su>=1 2 0 S +Z America/Port-au-Prince -4:49:20 - LMT 1890 +-4:49 - PPMT 1917 Ja 24 12 +-5 HT E%sT +R HN 1987 1988 - May Su>=1 0 1 D +R HN 1987 1988 - S lastSu 0 0 S +R HN 2006 o - May Su>=1 0 1 D +R HN 2006 o - Au M>=1 0 0 S +Z America/Tegucigalpa -5:48:52 - LMT 1921 Ap +-6 HN C%sT +Z America/Jamaica -5:7:10 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1974 +-5 u E%sT 1984 +-5 - EST +Z America/Martinique -4:4:20 - LMT 1890 +-4:4:20 - FFMT 1911 May +-4 - AST 1980 Ap 6 +-4 1 ADT 1980 S 28 +-4 - AST +R NI 1979 1980 - Mar Su>=16 0 1 D +R NI 1979 1980 - Jun M>=23 0 0 S +R NI 2005 o - Ap 10 0 1 D +R NI 2005 o - O Su>=1 0 0 S +R NI 2006 o - Ap 30 2 1 D +R NI 2006 o - O Su>=1 1 0 S +Z America/Managua -5:45:8 - LMT 1890 +-5:45:12 - MMT 1934 Jun 23 +-6 - CST 1973 May +-5 - EST 1975 F 16 +-6 NI C%sT 1992 Ja 1 4 +-5 - EST 1992 S 24 +-6 - CST 1993 +-5 - EST 1997 +-6 NI C%sT +Z America/Panama -5:18:8 - LMT 1890 +-5:19:36 - CMT 1908 Ap 22 +-5 - EST +L America/Panama America/Atikokan +L America/Panama America/Cayman +Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12 +-4 - AST 1942 May 3 +-4 u A%sT 1946 +-4 - AST +L America/Puerto_Rico America/Anguilla +L America/Puerto_Rico America/Antigua +L America/Puerto_Rico America/Aruba +L America/Puerto_Rico America/Curacao +L America/Puerto_Rico America/Blanc-Sablon +L America/Puerto_Rico America/Dominica +L America/Puerto_Rico America/Grenada +L America/Puerto_Rico America/Guadeloupe +L America/Puerto_Rico America/Kralendijk +L America/Puerto_Rico America/Lower_Princes +L America/Puerto_Rico America/Marigot +L America/Puerto_Rico America/Montserrat +L America/Puerto_Rico America/Port_of_Spain +L America/Puerto_Rico America/St_Barthelemy +L America/Puerto_Rico America/St_Kitts +L America/Puerto_Rico America/St_Lucia +L America/Puerto_Rico America/St_Thomas +L America/Puerto_Rico America/St_Vincent +L America/Puerto_Rico America/Tortola +Z America/Miquelon -3:44:40 - LMT 1911 May 15 +-4 - AST 1980 May +-3 - -03 1987 +-3 C -03/-02 +Z America/Grand_Turk -4:44:32 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1979 +-5 u E%sT 2015 Mar 8 2 +-4 - AST 2018 Mar 11 3 +-5 u E%sT +R A 1930 o - D 1 0 1 - +R A 1931 o - Ap 1 0 0 - +R A 1931 o - O 15 0 1 - +R A 1932 1940 - Mar 1 0 0 - +R A 1932 1939 - N 1 0 1 - +R A 1940 o - Jul 1 0 1 - +R A 1941 o - Jun 15 0 0 - +R A 1941 o - O 15 0 1 - +R A 1943 o - Au 1 0 0 - +R A 1943 o - O 15 0 1 - +R A 1946 o - Mar 1 0 0 - +R A 1946 o - O 1 0 1 - +R A 1963 o - O 1 0 0 - +R A 1963 o - D 15 0 1 - +R A 1964 1966 - Mar 1 0 0 - +R A 1964 1966 - O 15 0 1 - +R A 1967 o - Ap 2 0 0 - +R A 1967 1968 - O Su>=1 0 1 - +R A 1968 1969 - Ap Su>=1 0 0 - +R A 1974 o - Ja 23 0 1 - +R A 1974 o - May 1 0 0 - +R A 1988 o - D 1 0 1 - +R A 1989 1993 - Mar Su>=1 0 0 - +R A 1989 1992 - O Su>=15 0 1 - +R A 1999 o - O Su>=1 0 1 - +R A 2000 o - Mar 3 0 0 - +R A 2007 o - D 30 0 1 - +R A 2008 2009 - Mar Su>=15 0 0 - +R A 2008 o - O Su>=15 0 1 - +Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 +Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 +Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 13 +-3 A -03/-02 +Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar +-4 - -04 1991 May 7 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar +-4 - -04 1991 May 7 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 31 +-4 - -04 2004 Jul 25 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 Mar 4 +-4 - -04 1990 O 28 +-4 1 -03 1991 Mar 17 +-4 - -04 1991 O 6 +-3 1 -02 1992 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 Mar 4 +-4 - -04 1990 O 15 +-4 1 -03 1991 Mar +-4 - -04 1991 O 15 +-4 1 -03 1992 Mar +-4 - -04 1992 O 18 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 23 +-4 - -04 2004 S 26 +-3 A -03/-02 2008 O 18 +-3 - -03 +R Sa 2008 2009 - Mar Su>=8 0 0 - +R Sa 2007 2008 - O Su>=8 0 1 - +Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 +-3 1 -02 1990 Mar 14 +-4 - -04 1990 O 15 +-4 1 -03 1991 Mar +-4 - -04 1991 Jun +-3 - -03 1999 O 3 +-4 1 -03 2000 Mar 3 +-3 - -03 2004 May 31 +-4 - -04 2004 Jul 25 +-3 A -03/-02 2008 Ja 21 +-4 Sa -04/-03 2009 O 11 +-3 - -03 +Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 30 +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/La_Paz -4:32:36 - LMT 1890 +-4:32:36 - CMT 1931 O 15 +-4:32:36 1 BST 1932 Mar 21 +-4 - -04 +R B 1931 o - O 3 11 1 - +R B 1932 1933 - Ap 1 0 0 - +R B 1932 o - O 3 0 1 - +R B 1949 1952 - D 1 0 1 - +R B 1950 o - Ap 16 1 0 - +R B 1951 1952 - Ap 1 0 0 - +R B 1953 o - Mar 1 0 0 - +R B 1963 o - D 9 0 1 - +R B 1964 o - Mar 1 0 0 - +R B 1965 o - Ja 31 0 1 - +R B 1965 o - Mar 31 0 0 - +R B 1965 o - D 1 0 1 - +R B 1966 1968 - Mar 1 0 0 - +R B 1966 1967 - N 1 0 1 - +R B 1985 o - N 2 0 1 - +R B 1986 o - Mar 15 0 0 - +R B 1986 o - O 25 0 1 - +R B 1987 o - F 14 0 0 - +R B 1987 o - O 25 0 1 - +R B 1988 o - F 7 0 0 - +R B 1988 o - O 16 0 1 - +R B 1989 o - Ja 29 0 0 - +R B 1989 o - O 15 0 1 - +R B 1990 o - F 11 0 0 - +R B 1990 o - O 21 0 1 - +R B 1991 o - F 17 0 0 - +R B 1991 o - O 20 0 1 - +R B 1992 o - F 9 0 0 - +R B 1992 o - O 25 0 1 - +R B 1993 o - Ja 31 0 0 - +R B 1993 1995 - O Su>=11 0 1 - +R B 1994 1995 - F Su>=15 0 0 - +R B 1996 o - F 11 0 0 - +R B 1996 o - O 6 0 1 - +R B 1997 o - F 16 0 0 - +R B 1997 o - O 6 0 1 - +R B 1998 o - Mar 1 0 0 - +R B 1998 o - O 11 0 1 - +R B 1999 o - F 21 0 0 - +R B 1999 o - O 3 0 1 - +R B 2000 o - F 27 0 0 - +R B 2000 2001 - O Su>=8 0 1 - +R B 2001 2006 - F Su>=15 0 0 - +R B 2002 o - N 3 0 1 - +R B 2003 o - O 19 0 1 - +R B 2004 o - N 2 0 1 - +R B 2005 o - O 16 0 1 - +R B 2006 o - N 5 0 1 - +R B 2007 o - F 25 0 0 - +R B 2007 o - O Su>=8 0 1 - +R B 2008 2017 - O Su>=15 0 1 - +R B 2008 2011 - F Su>=15 0 0 - +R B 2012 o - F Su>=22 0 0 - +R B 2013 2014 - F Su>=15 0 0 - +R B 2015 o - F Su>=22 0 0 - +R B 2016 2019 - F Su>=15 0 0 - +R B 2018 o - N Su>=1 0 1 - +Z America/Noronha -2:9:40 - LMT 1914 +-2 B -02/-01 1990 S 17 +-2 - -02 1999 S 30 +-2 B -02/-01 2000 O 15 +-2 - -02 2001 S 13 +-2 B -02/-01 2002 O +-2 - -02 +Z America/Belem -3:13:56 - LMT 1914 +-3 B -03/-02 1988 S 12 +-3 - -03 +Z America/Santarem -3:38:48 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 2008 Jun 24 +-3 - -03 +Z America/Fortaleza -2:34 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 22 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Recife -2:19:36 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 15 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Araguaina -3:12:48 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1995 S 14 +-3 B -03/-02 2003 S 24 +-3 - -03 2012 O 21 +-3 B -03/-02 2013 S +-3 - -03 +Z America/Maceio -2:22:52 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1995 O 13 +-3 B -03/-02 1996 S 4 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 22 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Bahia -2:34:4 - LMT 1914 +-3 B -03/-02 2003 S 24 +-3 - -03 2011 O 16 +-3 B -03/-02 2012 O 21 +-3 - -03 +Z America/Sao_Paulo -3:6:28 - LMT 1914 +-3 B -03/-02 1963 O 23 +-3 1 -02 1964 +-3 B -03/-02 +Z America/Campo_Grande -3:38:28 - LMT 1914 +-4 B -04/-03 +Z America/Cuiaba -3:44:20 - LMT 1914 +-4 B -04/-03 2003 S 24 +-4 - -04 2004 O +-4 B -04/-03 +Z America/Porto_Velho -4:15:36 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 +Z America/Boa_Vista -4:2:40 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 1999 S 30 +-4 B -04/-03 2000 O 15 +-4 - -04 +Z America/Manaus -4:0:4 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 1993 S 28 +-4 B -04/-03 1994 S 22 +-4 - -04 +Z America/Eirunepe -4:39:28 - LMT 1914 +-5 B -05/-04 1988 S 12 +-5 - -05 1993 S 28 +-5 B -05/-04 1994 S 22 +-5 - -05 2008 Jun 24 +-4 - -04 2013 N 10 +-5 - -05 +Z America/Rio_Branco -4:31:12 - LMT 1914 +-5 B -05/-04 1988 S 12 +-5 - -05 2008 Jun 24 +-4 - -04 2013 N 10 +-5 - -05 +R x 1927 1931 - S 1 0 1 - +R x 1928 1932 - Ap 1 0 0 - +R x 1968 o - N 3 4u 1 - +R x 1969 o - Mar 30 3u 0 - +R x 1969 o - N 23 4u 1 - +R x 1970 o - Mar 29 3u 0 - +R x 1971 o - Mar 14 3u 0 - +R x 1970 1972 - O Su>=9 4u 1 - +R x 1972 1986 - Mar Su>=9 3u 0 - +R x 1973 o - S 30 4u 1 - +R x 1974 1987 - O Su>=9 4u 1 - +R x 1987 o - Ap 12 3u 0 - +R x 1988 1990 - Mar Su>=9 3u 0 - +R x 1988 1989 - O Su>=9 4u 1 - +R x 1990 o - S 16 4u 1 - +R x 1991 1996 - Mar Su>=9 3u 0 - +R x 1991 1997 - O Su>=9 4u 1 - +R x 1997 o - Mar 30 3u 0 - +R x 1998 o - Mar Su>=9 3u 0 - +R x 1998 o - S 27 4u 1 - +R x 1999 o - Ap 4 3u 0 - +R x 1999 2010 - O Su>=9 4u 1 - +R x 2000 2007 - Mar Su>=9 3u 0 - +R x 2008 o - Mar 30 3u 0 - +R x 2009 o - Mar Su>=9 3u 0 - +R x 2010 o - Ap Su>=1 3u 0 - +R x 2011 o - May Su>=2 3u 0 - +R x 2011 o - Au Su>=16 4u 1 - +R x 2012 2014 - Ap Su>=23 3u 0 - +R x 2012 2014 - S Su>=2 4u 1 - +R x 2016 2018 - May Su>=9 3u 0 - +R x 2016 2018 - Au Su>=9 4u 1 - +R x 2019 ma - Ap Su>=2 3u 0 - +R x 2019 ma - S Su>=2 4u 1 - +Z America/Santiago -4:42:45 - LMT 1890 +-4:42:45 - SMT 1910 Ja 10 +-5 - -05 1916 Jul +-4:42:45 - SMT 1918 S 10 +-4 - -04 1919 Jul +-4:42:45 - SMT 1927 S +-5 x -05/-04 1932 S +-4 - -04 1942 Jun +-5 - -05 1942 Au +-4 - -04 1946 Jul 15 +-4 1 -03 1946 S +-4 - -04 1947 Ap +-5 - -05 1947 May 21 23 +-4 x -04/-03 +Z America/Punta_Arenas -4:43:40 - LMT 1890 +-4:42:45 - SMT 1910 Ja 10 +-5 - -05 1916 Jul +-4:42:45 - SMT 1918 S 10 +-4 - -04 1919 Jul +-4:42:45 - SMT 1927 S +-5 x -05/-04 1932 S +-4 - -04 1942 Jun +-5 - -05 1942 Au +-4 - -04 1947 Ap +-5 - -05 1947 May 21 23 +-4 x -04/-03 2016 D 4 +-3 - -03 +Z Pacific/Easter -7:17:28 - LMT 1890 +-7:17:28 - EMT 1932 S +-7 x -07/-06 1982 Mar 14 3u +-6 x -06/-05 +Z Antarctica/Palmer 0 - -00 1965 +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1982 May +-4 x -04/-03 2016 D 4 +-3 - -03 +R CO 1992 o - May 3 0 1 - +R CO 1993 o - Ap 4 0 0 - +Z America/Bogota -4:56:16 - LMT 1884 Mar 13 +-4:56:16 - BMT 1914 N 23 +-5 CO -05/-04 +R EC 1992 o - N 28 0 1 - +R EC 1993 o - F 5 0 0 - +Z America/Guayaquil -5:19:20 - LMT 1890 +-5:14 - QMT 1931 +-5 EC -05/-04 +Z Pacific/Galapagos -5:58:24 - LMT 1931 +-5 - -05 1986 +-6 EC -06/-05 +R FK 1937 1938 - S lastSu 0 1 - +R FK 1938 1942 - Mar Su>=19 0 0 - +R FK 1939 o - O 1 0 1 - +R FK 1940 1942 - S lastSu 0 1 - +R FK 1943 o - Ja 1 0 0 - +R FK 1983 o - S lastSu 0 1 - +R FK 1984 1985 - Ap lastSu 0 0 - +R FK 1984 o - S 16 0 1 - +R FK 1985 2000 - S Su>=9 0 1 - +R FK 1986 2000 - Ap Su>=16 0 0 - +R FK 2001 2010 - Ap Su>=15 2 0 - +R FK 2001 2010 - S Su>=1 2 1 - +Z Atlantic/Stanley -3:51:24 - LMT 1890 +-3:51:24 - SMT 1912 Mar 12 +-4 FK -04/-03 1983 May +-3 FK -03/-02 1985 S 15 +-4 FK -04/-03 2010 S 5 2 +-3 - -03 +Z America/Cayenne -3:29:20 - LMT 1911 Jul +-4 - -04 1967 O +-3 - -03 +Z America/Guyana -3:52:39 - LMT 1911 Au +-4 - -04 1915 Mar +-3:45 - -0345 1975 Au +-3 - -03 1992 Mar 29 1 +-4 - -04 +R y 1975 1988 - O 1 0 1 - +R y 1975 1978 - Mar 1 0 0 - +R y 1979 1991 - Ap 1 0 0 - +R y 1989 o - O 22 0 1 - +R y 1990 o - O 1 0 1 - +R y 1991 o - O 6 0 1 - +R y 1992 o - Mar 1 0 0 - +R y 1992 o - O 5 0 1 - +R y 1993 o - Mar 31 0 0 - +R y 1993 1995 - O 1 0 1 - +R y 1994 1995 - F lastSu 0 0 - +R y 1996 o - Mar 1 0 0 - +R y 1996 2001 - O Su>=1 0 1 - +R y 1997 o - F lastSu 0 0 - +R y 1998 2001 - Mar Su>=1 0 0 - +R y 2002 2004 - Ap Su>=1 0 0 - +R y 2002 2003 - S Su>=1 0 1 - +R y 2004 2009 - O Su>=15 0 1 - +R y 2005 2009 - Mar Su>=8 0 0 - +R y 2010 ma - O Su>=1 0 1 - +R y 2010 2012 - Ap Su>=8 0 0 - +R y 2013 ma - Mar Su>=22 0 0 - +Z America/Asuncion -3:50:40 - LMT 1890 +-3:50:40 - AMT 1931 O 10 +-4 - -04 1972 O +-3 - -03 1974 Ap +-4 y -04/-03 +R PE 1938 o - Ja 1 0 1 - +R PE 1938 o - Ap 1 0 0 - +R PE 1938 1939 - S lastSu 0 1 - +R PE 1939 1940 - Mar Su>=24 0 0 - +R PE 1986 1987 - Ja 1 0 1 - +R PE 1986 1987 - Ap 1 0 0 - +R PE 1990 o - Ja 1 0 1 - +R PE 1990 o - Ap 1 0 0 - +R PE 1994 o - Ja 1 0 1 - +R PE 1994 o - Ap 1 0 0 - +Z America/Lima -5:8:12 - LMT 1890 +-5:8:36 - LMT 1908 Jul 28 +-5 PE -05/-04 +Z Atlantic/South_Georgia -2:26:8 - LMT 1890 +-2 - -02 +Z America/Paramaribo -3:40:40 - LMT 1911 +-3:40:52 - PMT 1935 +-3:40:36 - PMT 1945 O +-3:30 - -0330 1984 O +-3 - -03 +R U 1923 1925 - O 1 0 0:30 - +R U 1924 1926 - Ap 1 0 0 - +R U 1933 1938 - O lastSu 0 0:30 - +R U 1934 1941 - Mar lastSa 24 0 - +R U 1939 o - O 1 0 0:30 - +R U 1940 o - O 27 0 0:30 - +R U 1941 o - Au 1 0 0:30 - +R U 1942 o - D 14 0 0:30 - +R U 1943 o - Mar 14 0 0 - +R U 1959 o - May 24 0 0:30 - +R U 1959 o - N 15 0 0 - +R U 1960 o - Ja 17 0 1 - +R U 1960 o - Mar 6 0 0 - +R U 1965 o - Ap 4 0 1 - +R U 1965 o - S 26 0 0 - +R U 1968 o - May 27 0 0:30 - +R U 1968 o - D 1 0 0 - +R U 1970 o - Ap 25 0 1 - +R U 1970 o - Jun 14 0 0 - +R U 1972 o - Ap 23 0 1 - +R U 1972 o - Jul 16 0 0 - +R U 1974 o - Ja 13 0 1:30 - +R U 1974 o - Mar 10 0 0:30 - +R U 1974 o - S 1 0 0 - +R U 1974 o - D 22 0 1 - +R U 1975 o - Mar 30 0 0 - +R U 1976 o - D 19 0 1 - +R U 1977 o - Mar 6 0 0 - +R U 1977 o - D 4 0 1 - +R U 1978 1979 - Mar Su>=1 0 0 - +R U 1978 o - D 17 0 1 - +R U 1979 o - Ap 29 0 1 - +R U 1980 o - Mar 16 0 0 - +R U 1987 o - D 14 0 1 - +R U 1988 o - F 28 0 0 - +R U 1988 o - D 11 0 1 - +R U 1989 o - Mar 5 0 0 - +R U 1989 o - O 29 0 1 - +R U 1990 o - F 25 0 0 - +R U 1990 1991 - O Su>=21 0 1 - +R U 1991 1992 - Mar Su>=1 0 0 - +R U 1992 o - O 18 0 1 - +R U 1993 o - F 28 0 0 - +R U 2004 o - S 19 0 1 - +R U 2005 o - Mar 27 2 0 - +R U 2005 o - O 9 2 1 - +R U 2006 2015 - Mar Su>=8 2 0 - +R U 2006 2014 - O Su>=1 2 1 - +Z America/Montevideo -3:44:51 - LMT 1908 Jun 10 +-3:44:51 - MMT 1920 May +-4 - -04 1923 O +-3:30 U -0330/-03 1942 D 14 +-3 U -03/-0230 1960 +-3 U -03/-02 1968 +-3 U -03/-0230 1970 +-3 U -03/-02 1974 +-3 U -03/-0130 1974 Mar 10 +-3 U -03/-0230 1974 D 22 +-3 U -03/-02 +Z America/Caracas -4:27:44 - LMT 1890 +-4:27:40 - CMT 1912 F 12 +-4:30 - -0430 1965 +-4 - -04 2007 D 9 3 +-4:30 - -0430 2016 May 1 2:30 +-4 - -04 +Z Etc/GMT 0 - GMT +Z Etc/UTC 0 - UTC +L Etc/GMT GMT +L Etc/UTC Etc/Universal +L Etc/UTC Etc/Zulu +L Etc/GMT Etc/Greenwich +L Etc/GMT Etc/GMT-0 +L Etc/GMT Etc/GMT+0 +L Etc/GMT Etc/GMT0 +Z Etc/GMT-14 14 - +14 +Z Etc/GMT-13 13 - +13 +Z Etc/GMT-12 12 - +12 +Z Etc/GMT-11 11 - +11 +Z Etc/GMT-10 10 - +10 +Z Etc/GMT-9 9 - +09 +Z Etc/GMT-8 8 - +08 +Z Etc/GMT-7 7 - +07 +Z Etc/GMT-6 6 - +06 +Z Etc/GMT-5 5 - +05 +Z Etc/GMT-4 4 - +04 +Z Etc/GMT-3 3 - +03 +Z Etc/GMT-2 2 - +02 +Z Etc/GMT-1 1 - +01 +Z Etc/GMT+1 -1 - -01 +Z Etc/GMT+2 -2 - -02 +Z Etc/GMT+3 -3 - -03 +Z Etc/GMT+4 -4 - -04 +Z Etc/GMT+5 -5 - -05 +Z Etc/GMT+6 -6 - -06 +Z Etc/GMT+7 -7 - -07 +Z Etc/GMT+8 -8 - -08 +Z Etc/GMT+9 -9 - -09 +Z Etc/GMT+10 -10 - -10 +Z Etc/GMT+11 -11 - -11 +Z Etc/GMT+12 -12 - -12 +Z Factory 0 - -00 +L Africa/Nairobi Africa/Asmera +L Africa/Abidjan Africa/Timbuktu +L America/Argentina/Catamarca America/Argentina/ComodRivadavia +L America/Adak America/Atka +L America/Argentina/Buenos_Aires America/Buenos_Aires +L America/Argentina/Catamarca America/Catamarca +L America/Panama America/Coral_Harbour +L America/Argentina/Cordoba America/Cordoba +L America/Tijuana America/Ensenada +L America/Indiana/Indianapolis America/Fort_Wayne +L America/Nuuk America/Godthab +L America/Indiana/Indianapolis America/Indianapolis +L America/Argentina/Jujuy America/Jujuy +L America/Indiana/Knox America/Knox_IN +L America/Kentucky/Louisville America/Louisville +L America/Argentina/Mendoza America/Mendoza +L America/Toronto America/Montreal +L America/Rio_Branco America/Porto_Acre +L America/Argentina/Cordoba America/Rosario +L America/Tijuana America/Santa_Isabel +L America/Denver America/Shiprock +L America/Puerto_Rico America/Virgin +L Pacific/Auckland Antarctica/South_Pole +L Asia/Ashgabat Asia/Ashkhabad +L Asia/Kolkata Asia/Calcutta +L Asia/Shanghai Asia/Chongqing +L Asia/Shanghai Asia/Chungking +L Asia/Dhaka Asia/Dacca +L Asia/Shanghai Asia/Harbin +L Asia/Urumqi Asia/Kashgar +L Asia/Kathmandu Asia/Katmandu +L Asia/Macau Asia/Macao +L Asia/Yangon Asia/Rangoon +L Asia/Ho_Chi_Minh Asia/Saigon +L Asia/Jerusalem Asia/Tel_Aviv +L Asia/Thimphu Asia/Thimbu +L Asia/Makassar Asia/Ujung_Pandang +L Asia/Ulaanbaatar Asia/Ulan_Bator +L Atlantic/Faroe Atlantic/Faeroe +L Europe/Oslo Atlantic/Jan_Mayen +L Australia/Sydney Australia/ACT +L Australia/Sydney Australia/Canberra +L Australia/Hobart Australia/Currie +L Australia/Lord_Howe Australia/LHI +L Australia/Sydney Australia/NSW +L Australia/Darwin Australia/North +L Australia/Brisbane Australia/Queensland +L Australia/Adelaide Australia/South +L Australia/Hobart Australia/Tasmania +L Australia/Melbourne Australia/Victoria +L Australia/Perth Australia/West +L Australia/Broken_Hill Australia/Yancowinna +L America/Rio_Branco Brazil/Acre +L America/Noronha Brazil/DeNoronha +L America/Sao_Paulo Brazil/East +L America/Manaus Brazil/West +L America/Halifax Canada/Atlantic +L America/Winnipeg Canada/Central +L America/Toronto Canada/Eastern +L America/Edmonton Canada/Mountain +L America/St_Johns Canada/Newfoundland +L America/Vancouver Canada/Pacific +L America/Regina Canada/Saskatchewan +L America/Whitehorse Canada/Yukon +L America/Santiago Chile/Continental +L Pacific/Easter Chile/EasterIsland +L America/Havana Cuba +L Africa/Cairo Egypt +L Europe/Dublin Eire +L Etc/UTC Etc/UCT +L Europe/London Europe/Belfast +L Europe/Chisinau Europe/Tiraspol +L Europe/London GB +L Europe/London GB-Eire +L Etc/GMT GMT+0 +L Etc/GMT GMT-0 +L Etc/GMT GMT0 +L Etc/GMT Greenwich +L Asia/Hong_Kong Hongkong +L Atlantic/Reykjavik Iceland +L Asia/Tehran Iran +L Asia/Jerusalem Israel +L America/Jamaica Jamaica +L Asia/Tokyo Japan +L Pacific/Kwajalein Kwajalein +L Africa/Tripoli Libya +L America/Tijuana Mexico/BajaNorte +L America/Mazatlan Mexico/BajaSur +L America/Mexico_City Mexico/General +L Pacific/Auckland NZ +L Pacific/Chatham NZ-CHAT +L America/Denver Navajo +L Asia/Shanghai PRC +L Pacific/Kanton Pacific/Enderbury +L Pacific/Honolulu Pacific/Johnston +L Pacific/Pohnpei Pacific/Ponape +L Pacific/Pago_Pago Pacific/Samoa +L Pacific/Chuuk Pacific/Truk +L Pacific/Chuuk Pacific/Yap +L Europe/Warsaw Poland +L Europe/Lisbon Portugal +L Asia/Taipei ROC +L Asia/Seoul ROK +L Asia/Singapore Singapore +L Europe/Istanbul Turkey +L Etc/UTC UCT +L America/Anchorage US/Alaska +L America/Adak US/Aleutian +L America/Phoenix US/Arizona +L America/Chicago US/Central +L America/Indiana/Indianapolis US/East-Indiana +L America/New_York US/Eastern +L Pacific/Honolulu US/Hawaii +L America/Indiana/Knox US/Indiana-Starke +L America/Detroit US/Michigan +L America/Denver US/Mountain +L America/Los_Angeles US/Pacific +L Pacific/Pago_Pago US/Samoa +L Etc/UTC UTC +L Etc/UTC Universal +L Europe/Moscow W-SU +L Etc/UTC Zulu diff --git a/telegramer/include/pytz/zoneinfo/zone.tab b/telegramer/include/pytz/zoneinfo/zone.tab new file mode 100644 index 0000000..086458f --- /dev/null +++ b/telegramer/include/pytz/zoneinfo/zone.tab @@ -0,0 +1,454 @@ +# tzdb timezone descriptions (deprecated version) +# +# This file is in the public domain, so clarified as of +# 2009-05-17 by Arthur David Olson. +# +# From Paul Eggert (2021-09-20): +# This file is intended as a backward-compatibility aid for older programs. +# New programs should use zone1970.tab. This file is like zone1970.tab (see +# zone1970.tab's comments), but with the following additional restrictions: +# +# 1. This file contains only ASCII characters. +# 2. The first data column contains exactly one country code. +# +# Because of (2), each row stands for an area that is the intersection +# of a region identified by a country code and of a timezone where civil +# clocks have agreed since 1970; this is a narrower definition than +# that of zone1970.tab. +# +# Unlike zone1970.tab, a row's third column can be a Link from +# 'backward' instead of a Zone. +# +# This table is intended as an aid for users, to help them select timezones +# appropriate for their practical needs. It is not intended to take or +# endorse any position on legal or territorial claims. +# +#country- +#code coordinates TZ comments +AD +4230+00131 Europe/Andorra +AE +2518+05518 Asia/Dubai +AF +3431+06912 Asia/Kabul +AG +1703-06148 America/Antigua +AI +1812-06304 America/Anguilla +AL +4120+01950 Europe/Tirane +AM +4011+04430 Asia/Yerevan +AO -0848+01314 Africa/Luanda +AQ -7750+16636 Antarctica/McMurdo New Zealand time - McMurdo, South Pole +AQ -6617+11031 Antarctica/Casey Casey +AQ -6835+07758 Antarctica/Davis Davis +AQ -6640+14001 Antarctica/DumontDUrville Dumont-d'Urville +AQ -6736+06253 Antarctica/Mawson Mawson +AQ -6448-06406 Antarctica/Palmer Palmer +AQ -6734-06808 Antarctica/Rothera Rothera +AQ -690022+0393524 Antarctica/Syowa Syowa +AQ -720041+0023206 Antarctica/Troll Troll +AQ -7824+10654 Antarctica/Vostok Vostok +AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) +AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF) +AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) +AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) +AR -2649-06513 America/Argentina/Tucuman Tucuman (TM) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH) +AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) +AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) +AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) +AR -3319-06621 America/Argentina/San_Luis San Luis (SL) +AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) +AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) +AS -1416-17042 Pacific/Pago_Pago +AT +4813+01620 Europe/Vienna +AU -3133+15905 Australia/Lord_Howe Lord Howe Island +AU -5430+15857 Antarctica/Macquarie Macquarie Island +AU -4253+14719 Australia/Hobart Tasmania +AU -3749+14458 Australia/Melbourne Victoria +AU -3352+15113 Australia/Sydney New South Wales (most areas) +AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) +AU -2728+15302 Australia/Brisbane Queensland (most areas) +AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) +AU -3455+13835 Australia/Adelaide South Australia +AU -1228+13050 Australia/Darwin Northern Territory +AU -3157+11551 Australia/Perth Western Australia (most areas) +AU -3143+12852 Australia/Eucla Western Australia (Eucla) +AW +1230-06958 America/Aruba +AX +6006+01957 Europe/Mariehamn +AZ +4023+04951 Asia/Baku +BA +4352+01825 Europe/Sarajevo +BB +1306-05937 America/Barbados +BD +2343+09025 Asia/Dhaka +BE +5050+00420 Europe/Brussels +BF +1222-00131 Africa/Ouagadougou +BG +4241+02319 Europe/Sofia +BH +2623+05035 Asia/Bahrain +BI -0323+02922 Africa/Bujumbura +BJ +0629+00237 Africa/Porto-Novo +BL +1753-06251 America/St_Barthelemy +BM +3217-06446 Atlantic/Bermuda +BN +0456+11455 Asia/Brunei +BO -1630-06809 America/La_Paz +BQ +120903-0681636 America/Kralendijk +BR -0351-03225 America/Noronha Atlantic islands +BR -0127-04829 America/Belem Para (east); Amapa +BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) +BR -0803-03454 America/Recife Pernambuco +BR -0712-04812 America/Araguaina Tocantins +BR -0940-03543 America/Maceio Alagoas, Sergipe +BR -1259-03831 America/Bahia Bahia +BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) +BR -2027-05437 America/Campo_Grande Mato Grosso do Sul +BR -1535-05605 America/Cuiaba Mato Grosso +BR -0226-05452 America/Santarem Para (west) +BR -0846-06354 America/Porto_Velho Rondonia +BR +0249-06040 America/Boa_Vista Roraima +BR -0308-06001 America/Manaus Amazonas (east) +BR -0640-06952 America/Eirunepe Amazonas (west) +BR -0958-06748 America/Rio_Branco Acre +BS +2505-07721 America/Nassau +BT +2728+08939 Asia/Thimphu +BW -2439+02555 Africa/Gaborone +BY +5354+02734 Europe/Minsk +BZ +1730-08812 America/Belize +CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast) +CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE +CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) +CA +4606-06447 America/Moncton Atlantic - New Brunswick +CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) +CA +5125-05707 America/Blanc-Sablon AST - QC (Lower North Shore) +CA +4339-07923 America/Toronto Eastern - ON, QC (most areas) +CA +4901-08816 America/Nipigon Eastern - ON, QC (no DST 1967-73) +CA +4823-08915 America/Thunder_Bay Eastern - ON (Thunder Bay) +CA +6344-06828 America/Iqaluit Eastern - NU (most east areas) +CA +6608-06544 America/Pangnirtung Eastern - NU (Pangnirtung) +CA +484531-0913718 America/Atikokan EST - ON (Atikokan); NU (Coral H) +CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba +CA +4843-09434 America/Rainy_River Central - ON (Rainy R, Ft Frances) +CA +744144-0944945 America/Resolute Central - NU (Resolute) +CA +624900-0920459 America/Rankin_Inlet Central - NU (central) +CA +5024-10439 America/Regina CST - SK (most areas) +CA +5017-10750 America/Swift_Current CST - SK (midwest) +CA +5333-11328 America/Edmonton Mountain - AB; BC (E); SK (W) +CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) +CA +6227-11421 America/Yellowknife Mountain - NT (central) +CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +4906-11631 America/Creston MST - BC (Creston) +CA +5946-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) +CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) +CA +6043-13503 America/Whitehorse MST - Yukon (east) +CA +6404-13925 America/Dawson MST - Yukon (west) +CA +4916-12307 America/Vancouver Pacific - BC (most areas) +CC -1210+09655 Indian/Cocos +CD -0418+01518 Africa/Kinshasa Dem. Rep. of Congo (west) +CD -1140+02728 Africa/Lubumbashi Dem. Rep. of Congo (east) +CF +0422+01835 Africa/Bangui +CG -0416+01517 Africa/Brazzaville +CH +4723+00832 Europe/Zurich +CI +0519-00402 Africa/Abidjan +CK -2114-15946 Pacific/Rarotonga +CL -3327-07040 America/Santiago Chile (most areas) +CL -5309-07055 America/Punta_Arenas Region of Magallanes +CL -2709-10926 Pacific/Easter Easter Island +CM +0403+00942 Africa/Douala +CN +3114+12128 Asia/Shanghai Beijing Time +CN +4348+08735 Asia/Urumqi Xinjiang Time +CO +0436-07405 America/Bogota +CR +0956-08405 America/Costa_Rica +CU +2308-08222 America/Havana +CV +1455-02331 Atlantic/Cape_Verde +CW +1211-06900 America/Curacao +CX -1025+10543 Indian/Christmas +CY +3510+03322 Asia/Nicosia Cyprus (most areas) +CY +3507+03357 Asia/Famagusta Northern Cyprus +CZ +5005+01426 Europe/Prague +DE +5230+01322 Europe/Berlin Germany (most areas) +DE +4742+00841 Europe/Busingen Busingen +DJ +1136+04309 Africa/Djibouti +DK +5540+01235 Europe/Copenhagen +DM +1518-06124 America/Dominica +DO +1828-06954 America/Santo_Domingo +DZ +3647+00303 Africa/Algiers +EC -0210-07950 America/Guayaquil Ecuador (mainland) +EC -0054-08936 Pacific/Galapagos Galapagos Islands +EE +5925+02445 Europe/Tallinn +EG +3003+03115 Africa/Cairo +EH +2709-01312 Africa/El_Aaiun +ER +1520+03853 Africa/Asmara +ES +4024-00341 Europe/Madrid Spain (mainland) +ES +3553-00519 Africa/Ceuta Ceuta, Melilla +ES +2806-01524 Atlantic/Canary Canary Islands +ET +0902+03842 Africa/Addis_Ababa +FI +6010+02458 Europe/Helsinki +FJ -1808+17825 Pacific/Fiji +FK -5142-05751 Atlantic/Stanley +FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap +FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape +FM +0519+16259 Pacific/Kosrae Kosrae +FO +6201-00646 Atlantic/Faroe +FR +4852+00220 Europe/Paris +GA +0023+00927 Africa/Libreville +GB +513030-0000731 Europe/London +GD +1203-06145 America/Grenada +GE +4143+04449 Asia/Tbilisi +GF +0456-05220 America/Cayenne +GG +492717-0023210 Europe/Guernsey +GH +0533-00013 Africa/Accra +GI +3608-00521 Europe/Gibraltar +GL +6411-05144 America/Nuuk Greenland (most areas) +GL +7646-01840 America/Danmarkshavn National Park (east coast) +GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit +GL +7634-06847 America/Thule Thule/Pituffik +GM +1328-01639 Africa/Banjul +GN +0931-01343 Africa/Conakry +GP +1614-06132 America/Guadeloupe +GQ +0345+00847 Africa/Malabo +GR +3758+02343 Europe/Athens +GS -5416-03632 Atlantic/South_Georgia +GT +1438-09031 America/Guatemala +GU +1328+14445 Pacific/Guam +GW +1151-01535 Africa/Bissau +GY +0648-05810 America/Guyana +HK +2217+11409 Asia/Hong_Kong +HN +1406-08713 America/Tegucigalpa +HR +4548+01558 Europe/Zagreb +HT +1832-07220 America/Port-au-Prince +HU +4730+01905 Europe/Budapest +ID -0610+10648 Asia/Jakarta Java, Sumatra +ID -0002+10920 Asia/Pontianak Borneo (west, central) +ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas +IE +5320-00615 Europe/Dublin +IL +314650+0351326 Asia/Jerusalem +IM +5409-00428 Europe/Isle_of_Man +IN +2232+08822 Asia/Kolkata +IO -0720+07225 Indian/Chagos +IQ +3321+04425 Asia/Baghdad +IR +3540+05126 Asia/Tehran +IS +6409-02151 Atlantic/Reykjavik +IT +4154+01229 Europe/Rome +JE +491101-0020624 Europe/Jersey +JM +175805-0764736 America/Jamaica +JO +3157+03556 Asia/Amman +JP +353916+1394441 Asia/Tokyo +KE -0117+03649 Africa/Nairobi +KG +4254+07436 Asia/Bishkek +KH +1133+10455 Asia/Phnom_Penh +KI +0125+17300 Pacific/Tarawa Gilbert Islands +KI -0247-17143 Pacific/Kanton Phoenix Islands +KI +0152-15720 Pacific/Kiritimati Line Islands +KM -1141+04316 Indian/Comoro +KN +1718-06243 America/St_Kitts +KP +3901+12545 Asia/Pyongyang +KR +3733+12658 Asia/Seoul +KW +2920+04759 Asia/Kuwait +KY +1918-08123 America/Cayman +KZ +4315+07657 Asia/Almaty Kazakhstan (most areas) +KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda +KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay +KZ +5017+05710 Asia/Aqtobe Aqtobe/Aktobe +KZ +4431+05016 Asia/Aqtau Mangghystau/Mankistau +KZ +4707+05156 Asia/Atyrau Atyrau/Atirau/Gur'yev +KZ +5113+05121 Asia/Oral West Kazakhstan +LA +1758+10236 Asia/Vientiane +LB +3353+03530 Asia/Beirut +LC +1401-06100 America/St_Lucia +LI +4709+00931 Europe/Vaduz +LK +0656+07951 Asia/Colombo +LR +0618-01047 Africa/Monrovia +LS -2928+02730 Africa/Maseru +LT +5441+02519 Europe/Vilnius +LU +4936+00609 Europe/Luxembourg +LV +5657+02406 Europe/Riga +LY +3254+01311 Africa/Tripoli +MA +3339-00735 Africa/Casablanca +MC +4342+00723 Europe/Monaco +MD +4700+02850 Europe/Chisinau +ME +4226+01916 Europe/Podgorica +MF +1804-06305 America/Marigot +MG -1855+04731 Indian/Antananarivo +MH +0709+17112 Pacific/Majuro Marshall Islands (most areas) +MH +0905+16720 Pacific/Kwajalein Kwajalein +MK +4159+02126 Europe/Skopje +ML +1239-00800 Africa/Bamako +MM +1647+09610 Asia/Yangon +MN +4755+10653 Asia/Ulaanbaatar Mongolia (most areas) +MN +4801+09139 Asia/Hovd Bayan-Olgiy, Govi-Altai, Hovd, Uvs, Zavkhan +MN +4804+11430 Asia/Choibalsan Dornod, Sukhbaatar +MO +221150+1133230 Asia/Macau +MP +1512+14545 Pacific/Saipan +MQ +1436-06105 America/Martinique +MR +1806-01557 Africa/Nouakchott +MS +1643-06213 America/Montserrat +MT +3554+01431 Europe/Malta +MU -2010+05730 Indian/Mauritius +MV +0410+07330 Indian/Maldives +MW -1547+03500 Africa/Blantyre +MX +1924-09909 America/Mexico_City Central Time +MX +2105-08646 America/Cancun Eastern Standard Time - Quintana Roo +MX +2058-08937 America/Merida Central Time - Campeche, Yucatan +MX +2540-10019 America/Monterrey Central Time - Durango; Coahuila, Nuevo Leon, Tamaulipas (most areas) +MX +2550-09730 America/Matamoros Central Time US - Coahuila, Nuevo Leon, Tamaulipas (US border) +MX +2313-10625 America/Mazatlan Mountain Time - Baja California Sur, Nayarit, Sinaloa +MX +2838-10605 America/Chihuahua Mountain Time - Chihuahua (most areas) +MX +2934-10425 America/Ojinaga Mountain Time US - Chihuahua (US border) +MX +2904-11058 America/Hermosillo Mountain Standard Time - Sonora +MX +3232-11701 America/Tijuana Pacific Time US - Baja California +MX +2048-10515 America/Bahia_Banderas Central Time - Bahia de Banderas +MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula) +MY +0133+11020 Asia/Kuching Sabah, Sarawak +MZ -2558+03235 Africa/Maputo +NA -2234+01706 Africa/Windhoek +NC -2216+16627 Pacific/Noumea +NE +1331+00207 Africa/Niamey +NF -2903+16758 Pacific/Norfolk +NG +0627+00324 Africa/Lagos +NI +1209-08617 America/Managua +NL +5222+00454 Europe/Amsterdam +NO +5955+01045 Europe/Oslo +NP +2743+08519 Asia/Kathmandu +NR -0031+16655 Pacific/Nauru +NU -1901-16955 Pacific/Niue +NZ -3652+17446 Pacific/Auckland New Zealand (most areas) +NZ -4357-17633 Pacific/Chatham Chatham Islands +OM +2336+05835 Asia/Muscat +PA +0858-07932 America/Panama +PE -1203-07703 America/Lima +PF -1732-14934 Pacific/Tahiti Society Islands +PF -0900-13930 Pacific/Marquesas Marquesas Islands +PF -2308-13457 Pacific/Gambier Gambier Islands +PG -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas) +PG -0613+15534 Pacific/Bougainville Bougainville +PH +1435+12100 Asia/Manila +PK +2452+06703 Asia/Karachi +PL +5215+02100 Europe/Warsaw +PM +4703-05620 America/Miquelon +PN -2504-13005 Pacific/Pitcairn +PR +182806-0660622 America/Puerto_Rico +PS +3130+03428 Asia/Gaza Gaza Strip +PS +313200+0350542 Asia/Hebron West Bank +PT +3843-00908 Europe/Lisbon Portugal (mainland) +PT +3238-01654 Atlantic/Madeira Madeira Islands +PT +3744-02540 Atlantic/Azores Azores +PW +0720+13429 Pacific/Palau +PY -2516-05740 America/Asuncion +QA +2517+05132 Asia/Qatar +RE -2052+05528 Indian/Reunion +RO +4426+02606 Europe/Bucharest +RS +4450+02030 Europe/Belgrade +RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad +RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area +# The obsolescent zone.tab format cannot represent Europe/Simferopol well. +# Put it in RU section and list as UA. See "territorial claims" above. +# Programs should use zone1970.tab instead; see above. +UA +4457+03406 Europe/Simferopol Crimea +RU +5836+04939 Europe/Kirov MSK+00 - Kirov +RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd +RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan +RU +5134+04602 Europe/Saratov MSK+01 - Saratov +RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk +RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia +RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals +RU +5500+07324 Asia/Omsk MSK+03 - Omsk +RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk +RU +5322+08345 Asia/Barnaul MSK+04 - Altai +RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk +RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo +RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area +RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia +RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky +RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River +RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky +RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River +RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky +RU +5934+15048 Asia/Magadan MSK+08 - Magadan +RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); North Kuril Is +RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka +RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea +RW -0157+03004 Africa/Kigali +SA +2438+04643 Asia/Riyadh +SB -0932+16012 Pacific/Guadalcanal +SC -0440+05528 Indian/Mahe +SD +1536+03232 Africa/Khartoum +SE +5920+01803 Europe/Stockholm +SG +0117+10351 Asia/Singapore +SH -1555-00542 Atlantic/St_Helena +SI +4603+01431 Europe/Ljubljana +SJ +7800+01600 Arctic/Longyearbyen +SK +4809+01707 Europe/Bratislava +SL +0830-01315 Africa/Freetown +SM +4355+01228 Europe/San_Marino +SN +1440-01726 Africa/Dakar +SO +0204+04522 Africa/Mogadishu +SR +0550-05510 America/Paramaribo +SS +0451+03137 Africa/Juba +ST +0020+00644 Africa/Sao_Tome +SV +1342-08912 America/El_Salvador +SX +180305-0630250 America/Lower_Princes +SY +3330+03618 Asia/Damascus +SZ -2618+03106 Africa/Mbabane +TC +2128-07108 America/Grand_Turk +TD +1207+01503 Africa/Ndjamena +TF -492110+0701303 Indian/Kerguelen +TG +0608+00113 Africa/Lome +TH +1345+10031 Asia/Bangkok +TJ +3835+06848 Asia/Dushanbe +TK -0922-17114 Pacific/Fakaofo +TL -0833+12535 Asia/Dili +TM +3757+05823 Asia/Ashgabat +TN +3648+01011 Africa/Tunis +TO -210800-1751200 Pacific/Tongatapu +TR +4101+02858 Europe/Istanbul +TT +1039-06131 America/Port_of_Spain +TV -0831+17913 Pacific/Funafuti +TW +2503+12130 Asia/Taipei +TZ -0648+03917 Africa/Dar_es_Salaam +UA +5026+03031 Europe/Kiev Ukraine (most areas) +UA +4837+02218 Europe/Uzhgorod Transcarpathia +UA +4750+03510 Europe/Zaporozhye Zaporozhye and east Lugansk +UG +0019+03225 Africa/Kampala +UM +2813-17722 Pacific/Midway Midway Islands +UM +1917+16637 Pacific/Wake Wake Island +US +404251-0740023 America/New_York Eastern (most areas) +US +421953-0830245 America/Detroit Eastern - MI (most areas) +US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) +US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) +US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) +US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) +US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) +US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) +US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) +US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) +US +415100-0873900 America/Chicago Central (most areas) +US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) +US +411745-0863730 America/Indiana/Knox Central - IN (Starke) +US +450628-0873651 America/Menominee Central - MI (Wisconsin border) +US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) +US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) +US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) +US +394421-1045903 America/Denver Mountain (most areas) +US +433649-1161209 America/Boise Mountain - ID (south); OR (east) +US +332654-1120424 America/Phoenix MST - Arizona (except Navajo) +US +340308-1181434 America/Los_Angeles Pacific +US +611305-1495401 America/Anchorage Alaska (most areas) +US +581807-1342511 America/Juneau Alaska - Juneau area +US +571035-1351807 America/Sitka Alaska - Sitka area +US +550737-1313435 America/Metlakatla Alaska - Annette Island +US +593249-1394338 America/Yakutat Alaska - Yakutat +US +643004-1652423 America/Nome Alaska (west) +US +515248-1763929 America/Adak Aleutian Islands +US +211825-1575130 Pacific/Honolulu Hawaii +UY -345433-0561245 America/Montevideo +UZ +3940+06648 Asia/Samarkand Uzbekistan (west) +UZ +4120+06918 Asia/Tashkent Uzbekistan (east) +VA +415408+0122711 Europe/Vatican +VC +1309-06114 America/St_Vincent +VE +1030-06656 America/Caracas +VG +1827-06437 America/Tortola +VI +1821-06456 America/St_Thomas +VN +1045+10640 Asia/Ho_Chi_Minh +VU -1740+16825 Pacific/Efate +WF -1318-17610 Pacific/Wallis +WS -1350-17144 Pacific/Apia +YE +1245+04512 Asia/Aden +YT -1247+04514 Indian/Mayotte +ZA -2615+02800 Africa/Johannesburg +ZM -1525+02817 Africa/Lusaka +ZW -1750+03103 Africa/Harare diff --git a/telegramer/include/pytz/zoneinfo/zone1970.tab b/telegramer/include/pytz/zoneinfo/zone1970.tab new file mode 100644 index 0000000..c614be8 --- /dev/null +++ b/telegramer/include/pytz/zoneinfo/zone1970.tab @@ -0,0 +1,374 @@ +# tzdb timezone descriptions +# +# This file is in the public domain. +# +# From Paul Eggert (2018-06-27): +# This file contains a table where each row stands for a timezone where +# civil timestamps have agreed since 1970. Columns are separated by +# a single tab. Lines beginning with '#' are comments. All text uses +# UTF-8 encoding. The columns of the table are as follows: +# +# 1. The countries that overlap the timezone, as a comma-separated list +# of ISO 3166 2-character country codes. See the file 'iso3166.tab'. +# 2. Latitude and longitude of the timezone's principal location +# in ISO 6709 sign-degrees-minutes-seconds format, +# either ±DDMM±DDDMM or ±DDMMSS±DDDMMSS, +# first latitude (+ is north), then longitude (+ is east). +# 3. Timezone name used in value of TZ environment variable. +# Please see the theory.html file for how these names are chosen. +# If multiple timezones overlap a country, each has a row in the +# table, with each column 1 containing the country code. +# 4. Comments; present if and only if a country has multiple timezones. +# +# If a timezone covers multiple countries, the most-populous city is used, +# and that country is listed first in column 1; any other countries +# are listed alphabetically by country code. The table is sorted +# first by country code, then (if possible) by an order within the +# country that (1) makes some geographical sense, and (2) puts the +# most populous timezones first, where that does not contradict (1). +# +# This table is intended as an aid for users, to help them select timezones +# appropriate for their practical needs. It is not intended to take or +# endorse any position on legal or territorial claims. +# +#country- +#codes coordinates TZ comments +AD +4230+00131 Europe/Andorra +AE,OM +2518+05518 Asia/Dubai +AF +3431+06912 Asia/Kabul +AL +4120+01950 Europe/Tirane +AM +4011+04430 Asia/Yerevan +AQ -6617+11031 Antarctica/Casey Casey +AQ -6835+07758 Antarctica/Davis Davis +AQ -6736+06253 Antarctica/Mawson Mawson +AQ -6448-06406 Antarctica/Palmer Palmer +AQ -6734-06808 Antarctica/Rothera Rothera +AQ -720041+0023206 Antarctica/Troll Troll +AQ -7824+10654 Antarctica/Vostok Vostok +AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) +AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF) +AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) +AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) +AR -2649-06513 America/Argentina/Tucuman Tucumán (TM) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH) +AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) +AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) +AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) +AR -3319-06621 America/Argentina/San_Luis San Luis (SL) +AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) +AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) +AS,UM -1416-17042 Pacific/Pago_Pago Samoa, Midway +AT +4813+01620 Europe/Vienna +AU -3133+15905 Australia/Lord_Howe Lord Howe Island +AU -5430+15857 Antarctica/Macquarie Macquarie Island +AU -4253+14719 Australia/Hobart Tasmania +AU -3749+14458 Australia/Melbourne Victoria +AU -3352+15113 Australia/Sydney New South Wales (most areas) +AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) +AU -2728+15302 Australia/Brisbane Queensland (most areas) +AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) +AU -3455+13835 Australia/Adelaide South Australia +AU -1228+13050 Australia/Darwin Northern Territory +AU -3157+11551 Australia/Perth Western Australia (most areas) +AU -3143+12852 Australia/Eucla Western Australia (Eucla) +AZ +4023+04951 Asia/Baku +BB +1306-05937 America/Barbados +BD +2343+09025 Asia/Dhaka +BE +5050+00420 Europe/Brussels +BG +4241+02319 Europe/Sofia +BM +3217-06446 Atlantic/Bermuda +BN +0456+11455 Asia/Brunei +BO -1630-06809 America/La_Paz +BR -0351-03225 America/Noronha Atlantic islands +BR -0127-04829 America/Belem Pará (east); Amapá +BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) +BR -0803-03454 America/Recife Pernambuco +BR -0712-04812 America/Araguaina Tocantins +BR -0940-03543 America/Maceio Alagoas, Sergipe +BR -1259-03831 America/Bahia Bahia +BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) +BR -2027-05437 America/Campo_Grande Mato Grosso do Sul +BR -1535-05605 America/Cuiaba Mato Grosso +BR -0226-05452 America/Santarem Pará (west) +BR -0846-06354 America/Porto_Velho Rondônia +BR +0249-06040 America/Boa_Vista Roraima +BR -0308-06001 America/Manaus Amazonas (east) +BR -0640-06952 America/Eirunepe Amazonas (west) +BR -0958-06748 America/Rio_Branco Acre +BT +2728+08939 Asia/Thimphu +BY +5354+02734 Europe/Minsk +BZ +1730-08812 America/Belize +CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast) +CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE +CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) +CA +4606-06447 America/Moncton Atlantic - New Brunswick +CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) +CA,BS +4339-07923 America/Toronto Eastern - ON, QC (most areas), Bahamas +CA +4901-08816 America/Nipigon Eastern - ON, QC (no DST 1967-73) +CA +4823-08915 America/Thunder_Bay Eastern - ON (Thunder Bay) +CA +6344-06828 America/Iqaluit Eastern - NU (most east areas) +CA +6608-06544 America/Pangnirtung Eastern - NU (Pangnirtung) +CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba +CA +4843-09434 America/Rainy_River Central - ON (Rainy R, Ft Frances) +CA +744144-0944945 America/Resolute Central - NU (Resolute) +CA +624900-0920459 America/Rankin_Inlet Central - NU (central) +CA +5024-10439 America/Regina CST - SK (most areas) +CA +5017-10750 America/Swift_Current CST - SK (midwest) +CA +5333-11328 America/Edmonton Mountain - AB; BC (E); SK (W) +CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) +CA +6227-11421 America/Yellowknife Mountain - NT (central) +CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +5946-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) +CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) +CA +6043-13503 America/Whitehorse MST - Yukon (east) +CA +6404-13925 America/Dawson MST - Yukon (west) +CA +4916-12307 America/Vancouver Pacific - BC (most areas) +CC -1210+09655 Indian/Cocos +CH,DE,LI +4723+00832 Europe/Zurich Swiss time +CI,BF,GH,GM,GN,ML,MR,SH,SL,SN,TG +0519-00402 Africa/Abidjan +CK -2114-15946 Pacific/Rarotonga +CL -3327-07040 America/Santiago Chile (most areas) +CL -5309-07055 America/Punta_Arenas Region of Magallanes +CL -2709-10926 Pacific/Easter Easter Island +CN +3114+12128 Asia/Shanghai Beijing Time +CN +4348+08735 Asia/Urumqi Xinjiang Time +CO +0436-07405 America/Bogota +CR +0956-08405 America/Costa_Rica +CU +2308-08222 America/Havana +CV +1455-02331 Atlantic/Cape_Verde +CX -1025+10543 Indian/Christmas +CY +3510+03322 Asia/Nicosia Cyprus (most areas) +CY +3507+03357 Asia/Famagusta Northern Cyprus +CZ,SK +5005+01426 Europe/Prague +DE +5230+01322 Europe/Berlin Germany (most areas) +DK +5540+01235 Europe/Copenhagen +DO +1828-06954 America/Santo_Domingo +DZ +3647+00303 Africa/Algiers +EC -0210-07950 America/Guayaquil Ecuador (mainland) +EC -0054-08936 Pacific/Galapagos Galápagos Islands +EE +5925+02445 Europe/Tallinn +EG +3003+03115 Africa/Cairo +EH +2709-01312 Africa/El_Aaiun +ES +4024-00341 Europe/Madrid Spain (mainland) +ES +3553-00519 Africa/Ceuta Ceuta, Melilla +ES +2806-01524 Atlantic/Canary Canary Islands +FI,AX +6010+02458 Europe/Helsinki +FJ -1808+17825 Pacific/Fiji +FK -5142-05751 Atlantic/Stanley +FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap +FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape +FM +0519+16259 Pacific/Kosrae Kosrae +FO +6201-00646 Atlantic/Faroe +FR +4852+00220 Europe/Paris +GB,GG,IM,JE +513030-0000731 Europe/London +GE +4143+04449 Asia/Tbilisi +GF +0456-05220 America/Cayenne +GI +3608-00521 Europe/Gibraltar +GL +6411-05144 America/Nuuk Greenland (most areas) +GL +7646-01840 America/Danmarkshavn National Park (east coast) +GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit +GL +7634-06847 America/Thule Thule/Pituffik +GR +3758+02343 Europe/Athens +GS -5416-03632 Atlantic/South_Georgia +GT +1438-09031 America/Guatemala +GU,MP +1328+14445 Pacific/Guam +GW +1151-01535 Africa/Bissau +GY +0648-05810 America/Guyana +HK +2217+11409 Asia/Hong_Kong +HN +1406-08713 America/Tegucigalpa +HT +1832-07220 America/Port-au-Prince +HU +4730+01905 Europe/Budapest +ID -0610+10648 Asia/Jakarta Java, Sumatra +ID -0002+10920 Asia/Pontianak Borneo (west, central) +ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas +IE +5320-00615 Europe/Dublin +IL +314650+0351326 Asia/Jerusalem +IN +2232+08822 Asia/Kolkata +IO -0720+07225 Indian/Chagos +IQ +3321+04425 Asia/Baghdad +IR +3540+05126 Asia/Tehran +IS +6409-02151 Atlantic/Reykjavik +IT,SM,VA +4154+01229 Europe/Rome +JM +175805-0764736 America/Jamaica +JO +3157+03556 Asia/Amman +JP +353916+1394441 Asia/Tokyo +KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT -0117+03649 Africa/Nairobi +KG +4254+07436 Asia/Bishkek +KI +0125+17300 Pacific/Tarawa Gilbert Islands +KI -0247-17143 Pacific/Kanton Phoenix Islands +KI +0152-15720 Pacific/Kiritimati Line Islands +KP +3901+12545 Asia/Pyongyang +KR +3733+12658 Asia/Seoul +KZ +4315+07657 Asia/Almaty Kazakhstan (most areas) +KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda +KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay +KZ +5017+05710 Asia/Aqtobe Aqtöbe/Aktobe +KZ +4431+05016 Asia/Aqtau Mangghystaū/Mankistau +KZ +4707+05156 Asia/Atyrau Atyraū/Atirau/Gur'yev +KZ +5113+05121 Asia/Oral West Kazakhstan +LB +3353+03530 Asia/Beirut +LK +0656+07951 Asia/Colombo +LR +0618-01047 Africa/Monrovia +LT +5441+02519 Europe/Vilnius +LU +4936+00609 Europe/Luxembourg +LV +5657+02406 Europe/Riga +LY +3254+01311 Africa/Tripoli +MA +3339-00735 Africa/Casablanca +MC +4342+00723 Europe/Monaco +MD +4700+02850 Europe/Chisinau +MH +0709+17112 Pacific/Majuro Marshall Islands (most areas) +MH +0905+16720 Pacific/Kwajalein Kwajalein +MM +1647+09610 Asia/Yangon +MN +4755+10653 Asia/Ulaanbaatar Mongolia (most areas) +MN +4801+09139 Asia/Hovd Bayan-Ölgii, Govi-Altai, Hovd, Uvs, Zavkhan +MN +4804+11430 Asia/Choibalsan Dornod, Sükhbaatar +MO +221150+1133230 Asia/Macau +MQ +1436-06105 America/Martinique +MT +3554+01431 Europe/Malta +MU -2010+05730 Indian/Mauritius +MV +0410+07330 Indian/Maldives +MX +1924-09909 America/Mexico_City Central Time +MX +2105-08646 America/Cancun Eastern Standard Time - Quintana Roo +MX +2058-08937 America/Merida Central Time - Campeche, Yucatán +MX +2540-10019 America/Monterrey Central Time - Durango; Coahuila, Nuevo León, Tamaulipas (most areas) +MX +2550-09730 America/Matamoros Central Time US - Coahuila, Nuevo León, Tamaulipas (US border) +MX +2313-10625 America/Mazatlan Mountain Time - Baja California Sur, Nayarit, Sinaloa +MX +2838-10605 America/Chihuahua Mountain Time - Chihuahua (most areas) +MX +2934-10425 America/Ojinaga Mountain Time US - Chihuahua (US border) +MX +2904-11058 America/Hermosillo Mountain Standard Time - Sonora +MX +3232-11701 America/Tijuana Pacific Time US - Baja California +MX +2048-10515 America/Bahia_Banderas Central Time - Bahía de Banderas +MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula) +MY +0133+11020 Asia/Kuching Sabah, Sarawak +MZ,BI,BW,CD,MW,RW,ZM,ZW -2558+03235 Africa/Maputo Central Africa Time +NA -2234+01706 Africa/Windhoek +NC -2216+16627 Pacific/Noumea +NF -2903+16758 Pacific/Norfolk +NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE +0627+00324 Africa/Lagos West Africa Time +NI +1209-08617 America/Managua +NL +5222+00454 Europe/Amsterdam +NO,SJ +5955+01045 Europe/Oslo +NP +2743+08519 Asia/Kathmandu +NR -0031+16655 Pacific/Nauru +NU -1901-16955 Pacific/Niue +NZ,AQ -3652+17446 Pacific/Auckland New Zealand time +NZ -4357-17633 Pacific/Chatham Chatham Islands +PA,CA,KY +0858-07932 America/Panama EST - Panama, Cayman, ON (Atikokan), NU (Coral H) +PE -1203-07703 America/Lima +PF -1732-14934 Pacific/Tahiti Society Islands +PF -0900-13930 Pacific/Marquesas Marquesas Islands +PF -2308-13457 Pacific/Gambier Gambier Islands +PG,AQ -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas), Dumont d'Urville +PG -0613+15534 Pacific/Bougainville Bougainville +PH +1435+12100 Asia/Manila +PK +2452+06703 Asia/Karachi +PL +5215+02100 Europe/Warsaw +PM +4703-05620 America/Miquelon +PN -2504-13005 Pacific/Pitcairn +PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI +182806-0660622 America/Puerto_Rico AST +PS +3130+03428 Asia/Gaza Gaza Strip +PS +313200+0350542 Asia/Hebron West Bank +PT +3843-00908 Europe/Lisbon Portugal (mainland) +PT +3238-01654 Atlantic/Madeira Madeira Islands +PT +3744-02540 Atlantic/Azores Azores +PW +0720+13429 Pacific/Palau +PY -2516-05740 America/Asuncion +QA,BH +2517+05132 Asia/Qatar +RE,TF -2052+05528 Indian/Reunion Réunion, Crozet, Scattered Islands +RO +4426+02606 Europe/Bucharest +RS,BA,HR,ME,MK,SI +4450+02030 Europe/Belgrade +RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad +RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area +# Mention RU and UA alphabetically. See "territorial claims" above. +RU,UA +4457+03406 Europe/Simferopol Crimea +RU +5836+04939 Europe/Kirov MSK+00 - Kirov +RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd +RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan +RU +5134+04602 Europe/Saratov MSK+01 - Saratov +RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk +RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia +RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals +RU +5500+07324 Asia/Omsk MSK+03 - Omsk +RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk +RU +5322+08345 Asia/Barnaul MSK+04 - Altai +RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk +RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo +RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area +RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia +RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky +RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River +RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky +RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River +RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky +RU +5934+15048 Asia/Magadan MSK+08 - Magadan +RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); North Kuril Is +RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka +RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea +SA,AQ,KW,YE +2438+04643 Asia/Riyadh Arabia, Syowa +SB -0932+16012 Pacific/Guadalcanal +SC -0440+05528 Indian/Mahe +SD +1536+03232 Africa/Khartoum +SE +5920+01803 Europe/Stockholm +SG,MY +0117+10351 Asia/Singapore Singapore, peninsular Malaysia +SR +0550-05510 America/Paramaribo +SS +0451+03137 Africa/Juba +ST +0020+00644 Africa/Sao_Tome +SV +1342-08912 America/El_Salvador +SY +3330+03618 Asia/Damascus +TC +2128-07108 America/Grand_Turk +TD +1207+01503 Africa/Ndjamena +TF -492110+0701303 Indian/Kerguelen Kerguelen, St Paul Island, Amsterdam Island +TH,KH,LA,VN +1345+10031 Asia/Bangkok Indochina (most areas) +TJ +3835+06848 Asia/Dushanbe +TK -0922-17114 Pacific/Fakaofo +TL -0833+12535 Asia/Dili +TM +3757+05823 Asia/Ashgabat +TN +3648+01011 Africa/Tunis +TO -210800-1751200 Pacific/Tongatapu +TR +4101+02858 Europe/Istanbul +TV -0831+17913 Pacific/Funafuti +TW +2503+12130 Asia/Taipei +UA +5026+03031 Europe/Kiev Ukraine (most areas) +UA +4837+02218 Europe/Uzhgorod Transcarpathia +UA +4750+03510 Europe/Zaporozhye Zaporozhye and east Lugansk +UM +1917+16637 Pacific/Wake Wake Island +US +404251-0740023 America/New_York Eastern (most areas) +US +421953-0830245 America/Detroit Eastern - MI (most areas) +US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) +US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) +US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) +US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) +US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) +US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) +US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) +US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) +US +415100-0873900 America/Chicago Central (most areas) +US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) +US +411745-0863730 America/Indiana/Knox Central - IN (Starke) +US +450628-0873651 America/Menominee Central - MI (Wisconsin border) +US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) +US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) +US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) +US +394421-1045903 America/Denver Mountain (most areas) +US +433649-1161209 America/Boise Mountain - ID (south); OR (east) +US,CA +332654-1120424 America/Phoenix MST - Arizona (except Navajo), Creston BC +US +340308-1181434 America/Los_Angeles Pacific +US +611305-1495401 America/Anchorage Alaska (most areas) +US +581807-1342511 America/Juneau Alaska - Juneau area +US +571035-1351807 America/Sitka Alaska - Sitka area +US +550737-1313435 America/Metlakatla Alaska - Annette Island +US +593249-1394338 America/Yakutat Alaska - Yakutat +US +643004-1652423 America/Nome Alaska (west) +US +515248-1763929 America/Adak Aleutian Islands +US,UM +211825-1575130 Pacific/Honolulu Hawaii +UY -345433-0561245 America/Montevideo +UZ +3940+06648 Asia/Samarkand Uzbekistan (west) +UZ +4120+06918 Asia/Tashkent Uzbekistan (east) +VE +1030-06656 America/Caracas +VN +1045+10640 Asia/Ho_Chi_Minh Vietnam (south) +VU -1740+16825 Pacific/Efate +WF -1318-17610 Pacific/Wallis +WS -1350-17144 Pacific/Apia +ZA,LS,SZ -2615+02800 Africa/Johannesburg diff --git a/telegramer/include/pytz_deprecation_shim/__init__.py b/telegramer/include/pytz_deprecation_shim/__init__.py new file mode 100644 index 0000000..8b45162 --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/__init__.py @@ -0,0 +1,34 @@ +__all__ = [ + "AmbiguousTimeError", + "NonExistentTimeError", + "InvalidTimeError", + "UnknownTimeZoneError", + "PytzUsageWarning", + "FixedOffset", + "UTC", + "utc", + "build_tzinfo", + "timezone", + "fixed_offset_timezone", + "wrap_zone", +] + +from . import helpers +from ._exceptions import ( + AmbiguousTimeError, + InvalidTimeError, + NonExistentTimeError, + PytzUsageWarning, + UnknownTimeZoneError, +) +from ._impl import ( + UTC, + build_tzinfo, + fixed_offset_timezone, + timezone, + wrap_zone, +) + +# Compatibility aliases +utc = UTC +FixedOffset = fixed_offset_timezone diff --git a/telegramer/include/pytz_deprecation_shim/_common.py b/telegramer/include/pytz_deprecation_shim/_common.py new file mode 100644 index 0000000..ace322e --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/_common.py @@ -0,0 +1,13 @@ +import sys + +_PYTZ_IMPORTED = False + + +def pytz_imported(): + """Detects whether or not pytz has been imported without importing pytz.""" + global _PYTZ_IMPORTED + + if not _PYTZ_IMPORTED and "pytz" in sys.modules: + _PYTZ_IMPORTED = True + + return _PYTZ_IMPORTED diff --git a/telegramer/include/pytz_deprecation_shim/_compat.py b/telegramer/include/pytz_deprecation_shim/_compat.py new file mode 100644 index 0000000..5b73459 --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/_compat.py @@ -0,0 +1,15 @@ +import sys + +if sys.version_info[0] == 2: + from . import _compat_py2 as _compat_impl +else: + from . import _compat_py3 as _compat_impl + +UTC = _compat_impl.UTC +get_timezone = _compat_impl.get_timezone +get_timezone_file = _compat_impl.get_timezone_file +get_fixed_offset_zone = _compat_impl.get_fixed_offset_zone +is_ambiguous = _compat_impl.is_ambiguous +is_imaginary = _compat_impl.is_imaginary +enfold = _compat_impl.enfold +get_fold = _compat_impl.get_fold diff --git a/telegramer/include/pytz_deprecation_shim/_compat_py2.py b/telegramer/include/pytz_deprecation_shim/_compat_py2.py new file mode 100644 index 0000000..f473d26 --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/_compat_py2.py @@ -0,0 +1,43 @@ +from datetime import timedelta + +from dateutil import tz + +UTC = tz.UTC + + +def get_timezone(key): + if not key: + raise KeyError("Unknown time zone: %s" % key) + + try: + rv = tz.gettz(key) + except Exception: + rv = None + + if rv is None or not isinstance(rv, (tz.tzutc, tz.tzfile)): + raise KeyError("Unknown time zone: %s" % key) + + return rv + + +def get_timezone_file(f, key=None): + return tz.tzfile(f) + + +def get_fixed_offset_zone(offset): + return tz.tzoffset(None, timedelta(minutes=offset)) + + +def is_ambiguous(dt): + return tz.datetime_ambiguous(dt) + + +def is_imaginary(dt): + return not tz.datetime_exists(dt) + + +enfold = tz.enfold + + +def get_fold(dt): + return getattr(dt, "fold", 0) diff --git a/telegramer/include/pytz_deprecation_shim/_compat_py3.py b/telegramer/include/pytz_deprecation_shim/_compat_py3.py new file mode 100644 index 0000000..8881aba --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/_compat_py3.py @@ -0,0 +1,58 @@ +# Note: This file could use Python 3-only syntax, but at the moment this breaks +# the coverage job on Python 2. Until we make it so that coverage can ignore +# this file only on Python 2, we'll have to stick to 2/3-compatible syntax. +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + +import datetime + +UTC = datetime.timezone.utc + + +def get_timezone(key): + try: + return zoneinfo.ZoneInfo(key) + except (ValueError, OSError): + # TODO: Use `from e` when this file can use Python 3 syntax + raise KeyError(key) + + +def get_timezone_file(f, key=None): + return zoneinfo.ZoneInfo.from_file(f, key=key) + + +def get_fixed_offset_zone(offset): + return datetime.timezone(datetime.timedelta(minutes=offset)) + + +def is_imaginary(dt): + dt_rt = dt.astimezone(UTC).astimezone(dt.tzinfo) + + return not (dt == dt_rt) + + +def is_ambiguous(dt): + if is_imaginary(dt): + return False + + wall_0 = dt + wall_1 = dt.replace(fold=not dt.fold) + + # Ambiguous datetimes can only exist if the offset changes, so we don't + # actually have to check whether dst() or tzname() are different. + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + + return not same_offset + + +def enfold(dt, fold=1): + if dt.fold != fold: + return dt.replace(fold=fold) + else: + return dt + + +def get_fold(dt): + return dt.fold diff --git a/telegramer/include/pytz_deprecation_shim/_exceptions.py b/telegramer/include/pytz_deprecation_shim/_exceptions.py new file mode 100644 index 0000000..58d7af0 --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/_exceptions.py @@ -0,0 +1,75 @@ +from ._common import pytz_imported + + +class PytzUsageWarning(RuntimeWarning): + """Warning raised when accessing features specific to ``pytz``'s interface. + + This warning is used to direct users of ``pytz``-specific features like the + ``localize`` and ``normalize`` methods towards using the standard + ``tzinfo`` interface, so that these shims can be replaced with one of the + underlying libraries they are wrapping. + """ + + +class UnknownTimeZoneError(KeyError): + """Raised when no time zone is found for a specified key.""" + + +class InvalidTimeError(Exception): + """The base class for exceptions related to folds and gaps.""" + + +class AmbiguousTimeError(InvalidTimeError): + """Exception raised when ``is_dst=None`` for an ambiguous time (fold).""" + + +class NonExistentTimeError(InvalidTimeError): + """Exception raised when ``is_dst=None`` for a non-existent time (gap).""" + + +PYTZ_BASE_ERROR_MAPPING = {} + + +def _make_pytz_derived_errors( + InvalidTimeError_=InvalidTimeError, + AmbiguousTimeError_=AmbiguousTimeError, + NonExistentTimeError_=NonExistentTimeError, + UnknownTimeZoneError_=UnknownTimeZoneError, +): + if PYTZ_BASE_ERROR_MAPPING or not pytz_imported(): + return + + import pytz + + class InvalidTimeError(InvalidTimeError_, pytz.InvalidTimeError): + pass + + class AmbiguousTimeError(AmbiguousTimeError_, pytz.AmbiguousTimeError): + pass + + class NonExistentTimeError( + NonExistentTimeError_, pytz.NonExistentTimeError + ): + pass + + class UnknownTimeZoneError( + UnknownTimeZoneError_, pytz.UnknownTimeZoneError + ): + pass + + PYTZ_BASE_ERROR_MAPPING.update( + { + InvalidTimeError_: InvalidTimeError, + AmbiguousTimeError_: AmbiguousTimeError, + NonExistentTimeError_: NonExistentTimeError, + UnknownTimeZoneError_: UnknownTimeZoneError, + } + ) + + +def get_exception(exc_type, msg): + _make_pytz_derived_errors() + + out_exc_type = PYTZ_BASE_ERROR_MAPPING.get(exc_type, exc_type) + + return out_exc_type(msg) diff --git a/telegramer/include/pytz_deprecation_shim/_impl.py b/telegramer/include/pytz_deprecation_shim/_impl.py new file mode 100644 index 0000000..5443047 --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/_impl.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +import warnings +from datetime import tzinfo + +from . import _compat +from ._exceptions import ( + AmbiguousTimeError, + NonExistentTimeError, + PytzUsageWarning, + UnknownTimeZoneError, + get_exception, +) + +IS_DST_SENTINEL = object() +KEY_SENTINEL = object() + + +def timezone(key, _cache={}): + """Builds an IANA database time zone shim. + + This is the equivalent of ``pytz.timezone``. + + :param key: + A valid key from the IANA time zone database. + + :raises UnknownTimeZoneError: + If an unknown value is passed, this will raise an exception that can be + caught by :exc:`pytz_deprecation_shim.UnknownTimeZoneError` or + ``pytz.UnknownTimeZoneError``. Like + :exc:`zoneinfo.ZoneInfoNotFoundError`, both of those are subclasses of + :exc:`KeyError`. + """ + instance = _cache.get(key, None) + if instance is None: + if len(key) == 3 and key.lower() == "utc": + instance = _cache.setdefault(key, UTC) + else: + try: + zone = _compat.get_timezone(key) + except KeyError: + raise get_exception(UnknownTimeZoneError, key) + instance = _cache.setdefault(key, wrap_zone(zone, key=key)) + + return instance + + +def fixed_offset_timezone(offset, _cache={}): + """Builds a fixed offset time zone shim. + + This is the equivalent of ``pytz.FixedOffset``. An alias is available as + ``pytz_deprecation_shim.FixedOffset`` as well. + + :param offset: + A fixed offset from UTC, in minutes. This must be in the range ``-1439 + <= offset <= 1439``. + + :raises ValueError: + For offsets whose absolute value is greater than or equal to 24 hours. + + :return: + A shim time zone. + """ + if not (-1440 < offset < 1440): + raise ValueError("absolute offset is too large", offset) + + instance = _cache.get(offset, None) + if instance is None: + if offset == 0: + instance = _cache.setdefault(offset, UTC) + else: + zone = _compat.get_fixed_offset_zone(offset) + instance = _cache.setdefault(offset, wrap_zone(zone, key=None)) + + return instance + + +def build_tzinfo(zone, fp): + """Builds a shim object from a TZif file. + + This is a shim for ``pytz.build_tzinfo``. Given a value to use as the zone + IANA key and a file-like object containing a valid TZif file (i.e. + conforming to :rfc:`8536`), this builds a time zone object and wraps it in + a shim class. + + The argument names are chosen to match those in ``pytz.build_tzinfo``. + + :param zone: + A string to be used as the time zone object's IANA key. + + :param fp: + A readable file-like object emitting bytes, pointing to a valid TZif + file. + + :return: + A shim time zone. + """ + zone_file = _compat.get_timezone_file(fp) + + return wrap_zone(zone_file, key=zone) + + +def wrap_zone(tz, key=KEY_SENTINEL, _cache={}): + """Wrap an existing time zone object in a shim class. + + This is likely to be useful if you would like to work internally with + non-``pytz`` zones, but you expose an interface to callers relying on + ``pytz``'s interface. It may also be useful for passing non-``pytz`` zones + to libraries expecting to use ``pytz``'s interface. + + :param tz: + A :pep:`495`-compatible time zone, such as those provided by + :mod:`dateutil.tz` or :mod:`zoneinfo`. + + :param key: + The value for the IANA time zone key. This is optional for ``zoneinfo`` + zones, but required for ``dateutil.tz`` zones. + + :return: + A shim time zone. + """ + if key is KEY_SENTINEL: + key = getattr(tz, "key", KEY_SENTINEL) + + if key is KEY_SENTINEL: + raise TypeError( + "The `key` argument is required when wrapping zones that do not " + + "have a `key` attribute." + ) + + instance = _cache.get((id(tz), key), None) + if instance is None: + instance = _cache.setdefault((id(tz), key), _PytzShimTimezone(tz, key)) + + return instance + + +class _PytzShimTimezone(tzinfo): + # Add instance variables for _zone and _key because this will make error + # reporting with partially-initialized _BasePytzShimTimezone objects + # work better. + _zone = None + _key = None + + def __init__(self, zone, key): + self._key = key + self._zone = zone + + def utcoffset(self, dt): + return self._zone.utcoffset(dt) + + def dst(self, dt): + return self._zone.dst(dt) + + def tzname(self, dt): + return self._zone.tzname(dt) + + def fromutc(self, dt): + # The default fromutc implementation only works if tzinfo is "self" + dt_base = dt.replace(tzinfo=self._zone) + dt_out = self._zone.fromutc(dt_base) + + return dt_out.replace(tzinfo=self) + + def __str__(self): + if self._key is not None: + return str(self._key) + else: + return repr(self) + + def __repr__(self): + return "%s(%s, %s)" % ( + self.__class__.__name__, + repr(self._zone), + repr(self._key), + ) + + def unwrap_shim(self): + """Returns the underlying class that the shim is a wrapper for. + + This is a shim-specific method equivalent to + :func:`pytz_deprecation_shim.helpers.upgrade_tzinfo`. It is provided as + a method to allow end-users to upgrade shim timezones without requiring + an explicit dependency on ``pytz_deprecation_shim``, e.g.: + + .. code-block:: python + + if getattr(tz, "unwrap_shim", None) is None: + tz = tz.unwrap_shim() + """ + return self._zone + + @property + def zone(self): + warnings.warn( + "The zone attribute is specific to pytz's interface; " + + "please migrate to a new time zone provider. " + + "For more details on how to do so, see %s" + % PYTZ_MIGRATION_GUIDE_URL, + PytzUsageWarning, + stacklevel=2, + ) + + return self._key + + def localize(self, dt, is_dst=IS_DST_SENTINEL): + warnings.warn( + "The localize method is no longer necessary, as this " + + "time zone supports the fold attribute (PEP 495). " + + "For more details on migrating to a PEP 495-compliant " + + "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL, + PytzUsageWarning, + stacklevel=2, + ) + + if dt.tzinfo is not None: + raise ValueError("Not naive datetime (tzinfo is already set)") + + dt_out = dt.replace(tzinfo=self) + + if is_dst is IS_DST_SENTINEL: + return dt_out + + dt_ambiguous = _compat.is_ambiguous(dt_out) + dt_imaginary = ( + _compat.is_imaginary(dt_out) if not dt_ambiguous else False + ) + + if is_dst is None: + if dt_imaginary: + raise get_exception( + NonExistentTimeError, dt.replace(tzinfo=None) + ) + + if dt_ambiguous: + raise get_exception(AmbiguousTimeError, dt.replace(tzinfo=None)) + + elif dt_ambiguous or dt_imaginary: + # Start by normalizing the folds; dt_out may have fold=0 or fold=1, + # but we need to know the DST offset on both sides anyway, so we + # will get one datetime representing each side of the fold, then + # decide which one we're going to return. + if _compat.get_fold(dt_out): + dt_enfolded = dt_out + dt_out = _compat.enfold(dt_out, fold=0) + else: + dt_enfolded = _compat.enfold(dt_out, fold=1) + + # Now we want to decide whether the fold=0 or fold=1 represents + # what pytz would return for `is_dst=True` + enfolded_dst = bool(dt_enfolded.dst()) + if bool(dt_out.dst()) == enfolded_dst: + # If this is not a transition between standard time and + # daylight saving time, pytz will consider the larger offset + # the DST offset. + enfolded_dst = dt_enfolded.utcoffset() > dt_out.utcoffset() + + # The default we've established is that dt_out is fold=0; swap it + # for the fold=1 datetime if is_dst == True and the enfolded side + # is DST or if is_dst == False and the enfolded side is *not* DST. + if is_dst == enfolded_dst: + dt_out = dt_enfolded + + return dt_out + + def normalize(self, dt): + warnings.warn( + "The normalize method is no longer necessary, as this " + + "time zone supports the fold attribute (PEP 495). " + + "For more details on migrating to a PEP 495-compliant " + + "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL, + PytzUsageWarning, + stacklevel=2, + ) + + if dt.tzinfo is None: + raise ValueError("Naive time - no tzinfo set") + + if dt.tzinfo is self: + return dt + + return dt.astimezone(self) + + def __copy__(self): + return self + + def __deepcopy__(self, memo=None): + return self + + def __reduce__(self): + return wrap_zone, (self._zone, self._key) + + +UTC = wrap_zone(_compat.UTC, "UTC") +PYTZ_MIGRATION_GUIDE_URL = ( + "https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html" +) diff --git a/telegramer/include/pytz_deprecation_shim/helpers.py b/telegramer/include/pytz_deprecation_shim/helpers.py new file mode 100644 index 0000000..6b05b13 --- /dev/null +++ b/telegramer/include/pytz_deprecation_shim/helpers.py @@ -0,0 +1,90 @@ +""" +This module contains helper functions to ease the transition from ``pytz`` to +another :pep:`495`-compatible library. +""" +from . import _common, _compat +from ._impl import _PytzShimTimezone + +_PYTZ_BASE_CLASSES = None + + +def is_pytz_zone(tz): + """Check if a time zone is a ``pytz`` time zone. + + This will only import ``pytz`` if it has already been imported, and does + not rely on the existence of the ``localize`` or ``normalize`` methods + (since the shim classes also have these methods, but are not ``pytz`` + zones). + """ + + # If pytz is not in sys.modules, then we will assume the time zone is not a + # pytz zone. It is possible that someone has manipulated sys.modules to + # remove pytz, but that's the kind of thing that causes all kinds of other + # problems anyway, so we'll call that an unsupported configuration. + if not _common.pytz_imported(): + return False + + if _PYTZ_BASE_CLASSES is None: + _populate_pytz_base_classes() + + return isinstance(tz, _PYTZ_BASE_CLASSES) + + +def upgrade_tzinfo(tz): + """Convert a ``pytz`` or shim timezone into its modern equivalent. + + The shim classes are thin wrappers around :mod:`zoneinfo` or + :mod:`dateutil.tz` implementations of the :class:`datetime.tzinfo` base + class. This function removes the shim and returns the underlying "upgraded" + time zone. + + When passed a ``pytz`` zone (not a shim), this returns the non-``pytz`` + equivalent. This may fail if ``pytz`` is using a data source incompatible + with the upgraded provider's data source, or if the ``pytz`` zone was built + from a file rather than an IANA key. + + When passed an object that is not a shim or a ``pytz`` zone, this returns + the original object. + + :param tz: + A :class:`datetime.tzinfo` object. + + :raises KeyError: + If a ``pytz`` zone is passed to the function with no equivalent in the + :pep:`495`-compatible library's version of the Olson database. + + :return: + A :pep:`495`-compatible equivalent of any ``pytz`` or shim + class, or the original object. + """ + if isinstance(tz, _PytzShimTimezone): + return tz._zone + + if is_pytz_zone(tz): + if tz.zone is None: + # This is a fixed offset zone + offset = tz.utcoffset(None) + offset_minutes = offset.total_seconds() / 60 + + return _compat.get_fixed_offset_zone(offset_minutes) + + if tz.zone == "UTC": + return _compat.UTC + + return _compat.get_timezone(tz.zone) + + return tz + + +def _populate_pytz_base_classes(): + import pytz + from pytz.tzinfo import BaseTzInfo + + base_classes = (BaseTzInfo, pytz._FixedOffset) + + # In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo + if not isinstance(pytz.UTC, BaseTzInfo): # pragma: nocover + base_classes = base_classes + (type(pytz.UTC),) + + global _PYTZ_BASE_CLASSES + _PYTZ_BASE_CLASSES = base_classes diff --git a/telegramer/include/socks.py b/telegramer/include/socks.py index a2fe8a3..5092846 100644 --- a/telegramer/include/socks.py +++ b/telegramer/include/socks.py @@ -86,7 +86,7 @@ PROXY_TYPE_HTTP = HTTP = 3 PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP} -PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys())) +PRINTABLE_PROXY_TYPES = dict(list(zip(list(PROXY_TYPES.values()), list(PROXY_TYPES.keys())))) _orgsocket = _orig_socket = socket.socket diff --git a/telegramer/include/sockshandler.py b/telegramer/include/sockshandler.py index 26c8343..b9b2b67 100644 --- a/telegramer/include/sockshandler.py +++ b/telegramer/include/sockshandler.py @@ -10,23 +10,23 @@ import ssl try: - import urllib2 - import httplib + import urllib.request, urllib.error, urllib.parse + import http.client except ImportError: # Python 3 import urllib.request as urllib2 - import http.client as httplib + from http import client as httplib -import socks # $ pip install PySocks +from . import socks # $ pip install PySocks def merge_dict(a, b): d = a.copy() d.update(b) return d -class SocksiPyConnection(httplib.HTTPConnection): +class SocksiPyConnection(http.client.HTTPConnection): def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) - httplib.HTTPConnection.__init__(self, *args, **kwargs) + http.client.HTTPConnection.__init__(self, *args, **kwargs) def connect(self): self.sock = socks.socksocket() @@ -35,10 +35,10 @@ def connect(self): self.sock.settimeout(self.timeout) self.sock.connect((self.host, self.port)) -class SocksiPyConnectionS(httplib.HTTPSConnection): +class SocksiPyConnectionS(http.client.HTTPSConnection): def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) - httplib.HTTPSConnection.__init__(self, *args, **kwargs) + http.client.HTTPSConnection.__init__(self, *args, **kwargs) def connect(self): sock = socks.socksocket() @@ -48,11 +48,11 @@ def connect(self): sock.connect((self.host, self.port)) self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file) -class SocksiPyHandler(urllib2.HTTPHandler, urllib2.HTTPSHandler): +class SocksiPyHandler(urllib.request.HTTPHandler, urllib.request.HTTPSHandler): def __init__(self, *args, **kwargs): self.args = args self.kw = kwargs - urllib2.HTTPHandler.__init__(self) + urllib.request.HTTPHandler.__init__(self) def http_open(self, req): def build(host, port=None, timeout=0, **kwargs): @@ -74,6 +74,6 @@ def build(host, port=None, timeout=0, **kwargs): port = int(sys.argv[1]) except (ValueError, IndexError): port = 9050 - opener = urllib2.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "localhost", port)) - print("HTTP: " + opener.open("http://httpbin.org/ip").read().decode()) - print("HTTPS: " + opener.open("https://httpbin.org/ip").read().decode()) + opener = urllib.request.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "localhost", port)) + print(("HTTP: " + opener.open("http://httpbin.org/ip").read().decode())) + print(("HTTPS: " + opener.open("https://httpbin.org/ip").read().decode())) diff --git a/telegramer/include/telegram/__init__.py b/telegramer/include/telegram/__init__.py index cf45c69..c0b36de 100644 --- a/telegramer/include/telegram/__init__.py +++ b/telegramer/include/telegram/__init__.py @@ -1,7 +1,7 @@ -''#!/usr/bin/env python +#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,10 +19,24 @@ """A library that provides a Python interface to the Telegram Bot API""" from .base import TelegramObject +from .botcommand import BotCommand from .user import User from .files.chatphoto import ChatPhoto from .chat import Chat -from .chatmember import ChatMember +from .chatlocation import ChatLocation +from .chatinvitelink import ChatInviteLink +from .chatjoinrequest import ChatJoinRequest +from .chatmember import ( + ChatMember, + ChatMemberOwner, + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberRestricted, + ChatMemberLeft, + ChatMemberBanned, +) +from .chatmemberupdated import ChatMemberUpdated +from .chatpermissions import ChatPermissions from .files.photosize import PhotoSize from .files.audio import Audio from .files.voice import Voice @@ -35,7 +49,9 @@ from .files.venue import Venue from .files.videonote import VideoNote from .chataction import ChatAction +from .dice import Dice from .userprofilephotos import UserProfilePhotos +from .keyboardbuttonpolltype import KeyboardButtonPollType from .keyboardbutton import KeyboardButton from .replymarkup import ReplyMarkup from .replykeyboardmarkup import ReplyKeyboardMarkup @@ -46,7 +62,17 @@ from .files.file import File from .parsemode import ParseMode from .messageentity import MessageEntity +from .messageid import MessageId from .games.game import Game +from .poll import Poll, PollOption, PollAnswer +from .voicechat import ( + VoiceChatStarted, + VoiceChatEnded, + VoiceChatParticipantsInvited, + VoiceChatScheduled, +) +from .loginurl import LoginUrl +from .proximityalerttriggered import ProximityAlertTriggered from .games.callbackgame import CallbackGame from .payment.shippingaddress import ShippingAddress from .payment.orderinfo import OrderInfo @@ -57,11 +83,12 @@ from .passport.data import IdDocumentData, PersonalDetails, ResidentialAddress from .passport.encryptedpassportelement import EncryptedPassportElement from .passport.passportdata import PassportData +from .inline.inlinekeyboardbutton import InlineKeyboardButton +from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from .messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from .message import Message from .callbackquery import CallbackQuery from .choseninlineresult import ChosenInlineResult -from .inline.inlinekeyboardbutton import InlineKeyboardButton -from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup from .inline.inputmessagecontent import InputMessageContent from .inline.inlinequery import InlineQuery from .inline.inlinequeryresult import InlineQueryResult @@ -88,69 +115,214 @@ from .inline.inputtextmessagecontent import InputTextMessageContent from .inline.inputlocationmessagecontent import InputLocationMessageContent from .inline.inputvenuemessagecontent import InputVenueMessageContent -from .inline.inputcontactmessagecontent import InputContactMessageContent from .payment.labeledprice import LabeledPrice +from .inline.inputinvoicemessagecontent import InputInvoiceMessageContent +from .inline.inputcontactmessagecontent import InputContactMessageContent from .payment.shippingoption import ShippingOption from .payment.precheckoutquery import PreCheckoutQuery from .payment.shippingquery import ShippingQuery from .webhookinfo import WebhookInfo from .games.gamehighscore import GameHighScore from .update import Update -from .files.inputmedia import (InputMedia, InputMediaVideo, InputMediaPhoto, InputMediaAnimation, - InputMediaAudio, InputMediaDocument) +from .files.inputmedia import ( + InputMedia, + InputMediaVideo, + InputMediaPhoto, + InputMediaAnimation, + InputMediaAudio, + InputMediaDocument, +) +from .constants import ( + MAX_MESSAGE_LENGTH, + MAX_CAPTION_LENGTH, + SUPPORTED_WEBHOOK_PORTS, + MAX_FILESIZE_DOWNLOAD, + MAX_FILESIZE_UPLOAD, + MAX_MESSAGES_PER_SECOND_PER_CHAT, + MAX_MESSAGES_PER_SECOND, + MAX_MESSAGES_PER_MINUTE_PER_GROUP, +) +from .passport.passportelementerrors import ( + PassportElementError, + PassportElementErrorDataField, + PassportElementErrorFile, + PassportElementErrorFiles, + PassportElementErrorFrontSide, + PassportElementErrorReverseSide, + PassportElementErrorSelfie, + PassportElementErrorTranslationFile, + PassportElementErrorTranslationFiles, + PassportElementErrorUnspecified, +) +from .passport.credentials import ( + Credentials, + DataCredentials, + SecureData, + SecureValue, + FileCredentials, + TelegramDecryptionError, +) +from .botcommandscope import ( + BotCommandScope, + BotCommandScopeDefault, + BotCommandScopeAllPrivateChats, + BotCommandScopeAllGroupChats, + BotCommandScopeAllChatAdministrators, + BotCommandScopeChat, + BotCommandScopeChatAdministrators, + BotCommandScopeChatMember, +) from .bot import Bot -from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS, - MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD, - MAX_MESSAGES_PER_SECOND_PER_CHAT, MAX_MESSAGES_PER_SECOND, - MAX_MESSAGES_PER_MINUTE_PER_GROUP) -from .passport.passportelementerrors import (PassportElementError, - PassportElementErrorDataField, - PassportElementErrorFile, - PassportElementErrorFiles, - PassportElementErrorFrontSide, - PassportElementErrorReverseSide, - PassportElementErrorSelfie, - PassportElementErrorTranslationFile, - PassportElementErrorTranslationFiles, - PassportElementErrorUnspecified) -from .passport.credentials import (Credentials, - DataCredentials, - SecureData, - FileCredentials, - TelegramDecryptionError) -from .version import __version__ # flake8: noqa +from .version import __version__, bot_api_version # noqa: F401 __author__ = 'devs@python-telegram-bot.org' -__all__ = [ - 'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatAction', 'ChosenInlineResult', 'CallbackQuery', - 'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton', - 'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResult', - 'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio', - 'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif', - 'InlineQueryResultCachedMpeg4Gif', 'InlineQueryResultCachedPhoto', - 'InlineQueryResultCachedSticker', 'InlineQueryResultCachedVideo', - 'InlineQueryResultCachedVoice', 'InlineQueryResultContact', 'InlineQueryResultDocument', - 'InlineQueryResultGif', 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif', - 'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo', - 'InlineQueryResultVoice', 'InlineQueryResultGame', 'InputContactMessageContent', 'InputFile', - 'InputLocationMessageContent', 'InputMessageContent', 'InputTextMessageContent', - 'InputVenueMessageContent', 'KeyboardButton', 'Location', 'EncryptedCredentials', - 'PassportFile', 'EncryptedPassportElement', 'PassportData', 'Message', 'MessageEntity', - 'ParseMode', 'PhotoSize', 'ReplyKeyboardRemove', 'ReplyKeyboardMarkup', 'ReplyMarkup', - 'Sticker', 'TelegramError', 'TelegramObject', 'Update', 'User', 'UserProfilePhotos', 'Venue', - 'Video', 'Voice', 'MAX_MESSAGE_LENGTH', 'MAX_CAPTION_LENGTH', 'SUPPORTED_WEBHOOK_PORTS', - 'MAX_FILESIZE_DOWNLOAD', 'MAX_FILESIZE_UPLOAD', 'MAX_MESSAGES_PER_SECOND_PER_CHAT', - 'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'WebhookInfo', 'Animation', - 'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption', - 'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto', - 'StickerSet', 'MaskPosition', 'CallbackGame', 'InputMedia', 'InputMediaPhoto', - 'InputMediaVideo', 'PassportElementError', 'PassportElementErrorFile', - 'PassportElementErrorReverseSide', 'PassportElementErrorFrontSide', - 'PassportElementErrorFiles', 'PassportElementErrorDataField', 'PassportElementErrorFile', - 'Credentials', 'DataCredentials', 'SecureData', 'FileCredentials', 'IdDocumentData', - 'PersonalDetails', 'ResidentialAddress', 'InputMediaVideo', 'InputMediaAnimation', - 'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError', - 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile', - 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified' -] +__all__ = ( # Keep this alphabetically ordered + 'Animation', + 'Audio', + 'Bot', + 'BotCommand', + 'BotCommandScope', + 'BotCommandScopeAllChatAdministrators', + 'BotCommandScopeAllGroupChats', + 'BotCommandScopeAllPrivateChats', + 'BotCommandScopeChat', + 'BotCommandScopeChatAdministrators', + 'BotCommandScopeChatMember', + 'BotCommandScopeDefault', + 'CallbackGame', + 'CallbackQuery', + 'Chat', + 'ChatAction', + 'ChatInviteLink', + 'ChatJoinRequest', + 'ChatLocation', + 'ChatMember', + 'ChatMemberOwner', + 'ChatMemberAdministrator', + 'ChatMemberMember', + 'ChatMemberRestricted', + 'ChatMemberLeft', + 'ChatMemberBanned', + 'ChatMemberUpdated', + 'ChatPermissions', + 'ChatPhoto', + 'ChosenInlineResult', + 'Contact', + 'Credentials', + 'DataCredentials', + 'Dice', + 'Document', + 'EncryptedCredentials', + 'EncryptedPassportElement', + 'File', + 'FileCredentials', + 'ForceReply', + 'Game', + 'GameHighScore', + 'IdDocumentData', + 'InlineKeyboardButton', + 'InlineKeyboardMarkup', + 'InlineQuery', + 'InlineQueryResult', + 'InlineQueryResultArticle', + 'InlineQueryResultAudio', + 'InlineQueryResultCachedAudio', + 'InlineQueryResultCachedDocument', + 'InlineQueryResultCachedGif', + 'InlineQueryResultCachedMpeg4Gif', + 'InlineQueryResultCachedPhoto', + 'InlineQueryResultCachedSticker', + 'InlineQueryResultCachedVideo', + 'InlineQueryResultCachedVoice', + 'InlineQueryResultContact', + 'InlineQueryResultDocument', + 'InlineQueryResultGame', + 'InlineQueryResultGif', + 'InlineQueryResultLocation', + 'InlineQueryResultMpeg4Gif', + 'InlineQueryResultPhoto', + 'InlineQueryResultVenue', + 'InlineQueryResultVideo', + 'InlineQueryResultVoice', + 'InputContactMessageContent', + 'InputFile', + 'InputInvoiceMessageContent', + 'InputLocationMessageContent', + 'InputMedia', + 'InputMediaAnimation', + 'InputMediaAudio', + 'InputMediaDocument', + 'InputMediaPhoto', + 'InputMediaVideo', + 'InputMessageContent', + 'InputTextMessageContent', + 'InputVenueMessageContent', + 'Invoice', + 'KeyboardButton', + 'KeyboardButtonPollType', + 'LabeledPrice', + 'Location', + 'LoginUrl', + 'MAX_CAPTION_LENGTH', + 'MAX_FILESIZE_DOWNLOAD', + 'MAX_FILESIZE_UPLOAD', + 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', + 'MAX_MESSAGES_PER_SECOND', + 'MAX_MESSAGES_PER_SECOND_PER_CHAT', + 'MAX_MESSAGE_LENGTH', + 'MaskPosition', + 'Message', + 'MessageAutoDeleteTimerChanged', + 'MessageEntity', + 'MessageId', + 'OrderInfo', + 'ParseMode', + 'PassportData', + 'PassportElementError', + 'PassportElementErrorDataField', + 'PassportElementErrorFile', + 'PassportElementErrorFiles', + 'PassportElementErrorFrontSide', + 'PassportElementErrorReverseSide', + 'PassportElementErrorSelfie', + 'PassportElementErrorTranslationFile', + 'PassportElementErrorTranslationFiles', + 'PassportElementErrorUnspecified', + 'PassportFile', + 'PersonalDetails', + 'PhotoSize', + 'Poll', + 'PollAnswer', + 'PollOption', + 'PreCheckoutQuery', + 'ProximityAlertTriggered', + 'ReplyKeyboardMarkup', + 'ReplyKeyboardRemove', + 'ReplyMarkup', + 'ResidentialAddress', + 'SUPPORTED_WEBHOOK_PORTS', + 'SecureData', + 'SecureValue', + 'ShippingAddress', + 'ShippingOption', + 'ShippingQuery', + 'Sticker', + 'StickerSet', + 'SuccessfulPayment', + 'TelegramDecryptionError', + 'TelegramError', + 'TelegramObject', + 'Update', + 'User', + 'UserProfilePhotos', + 'Venue', + 'Video', + 'VideoNote', + 'Voice', + 'VoiceChatStarted', + 'VoiceChatEnded', + 'VoiceChatScheduled', + 'VoiceChatParticipantsInvited', + 'WebhookInfo', +) diff --git a/telegramer/include/telegram/__main__.py b/telegramer/include/telegram/__main__.py index ad1db8d..2e7a7de 100644 --- a/telegramer/include/telegram/__main__.py +++ b/telegramer/include/telegram/__main__.py @@ -1,7 +1,7 @@ # !/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,22 +16,37 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0114 +import subprocess import sys +from typing import Optional import certifi -import future from . import __version__ as telegram_ver +from .constants import BOT_API_VERSION -def print_ver_info(): - print('python-telegram-bot {0}'.format(telegram_ver)) - print('certifi {0}'.format(certifi.__version__)) - print('future {0}'.format(future.__version__)) - print('Python {0}'.format(sys.version.replace('\n', ' '))) +def _git_revision() -> Optional[str]: + try: + output = subprocess.check_output( # skipcq: BAN-B607 + ["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT + ) + except (subprocess.SubprocessError, OSError): + return None + return output.decode().strip() -def main(): +def print_ver_info() -> None: # skipcq: PY-D0003 + git_revision = _git_revision() + print(f'python-telegram-bot {telegram_ver}' + (f' ({git_revision})' if git_revision else '')) + print(f'Bot API {BOT_API_VERSION}') + print(f'certifi {certifi.__version__}') # type: ignore[attr-defined] + sys_version = sys.version.replace('\n', ' ') + print(f'Python {sys_version}') + + +def main() -> None: # skipcq: PY-D0003 print_ver_info() diff --git a/telegramer/include/telegram/base.py b/telegramer/include/telegram/base.py index 80259ac..f119d96 100644 --- a/telegramer/include/telegram/base.py +++ b/telegramer/include/telegram/base.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,58 +17,110 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """Base class for Telegram Objects.""" - try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] + +import warnings +from typing import TYPE_CHECKING, List, Optional, Tuple, Type, TypeVar + +from telegram.utils.types import JSONDict +from telegram.utils.deprecate import set_new_attribute_deprecated + +if TYPE_CHECKING: + from telegram import Bot -from abc import ABCMeta +TO = TypeVar('TO', bound='TelegramObject', covariant=True) -class TelegramObject(object): - """Base class for most telegram objects.""" +class TelegramObject: + """Base class for most Telegram objects.""" - __metaclass__ = ABCMeta - _id_attrs = () + _id_attrs: Tuple[object, ...] = () - def __str__(self): + # Adding slots reduces memory usage & allows for faster attribute access. + # Only instance variables should be added to __slots__. + # We add __dict__ here for backward compatibility & also to avoid repetition for subclasses. + __slots__ = ('__dict__',) + + def __str__(self) -> str: return str(self.to_dict()) - def __getitem__(self, item): - return self.__dict__[item] + def __getitem__(self, item: str) -> object: + return getattr(self, item, None) + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + + @staticmethod + def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]: + return None if data is None else data.copy() @classmethod - def de_json(cls, data, bot): - if not data: - return None + def de_json(cls: Type[TO], data: Optional[JSONDict], bot: 'Bot') -> Optional[TO]: + """Converts JSON data to a Telegram object. - data = data.copy() + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with this object. - return data + Returns: + The Telegram object. - def to_json(self): """ + data = cls._parse_data(data) + + if data is None: + return None + + if cls == TelegramObject: + return cls() + return cls(bot=bot, **data) # type: ignore[call-arg] + + @classmethod + def de_list(cls: Type[TO], data: Optional[List[JSONDict]], bot: 'Bot') -> List[Optional[TO]]: + """Converts JSON data to a list of Telegram objects. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with these objects. + Returns: - :obj:`str` + A list of Telegram objects. """ + if not data: + return [] + + return [cls.de_json(d, bot) for d in data] + def to_json(self) -> str: + """Gives a JSON representation of object. + + Returns: + :obj:`str` + """ return json.dumps(self.to_dict()) - def to_dict(self): - data = dict() + def to_dict(self) -> JSONDict: + """Gives representation of object as :obj:`dict`. - for key in iter(self.__dict__): - if key in ('bot', - '_id_attrs', - '_credentials', - '_decrypted_credentials', - '_decrypted_data', - '_decrypted_secret'): + Returns: + :obj:`dict` + """ + data = {} + + # We want to get all attributes for the class, using self.__slots__ only includes the + # attributes used by that class itself, and not its superclass(es). Hence we get its MRO + # and then get their attributes. The `[:-2]` slice excludes the `object` class & the + # TelegramObject class itself. + attrs = {attr for cls in self.__class__.__mro__[:-2] for attr in cls.__slots__} + for key in attrs: + if key == 'bot' or key.startswith('_'): continue - value = self.__dict__[key] + value = getattr(self, key, None) if value is not None: if hasattr(value, 'to_dict'): data[key] = value.to_dict() @@ -79,12 +131,22 @@ def to_dict(self): data['from'] = data.pop('from_user', None) return data - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): + if self._id_attrs == (): + warnings.warn( + f"Objects of type {self.__class__.__name__} can not be meaningfully tested for" + " equivalence." + ) + if other._id_attrs == (): + warnings.warn( + f"Objects of type {other.__class__.__name__} can not be meaningfully tested" + " for equivalence." + ) return self._id_attrs == other._id_attrs - return super(TelegramObject, self).__eq__(other) # pylint: disable=no-member + return super().__eq__(other) # pylint: disable=no-member - def __hash__(self): + def __hash__(self) -> int: if self._id_attrs: return hash((self.__class__, self._id_attrs)) # pylint: disable=no-member - return super(TelegramObject, self).__hash__() + return super().__hash__() diff --git a/telegramer/include/telegram/bot.py b/telegramer/include/telegram/bot.py index dbdd8df..cc983d5 100644 --- a/telegramer/include/telegram/bot.py +++ b/telegramer/include/telegram/bot.py @@ -1,9 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# pylint: disable=E0611,E0213,E1102,C0103,E1101,W0613,R0913,R0904 +# pylint: disable=E0611,E0213,E1102,E1101,R0913,R0904 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -21,58 +20,114 @@ """This module contains an object that represents a Telegram Bot.""" import functools -try: - import ujson as json -except ImportError: - import json import logging import warnings from datetime import datetime -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass +from typing import ( + TYPE_CHECKING, + Callable, + List, + Optional, + Tuple, + TypeVar, + Union, + no_type_check, + Dict, + cast, + Sequence, +) try: - string_types -except NameError: - string_types = str + import ujson as json +except ImportError: + import json # type: ignore[no-redef] # noqa: F723 +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization -from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File, - ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet, - PhotoSize, Audio, Document, Sticker, Video, Animation, Voice, VideoNote, - Location, Venue, Contact, InputFile) + CRYPTO_INSTALLED = True +except ImportError: + default_backend = None # type: ignore[assignment] + serialization = None # type: ignore[assignment] + CRYPTO_INSTALLED = False + +from telegram import ( + Animation, + Audio, + BotCommand, + BotCommandScope, + Chat, + ChatMember, + ChatPermissions, + ChatPhoto, + Contact, + Document, + File, + GameHighScore, + Location, + MaskPosition, + Message, + MessageId, + PassportElementError, + PhotoSize, + Poll, + ReplyMarkup, + ShippingOption, + Sticker, + StickerSet, + TelegramObject, + Update, + User, + UserProfilePhotos, + Venue, + Video, + VideoNote, + Voice, + WebhookInfo, + InlineKeyboardMarkup, + ChatInviteLink, +) +from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError -from telegram.utils.helpers import to_timestamp +from telegram.utils.deprecate import TelegramDeprecationWarning +from telegram.utils.helpers import ( + DEFAULT_NONE, + DefaultValue, + to_timestamp, + is_local_file, + parse_file_input, + DEFAULT_20, +) from telegram.utils.request import Request - -logging.getLogger(__name__).addHandler(logging.NullHandler()) - - -def info(func): - @functools.wraps(func) - def decorator(self, *args, **kwargs): - if not self.bot: - self.get_me() - - result = func(self, *args, **kwargs) - return result - - return decorator - - -def log(func): +from telegram.utils.types import FileInput, JSONDict, ODVInput, DVInput + +if TYPE_CHECKING: + from telegram.ext import Defaults + from telegram import ( + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + InputMedia, + InlineQueryResult, + LabeledPrice, + MessageEntity, + ) + +RT = TypeVar('RT') + + +def log( # skipcq: PY-D0003 + func: Callable[..., RT], *args: object, **kwargs: object # pylint: disable=W0613 +) -> Callable[..., RT]: logger = logging.getLogger(func.__module__) @functools.wraps(func) - def decorator(self, *args, **kwargs): + def decorator(*args: object, **kwargs: object) -> RT: # pylint: disable=W0613 logger.debug('Entering: %s', func.__name__) - result = func(self, *args, **kwargs) + result = func(*args, **kwargs) logger.debug(result) logger.debug('Exiting: %s', func.__name__) return result @@ -80,36 +135,19 @@ def decorator(self, *args, **kwargs): return decorator -def message(func): - @functools.wraps(func) - def decorator(self, *args, **kwargs): - url, data = func(self, *args, **kwargs) - if kwargs.get('reply_to_message_id'): - data['reply_to_message_id'] = kwargs.get('reply_to_message_id') - - if kwargs.get('disable_notification'): - data['disable_notification'] = kwargs.get('disable_notification') - - if kwargs.get('reply_markup'): - reply_markup = kwargs.get('reply_markup') - if isinstance(reply_markup, ReplyMarkup): - data['reply_markup'] = reply_markup.to_json() - else: - data['reply_markup'] = reply_markup - - result = self._request.post(url, data, timeout=kwargs.get('timeout')) - - if result is True: - return result - - return Message.de_json(result, self) - - return decorator - - class Bot(TelegramObject): """This object represents a Telegram Bot. + .. versionadded:: 13.2 + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`bot` is equal. + + Note: + Most bot methods have the argument ``api_kwargs`` which allows to pass arbitrary keywords + to the Telegram API. This can be used to access new features of the API before they were + incorporated into PTB. However, this is not guaranteed to work, i.e. it will fail for + passing files. + Args: token (:obj:`str`): Bot's unique authentication. base_url (:obj:`str`, optional): Telegram Bot API service URL. @@ -118,13 +156,50 @@ class Bot(TelegramObject): :obj:`telegram.utils.request.Request`. private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. private_key_password (:obj:`bytes`, optional): Password for above private key. + defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to + be used if not set explicitly in the bot methods. + + .. deprecated:: 13.6 + Passing :class:`telegram.ext.Defaults` to :class:`telegram.Bot` is deprecated. If + you want to use :class:`telegram.ext.Defaults`, please use + :class:`telegram.ext.ExtBot` instead. """ - def __init__(self, token, base_url=None, base_file_url=None, request=None, private_key=None, - private_key_password=None): + __slots__ = ( + 'token', + 'base_url', + 'base_file_url', + 'private_key', + 'defaults', + '_bot', + '_commands', + '_request', + 'logger', + ) + + def __init__( + self, + token: str, + base_url: str = None, + base_file_url: str = None, + request: 'Request' = None, + private_key: bytes = None, + private_key_password: bytes = None, + defaults: 'Defaults' = None, + ): self.token = self._validate_token(token) + # Gather default + self.defaults = defaults + + if self.defaults: + warnings.warn( + 'Passing Defaults to telegram.Bot is deprecated. Use telegram.ext.ExtBot instead.', + TelegramDeprecationWarning, + stacklevel=3, + ) + if base_url is None: base_url = 'https://api.telegram.org/bot' @@ -133,21 +208,144 @@ def __init__(self, token, base_url=None, base_file_url=None, request=None, priva self.base_url = str(base_url) + str(self.token) self.base_file_url = str(base_file_url) + str(self.token) - self.bot = None + self._bot: Optional[User] = None + self._commands: Optional[List[BotCommand]] = None self._request = request or Request() + self.private_key = None self.logger = logging.getLogger(__name__) if private_key: - self.private_key = serialization.load_pem_private_key(private_key, - password=private_key_password, - backend=default_backend()) + if not CRYPTO_INSTALLED: + raise RuntimeError( + 'To use Telegram Passports, PTB must be installed via `pip install ' + 'python-telegram-bot[passport]`.' + ) + self.private_key = serialization.load_pem_private_key( + private_key, password=private_key_password, backend=default_backend() + ) + + # The ext_bot argument is a little hack to get warnings handled correctly. + # It's not very clean, but the warnings will be dropped at some point anyway. + def __setattr__(self, key: str, value: object, ext_bot: bool = False) -> None: + if issubclass(self.__class__, Bot) and self.__class__ is not Bot and not ext_bot: + object.__setattr__(self, key, value) + return + super().__setattr__(key, value) + + def _insert_defaults( + self, data: Dict[str, object], timeout: ODVInput[float] + ) -> Optional[float]: + """ + Inserts the defaults values for optional kwargs for which tg.ext.Defaults provides + convenience functionality, i.e. the kwargs with a tg.utils.helpers.DefaultValue default + + data is edited in-place. As timeout is not passed via the kwargs, it needs to be passed + separately and gets returned. + + This can only work, if all kwargs that may have defaults are passed in data! + """ + effective_timeout = DefaultValue.get_value(timeout) + + # If we have no Defaults, we just need to replace DefaultValue instances + # with the actual value + if not self.defaults: + data.update((key, DefaultValue.get_value(value)) for key, value in data.items()) + return effective_timeout + + # if we have Defaults, we replace all DefaultValue instances with the relevant + # Defaults value. If there is none, we fall back to the default value of the bot method + for key, val in data.items(): + if isinstance(val, DefaultValue): + data[key] = self.defaults.api_defaults.get(key, val.value) + + if isinstance(timeout, DefaultValue): + # If we get here, we use Defaults.timeout, unless that's not set, which is the + # case if isinstance(self.defaults.timeout, DefaultValue) + return ( + self.defaults.timeout + if not isinstance(self.defaults.timeout, DefaultValue) + else effective_timeout + ) + return effective_timeout + + def _post( + self, + endpoint: str, + data: JSONDict = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[bool, JSONDict, None]: + if data is None: + data = {} + + if api_kwargs: + if data: + data.update(api_kwargs) + else: + data = api_kwargs + + # Insert is in-place, so no return value for data + if endpoint != 'getUpdates': + effective_timeout = self._insert_defaults(data, timeout) + else: + effective_timeout = cast(float, timeout) + # Drop any None values because Telegram doesn't handle them well + data = {key: value for key, value in data.items() if value is not None} + + return self.request.post( + f'{self.base_url}/{endpoint}', data=data, timeout=effective_timeout + ) + + def _message( + self, + endpoint: str, + data: JSONDict, + reply_to_message_id: int = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: ReplyMarkup = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> Union[bool, Message]: + if reply_to_message_id is not None: + data['reply_to_message_id'] = reply_to_message_id + + if protect_content: + data['protect_content'] = protect_content + + # We don't check if (DEFAULT_)None here, so that _put is able to insert the defaults + # correctly, if necessary + data['disable_notification'] = disable_notification + data['allow_sending_without_reply'] = allow_sending_without_reply + + if reply_markup is not None: + if isinstance(reply_markup, ReplyMarkup): + # We need to_json() instead of to_dict() here, because reply_markups may be + # attached to media messages, which aren't json dumped by utils.request + data['reply_markup'] = reply_markup.to_json() + else: + data['reply_markup'] = reply_markup + + if data.get('media') and (data['media'].parse_mode == DEFAULT_NONE): + if self.defaults: + data['media'].parse_mode = DefaultValue.get_value(self.defaults.parse_mode) + else: + data['media'].parse_mode = None + + result = self._post(endpoint, data, timeout=timeout, api_kwargs=api_kwargs) + + if result is True: + return result + + return Message.de_json(result, self) # type: ignore[return-value, arg-type] @property - def request(self): + def request(self) -> Request: # skip-cq: PY-D0003 return self._request @staticmethod - def _validate_token(token): + def _validate_token(token: str) -> str: """A very basic validation on token.""" if any(x.isspace() for x in token): raise InvalidToken() @@ -159,189 +357,274 @@ def _validate_token(token): return token @property - @info - def id(self): - """:obj:`int`: Unique identifier for this bot.""" + def bot(self) -> User: + """:class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`.""" + if self._bot is None: + self._bot = self.get_me() + return self._bot + @property + def id(self) -> int: # pylint: disable=C0103 + """:obj:`int`: Unique identifier for this bot.""" return self.bot.id @property - @info - def first_name(self): + def first_name(self) -> str: """:obj:`str`: Bot's first name.""" - return self.bot.first_name @property - @info - def last_name(self): + def last_name(self) -> str: """:obj:`str`: Optional. Bot's last name.""" - - return self.bot.last_name + return self.bot.last_name # type: ignore @property - @info - def username(self): + def username(self) -> str: """:obj:`str`: Bot's username.""" + return self.bot.username # type: ignore - return self.bot.username + @property + def link(self) -> str: + """:obj:`str`: Convenience property. Returns the t.me link of the bot.""" + return f"https://t.me/{self.username}" @property - def name(self): - """:obj:`str`: Bot's @username.""" + def can_join_groups(self) -> bool: + """:obj:`bool`: Bot's :attr:`telegram.User.can_join_groups` attribute.""" + return self.bot.can_join_groups # type: ignore + + @property + def can_read_all_group_messages(self) -> bool: + """:obj:`bool`: Bot's :attr:`telegram.User.can_read_all_group_messages` attribute.""" + return self.bot.can_read_all_group_messages # type: ignore + + @property + def supports_inline_queries(self) -> bool: + """:obj:`bool`: Bot's :attr:`telegram.User.supports_inline_queries` attribute.""" + return self.bot.supports_inline_queries # type: ignore - return '@{0}'.format(self.username) + @property + def commands(self) -> List[BotCommand]: + """ + List[:class:`BotCommand`]: Bot's commands as available in the default scope. + + .. deprecated:: 13.7 + This property has been deprecated since there can be different commands available for + different scopes. + """ + warnings.warn( + "Bot.commands has been deprecated since there can be different command " + "lists for different scopes.", + TelegramDeprecationWarning, + stacklevel=2, + ) + + if self._commands is None: + self._commands = self.get_my_commands() + return self._commands + + @property + def name(self) -> str: + """:obj:`str`: Bot's @username.""" + return f'@{self.username}' @log - def get_me(self, timeout=None, **kwargs): + def get_me(self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None) -> User: """A simple method for testing your bot's auth token. Requires no parameters. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.User`: A :class:`telegram.User` instance representing that bot if the credentials are valid, :obj:`None` otherwise. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getMe'.format(self.base_url) + result = self._post('getMe', timeout=timeout, api_kwargs=api_kwargs) - result = self._request.get(url, timeout=timeout) + self._bot = User.de_json(result, self) # type: ignore[return-value, arg-type] - self.bot = User.de_json(result, self) - - return self.bot + return self._bot # type: ignore[return-value] @log - @message - def send_message(self, - chat_id, - text, - parse_mode=None, - disable_web_page_preview=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - **kwargs): + def send_message( + self, + chat_id: Union[int, str], + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> Message: """Use this method to send text messages. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - text (:obj:`str`): Text of the message to be sent. Max 4096 characters. Also found as - :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. + of the target channel (in the format ``@channelusername``). + text (:obj:`str`): Text of the message to be sent. Max 4096 characters after entities + parsing. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. See the constants in :class:`telegram.ParseMode` for the available modes. + entities (List[:class:`telegram.MessageEntity`], optional): List of special entities + that appear in message text, which can be specified instead of :attr:`parse_mode`. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this message. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of sent messages from + forwarding and saving. + + .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendMessage'.format(self.base_url) - - data = {'chat_id': chat_id, 'text': text} - - if parse_mode: - data['parse_mode'] = parse_mode - if disable_web_page_preview: - data['disable_web_page_preview'] = disable_web_page_preview + data: JSONDict = { + 'chat_id': chat_id, + 'text': text, + 'parse_mode': parse_mode, + 'disable_web_page_preview': disable_web_page_preview, + } - return url, data + if entities: + data['entities'] = [me.to_dict() for me in entities] + + return self._message( # type: ignore[return-value] + 'sendMessage', + data, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - def delete_message(self, chat_id, message_id, timeout=None, **kwargs): + def delete_message( + self, + chat_id: Union[str, int], + message_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ - Use this method to delete a message. A message can only be deleted if it was sent less - than 48 hours ago. Any such recently sent outgoing message may be deleted. Additionally, - if the bot is an administrator in a group chat, it can delete any message. If the bot is - an administrator in a supergroup, it can delete messages from any other user and service - messages about people joining or leaving the group (other types of service messages may - only be removed by the group creator). In channels, bots can only remove their own - messages. + Use this method to delete a message, including service messages, with the following + limitations: + + - A message can only be deleted if it was sent less than 48 hours ago. + - A dice message in a private chat can only be deleted if it was sent more than 24 + hours ago. + - Bots can delete outgoing messages in private chats, groups, and supergroups. + - Bots can delete incoming messages in private chats. + - Bots granted :attr:`telegram.ChatMember.can_post_messages` permissions can delete + outgoing messages in channels. + - If the bot is an administrator of a group, it can delete any message there. + - If the bot has :attr:`telegram.ChatMember.can_delete_messages` permission in a + supergroup or a channel, it can delete any message there. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). message_id (:obj:`int`): Identifier of the message to delete. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout - from the server (instead of the one specified during creation of the connection - pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/deleteMessage'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id, 'message_id': message_id} - data = {'chat_id': chat_id, 'message_id': message_id} + result = self._post('deleteMessage', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) - - return result + return result # type: ignore[return-value] @log - @message - def forward_message(self, - chat_id, - from_chat_id, - message_id, - disable_notification=False, - timeout=None, - **kwargs): - """Use this method to forward messages of any kind. + def forward_message( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_id: int, + disable_notification: DVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> Message: + """Use this method to forward messages of any kind. Service messages can't be forwarded. + + Note: + Since the release of Bot API 5.5 it can be impossible to forward messages from + some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and + :attr:`telegram.Chat.has_protected_content` to check this. + + As a workaround, it is still possible to use :meth:`copy_message`. However, this + behaviour is undocumented and might be changed by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the - original message was sent (or channel username in the format @channelusername). + original message was sent (or channel username in the format ``@channelusername``). + message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. - message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout - from the server (instead of the one specified during creation of the connection - pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/forwardMessage'.format(self.base_url) - - data = {} + data: JSONDict = {} if chat_id: data['chat_id'] = chat_id @@ -349,21 +632,32 @@ def forward_message(self, data['from_chat_id'] = from_chat_id if message_id: data['message_id'] = message_id - - return url, data + return self._message( # type: ignore[return-value] + 'forwardMessage', + data, + disable_notification=disable_notification, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - @message - def send_photo(self, - chat_id, - photo, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - **kwargs): + def send_photo( + self, + chat_id: Union[int, str], + photo: Union[FileInput, 'PhotoSize'], + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> Message: """Use this method to send photos. Note: @@ -372,73 +666,107 @@ def send_photo(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - photo (:obj:`str` | `filelike object` | :class:`telegram.PhotoSize`): Photo to send. + of the target channel (in the format ``@channelusername``). + photo (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.PhotoSize`): Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the photo, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 caption (:obj:`str`, optional): Photo caption (may also be used when resending photos - by file_id), 0-200 characters. + by file_id), 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendPhoto'.format(self.base_url) - - if isinstance(photo, PhotoSize): - photo = photo.file_id - elif InputFile.is_file(photo): - photo = InputFile(photo) - - data = {'chat_id': chat_id, 'photo': photo} + data: JSONDict = { + 'chat_id': chat_id, + 'photo': parse_file_input(photo, PhotoSize, filename=filename), + 'parse_mode': parse_mode, + } if caption: data['caption'] = caption - if parse_mode: - data['parse_mode'] = parse_mode - - return url, data - - @log - @message - def send_audio(self, - chat_id, - audio, - duration=None, - performer=None, - title=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - thumb=None, - **kwargs): + + if caption_entities: + data['caption_entities'] = [me.to_dict() for me in caption_entities] + + return self._message( # type: ignore[return-value] + 'sendPhoto', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + @log + def send_audio( + self, + chat_id: Union[int, str], + audio: Union[FileInput, 'Audio'], + duration: int = None, + performer: str = None, + title: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the - music player. Your audio must be in the .mp3 format. On success, the sent Message is - returned. Bots can currently send audio files of up to 50 MB in size, this limit may be - changed in the future. + music player. Your audio must be in the .mp3 or .m4a format. - For sending voice messages, use the sendVoice method instead. + Bots can currently send audio files of up to 50 MB in size, this limit may be changed in + the future. + + For sending voice messages, use the :meth:`send_voice` method instead. Note: The audio argument can be either a file_id, an URL or a file from disk @@ -446,48 +774,71 @@ def send_audio(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - audio (:obj:`str` | `filelike object` | :class:`telegram.Audio`): Audio file to send. + of the target channel (in the format ``@channelusername``). + audio (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Audio`): Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. Lastly you can pass an existing :class:`telegram.Audio` object to send. - caption (:obj:`str`, optional): Audio caption, 0-200 characters. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the audio, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + caption (:obj:`str`, optional): Audio caption, 0-1024 characters after entities + parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. duration (:obj:`int`, optional): Duration of sent audio in seconds. performer (:obj:`str`, optional): Performer. title (:obj:`str`, optional): Track name. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail + of the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendAudio'.format(self.base_url) - - if isinstance(audio, Audio): - audio = audio.file_id - elif InputFile.is_file(audio): - audio = InputFile(audio) - - data = {'chat_id': chat_id, 'audio': audio} + data: JSONDict = { + 'chat_id': chat_id, + 'audio': parse_file_input(audio, Audio, filename=filename), + 'parse_mode': parse_mode, + } if duration: data['duration'] = duration @@ -497,30 +848,48 @@ def send_audio(self, data['title'] = title if caption: data['caption'] = caption - if parse_mode: - data['parse_mode'] = parse_mode + + if caption_entities: + data['caption_entities'] = [me.to_dict() for me in caption_entities] if thumb: - if InputFile.is_file(thumb): - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb - - return url, data - - @log - @message - def send_document(self, - chat_id, - document, - filename=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - thumb=None, - **kwargs): - """Use this method to send general files. + data['thumb'] = parse_file_input(thumb, attach=True) + + return self._message( # type: ignore[return-value] + 'sendAudio', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + @log + def send_document( + self, + chat_id: Union[int, str], + document: Union[FileInput, 'Document'], + filename: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + disable_content_type_detection: bool = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> Message: + """ + Use this method to send general files. + + Bots can currently send files of any type of up to 50 MB in size, this limit may be + changed in the future. Note: The document argument can be either a file_id, an URL or a file from disk @@ -528,71 +897,106 @@ def send_document(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - document (:obj:`str` | `filelike object` | :class:`telegram.Document`): File to send. + of the target channel (in the format ``@channelusername``). + document (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Document`): File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. Lastly you can pass an existing :class:`telegram.Document` object to send. - filename (:obj:`str`, optional): File name that shows in telegram message (it is useful - when you send file generated by temp module, for example). Undocumented. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the document, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. caption (:obj:`str`, optional): Document caption (may also be used when resending - documents by file_id), 0-200 characters. + documents by file_id), 0-1024 characters after entities parsing. + disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side + content type detection for files uploaded using multipart/form-data. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail + of the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendDocument'.format(self.base_url) - - if isinstance(document, Document): - document = document.file_id - elif InputFile.is_file(document): - document = InputFile(document, filename=filename) - - data = {'chat_id': chat_id, 'document': document} + data: JSONDict = { + 'chat_id': chat_id, + 'document': parse_file_input(document, Document, filename=filename), + 'parse_mode': parse_mode, + } if caption: data['caption'] = caption - if parse_mode: - data['parse_mode'] = parse_mode - if thumb: - if InputFile.is_file(thumb): - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb - return url, data + if caption_entities: + data['caption_entities'] = [me.to_dict() for me in caption_entities] + if disable_content_type_detection is not None: + data['disable_content_type_detection'] = disable_content_type_detection + if thumb: + data['thumb'] = parse_file_input(thumb, attach=True) + + return self._message( # type: ignore[return-value] + 'sendDocument', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - @message - def send_sticker(self, - chat_id, - sticker, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - **kwargs): - """Use this method to send .webp stickers. + def send_sticker( + self, + chat_id: Union[int, str], + sticker: Union[FileInput, 'Sticker'], + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> Message: + """ + Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers. Note: The sticker argument can be either a file_id, an URL or a file from disk @@ -600,119 +1004,167 @@ def send_sticker(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - sticker (:obj:`str` | `filelike object` :class:`telegram.Sticker`): Sticker to send. + of the target channel (in the format ``@channelusername``). + sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Sticker`): Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .webp file from the Internet, or upload a new one using multipart/form-data. Lastly you can pass an existing :class:`telegram.Sticker` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendSticker'.format(self.base_url) - - if isinstance(sticker, Sticker): - sticker = sticker.file_id - elif InputFile.is_file(sticker): - sticker = InputFile(sticker) - - data = {'chat_id': chat_id, 'sticker': sticker} - - return url, data + data: JSONDict = {'chat_id': chat_id, 'sticker': parse_file_input(sticker, Sticker)} + + return self._message( # type: ignore[return-value] + 'sendSticker', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - @message - def send_video(self, - chat_id, - video, - duration=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - width=None, - height=None, - parse_mode=None, - supports_streaming=None, - thumb=None, - **kwargs): + def send_video( + self, + chat_id: Union[int, str], + video: Union[FileInput, 'Video'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + width: int = None, + height: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: bool = None, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). + Bots can currently send video files of up to 50 MB in size, this limit may be changed in + the future. + Note: - The video argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` + * The video argument can be either a file_id, an URL or a file from disk + ``open(filename, 'rb')`` + * ``thumb`` will be ignored for small video files, for which Telegram can easily + generate thumb nails. However, this behaviour is undocumented and might be changed + by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - video (:obj:`str` | `filelike object` | :class:`telegram.Video`): Video file to send. + of the target channel (in the format ``@channelusername``). + video (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Video`): Video file to send. Pass a file_id as String to send an video file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an video file from the Internet, or upload a new one using multipart/form-data. Lastly you can pass an existing :class:`telegram.Video` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the video, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 duration (:obj:`int`, optional): Duration of sent video in seconds. width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. caption (:obj:`str`, optional): Video caption (may also be used when resending videos - by file_id), 0-200 characters. + by file_id), 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - supports_streaming (:obj:`bool`, optional): Pass True, if the uploaded video is + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. + supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is suitable for streaming. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail + of the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendVideo'.format(self.base_url) - - if isinstance(video, Video): - video = video.file_id - elif InputFile.is_file(video): - video = InputFile(video) - - data = {'chat_id': chat_id, 'video': video} + data: JSONDict = { + 'chat_id': chat_id, + 'video': parse_file_input(video, Video, filename=filename), + 'parse_mode': parse_mode, + } if duration: data['duration'] = duration if caption: data['caption'] = caption - if parse_mode: - data['parse_mode'] = parse_mode + if caption_entities: + data['caption_entities'] = [me.to_dict() for me in caption_entities] if supports_streaming: data['supports_streaming'] = supports_streaming if width: @@ -720,146 +1172,224 @@ def send_video(self, if height: data['height'] = height if thumb: - if InputFile.is_file(thumb): - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb - - return url, data - - @log - @message - def send_video_note(self, - chat_id, - video_note, - duration=None, - length=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - thumb=None, - **kwargs): - """Use this method to send video messages. + data['thumb'] = parse_file_input(thumb, attach=True) + + return self._message( # type: ignore[return-value] + 'sendVideo', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + @log + def send_video_note( + self, + chat_id: Union[int, str], + video_note: Union[FileInput, 'VideoNote'], + duration: int = None, + length: int = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: str = None, + protect_content: bool = None, + ) -> Message: + """ + As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. + Use this method to send video messages. Note: - The video_note argument can be either a file_id or a file from disk - ``open(filename, 'rb')`` + * The video_note argument can be either a file_id or a file from disk + ``open(filename, 'rb')`` + * ``thumb`` will be ignored for small video files, for which Telegram can easily + generate thumb nails. However, this behaviour is undocumented and might be changed + by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - video_note (:obj:`str` | `filelike object` | :class:`telegram.VideoNote`): Video note + of the target channel (in the format ``@channelusername``). + video_note (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.VideoNote`): Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. Or you can pass an existing :class:`telegram.VideoNote` object to send. Sending video notes by a URL is currently unsupported. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the video note, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 duration (:obj:`int`, optional): Duration of sent video in seconds. - length (:obj:`int`, optional): Video width and height + length (:obj:`int`, optional): Video width and height, i.e. diameter of the video + message. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail + of the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendVideoNote'.format(self.base_url) - - if isinstance(video_note, VideoNote): - video_note = video_note.file_id - elif InputFile.is_file(video_note): - video_note = InputFile(video_note) - - data = {'chat_id': chat_id, 'video_note': video_note} + data: JSONDict = { + 'chat_id': chat_id, + 'video_note': parse_file_input(video_note, VideoNote, filename=filename), + } if duration is not None: data['duration'] = duration if length is not None: data['length'] = length if thumb: - if InputFile.is_file(thumb): - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb - - return url, data - - @log - @message - def send_animation(self, - chat_id, - animation, - duration=None, - width=None, - height=None, - thumb=None, - caption=None, - parse_mode=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - **kwargs): + data['thumb'] = parse_file_input(thumb, attach=True) + + return self._message( # type: ignore[return-value] + 'sendVideoNote', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + @log + def send_animation( + self, + chat_id: Union[int, str], + animation: Union[FileInput, 'Animation'], + duration: int = None, + width: int = None, + height: int = None, + thumb: FileInput = None, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). + Bots can currently send animation files of up to 50 MB in size, this limit may be changed + in the future. + + Note: + ``thumb`` will be ignored for small files, for which Telegram can easily + generate thumb nails. However, this behaviour is undocumented and might be changed + by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - animation (:obj:`str` | `filelike object` | :class:`telegram.Animation`): Animation to + of the target channel (in the format ``@channelusername``). + animation (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Animation`): Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. Lastly you can pass an existing :class:`telegram.Animation` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the animation, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 duration (:obj:`int`, optional): Duration of sent animation in seconds. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail + of the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. caption (:obj:`str`, optional): Animation caption (may also be used when resending - animations by file_id), 0-200 characters. + animations by file_id), 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendAnimation'.format(self.base_url) - - if isinstance(animation, Animation): - animation = animation.file_id - elif InputFile.is_file(animation): - animation = InputFile(animation) - - data = {'chat_id': chat_id, 'animation': animation} + data: JSONDict = { + 'chat_id': chat_id, + 'animation': parse_file_input(animation, Animation, filename=filename), + 'parse_mode': parse_mode, + } if duration: data['duration'] = duration @@ -868,33 +1398,47 @@ def send_animation(self, if height: data['height'] = height if thumb: - if InputFile.is_file(thumb): - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb + data['thumb'] = parse_file_input(thumb, attach=True) if caption: data['caption'] = caption - if parse_mode: - data['parse_mode'] = parse_mode - - return url, data + if caption_entities: + data['caption_entities'] = [me.to_dict() for me in caption_entities] + + return self._message( # type: ignore[return-value] + 'sendAnimation', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - @message - def send_voice(self, - chat_id, - voice, - duration=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - **kwargs): + def send_voice( + self, + chat_id: Union[int, str], + voice: Union[FileInput, 'Voice'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .ogg file - encoded with OPUS (other formats may be sent as Audio or Document). + encoded with OPUS (other formats may be sent as Audio or Document). Bots can currently + send voice messages of up to 50 MB in size, this limit may be changed in the future. Note: The voice argument can be either a file_id, an URL or a file from disk @@ -902,107 +1446,167 @@ def send_voice(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - voice (:obj:`str` | `filelike object` | :class:`telegram.Voice`): Voice file to send. + of the target channel (in the format ``@channelusername``). + voice (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Voice`): Voice file to send. Pass a file_id as String to send an voice file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an voice file from the Internet, or upload a new one using multipart/form-data. Lastly you can pass an existing :class:`telegram.Voice` object to send. - caption (:obj:`str`, optional): Voice message caption, 0-200 characters. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the voice, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + caption (:obj:`str`, optional): Voice message caption, 0-1024 characters after entities + parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. duration (:obj:`int`, optional): Duration of the voice message in seconds. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendVoice'.format(self.base_url) - - if isinstance(voice, Voice): - voice = voice.file_id - elif InputFile.is_file(voice): - voice = InputFile(voice) - - data = {'chat_id': chat_id, 'voice': voice} + data: JSONDict = { + 'chat_id': chat_id, + 'voice': parse_file_input(voice, Voice, filename=filename), + 'parse_mode': parse_mode, + } if duration: data['duration'] = duration if caption: data['caption'] = caption - if parse_mode: - data['parse_mode'] = parse_mode - return url, data + if caption_entities: + data['caption_entities'] = [me.to_dict() for me in caption_entities] + + return self._message( # type: ignore[return-value] + 'sendVoice', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - def send_media_group(self, - chat_id, - media, - disable_notification=None, - reply_to_message_id=None, - timeout=20, - **kwargs): + def send_media_group( + self, + chat_id: Union[int, str], + media: List[ + Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> List[Message]: """Use this method to send a group of photos or videos as an album. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). - media (List[:class:`telegram.InputMedia`]): An array describing photos and videos to be - sent, must include 2–10 items. + of the target channel (in the format ``@channelusername``). + media (List[:class:`telegram.InputMediaAudio`, :class:`telegram.InputMediaDocument`, \ + :class:`telegram.InputMediaPhoto`, :class:`telegram.InputMediaVideo`]): An array + describing messages to be sent, must include 2–10 items. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: List[:class:`telegram.Message`]: An array of the sent Messages. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ + data: JSONDict = { + 'chat_id': chat_id, + 'media': media, + 'disable_notification': disable_notification, + 'allow_sending_without_reply': allow_sending_without_reply, + } - url = '{0}/sendMediaGroup'.format(self.base_url) - - data = {'chat_id': chat_id, 'media': media} + for med in data['media']: + if med.parse_mode == DEFAULT_NONE: + if self.defaults: + med.parse_mode = DefaultValue.get_value(self.defaults.parse_mode) + else: + med.parse_mode = None if reply_to_message_id: data['reply_to_message_id'] = reply_to_message_id - if disable_notification: - data['disable_notification'] = disable_notification - - result = self._request.post(url, data, timeout=timeout) - - return [Message.de_json(res, self) for res in result] - - @log - @message - def send_location(self, - chat_id, - latitude=None, - longitude=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - location=None, - live_period=None, - **kwargs): + + if protect_content: + data['protect_content'] = protect_content + + result = self._post('sendMediaGroup', data, timeout=timeout, api_kwargs=api_kwargs) + + return Message.de_list(result, self) # type: ignore + + @log + def send_location( + self, + chat_id: Union[int, str], + latitude: float = None, + longitude: float = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + location: Location = None, + live_period: int = None, + api_kwargs: JSONDict = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> Message: """Use this method to send point on the map. Note: @@ -1010,105 +1614,150 @@ def send_location(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). latitude (:obj:`float`, optional): Latitude of location. longitude (:obj:`float`, optional): Longitude of location. location (:class:`telegram.Location`, optional): The location to send. + horizontal_accuracy (:obj:`int`, optional): The radius of uncertainty for the location, + measured in meters; 0-1500. live_period (:obj:`int`, optional): Period in seconds for which the location will be updated, should be between 60 and 86400. + heading (:obj:`int`, optional): For live locations, a direction in which the user is + moving, in degrees. Must be between 1 and 360 if specified. + proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance + for proximity alerts about approaching another chat member, in meters. Must be + between 1 and 100000 if specified. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendLocation'.format(self.base_url) - - if not (all([latitude, longitude]) or location): - raise ValueError("Either location or latitude and longitude must be passed as" - "argument.") + if not ((latitude is not None and longitude is not None) or location): + raise ValueError( + "Either location or latitude and longitude must be passed as argument." + ) - if not ((latitude is not None or longitude is not None) ^ bool(location)): - raise ValueError("Either location or latitude and longitude must be passed as" - "argument. Not both.") + if not (latitude is not None or longitude is not None) ^ bool(location): + raise ValueError( + "Either location or latitude and longitude must be passed as argument. Not both." + ) if isinstance(location, Location): latitude = location.latitude longitude = location.longitude - data = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude} + data: JSONDict = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude} if live_period: data['live_period'] = live_period - - return url, data + if horizontal_accuracy: + data['horizontal_accuracy'] = horizontal_accuracy + if heading: + data['heading'] = heading + if proximity_alert_radius: + data['proximity_alert_radius'] = proximity_alert_radius + + return self._message( # type: ignore[return-value] + 'sendLocation', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - @message - def edit_message_live_location(self, - chat_id=None, - message_id=None, - inline_message_id=None, - latitude=None, - longitude=None, - location=None, - reply_markup=None, - **kwargs): + def edit_message_live_location( + self, + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + latitude: float = None, + longitude: float = None, + location: Location = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + ) -> Union[Message, bool]: """Use this method to edit live location messages sent by the bot or via the bot - (for inline bots). A location can be edited until its :attr:`live_period` expires or - editing is explicitly disabled by a call to :attr:`stop_message_live_location`. + (for inline bots). A location can be edited until its :attr:`telegram.Location.live_period` + expires or editing is explicitly disabled by a call to :meth:`stop_message_live_location`. Note: You can either supply a :obj:`latitude` and :obj:`longitude` or a :obj:`location`. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. Unique identifier for the target chat or username of the target channel + (in the format ``@channelusername``). message_id (:obj:`int`, optional): Required if inline_message_id is not specified. - Identifier of the sent message. + Identifier of the message to edit. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. latitude (:obj:`float`, optional): Latitude of location. longitude (:obj:`float`, optional): Longitude of location. location (:class:`telegram.Location`, optional): The location to send. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A - JSON-serialized object for an inline keyboard, custom reply keyboard, instructions - to remove reply keyboard or to force a reply from the user. + horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the + location, measured in meters; 0-1500. + heading (:obj:`int`, optional): Direction in which the user is moving, in degrees. Must + be between 1 and 360 if specified. + proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts + about approaching another chat member, in meters. Must be between 1 and 100000 if + specified. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for a new inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :class:`telegram.Message`: On success the edited message. + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. """ - - url = '{0}/editMessageLiveLocation'.format(self.base_url) - if not (all([latitude, longitude]) or location): - raise ValueError("Either location or latitude and longitude must be passed as" - "argument.") - if not ((latitude is not None or longitude is not None) ^ bool(location)): - raise ValueError("Either location or latitude and longitude must be passed as" - "argument. Not both.") + raise ValueError( + "Either location or latitude and longitude must be passed as argument." + ) + if not (latitude is not None or longitude is not None) ^ bool(location): + raise ValueError( + "Either location or latitude and longitude must be passed as argument. Not both." + ) if isinstance(location, Location): latitude = location.latitude longitude = location.longitude - data = {'latitude': latitude, 'longitude': longitude} + data: JSONDict = {'latitude': latitude, 'longitude': longitude} if chat_id: data['chat_id'] = chat_id @@ -1116,41 +1765,55 @@ def edit_message_live_location(self, data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id - - return url, data + if horizontal_accuracy: + data['horizontal_accuracy'] = horizontal_accuracy + if heading: + data['heading'] = heading + if proximity_alert_radius: + data['proximity_alert_radius'] = proximity_alert_radius + + return self._message( + 'editMessageLiveLocation', + data, + timeout=timeout, + reply_markup=reply_markup, + api_kwargs=api_kwargs, + ) @log - @message - def stop_message_live_location(self, - chat_id=None, - message_id=None, - inline_message_id=None, - reply_markup=None, - **kwargs): + def stop_message_live_location( + self, + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: """Use this method to stop updating a live location message sent by the bot or via the bot (for inline bots) before live_period expires. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`): Required if inline_message_id is not specified. + Unique identifier for the target chat or username of the target channel + (in the format ``@channelusername``). message_id (:obj:`int`, optional): Required if inline_message_id is not specified. - Identifier of the sent message. + Identifier of the sent message with live location to stop. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A - JSON-serialized object for an inline keyboard, custom reply keyboard, instructions - to remove reply keyboard or to force a reply from the user. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for a new inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :class:`telegram.Message`: On success the edited message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + sent Message is returned, otherwise :obj:`True` is returned. """ - - url = '{0}/stopMessageLiveLocation'.format(self.base_url) - - data = {} + data: JSONDict = {} if chat_id: data['chat_id'] = chat_id @@ -1159,34 +1822,48 @@ def stop_message_live_location(self, if inline_message_id: data['inline_message_id'] = inline_message_id - return url, data - - @log - @message - def send_venue(self, - chat_id, - latitude=None, - longitude=None, - title=None, - address=None, - foursquare_id=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - venue=None, - foursquare_type=None, - **kwargs): + return self._message( + 'stopMessageLiveLocation', + data, + timeout=timeout, + reply_markup=reply_markup, + api_kwargs=api_kwargs, + ) + + @log + def send_venue( + self, + chat_id: Union[int, str], + latitude: float = None, + longitude: float = None, + title: str = None, + address: str = None, + foursquare_id: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + venue: Venue = None, + foursquare_type: str = None, + api_kwargs: JSONDict = None, + google_place_id: str = None, + google_place_type: str = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> Message: """Use this method to send information about a venue. Note: - you can either supply :obj:`venue`, or :obj:`latitude`, :obj:`longitude`, - :obj:`title` and :obj:`address` and optionally :obj:`foursquare_id` and optionally - :obj:`foursquare_type`. + * You can either supply :obj:`venue`, or :obj:`latitude`, :obj:`longitude`, + :obj:`title` and :obj:`address` and optionally :obj:`foursquare_id` and + :obj:`foursquare_type` or optionally :obj:`google_place_id` and + :obj:`google_place_type`. + * Foursquare details and Google Pace details are mutually exclusive. However, this + behaviour is undocumented and might be changed by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). latitude (:obj:`float`, optional): Latitude of venue. longitude (:obj:`float`, optional): Longitude of venue. title (:obj:`str`, optional): Name of the venue. @@ -1195,31 +1872,43 @@ def send_venue(self, foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) + google_place_id (:obj:`str`, optional): Google Places identifier of the venue. + google_place_type (:obj:`str`, optional): Google Places type of the venue. (See + `supported types \ + `_.) venue (:class:`telegram.Venue`, optional): The venue to send. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendVenue'.format(self.base_url) - if not (venue or all([latitude, longitude, address, title])): - raise ValueError("Either venue or latitude, longitude, address and title must be" - "passed as arguments.") + raise ValueError( + "Either venue or latitude, longitude, address and title must be" + "passed as arguments." + ) if isinstance(venue, Venue): latitude = venue.location.latitude @@ -1228,36 +1917,55 @@ def send_venue(self, title = venue.title foursquare_id = venue.foursquare_id foursquare_type = venue.foursquare_type + google_place_id = venue.google_place_id + google_place_type = venue.google_place_type - data = { + data: JSONDict = { 'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude, 'address': address, - 'title': title + 'title': title, } if foursquare_id: data['foursquare_id'] = foursquare_id if foursquare_type: data['foursquare_type'] = foursquare_type + if google_place_id: + data['google_place_id'] = google_place_id + if google_place_type: + data['google_place_type'] = google_place_type + + return self._message( # type: ignore[return-value] + 'sendVenue', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) - return url, data - - @log - @message - def send_contact(self, - chat_id, - phone_number=None, - first_name=None, - last_name=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - contact=None, - vcard=None, - **kwargs): + @log + def send_contact( + self, + chat_id: Union[int, str], + phone_number: str = None, + first_name: str = None, + last_name: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + contact: Contact = None, + vcard: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> Message: """Use this method to send phone contacts. Note: @@ -1266,7 +1974,7 @@ def send_contact(self, Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). phone_number (:obj:`str`, optional): Contact's phone number. first_name (:obj:`str`, optional): Contact's first name. last_name (:obj:`str`, optional): Contact's last name. @@ -1275,28 +1983,35 @@ def send_contact(self, contact (:class:`telegram.Contact`, optional): The contact to send. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendContact'.format(self.base_url) - if (not contact) and (not all([phone_number, first_name])): - raise ValueError("Either contact or phone_number and first_name must be passed as" - "arguments.") + raise ValueError( + "Either contact or phone_number and first_name must be passed as arguments." + ) if isinstance(contact, Contact): phone_number = contact.phone_number @@ -1304,129 +2019,240 @@ def send_contact(self, last_name = contact.last_name vcard = contact.vcard - data = {'chat_id': chat_id, 'phone_number': phone_number, 'first_name': first_name} + data: JSONDict = { + 'chat_id': chat_id, + 'phone_number': phone_number, + 'first_name': first_name, + } if last_name: data['last_name'] = last_name if vcard: data['vcard'] = vcard - return url, data + return self._message( # type: ignore[return-value] + 'sendContact', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - @message - def send_game(self, - chat_id, - game_short_name, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - **kwargs): + def send_game( + self, + chat_id: Union[int, str], + game_short_name: str, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> Message: """Use this method to send a game. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat. game_short_name (:obj:`str`): Short name of the game, serves as the unique identifier - for the game. Set up your games via Botfather. + for the game. Set up your games via `@BotFather `_. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A - JSON-serialized object for an inline keyboard, custom reply keyboard, instructions - to remove reply keyboard or to force a reply from the user. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for a new inline keyboard. If empty, one ‘Play game_title’ button will be + shown. If not empty, the first button must launch the game. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendGame'.format(self.base_url) - - data = {'chat_id': chat_id, 'game_short_name': game_short_name} - - return url, data + data: JSONDict = {'chat_id': chat_id, 'game_short_name': game_short_name} + + return self._message( # type: ignore[return-value] + 'sendGame', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - def send_chat_action(self, chat_id, action, timeout=None, **kwargs): + def send_chat_action( + self, + chat_id: Union[str, int], + action: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, - Telegram clients clear its typing status). + Telegram clients clear its typing status). Telegram only recommends using this method when + a response from the bot will take a noticeable amount of time to arrive. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). action(:class:`telegram.ChatAction` | :obj:`str`): Type of action to broadcast. Choose one, depending on what the user is about to receive. For convenience look at the constants in :class:`telegram.ChatAction` timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: ``True`` on success. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendChatAction'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id, 'action': action} - data = {'chat_id': chat_id, 'action': action} - data.update(kwargs) + result = self._post('sendChatAction', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) + return result # type: ignore[return-value] - return result + def _effective_inline_results( # pylint: disable=R0201 + self, + results: Union[ + Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] + ], + next_offset: str = None, + current_offset: str = None, + ) -> Tuple[Sequence['InlineQueryResult'], Optional[str]]: + """ + Builds the effective results from the results input. + We make this a stand-alone method so tg.ext.ExtBot can wrap it. + + Returns: + Tuple of 1. the effective results and 2. correct the next_offset - @log - def answer_inline_query(self, - inline_query_id, - results, - cache_time=300, - is_personal=None, - next_offset=None, - switch_pm_text=None, - switch_pm_parameter=None, - timeout=None, - **kwargs): """ - Use this method to send answers to an inline query. No more than 50 results per query are - allowed. + if current_offset is not None and next_offset is not None: + raise ValueError('`current_offset` and `next_offset` are mutually exclusive!') - Args: - inline_query_id (:obj:`str`): Unique identifier for the answered query. - results (List[:class:`telegram.InlineQueryResult`)]: A list of results for the inline - query. + if current_offset is not None: + # Convert the string input to integer + if current_offset == '': + current_offset_int = 0 + else: + current_offset_int = int(current_offset) + + # for now set to empty string, stating that there are no more results + # might change later + next_offset = '' + + if callable(results): + callable_output = results(current_offset_int) + if not callable_output: + effective_results: Sequence['InlineQueryResult'] = [] + else: + effective_results = callable_output + # the callback *might* return more results on the next call, so we increment + # the page count + next_offset = str(current_offset_int + 1) + else: + if len(results) > (current_offset_int + 1) * MAX_INLINE_QUERY_RESULTS: + # we expect more results for the next page + next_offset_int = current_offset_int + 1 + next_offset = str(next_offset_int) + effective_results = results[ + current_offset_int + * MAX_INLINE_QUERY_RESULTS : next_offset_int + * MAX_INLINE_QUERY_RESULTS + ] + else: + effective_results = results[current_offset_int * MAX_INLINE_QUERY_RESULTS :] + else: + effective_results = results # type: ignore[assignment] + + return effective_results, next_offset + + @log + def answer_inline_query( + self, + inline_query_id: str, + results: Union[ + Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] + ], + cache_time: int = 300, + is_personal: bool = None, + next_offset: str = None, + switch_pm_text: str = None, + switch_pm_parameter: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + current_offset: str = None, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to send answers to an inline query. No more than 50 results per query are + allowed. + + Warning: + In most use cases :attr:`current_offset` should not be passed manually. Instead of + calling this method directly, use the shortcut :meth:`telegram.InlineQuery.answer` with + ``auto_pagination=True``, which will take care of passing the correct value. + + Args: + inline_query_id (:obj:`str`): Unique identifier for the answered query. + results (List[:class:`telegram.InlineQueryResult`] | Callable): A list of results for + the inline query. In case :attr:`current_offset` is passed, ``results`` may also be + a callable that accepts the current page index starting from 0. It must return + either a list of :class:`telegram.InlineQueryResult` instances or :obj:`None` if + there are no more results. cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the - result of the inline query may be cached on the server. Defaults to 300. - is_personal (:obj:`bool`, optional): Pass True, if results may be cached on the server - side only for the user that sent the query. By default, results may be returned to - any user who sends the same query. + result of the inline query may be cached on the server. Defaults to ``300``. + is_personal (:obj:`bool`, optional): Pass :obj:`True`, if results may be cached on + the server side only for the user that sent the query. By default, + results may be returned to any user who sends the same query. next_offset (:obj:`str`, optional): Pass the offset that a client should send in the next query with the same text to receive more results. Pass an empty string if there are no more results or if you don't support pagination. Offset length can't exceed 64 bytes. switch_pm_text (:obj:`str`, optional): If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the - bot a start message with the parameter switch_pm_parameter. + bot a start message with the parameter ``switch_pm_parameter``. switch_pm_parameter (:obj:`str`, optional): Deep-linking parameter for the /start message sent to the bot when user presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed. + current_offset (:obj:`str`, optional): The :attr:`telegram.InlineQuery.offset` of + the inline query to answer. If passed, PTB will automatically take care of + the pagination for you, i.e. pass the correct ``next_offset`` and truncate the + results list/get the results from the callable you passed. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - he read timeout from the server (instead of the one specified during creation of + the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Example: An inline bot that sends YouTube videos can ask the user to connect the bot to their @@ -1438,17 +2264,54 @@ def answer_inline_query(self, where they wanted to use the bot's inline capabilities. Returns: - :obj:`bool` On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/answerInlineQuery'.format(self.base_url) - results = [res.to_dict() for res in results] - - data = {'inline_query_id': inline_query_id, 'results': results} + @no_type_check + def _set_defaults(res): + # pylint: disable=W0212 + if hasattr(res, 'parse_mode') and res.parse_mode == DEFAULT_NONE: + if self.defaults: + res.parse_mode = self.defaults.parse_mode + else: + res.parse_mode = None + if hasattr(res, 'input_message_content') and res.input_message_content: + if ( + hasattr(res.input_message_content, 'parse_mode') + and res.input_message_content.parse_mode == DEFAULT_NONE + ): + if self.defaults: + res.input_message_content.parse_mode = DefaultValue.get_value( + self.defaults.parse_mode + ) + else: + res.input_message_content.parse_mode = None + if ( + hasattr(res.input_message_content, 'disable_web_page_preview') + and res.input_message_content.disable_web_page_preview == DEFAULT_NONE + ): + if self.defaults: + res.input_message_content.disable_web_page_preview = ( + DefaultValue.get_value(self.defaults.disable_web_page_preview) + ) + else: + res.input_message_content.disable_web_page_preview = None + + effective_results, next_offset = self._effective_inline_results( + results=results, next_offset=next_offset, current_offset=current_offset + ) + + # Apply defaults + for result in effective_results: + _set_defaults(result) + + results_dicts = [res.to_dict() for res in effective_results] + + data: JSONDict = {'inline_query_id': inline_query_id, 'results': results_dicts} if cache_time or cache_time == 0: data['cache_time'] = cache_time @@ -1461,14 +2324,22 @@ def answer_inline_query(self, if switch_pm_parameter: data['switch_pm_parameter'] = switch_pm_parameter - data.update(kwargs) - - result = self._request.post(url, data, timeout=timeout) - - return result + return self._post( # type: ignore[return-value] + 'answerInlineQuery', + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) @log - def get_user_profile_photos(self, user_id, offset=None, limit=100, timeout=None, **kwargs): + def get_user_profile_photos( + self, + user_id: Union[str, int], + offset: int = None, + limit: int = 100, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Optional[UserProfilePhotos]: """Use this method to get a list of profile pictures for a user. Args: @@ -1476,44 +2347,55 @@ def get_user_profile_photos(self, user_id, offset=None, limit=100, timeout=None, offset (:obj:`int`, optional): Sequential number of the first photo to be returned. By default, all photos are returned. limit (:obj:`int`, optional): Limits the number of photos to be retrieved. Values - between 1-100 are accepted. Defaults to 100. + between 1-100 are accepted. Defaults to ``100``. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.UserProfilePhotos` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getUserProfilePhotos'.format(self.base_url) - - data = {'user_id': user_id} + data: JSONDict = {'user_id': user_id} if offset is not None: data['offset'] = offset if limit: data['limit'] = limit - data.update(kwargs) - result = self._request.post(url, data, timeout=timeout) + result = self._post('getUserProfilePhotos', data, timeout=timeout, api_kwargs=api_kwargs) - return UserProfilePhotos.de_json(result, self) + return UserProfilePhotos.de_json(result, self) # type: ignore[return-value, arg-type] @log - def get_file(self, file_id, timeout=None, **kwargs): + def get_file( + self, + file_id: Union[ + str, Animation, Audio, ChatPhoto, Document, PhotoSize, Sticker, Video, VideoNote, Voice + ], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> File: """ Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. The file can then be downloaded - with :attr:`telegram.File.download`. It is guaranteed that the link will be + with :meth:`telegram.File.download`. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling get_file again. + Note: + This function may not preserve the original file name and MIME type. + You should save the file's MIME type and name (if available) when the File object + is received. + Args: - file_id (:obj:`str` | :class:`telegram.Audio` | :class:`telegram.Document` | \ + file_id (:obj:`str` | :class:`telegram.Animation` | :class:`telegram.Audio` | \ + :class:`telegram.ChatPhoto` | :class:`telegram.Document` | \ :class:`telegram.PhotoSize` | :class:`telegram.Sticker` | \ :class:`telegram.Video` | :class:`telegram.VideoNote` | \ :class:`telegram.Voice`): @@ -1522,135 +2404,277 @@ def get_file(self, file_id, timeout=None, **kwargs): timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getFile'.format(self.base_url) - try: - file_id = file_id.file_id + file_id = file_id.file_id # type: ignore[union-attr] except AttributeError: pass - data = {'file_id': file_id} - data.update(kwargs) + data: JSONDict = {'file_id': file_id} + + result = self._post('getFile', data, timeout=timeout, api_kwargs=api_kwargs) + + if result.get('file_path') and not is_local_file( # type: ignore[union-attr] + result['file_path'] # type: ignore[index] + ): + result['file_path'] = '{}/{}'.format( # type: ignore[index] + self.base_file_url, result['file_path'] # type: ignore[index] + ) - result = self._request.post(url, data, timeout=timeout) + return File.de_json(result, self) # type: ignore[return-value, arg-type] - if result.get('file_path'): - result['file_path'] = '%s/%s' % (self.base_file_url, result['file_path']) + @log + def kick_chat_member( + self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + until_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + revoke_messages: bool = None, + ) -> bool: + """ + Deprecated, use :func:`~telegram.Bot.ban_chat_member` instead. - return File.de_json(result, self) + .. deprecated:: 13.7 + + """ + warnings.warn( + '`bot.kick_chat_member` is deprecated. Use `bot.ban_chat_member` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + return self.ban_chat_member( + chat_id=chat_id, + user_id=user_id, + timeout=timeout, + until_date=until_date, + api_kwargs=api_kwargs, + revoke_messages=revoke_messages, + ) @log - def kick_chat_member(self, chat_id, user_id, timeout=None, until_date=None, **kwargs): + def ban_chat_member( + self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + until_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + revoke_messages: bool = None, + ) -> bool: """ - Use this method to kick a user from a group or a supergroup. In the case of supergroups, - the user will not be able to return to the group on their own using invite links, etc., - unless unbanned first. The bot must be an administrator in the group for this to work. + Use this method to ban a user from a group, supergroup or a channel. In the case of + supergroups and channels, the user will not be able to return to the group on their own + using invite links, etc., unless unbanned first. The bot must be an administrator in the + chat for this to work and must have the appropriate admin rights. + + .. versionadded:: 13.7 Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username + of the target supergroup or channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 - seconds from the current time they are considered to be banned forever. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. - - Note: - In regular groups (non-supergroups), this method will only work if the - 'All Members Are Admins' setting is off in the target group. Otherwise - members may only be removed by the group's creator or by the member that added them. + seconds from the current time they are considered to be banned forever. Applied + for supergroups and channels only. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + revoke_messages (:obj:`bool`, optional): Pass :obj:`True` to delete all messages from + the chat for the user that is being removed. If :obj:`False`, the user will be able + to see messages in the group that were sent before the user was removed. + Always :obj:`True` for supergroups and channels. + + .. versionadded:: 13.4 + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool` On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/kickChatMember'.format(self.base_url) - - data = {'chat_id': chat_id, 'user_id': user_id} - data.update(kwargs) + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} if until_date is not None: if isinstance(until_date, datetime): - until_date = to_timestamp(until_date) + until_date = to_timestamp( + until_date, tzinfo=self.defaults.tzinfo if self.defaults else None + ) data['until_date'] = until_date - result = self._request.post(url, data, timeout=timeout) + if revoke_messages is not None: + data['revoke_messages'] = revoke_messages - return result + result = self._post('banChatMember', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] @log - def unban_chat_member(self, chat_id, user_id, timeout=None, **kwargs): - """Use this method to unban a previously kicked user in a supergroup. + def ban_chat_sender_chat( + self, + chat_id: Union[str, int], + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to ban a channel chat in a supergroup or a channel. Until the chat is + unbanned, the owner of the banned chat won't be able to send messages on behalf of **any of + their channels**. The bot must be an administrator in the supergroup or channel for this + to work and must have the appropriate administrator rights. + + .. versionadded:: 13.9 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username + of the target supergroup or channel (in the format ``@channelusername``). + sender_chat_id (:obj:`int`): Unique identifier of the target sender chat. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id} - The user will not return to the group automatically, but will be able to join via link, - etc. The bot must be an administrator in the group for this to work. + result = self._post('banChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + + @log + def unban_chat_member( + self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + only_if_banned: bool = None, + ) -> bool: + """Use this method to unban a previously kicked user in a supergroup or channel. + + The user will *not* return to the group or channel automatically, but will be able to join + via link, etc. The bot must be an administrator for this to work. By default, this method + guarantees that after the call the user is not a member of the chat, but will be able to + join it. So if the user is a member of the chat they will also be *removed* from the chat. + If you don't want this, use the parameter :attr:`only_if_banned`. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + of the target supergroup or channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. + only_if_banned (:obj:`bool`, optional): Do nothing if the user is not banned. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool` On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/unbanChatMember'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} - data = {'chat_id': chat_id, 'user_id': user_id} - data.update(kwargs) + if only_if_banned is not None: + data['only_if_banned'] = only_if_banned - result = self._request.post(url, data, timeout=timeout) + result = self._post('unbanChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] + + @log + def unban_chat_sender_chat( + self, + chat_id: Union[str, int], + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to unban a previously banned channel in a supergroup or channel. + The bot must be an administrator for this to work and must have the + appropriate administrator rights. + + .. versionadded:: 13.9 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target supergroup or channel (in the format ``@channelusername``). + sender_chat_id (:obj:`int`): Unique identifier of the target sender chat. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id} + + result = self._post('unbanChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] @log - def answer_callback_query(self, - callback_query_id, - text=None, - show_alert=False, - url=None, - cache_time=None, - timeout=None, - **kwargs): + def answer_callback_query( + self, + callback_query_id: str, + text: str = None, + show_alert: bool = False, + url: str = None, + cache_time: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. Alternatively, the user can be redirected to the specified Game URL. For this option to - work, you must first create a game for your bot via BotFather and accept the terms. - Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with - a parameter. + work, you must first create a game for your bot via `@BotFather `_ + and accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX that open + your bot with a parameter. Args: callback_query_id (:obj:`str`): Unique identifier for the query to be answered. text (:obj:`str`, optional): Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters. - show_alert (:obj:`bool`, optional): If true, an alert will be shown by the client - instead of a notification at the top of the chat screen. Defaults to false. + show_alert (:obj:`bool`, optional): If :obj:`True`, an alert will be shown by the + client instead of a notification at the top of the chat screen. Defaults to + :obj:`False`. url (:obj:`str`, optional): URL that will be opened by the user's client. If you have - created a Game and accepted the conditions via @Botfather, specify the URL that + created a Game and accepted the conditions via + `@BotFather `_, specify the URL that opens your game - note that this will only work if the query comes from a callback game button. Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with a parameter. @@ -1659,18 +2683,17 @@ def answer_callback_query(self, timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool` On success, ``True`` is returned. + :obj:`bool` On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url_ = '{0}/answerCallbackQuery'.format(self.base_url) - - data = {'callback_query_id': callback_query_id} + data: JSONDict = {'callback_query_id': callback_query_id} if text: data['text'] = text @@ -1680,60 +2703,65 @@ def answer_callback_query(self, data['url'] = url if cache_time is not None: data['cache_time'] = cache_time - data.update(kwargs) - result = self._request.post(url_, data, timeout=timeout) + result = self._post('answerCallbackQuery', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - @message - def edit_message_text(self, - text, - chat_id=None, - message_id=None, - inline_message_id=None, - parse_mode=None, - disable_web_page_preview=None, - reply_markup=None, - timeout=None, - **kwargs): + def edit_message_text( + self, + text: str, + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + ) -> Union[Message, bool]: """ - Use this method to edit text and game messages sent by the bot or via the bot (for inline - bots). + Use this method to edit text and game messages. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. Unique identifier for the target chat or username of the target channel + (in the format ``@channelusername``) message_id (:obj:`int`, optional): Required if inline_message_id is not specified. - Identifier of the sent message. + Identifier of the message to edit. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. - text (:obj:`str`): New text of the message. - parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show bold, - italic, fixed-width text or inline URLs in your bot's message. See the constants in - :class:`telegram.ParseMode` for the available modes. + text (:obj:`str`): New text of the message, 1-4096 characters after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to + show bold, italic, fixed-width text or inline URLs in your bot's message. See the + constants in :class:`telegram.ParseMode` for the available modes. + entities (List[:class:`telegram.MessageEntity`], optional): List of special entities + that appear in message text, which can be specified instead of :attr:`parse_mode`. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this message. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A - JSON-serialized object for an inline keyboard, custom reply keyboard, instructions - to remove reply keyboard or to force a reply from the user. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise ``True`` is returned. + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/editMessageText'.format(self.base_url) - - data = {'text': text} + data: JSONDict = { + 'text': text, + 'parse_mode': parse_mode, + 'disable_web_page_preview': disable_web_page_preview, + } if chat_id: data['chat_id'] = chat_id @@ -1741,68 +2769,77 @@ def edit_message_text(self, data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id - if parse_mode: - data['parse_mode'] = parse_mode - if disable_web_page_preview: - data['disable_web_page_preview'] = disable_web_page_preview - - return url, data - - @log - @message - def edit_message_caption(self, - chat_id=None, - message_id=None, - inline_message_id=None, - caption=None, - reply_markup=None, - timeout=None, - parse_mode=None, - **kwargs): - """ - Use this method to edit captions of messages sent by the bot or via the bot - (for inline bots). + if entities: + data['entities'] = [me.to_dict() for me in entities] + + return self._message( + 'editMessageText', + data, + timeout=timeout, + reply_markup=reply_markup, + api_kwargs=api_kwargs, + ) + + @log + def edit_message_caption( + self, + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + caption: str = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + ) -> Union[Message, bool]: + """ + Use this method to edit captions of messages. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. Unique identifier for the target chat or username of the target channel + (in the format ``@channelusername``) message_id (:obj:`int`, optional): Required if inline_message_id is not specified. - Identifier of the sent message. + Identifier of the message to edit. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. - caption (:obj:`str`, optional): New caption of the message. + caption (:obj:`str`, optional): New caption of the message, 0-1024 characters after + entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A - JSON-serialized object for an inline keyboard, custom reply keyboard, instructions - to remove reply keyboard or to force a reply from the user. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise ``True`` is returned. + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ if inline_message_id is None and (chat_id is None or message_id is None): raise ValueError( 'edit_message_caption: Both chat_id and message_id are required when ' - 'inline_message_id is not specified') + 'inline_message_id is not specified' + ) - url = '{0}/editMessageCaption'.format(self.base_url) - - data = {} + data: JSONDict = {'parse_mode': parse_mode} if caption: data['caption'] = caption - if parse_mode: - data['parse_mode'] = parse_mode + if caption_entities: + data['caption_entities'] = [me.to_dict() for me in caption_entities] if chat_id: data['chat_id'] = chat_id if message_id: @@ -1810,51 +2847,64 @@ def edit_message_caption(self, if inline_message_id: data['inline_message_id'] = inline_message_id - return url, data + return self._message( + 'editMessageCaption', + data, + timeout=timeout, + reply_markup=reply_markup, + api_kwargs=api_kwargs, + ) @log - @message - def edit_message_media(self, - chat_id=None, - message_id=None, - inline_message_id=None, - media=None, - reply_markup=None, - timeout=None, - **kwargs): - """Use this method to edit audio, document, photo, or video messages. If a message is a - part of a message album, then it can be edited only to a photo or a video. Otherwise, - message type can be changed arbitrarily. When inline message is edited, new file can't be - uploaded. Use previously uploaded file via its file_id or specify a URL. On success, if the - edited message was sent by the bot, the edited Message is returned, otherwise True is - returned. + def edit_message_media( + self, + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + media: 'InputMedia' = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: + """ + Use this method to edit animation, audio, document, photo, or video messages. If a message + is part of a message album, then it can be edited only to an audio for audio albums, only + to a document for document albums and to a photo or a video otherwise. When an inline + message is edited, a new file can't be uploaded. Use a previously uploaded file via its + ``file_id`` or specify a URL. Args: - chat_id (:obj:`int` | :obj:`str`, optional): Unique identifier for the target chat or - username of the target`channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. Unique identifier for the target chat or username of the target channel + (in the format ``@channelusername``). message_id (:obj:`int`, optional): Required if inline_message_id is not specified. - Identifier of the sent message. + Identifier of the message to edit. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. media (:class:`telegram.InputMedia`): An object for a new media content of the message. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A - JSON-serialized object for an inline keyboard, custom reply keyboard, instructions - to remove reply keyboard or to force a reply from the user. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. - """ + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + Raises: + :class:`telegram.error.TelegramError` + """ if inline_message_id is None and (chat_id is None or message_id is None): raise ValueError( - 'edit_message_caption: Both chat_id and message_id are required when ' - 'inline_message_id is not specified') - - url = '{0}/editMessageMedia'.format(self.base_url) + 'edit_message_media: Both chat_id and message_id are required when ' + 'inline_message_id is not specified' + ) - data = {'media': media} + data: JSONDict = {'media': media} if chat_id: data['chat_id'] = chat_id @@ -1863,52 +2913,59 @@ def edit_message_media(self, if inline_message_id: data['inline_message_id'] = inline_message_id - return url, data + return self._message( + 'editMessageMedia', + data, + timeout=timeout, + reply_markup=reply_markup, + api_kwargs=api_kwargs, + ) @log - @message - def edit_message_reply_markup(self, - chat_id=None, - message_id=None, - inline_message_id=None, - reply_markup=None, - timeout=None, - **kwargs): + def edit_message_reply_markup( + self, + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + reply_markup: Optional['InlineKeyboardMarkup'] = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: """ Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. Unique identifier for the target chat or username of the target channel + (in the format ``@channelusername``). message_id (:obj:`int`, optional): Required if inline_message_id is not specified. - Identifier of the sent message. + Identifier of the message to edit. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A - JSON-serialized object for an inline keyboard, custom reply keyboard, instructions - to remove reply keyboard or to force a reply from the user. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :class:`telegram.Message`: On success, if edited message is sent by the bot, the - editedMessage is returned, otherwise ``True`` is returned. + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ if inline_message_id is None and (chat_id is None or message_id is None): raise ValueError( 'edit_message_reply_markup: Both chat_id and message_id are required when ' - 'inline_message_id is not specified') - - url = '{0}/editMessageReplyMarkup'.format(self.base_url) + 'inline_message_id is not specified' + ) - data = {} + data: JSONDict = {} if chat_id: data['chat_id'] = chat_id @@ -1917,16 +2974,24 @@ def edit_message_reply_markup(self, if inline_message_id: data['inline_message_id'] = inline_message_id - return url, data + return self._message( + 'editMessageReplyMarkup', + data, + timeout=timeout, + reply_markup=reply_markup, + api_kwargs=api_kwargs, + ) @log - def get_updates(self, - offset=None, - limit=100, - timeout=0, - read_latency=2., - allowed_updates=None, - **kwargs): + def get_updates( + self, + offset: int = None, + limit: int = 100, + timeout: float = 0, + read_latency: float = 2.0, + allowed_updates: List[str] = None, + api_kwargs: JSONDict = None, + ) -> List[Update]: """Use this method to receive incoming updates using long polling. Args: @@ -1934,25 +2999,30 @@ def get_updates(self, greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as getUpdates is called with an - offset higher than its update_id. The negative offset can be specified to retrieve - updates starting from -offset update from the end of the updates queue. All - previous updates will forgotten. + offset higher than its :attr:`telegram.Update.update_id`. The negative offset can + be specified to retrieve updates starting from -offset update from the end of the + updates queue. All previous updates will forgotten. limit (:obj:`int`, optional): Limits the number of updates to be retrieved. Values - between 1-100 are accepted. Defaults to 100. - timeout (:obj:`int`, optional): Timeout in seconds for long polling. Defaults to 0, + between 1-100 are accepted. Defaults to ``100``. + timeout (:obj:`int`, optional): Timeout in seconds for long polling. Defaults to ``0``, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only. - allowed_updates (List[:obj:`str`]), optional): List the types of updates you want your - bot to receive. For example, specify ["message", "edited_channel_post", - "callback_query"] to only receive updates of these types. See - :class:`telegram.Update` for a complete list of available update types. - Specify an empty list to receive all updates regardless of type (default). If not - specified, the previous setting will be used. Please note that this parameter - doesn't affect updates created before the call to the get_updates, so unwanted - updates may be received for a short period of time. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. - - Notes: + read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving + the reply from server. Will be added to the ``timeout`` value and used as the read + timeout from server. Defaults to ``2``. + allowed_updates (List[:obj:`str`]), optional): A JSON-serialized list the types of + updates you want your bot to receive. For example, specify ["message", + "edited_channel_post", "callback_query"] to only receive updates of these types. + See :class:`telegram.Update` for a complete list of available update types. + Specify an empty list to receive all updates except + :attr:`telegram.Update.chat_member` (default). If not specified, the previous + setting will be used. Please note that this parameter doesn't affect updates + created before the call to the get_updates, so unwanted updates may be received for + a short period of time. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Note: 1. This method will not work if an outgoing webhook is set up. 2. In order to avoid getting duplicate updates, recalculate offset after each server response. @@ -1962,12 +3032,10 @@ def get_updates(self, List[:class:`telegram.Update`] Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getUpdates'.format(self.base_url) - - data = {'timeout': timeout} + data: JSONDict = {'timeout': timeout} if offset: data['offset'] = offset @@ -1975,39 +3043,50 @@ def get_updates(self, data['limit'] = limit if allowed_updates is not None: data['allowed_updates'] = allowed_updates - data.update(kwargs) # Ideally we'd use an aggressive read timeout for the polling. However, # * Short polling should return within 2 seconds. # * Long polling poses a different problem: the connection might have been dropped while # waiting for the server to return and there's no way of knowing the connection had been # dropped in real time. - result = self._request.post(url, data, timeout=float(read_latency) + float(timeout)) + result = cast( + List[JSONDict], + self._post( + 'getUpdates', + data, + timeout=float(read_latency) + float(timeout), + api_kwargs=api_kwargs, + ), + ) if result: self.logger.debug('Getting updates: %s', [u['update_id'] for u in result]) else: self.logger.debug('No new updates found.') - return [Update.de_json(u, self) for u in result] + return Update.de_list(result, self) # type: ignore[return-value] @log - def set_webhook(self, - url=None, - certificate=None, - timeout=None, - max_connections=40, - allowed_updates=None, - **kwargs): + def set_webhook( + self, + url: str = None, + certificate: FileInput = None, + timeout: ODVInput[float] = DEFAULT_NONE, + max_connections: int = 40, + allowed_updates: List[str] = None, + api_kwargs: JSONDict = None, + ip_address: str = None, + drop_pending_updates: bool = None, + ) -> bool: """ Use this method to specify a url and receive incoming updates via an outgoing webhook. - Whenever there is an update for the bot, we will send an HTTPS POST request to the + Whenever there is an update for the bot, Telegram will send an HTTPS POST request to the specified url, containing a JSON-serialized Update. In case of an unsuccessful request, - we will give up after a reasonable amount of attempts. + Telegram will give up after a reasonable amount of attempts. - If you'd like to make sure that the Webhook request comes from Telegram, we recommend - using a secret path in the URL, e.g. https://www.example.com/. Since nobody else - knows your bot's token, you can be pretty sure it's us. + If you'd like to make sure that the Webhook request comes from Telegram, Telegram + recommends using a secret path in the URL, e.g. https://www.example.com/. Since + nobody else knows your bot's token, you can be pretty sure it's us. Note: The certificate argument should be a file from disk ``open(filename, 'rb')``. @@ -2018,249 +3097,301 @@ def set_webhook(self, certificate (:obj:`filelike`): Upload your public key certificate so that the root certificate in use can be checked. See our self-signed guide for details. (https://goo.gl/rw7w6Y) + ip_address (:obj:`str`, optional): The fixed IP address which will be used to send + webhook requests instead of the IP address resolved through DNS. max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS - connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower - values to limit the load on your bot's server, and higher values to increase your - bot's throughput. - allowed_updates (List[:obj:`str`], optional): List the types of updates you want your - bot to receive. For example, specify ["message", "edited_channel_post", - "callback_query"] to only receive updates of these types. See - :class:`telegram.Update` for a complete list of available update types. Specify an - empty list to receive all updates regardless of type (default). If not specified, - the previous setting will be used. Please note that this parameter doesn't affect - updates created before the call to the set_webhook, so unwanted updates may be - received for a short period of time. + connections to the webhook for update delivery, 1-100. Defaults to ``40``. Use + lower values to limit the load on your bot's server, and higher values to increase + your bot's throughput. + allowed_updates (List[:obj:`str`], optional): A JSON-serialized list the types of + updates you want your bot to receive. For example, specify ["message", + "edited_channel_post", "callback_query"] to only receive updates of these types. + See :class:`telegram.Update` for a complete list of available update types. + Specify an empty list to receive all updates except + :attr:`telegram.Update.chat_member` (default). If not specified, the previous + setting will be used. Please note that this parameter doesn't affect updates + created before the call to the set_webhook, so unwanted updates may be received for + a short period of time. + drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending + updates. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Note: - 1. You will not be able to receive updates using get_updates for as long as an outgoing - webhook is set up. + 1. You will not be able to receive updates using :meth:`get_updates` for long as an + outgoing webhook is set up. 2. To use a self-signed certificate, you need to upload your public key certificate using certificate parameter. Please upload as InputFile, sending a String will not work. - 3. Ports currently supported for Webhooks: 443, 80, 88, 8443. + 3. Ports currently supported for Webhooks: ``443``, ``80``, ``88``, ``8443``. + + If you're having any trouble setting up webhooks, please check out this `guide to + Webhooks`_. Returns: - :obj:`bool` On success, ``True`` is returned. + :obj:`bool` On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` - - """ - url_ = '{0}/setWebhook'.format(self.base_url) - - # Backwards-compatibility: 'url' used to be named 'webhook_url' - if 'webhook_url' in kwargs: # pragma: no cover - warnings.warn("The 'webhook_url' parameter has been renamed to 'url' in accordance " - "with the API") + :class:`telegram.error.TelegramError` - if url is not None: - raise ValueError("The parameters 'url' and 'webhook_url' are mutually exclusive") + .. _`guide to Webhooks`: https://core.telegram.org/bots/webhooks - url = kwargs['webhook_url'] - del kwargs['webhook_url'] - - data = {} + """ + data: JSONDict = {} if url is not None: data['url'] = url if certificate: - if InputFile.is_file(certificate): - certificate = InputFile(certificate) - data['certificate'] = certificate + data['certificate'] = parse_file_input(certificate) if max_connections is not None: data['max_connections'] = max_connections if allowed_updates is not None: data['allowed_updates'] = allowed_updates - data.update(kwargs) + if ip_address: + data['ip_address'] = ip_address + if drop_pending_updates: + data['drop_pending_updates'] = drop_pending_updates - result = self._request.post(url_, data, timeout=timeout) + result = self._post('setWebhook', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def delete_webhook(self, timeout=None, **kwargs): + def delete_webhook( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + drop_pending_updates: bool = None, + ) -> bool: """ Use this method to remove webhook integration if you decide to switch back to - getUpdates. Requires no parameters. + :meth:`get_updates()`. Args: + drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending + updates. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool` On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/deleteWebhook'.format(self.base_url) + data = {} - data = kwargs + if drop_pending_updates: + data['drop_pending_updates'] = drop_pending_updates - result = self._request.post(url, data, timeout=timeout) + result = self._post('deleteWebhook', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def leave_chat(self, chat_id, timeout=None, **kwargs): + def leave_chat( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Use this method for your bot to leave a group, supergroup or channel. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool` On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/leaveChat'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id} - data = {'chat_id': chat_id} - data.update(kwargs) + result = self._post('leaveChat', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) - - return result + return result # type: ignore[return-value] @log - def get_chat(self, chat_id, timeout=None, **kwargs): + def get_chat( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Chat: """ Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Chat` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getChat'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id} - data = {'chat_id': chat_id} - data.update(kwargs) + result = self._post('getChat', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) - - return Chat.de_json(result, self) + return Chat.de_json(result, self) # type: ignore[return-value, arg-type] @log - def get_chat_administrators(self, chat_id, timeout=None, **kwargs): + def get_chat_administrators( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> List[ChatMember]: """ - Use this method to get a list of administrators in a chat. On success, returns an Array of - ChatMember objects that contains information about all chat administrators except other - bots. If the chat is a group or a supergroup and no administrators were appointed, - only the creator will be returned. + Use this method to get a list of administrators in a chat. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - List[:class:`telegram.ChatMember`] + List[:class:`telegram.ChatMember`]: On success, returns a list of ``ChatMember`` + objects that contains information about all chat administrators except + other bots. If the chat is a group or a supergroup and no administrators were + appointed, only the creator will be returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getChatAdministrators'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id} + + result = self._post('getChatAdministrators', data, timeout=timeout, api_kwargs=api_kwargs) - data = {'chat_id': chat_id} - data.update(kwargs) + return ChatMember.de_list(result, self) # type: ignore - result = self._request.post(url, data, timeout=timeout) + @log + def get_chat_members_count( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> int: + """ + Deprecated, use :func:`~telegram.Bot.get_chat_member_count` instead. - return [ChatMember.de_json(x, self) for x in result] + .. deprecated:: 13.7 + """ + warnings.warn( + '`bot.get_chat_members_count` is deprecated. ' + 'Use `bot.get_chat_member_count` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + return self.get_chat_member_count(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs) @log - def get_chat_members_count(self, chat_id, timeout=None, **kwargs): - """Use this method to get the number of members in a chat + def get_chat_member_count( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> int: + """Use this method to get the number of members in a chat. + + .. versionadded:: 13.7 Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - int: Number of members in the chat. + :obj:`int`: Number of members in the chat. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getChatMembersCount'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id} - data = {'chat_id': chat_id} - data.update(kwargs) + result = self._post('getChatMemberCount', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) - - return result + return result # type: ignore[return-value] @log - def get_chat_member(self, chat_id, user_id, timeout=None, **kwargs): + def get_chat_member( + self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> ChatMember: """Use this method to get information about a member of a chat. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + of the target supergroup or channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.ChatMember` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getChatMember'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} - data = {'chat_id': chat_id, 'user_id': user_id} - data.update(kwargs) + result = self._post('getChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) - - return ChatMember.de_json(result, self) + return ChatMember.de_json(result, self) # type: ignore[return-value, arg-type] @log - def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, **kwargs): + def set_chat_sticker_set( + self, + chat_id: Union[str, int], + sticker_set_name: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Use this method to set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned - in :attr:`get_chat` requests to check if the bot can use this method. + in :meth:`get_chat` requests to check if the bot can use this method. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username @@ -2270,27 +3401,29 @@ def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, **kwargs timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. - + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: True on success. + :obj:`bool`: On success, :obj:`True` is returned. """ + data: JSONDict = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name} - url = '{0}/setChatStickerSet'.format(self.base_url) - - data = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name} - - result = self._request.post(url, data, timeout=timeout) + result = self._post('setChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def delete_chat_sticker_set(self, chat_id, timeout=None, **kwargs): + def delete_chat_sticker_set( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned in - :attr:`get_chat` requests to check if the bot can use this method. + :meth:`get_chat` requests to check if the bot can use this method. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username @@ -2298,70 +3431,66 @@ def delete_chat_sticker_set(self, chat_id, timeout=None, **kwargs): timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: True on success. + :obj:`bool`: On success, :obj:`True` is returned. """ + data: JSONDict = {'chat_id': chat_id} - url = '{0}/deleteChatStickerSet'.format(self.base_url) - - data = {'chat_id': chat_id} + result = self._post('deleteChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) - - return result + return result # type: ignore[return-value] - def get_webhook_info(self, timeout=None, **kwargs): + def get_webhook_info( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> WebhookInfo: """Use this method to get current webhook status. Requires no parameters. - If the bot is using getUpdates, will return an object with the url field empty. + If the bot is using :meth:`get_updates`, will return an object with the + :attr:`telegram.WebhookInfo.url` field empty. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.WebhookInfo` """ - url = '{0}/getWebhookInfo'.format(self.base_url) + result = self._post('getWebhookInfo', None, timeout=timeout, api_kwargs=api_kwargs) - data = kwargs - - result = self._request.post(url, data, timeout=timeout) - - return WebhookInfo.de_json(result, self) + return WebhookInfo.de_json(result, self) # type: ignore[return-value, arg-type] @log - @message - def set_game_score(self, - user_id, - score, - chat_id=None, - message_id=None, - inline_message_id=None, - force=None, - disable_edit_message=None, - timeout=None, - **kwargs): + def set_game_score( + self, + user_id: Union[int, str], + score: int, + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + force: bool = None, + disable_edit_message: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: """ - Use this method to set the score of the specified user in a game. On success, if the - message was sent by the bot, returns the edited Message, otherwise returns True. Returns - an error, if the new score is not greater than the user's current score in the chat and - force is False. + Use this method to set the score of the specified user in a game. Args: user_id (:obj:`int`): User identifier. score (:obj:`int`): New score, must be non-negative. - force (:obj:`bool`, optional): Pass True, if the high score is allowed to decrease. - This can be useful when fixing mistakes or banning cheaters - disable_edit_message (:obj:`bool`, optional): Pass True, if the game message should not - be automatically edited to include the current scoreboard. - chat_id (int|str, optional): Required if inline_message_id is not specified. Unique - identifier for the target chat. + force (:obj:`bool`, optional): Pass :obj:`True`, if the high score is allowed to + decrease. This can be useful when fixing mistakes or banning cheaters. + disable_edit_message (:obj:`bool`, optional): Pass :obj:`True`, if the game message + should not be automatically edited to include the current scoreboard. + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. Unique identifier for the target chat. message_id (:obj:`int`, optional): Required if inline_message_id is not specified. Identifier of the sent message. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not @@ -2369,20 +3498,19 @@ def set_game_score(self, timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: The edited message, or if the message wasn't sent by the bot - , ``True``. + , :obj:`True`. Raises: - :class:`telegram.TelegramError`: If the new score is not greater than the user's - current score in the chat and force is False. + :class:`telegram.error.TelegramError`: If the new score is not greater than the user's + current score in the chat and force is :obj:`False`. """ - url = '{0}/setGameScore'.format(self.base_url) - - data = {'user_id': user_id, 'score': score} + data: JSONDict = {'user_id': user_id, 'score': score} if chat_id: data['chat_id'] = chat_id @@ -2395,22 +3523,34 @@ def set_game_score(self, if disable_edit_message is not None: data['disable_edit_message'] = disable_edit_message - return url, data + return self._message( + 'setGameScore', + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) @log - def get_game_high_scores(self, - user_id, - chat_id=None, - message_id=None, - inline_message_id=None, - timeout=None, - **kwargs): + def get_game_high_scores( + self, + user_id: Union[int, str], + chat_id: Union[str, int] = None, + message_id: int = None, + inline_message_id: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> List[GameHighScore]: """ Use this method to get data for high score tables. Will return the score of the specified - user and several of his neighbors in a game + user and several of their neighbors in a game. + + Note: + This method will currently return scores for the target user, plus two of their + closest neighbors on each side. Will also return the top three users if the user and + his neighbors are not among them. Please note that this behavior is subject to change. Args: - user_id (:obj:`int`): User identifier. + user_id (:obj:`int`): Target user id. chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat. message_id (:obj:`int`, optional): Required if inline_message_id is not specified. @@ -2420,18 +3560,17 @@ def get_game_high_scores(self, timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: List[:class:`telegram.GameHighScore`] Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/getGameHighScores'.format(self.base_url) - - data = {'user_id': user_id} + data: JSONDict = {'user_id': user_id} if chat_id: data['chat_id'] = chat_id @@ -2439,55 +3578,92 @@ def get_game_high_scores(self, data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id - data.update(kwargs) - - result = self._request.post(url, data, timeout=timeout) - - return [GameHighScore.de_json(hs, self) for hs in result] - - @log - @message - def send_invoice(self, - chat_id, - title, - description, - payload, - provider_token, - start_parameter, - currency, - prices, - photo_url=None, - photo_size=None, - photo_width=None, - photo_height=None, - need_name=None, - need_phone_number=None, - need_email=None, - need_shipping_address=None, - is_flexible=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - provider_data=None, - send_phone_number_to_provider=None, - send_email_to_provider=None, - timeout=None, - **kwargs): + + result = self._post('getGameHighScores', data, timeout=timeout, api_kwargs=api_kwargs) + + return GameHighScore.de_list(result, self) # type: ignore + + @log + def send_invoice( + self, + chat_id: Union[int, str], + title: str, + description: str, + payload: str, + provider_token: str, + currency: str, + prices: List['LabeledPrice'], + start_parameter: str = None, + photo_url: str = None, + photo_size: int = None, + photo_width: int = None, + photo_height: int = None, + need_name: bool = None, + need_phone_number: bool = None, + need_email: bool = None, + need_shipping_address: bool = None, + is_flexible: bool = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: InlineKeyboardMarkup = None, + provider_data: Union[str, object] = None, + send_phone_number_to_provider: bool = None, + send_email_to_provider: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + max_tip_amount: int = None, + suggested_tip_amounts: List[int] = None, + protect_content: bool = None, + ) -> Message: """Use this method to send invoices. + Warning: + As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order + of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. + + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter :attr:`start_parameter` is optional. + Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. - title (:obj:`str`): Product name. - description (:obj:`str`): Product description. + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + title (:obj:`str`): Product name, 1-32 characters. + description (:obj:`str`): Product description, 1-255 characters. payload (:obj:`str`): Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. - provider_token (:obj:`str`): Payments provider token, obtained via Botfather. - start_parameter (:obj:`str`): Unique deep-linking parameter that can be used to - generate this invoice when used as a start parameter. + provider_token (:obj:`str`): Payments provider token, obtained via + `@BotFather `_. currency (:obj:`str`): Three-letter ISO 4217 currency code. - prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a list of components - (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.). - provider_data (:obj:`str` | :obj:`object`, optional): JSON-encoded data about the + prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a JSON-serialized list + of components (e.g. product price, tax, discount, delivery cost, delivery tax, + bonus, etc.). + max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the + smallest units of the currency (integer, not float/double). For example, for a + maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the exp parameter in + `currencies.json `_, it + shows the number of digits past the decimal point for each currency (2 for the + majority of currencies). Defaults to ``0``. + + .. versionadded:: 13.5 + suggested_tip_amounts (List[:obj:`int`], optional): A JSON-serialized array of + suggested amounts of tips in the smallest units of the currency (integer, not + float/double). At most 4 suggested tip amounts can be specified. The suggested tip + amounts must be positive, passed in a strictly increased order and must not exceed + ``max_tip_amount``. + + .. versionadded:: 13.5 + start_parameter (:obj:`str`, optional): Unique deep-linking parameter. If left empty, + *forwarded copies* of the sent message will have a *Pay* button, allowing + multiple users to pay directly from the forwarded message, using the same invoice. + If non-empty, forwarded copies of the sent message will have a *URL* button with a + deep link to the bot (instead of a *Pay* button), with the value used as the + start parameter. + + .. versionchanged:: 13.5 + As of Bot API 5.2, this parameter is optional. + provider_data (:obj:`str` | :obj:`object`, optional): JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider. When an object is passed, it will be encoded as JSON. @@ -2497,53 +3673,64 @@ def send_invoice(self, photo_size (:obj:`str`, optional): Photo size. photo_width (:obj:`int`, optional): Photo width. photo_height (:obj:`int`, optional): Photo height. - need_name (:obj:`bool`, optional): Pass True, if you require the user's full name to - complete the order. - need_phone_number (:obj:`bool`, optional): Pass True, if you require the user's + need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full + name to complete the order. + need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's phone number to complete the order. - need_email (:obj:`bool`, optional): Pass True, if you require the user's email to - complete the order. - need_shipping_address (:obj:`bool`, optional): Pass True, if you require the user's - shipping address to complete the order. - send_phone_number_to_provider (:obj:`bool`, optional): Pass True, if user's phone - number should be sent to provider. - send_email_to_provider (:obj:`bool`, optional): Pass True, if user's email address - should be sent to provider. - is_flexible (:obj:`bool`, optional): Pass True, if the final price depends on the - shipping method. + need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email + to complete the order. + need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the + user's shipping address to complete the order. + send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's + phone number should be sent to provider. + send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email + address should be sent to provider. + is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on + the shipping method. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. - reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. - An inlinekeyboard. If empty, one 'Pay total price' button will be shown. - If not empty, the first button must be a Pay button. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for an inline keyboard. If empty, one 'Pay total price' button will be + shown. If not empty, the first button must be a Pay button. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/sendInvoice'.format(self.base_url) - - data = { + data: JSONDict = { 'chat_id': chat_id, 'title': title, 'description': description, 'payload': payload, 'provider_token': provider_token, - 'start_parameter': start_parameter, 'currency': currency, - 'prices': [p.to_dict() for p in prices] + 'prices': [p.to_dict() for p in prices], } + if max_tip_amount is not None: + data['max_tip_amount'] = max_tip_amount + if suggested_tip_amounts is not None: + data['suggested_tip_amounts'] = suggested_tip_amounts + if start_parameter is not None: + data['start_parameter'] = start_parameter if provider_data is not None: - if isinstance(provider_data, string_types): + if isinstance(provider_data, str): data['provider_data'] = provider_data else: data['provider_data'] = json.dumps(provider_data) @@ -2566,46 +3753,60 @@ def send_invoice(self, if is_flexible is not None: data['is_flexible'] = is_flexible if send_phone_number_to_provider is not None: - data['send_phone_number_to_provider'] = send_email_to_provider + data['send_phone_number_to_provider'] = send_phone_number_to_provider if send_email_to_provider is not None: data['send_email_to_provider'] = send_email_to_provider - return url, data + return self._message( # type: ignore[return-value] + 'sendInvoice', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) @log - def answer_shipping_query(self, - shipping_query_id, - ok, - shipping_options=None, - error_message=None, - timeout=None, - **kwargs): + def answer_shipping_query( # pylint: disable=C0103 + self, + shipping_query_id: str, + ok: bool, + shipping_options: List[ShippingOption] = None, + error_message: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ - If you sent an invoice requesting a shipping address and the parameter is_flexible was - specified, the Bot API will send an Update with a shipping_query field to the bot. Use - this method to reply to shipping queries. + If you sent an invoice requesting a shipping address and the parameter ``is_flexible`` was + specified, the Bot API will send an :class:`telegram.Update` with a + :attr:`Update.shipping_query` field to the bot. Use this method to reply to shipping + queries. Args: shipping_query_id (:obj:`str`): Unique identifier for the query to be answered. - ok (:obj:`bool`): Specify True if delivery to the specified address is possible and - False if there are any problems (for example, if delivery to the specified address - is not possible). + ok (:obj:`bool`): Specify :obj:`True` if delivery to the specified address is possible + and :obj:`False` if there are any problems (for example, if delivery to the + specified address is not possible). shipping_options (List[:class:`telegram.ShippingOption`]), optional]: Required if ok is - True. A JSON-serialized array of available shipping options. - error_message (:obj:`str`, optional): Required if ok is False. Error message in + :obj:`True`. A JSON-serialized array of available shipping options. + error_message (:obj:`str`, optional): Required if ok is :obj:`False`. Error message in human readable form that explains why it is impossible to complete the order (e.g. "Sorry, delivery to your desired address is unavailable"). Telegram will display this message to the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`; On success, True is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ ok = bool(ok) @@ -2613,34 +3814,43 @@ def answer_shipping_query(self, if ok and (shipping_options is None or error_message is not None): raise TelegramError( 'answerShippingQuery: If ok is True, shipping_options ' - 'should not be empty and there should not be error_message') + 'should not be empty and there should not be error_message' + ) if not ok and (shipping_options is not None or error_message is None): raise TelegramError( 'answerShippingQuery: If ok is False, error_message ' - 'should not be empty and there should not be shipping_options') + 'should not be empty and there should not be shipping_options' + ) - url_ = '{0}/answerShippingQuery'.format(self.base_url) - - data = {'shipping_query_id': shipping_query_id, 'ok': ok} + data: JSONDict = {'shipping_query_id': shipping_query_id, 'ok': ok} if ok: + if not shipping_options: + # not using an assert statement directly here since they are removed in + # the optimized bytecode + raise AssertionError data['shipping_options'] = [option.to_dict() for option in shipping_options] if error_message is not None: data['error_message'] = error_message - data.update(kwargs) - result = self._request.post(url_, data, timeout=timeout) + result = self._post('answerShippingQuery', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def answer_pre_checkout_query(self, pre_checkout_query_id, ok, - error_message=None, timeout=None, **kwargs): + def answer_pre_checkout_query( # pylint: disable=C0103 + self, + pre_checkout_query_id: str, + ok: bool, + error_message: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ Once the user has confirmed their payment and shipping details, the Bot API sends the final - confirmation in the form of an Update with the field pre_checkout_query. Use this method to - respond to such pre-checkout queries. + confirmation in the form of an :class:`telegram.Update` with the field + :attr:`Update.pre_checkout_query`. Use this method to respond to such pre-checkout queries. Note: The Bot API must receive an answer within 10 seconds after the pre-checkout @@ -2648,53 +3858,64 @@ def answer_pre_checkout_query(self, pre_checkout_query_id, ok, Args: pre_checkout_query_id (:obj:`str`): Unique identifier for the query to be answered. - ok (:obj:`bool`): Specify True if everything is alright (goods are available, etc.) and - the bot is ready to proceed with the order. Use False if there are any problems. - error_message (:obj:`str`, optional): Required if ok is False. Error message in human - readable form that explains the reason for failure to proceed with the checkout - (e.g. "Sorry, somebody just bought the last of our amazing black T-shirts while you - were busy filling out your payment details. Please choose a different color or - garment!"). Telegram will display this message to the user. + ok (:obj:`bool`): Specify :obj:`True` if everything is alright + (goods are available, etc.) and the bot is ready to proceed with the order. Use + :obj:`False` if there are any problems. + error_message (:obj:`str`, optional): Required if ok is :obj:`False`. Error message + in human readable form that explains the reason for failure to proceed with + the checkout (e.g. "Sorry, somebody just bought the last of our amazing black + T-shirts while you were busy filling out your payment details. Please choose a + different color or garment!"). Telegram will display this message to the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ ok = bool(ok) - if not (ok ^ (error_message is not None)): + if not (ok ^ (error_message is not None)): # pylint: disable=C0325 raise TelegramError( 'answerPreCheckoutQuery: If ok is True, there should ' 'not be error_message; if ok is False, error_message ' - 'should not be empty') + 'should not be empty' + ) - url_ = '{0}/answerPreCheckoutQuery'.format(self.base_url) - - data = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok} + data: JSONDict = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok} if error_message is not None: data['error_message'] = error_message - data.update(kwargs) - result = self._request.post(url_, data, timeout=timeout) + result = self._post('answerPreCheckoutQuery', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messages=None, - can_send_media_messages=None, can_send_other_messages=None, - can_add_web_page_previews=None, timeout=None, **kwargs): + def restrict_chat_member( + self, + chat_id: Union[str, int], + user_id: Union[str, int], + permissions: ChatPermissions, + until_date: Union[int, datetime] = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ Use this method to restrict a user in a supergroup. The bot must be an administrator in - the supergroup for this to work and must have the appropriate admin rights. Pass True for - all boolean parameters to lift restrictions from a user. + the supergroup for this to work and must have the appropriate admin rights. Pass + :obj:`True` for all boolean parameters to lift restrictions from a user. + + Note: + Since Bot API 4.4, :meth:`restrict_chat_member` takes the new user permissions in a + single argument of type :class:`telegram.ChatPermissions`. The old way of passing + parameters will not keep working forever. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username @@ -2704,98 +3925,116 @@ def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messa will be lifted for the user, unix time. If user is restricted for more than 366 days or less than 30 seconds from the current time, they are considered to be restricted forever. - can_send_messages (:obj:`bool`, optional): Pass True, if the user can send text - messages, contacts, locations and venues. - can_send_media_messages (:obj:`bool`, optional): Pass True, if the user can send - audios, documents, photos, videos, video notes and voice notes, implies - can_send_messages. - can_send_other_messages (:obj:`bool`, optional): Pass True, if the user can send - animations, games, stickers and use inline bots, implies can_send_media_messages. - can_add_web_page_previews (:obj:`bool`, optional): Pass True, if the user may add - web page previews to their messages, implies can_send_media_messages. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + permissions (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user + permissions. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: Returns True on success. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` - + :class:`telegram.error.TelegramError` """ - url = '{0}/restrictChatMember'.format(self.base_url) - - data = {'chat_id': chat_id, 'user_id': user_id} + data: JSONDict = { + 'chat_id': chat_id, + 'user_id': user_id, + 'permissions': permissions.to_dict(), + } if until_date is not None: if isinstance(until_date, datetime): - until_date = to_timestamp(until_date) + until_date = to_timestamp( + until_date, tzinfo=self.defaults.tzinfo if self.defaults else None + ) data['until_date'] = until_date - if can_send_messages is not None: - data['can_send_messages'] = can_send_messages - if can_send_media_messages is not None: - data['can_send_media_messages'] = can_send_media_messages - if can_send_other_messages is not None: - data['can_send_other_messages'] = can_send_other_messages - if can_add_web_page_previews is not None: - data['can_add_web_page_previews'] = can_add_web_page_previews - data.update(kwargs) - - result = self._request.post(url, data, timeout=timeout) - return result + result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] @log - def promote_chat_member(self, chat_id, user_id, can_change_info=None, - can_post_messages=None, can_edit_messages=None, - can_delete_messages=None, can_invite_users=None, - can_restrict_members=None, can_pin_messages=None, - can_promote_members=None, timeout=None, **kwargs): + def promote_chat_member( + self, + chat_id: Union[str, int], + user_id: Union[str, int], + can_change_info: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_invite_users: bool = None, + can_restrict_members: bool = None, + can_pin_messages: bool = None, + can_promote_members: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + is_anonymous: bool = None, + can_manage_chat: bool = None, + can_manage_voice_chats: bool = None, + ) -> bool: """ Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. - Pass False for all boolean parameters to demote a user + Pass :obj:`False` for all boolean parameters to demote a user. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target supergroup (in the format @supergroupusername). + of the target channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. - can_change_info (:obj:`bool`, optional): Pass True, if the administrator can change - chat title, photo and other settings. - can_post_messages (:obj:`bool`, optional): Pass True, if the administrator can + is_anonymous (:obj:`bool`, optional): Pass :obj:`True`, if the administrator's presence + in the chat is hidden. + can_manage_chat (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + access the chat event log, chat statistics, message statistics in channels, see + channel members, see anonymous administrators in supergroups and ignore slow mode. + Implied by any other administrator privilege. + + .. versionadded:: 13.4 + + can_manage_voice_chats (:obj:`bool`, optional): Pass :obj:`True`, if the administrator + can manage voice chats. + + .. versionadded:: 13.4 + + can_change_info (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + change chat title, photo and other settings. + can_post_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can create channel posts, channels only. - can_edit_messages (:obj:`bool`, optional): Pass True, if the administrator can edit - messages of other users, channels only. - can_delete_messages (:obj:`bool`, optional): Pass True, if the administrator can + can_edit_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + edit messages of other users and can pin messages, channels only. + can_delete_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can delete messages of other users. - can_invite_users (:obj:`bool`, optional): Pass True, if the administrator can invite - new users to the chat. - can_restrict_members (:obj:`bool`, optional): Pass True, if the administrator can - restrict, ban or unban chat members. - can_pin_messages (:obj:`bool`, optional): Pass True, if the administrator can pin - messages, supergroups only. - can_promote_members (:obj:`bool`, optional): Pass True, if the administrator can add - new administrators with a subset of his own privileges or demote administrators + can_invite_users (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + invite new users to the chat. + can_restrict_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator + can restrict, ban or unban chat members. + can_pin_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + pin messages, supergroups only. + can_promote_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: Returns True on success. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/promoteChatMember'.format(self.base_url) - - data = {'chat_id': chat_id, 'user_id': user_id} + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} + if is_anonymous is not None: + data['is_anonymous'] = is_anonymous if can_change_info is not None: data['can_change_info'] = can_change_info if can_post_messages is not None: @@ -2812,283 +4051,710 @@ def promote_chat_member(self, chat_id, user_id, can_change_info=None, data['can_pin_messages'] = can_pin_messages if can_promote_members is not None: data['can_promote_members'] = can_promote_members - data.update(kwargs) + if can_manage_chat is not None: + data['can_manage_chat'] = can_manage_chat + if can_manage_voice_chats is not None: + data['can_manage_voice_chats'] = can_manage_voice_chats - result = self._request.post(url, data, timeout=timeout) + result = self._post('promoteChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def export_chat_invite_link(self, chat_id, timeout=None, **kwargs): + def set_chat_permissions( + self, + chat_id: Union[str, int], + permissions: ChatPermissions, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ - Use this method to export an invite link to a supergroup or a channel. The bot must be an - administrator in the chat for this to work and must have the appropriate admin rights. + Use this method to set default chat permissions for all members. The bot must be an + administrator in the group or a supergroup for this to work and must have the + :attr:`telegram.ChatMember.can_restrict_members` admin rights. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of + the target supergroup (in the format `@supergroupusername`). + permissions (:class:`telegram.ChatPermissions`): New default chat permissions. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`str`: Exported invite link on success. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/exportChatInviteLink'.format(self.base_url) - - data = {'chat_id': chat_id} - data.update(kwargs) + data: JSONDict = {'chat_id': chat_id, 'permissions': permissions.to_dict()} - result = self._request.post(url, data, timeout=timeout) + result = self._post('setChatPermissions', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_chat_photo(self, chat_id, photo, timeout=None, **kwargs): - """Use this method to set a new profile photo for the chat. - - Photos can't be changed for private chats. The bot must be an administrator in the chat - for this to work and must have the appropriate admin rights. + def set_chat_administrator_custom_title( + self, + chat_id: Union[int, str], + user_id: Union[int, str], + custom_title: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to set a custom title for administrators promoted by the bot in a + supergroup. The bot must be an administrator for this to work. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). - photo (`filelike object`): New chat photo. + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of + the target supergroup (in the format `@supergroupusername`). + user_id (:obj:`int`): Unique identifier of the target administrator. + custom_title (:obj:`str`): New custom title for the administrator; 0-16 characters, + emoji are not allowed. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments - - Note: - In regular groups (non-supergroups), this method will only work if the - 'All Members Are Admins' setting is off in the target group. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: Returns True on success. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/setChatPhoto'.format(self.base_url) - - if InputFile.is_file(photo): - photo = InputFile(photo) + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id, 'custom_title': custom_title} - data = {'chat_id': chat_id, 'photo': photo} - data.update(kwargs) + result = self._post( + 'setChatAdministratorCustomTitle', data, timeout=timeout, api_kwargs=api_kwargs + ) - result = self._request.post(url, data, timeout=timeout) - - return result + return result # type: ignore[return-value] @log - def delete_chat_photo(self, chat_id, timeout=None, **kwargs): + def export_chat_invite_link( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> str: """ - Use this method to delete a chat photo. Photos can't be changed for private chats. The bot - must be an administrator in the chat for this to work and must have the appropriate admin - rights. + Use this method to generate a new primary invite link for a chat; any previously generated + link is revoked. The bot must be an administrator in the chat for this to work and must + have the appropriate admin rights. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Note: - In regular groups (non-supergroups), this method will only work if the - 'All Members Are Admins' setting is off in the target group. + Each administrator in a chat generates their own invite links. Bots can't use invite + links generated by other administrators. If you want your bot to work with invite + links, it will need to generate its own link using :meth:`export_chat_invite_link` or + by calling the :meth:`get_chat` method. If your bot needs to generate a new primary + invite link replacing its previous one, use :attr:`export_chat_invite_link` again. Returns: - :obj:`bool`: Returns ``True`` on success. + :obj:`str`: New invite link on success. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/deleteChatPhoto'.format(self.base_url) - - data = {'chat_id': chat_id} - data.update(kwargs) + data: JSONDict = {'chat_id': chat_id} - result = self._request.post(url, data, timeout=timeout) + result = self._post('exportChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_chat_title(self, chat_id, title, timeout=None, **kwargs): + def create_chat_invite_link( + self, + chat_id: Union[str, int], + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, + ) -> ChatInviteLink: """ - Use this method to change the title of a chat. Titles can't be changed for private chats. - The bot must be an administrator in the chat for this to work and must have the appropriate - admin rights. + Use this method to create an additional invite link for a chat. The bot must be an + administrator in the chat for this to work and must have the appropriate admin rights. + The link can be revoked using the method :meth:`revoke_chat_invite_link`. + + .. versionadded:: 13.4 Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). - title (:obj:`str`): New chat title, 1-255 characters. + of the target channel (in the format ``@channelusername``). + expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will + expire. Integer input will be interpreted as Unix timestamp. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + member_limit (:obj:`int`, optional): Maximum number of users that can be members of + the chat simultaneously after joining the chat via this invite link; 1-99999. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + name (:obj:`str`, optional): Invite link name; 0-32 characters. - Note: - In regular groups (non-supergroups), this method will only work if the - 'All Members Are Admins' setting is off in the target group. + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat + via the link need to be approved by chat administrators. + If :obj:`True`, ``member_limit`` can't be specified. + + .. versionadded:: 13.8 Returns: - :obj:`bool`: Returns ``True`` on success. + :class:`telegram.ChatInviteLink` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/setChatTitle'.format(self.base_url) + if creates_join_request and member_limit: + raise ValueError( + "If `creates_join_request` is `True`, `member_limit` can't be specified." + ) - data = {'chat_id': chat_id, 'title': title} - data.update(kwargs) + data: JSONDict = { + 'chat_id': chat_id, + } - result = self._request.post(url, data, timeout=timeout) + if expire_date is not None: + if isinstance(expire_date, datetime): + expire_date = to_timestamp( + expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None + ) + data['expire_date'] = expire_date - return result + if member_limit is not None: + data['member_limit'] = member_limit + + if name is not None: + data['name'] = name + + if creates_join_request is not None: + data['creates_join_request'] = creates_join_request + + result = self._post('createChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] @log - def set_chat_description(self, chat_id, description, timeout=None, **kwargs): + def edit_chat_invite_link( + self, + chat_id: Union[str, int], + invite_link: str, + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, + ) -> ChatInviteLink: """ - Use this method to change the description of a supergroup or a channel. The bot must be an + Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Note: + Though not stated explicitly in the official docs, Telegram changes not only the + optional parameters that are explicitly passed, but also replaces all other optional + parameters to the default values. However, since not documented, this behaviour may + change unbeknown to PTB. + + .. versionadded:: 13.4 + Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). - description (:obj:`str`): New chat description, 1-255 characters. + of the target channel (in the format ``@channelusername``). + invite_link (:obj:`str`): The invite link to edit. + expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will + expire. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + member_limit (:obj:`int`, optional): Maximum number of users that can be members of + the chat simultaneously after joining the chat via this invite link; 1-99999. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + name (:obj:`str`, optional): Invite link name; 0-32 characters. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat + via the link need to be approved by chat administrators. + If :obj:`True`, ``member_limit`` can't be specified. + + .. versionadded:: 13.8 Returns: - :obj:`bool`: Returns ``True`` on success. + :class:`telegram.ChatInviteLink` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/setChatDescription'.format(self.base_url) + if creates_join_request and member_limit: + raise ValueError( + "If `creates_join_request` is `True`, `member_limit` can't be specified." + ) - data = {'chat_id': chat_id, 'description': description} - data.update(kwargs) + data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} - result = self._request.post(url, data, timeout=timeout) + if expire_date is not None: + if isinstance(expire_date, datetime): + expire_date = to_timestamp( + expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None + ) + data['expire_date'] = expire_date - return result + if member_limit is not None: + data['member_limit'] = member_limit + + if name is not None: + data['name'] = name + + if creates_join_request is not None: + data['creates_join_request'] = creates_join_request + + result = self._post('editChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] @log - def pin_chat_message(self, chat_id, message_id, disable_notification=None, timeout=None, - **kwargs): + def revoke_chat_invite_link( + self, + chat_id: Union[str, int], + invite_link: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> ChatInviteLink: """ - Use this method to pin a message in a supergroup. The bot must be an administrator in the + Use this method to revoke an invite link created by the bot. If the primary link is + revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + .. versionadded:: 13.4 + Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). - message_id (:obj:`int`): Identifier of a message to pin. - disable_notification (:obj:`bool`, optional): Pass True, if it is not necessary to send - a notification to all group members about the new pinned message. + of the target channel (in the format ``@channelusername``). + invite_link (:obj:`str`): The invite link to edit. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: Returns ``True`` on success. + :class:`telegram.ChatInviteLink` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/pinChatMessage'.format(self.base_url) + data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} - data = {'chat_id': chat_id, 'message_id': message_id} + result = self._post('revokeChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) - if disable_notification is not None: - data['disable_notification'] = disable_notification - data.update(kwargs) + return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] - result = self._request.post(url, data, timeout=timeout) + @log + def approve_chat_join_request( + self, + chat_id: Union[str, int], + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to approve a chat join request. - return result + The bot must be an administrator in the chat for this to work and must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. - @log - def unpin_chat_message(self, chat_id, timeout=None, **kwargs): - """ - Use this method to unpin a message in a supergroup. The bot must be an administrator in the - chat for this to work and must have the appropriate admin rights. + .. versionadded:: 13.8 Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target`channel (in the format @channelusername). + of the target channel (in the format ``@channelusername``). + user_id (:obj:`int`): Unique identifier of the target user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: Returns ``True`` on success. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` - + :class:`telegram.error.TelegramError` """ - url = '{0}/unpinChatMessage'.format(self.base_url) - - data = {'chat_id': chat_id} - data.update(kwargs) + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} - result = self._request.post(url, data, timeout=timeout) + result = self._post('approveChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def get_sticker_set(self, name, timeout=None, **kwargs): - """Use this method to get a sticker set. + def decline_chat_join_request( + self, + chat_id: Union[str, int], + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to decline a chat join request. - Args: - name (:obj:`str`): Short name of the sticker set that is used in t.me/addstickers/ - URLs (e.g., animals) - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during - creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + The bot must be an administrator in the chat for this to work and must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. - Returns: - :class:`telegram.StickerSet` + .. versionadded:: 13.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + user_id (:obj:`int`): Unique identifier of the target user. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} + + result = self._post('declineChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + + @log + def set_chat_photo( + self, + chat_id: Union[str, int], + photo: FileInput, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to set a new profile photo for the chat. + + Photos can't be changed for private chats. The bot must be an administrator in the chat + for this to work and must have the appropriate admin rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + photo (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`): New chat photo. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'photo': parse_file_input(photo)} + + result = self._post('setChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + + @log + def delete_chat_photo( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to delete a chat photo. Photos can't be changed for private chats. The bot + must be an administrator in the chat for this to work and must have the appropriate admin + rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id} + + result = self._post('deleteChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + + @log + def set_chat_title( + self, + chat_id: Union[str, int], + title: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to change the title of a chat. Titles can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate + admin rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + title (:obj:`str`): New chat title, 1-255 characters. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'title': title} + + result = self._post('setChatTitle', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + + @log + def set_chat_description( + self, + chat_id: Union[str, int], + description: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to change the description of a group, a supergroup or a channel. The bot + must be an administrator in the chat for this to work and must have the appropriate admin + rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + description (:obj:`str`): New chat description, 0-255 characters. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'description': description} + + result = self._post('setChatDescription', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + + @log + def pin_chat_message( + self, + chat_id: Union[str, int], + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to add a message to the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this to work + and must have the :attr:`telegram.ChatMember.can_pin_messages` admin right in a supergroup + or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + message_id (:obj:`int`): Identifier of a message to pin. + disable_notification (:obj:`bool`, optional): Pass :obj:`True`, if it is not necessary + to send a notification to all chat members about the new pinned message. + Notifications are always disabled in channels and private chats. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + 'chat_id': chat_id, + 'message_id': message_id, + 'disable_notification': disable_notification, + } + + return self._post( # type: ignore[return-value] + 'pinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs + ) + + @log + def unpin_chat_message( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + message_id: int = None, + ) -> bool: + """ + Use this method to remove a message from the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this to work + and must have the :attr:`telegram.ChatMember.can_pin_messages` admin right in a + supergroup or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + message_id (:obj:`int`, optional): Identifier of a message to unpin. If not specified, + the most recent pinned message (by sending date) will be unpinned. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id} + + if message_id is not None: + data['message_id'] = message_id + return self._post( # type: ignore[return-value] + 'unpinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs + ) + + @log + def unpin_all_chat_messages( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ - url = '{0}/getStickerSet'.format(self.base_url) + Use this method to clear the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this + to work and must have the :attr:`telegram.ChatMember.can_pin_messages` admin right in a + supergroup or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel. - data = {'name': name} - data.update(kwargs) + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. - result = self._request.post(url, data, timeout=timeout) + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` - return StickerSet.de_json(result, self) + """ + data: JSONDict = {'chat_id': chat_id} + + return self._post( # type: ignore[return-value] + 'unpinAllChatMessages', data, timeout=timeout, api_kwargs=api_kwargs + ) @log - def upload_sticker_file(self, user_id, png_sticker, timeout=None, **kwargs): + def get_sticker_set( + self, + name: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> StickerSet: + """Use this method to get a sticker set. + + Args: + name (:obj:`str`): Name of the sticker set. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during + creation of the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.StickerSet` + + Raises: + :class:`telegram.error.TelegramError` + """ - Use this method to upload a .png file with a sticker for later use in - :attr:`create_new_sticker_set` and :attr:`add_sticker_to_set` methods (can be used multiple + data: JSONDict = {'name': name} + + result = self._post('getStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) + + return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type] + + @log + def upload_sticker_file( + self, + user_id: Union[str, int], + png_sticker: FileInput, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + ) -> File: + """ + Use this method to upload a ``.PNG`` file with a sticker for later use in + :meth:`create_new_sticker_set` and :meth:`add_sticker_to_set` methods (can be used multiple times). Note: @@ -3097,43 +4763,60 @@ def upload_sticker_file(self, user_id, png_sticker, timeout=None, **kwargs): Args: user_id (:obj:`int`): User identifier of sticker file owner. - png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, - must be up to 512 kilobytes in size, dimensions must not exceed 512px, - and either width or height must be exactly 512px. + png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`): + **PNG** image with the sticker, must be up to 512 kilobytes in size, + dimensions must not exceed 512px, and either width or height must be exactly 512px. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :class:`telegram.File`: The uploaded File + :class:`telegram.File`: On success, the uploaded File is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/uploadStickerFile'.format(self.base_url) - - if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) - - data = {'user_id': user_id, 'png_sticker': png_sticker} - data.update(kwargs) + data: JSONDict = {'user_id': user_id, 'png_sticker': parse_file_input(png_sticker)} - result = self._request.post(url, data, timeout=timeout) + result = self._post('uploadStickerFile', data, timeout=timeout, api_kwargs=api_kwargs) - return File.de_json(result, self) + return File.de_json(result, self) # type: ignore[return-value, arg-type] @log - def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis, - contains_masks=None, mask_position=None, timeout=None, **kwargs): - """Use this method to create new sticker set owned by a user. - + def create_new_sticker_set( + self, + user_id: Union[str, int], + name: str, + title: str, + emojis: str, + png_sticker: FileInput = None, + contains_masks: bool = None, + mask_position: MaskPosition = None, + timeout: DVInput[float] = DEFAULT_20, + tgs_sticker: FileInput = None, + api_kwargs: JSONDict = None, + webm_sticker: FileInput = None, + ) -> bool: + """ + Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. + You must use exactly one of the fields ``png_sticker``, ``tgs_sticker``, or + ``webm_sticker``. + + Warning: + As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the + arguments had to be changed. Use keyword arguments to make sure that the arguments are + passed correctly. Note: - The png_sticker argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` + The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from + disk ``open(filename, 'rb')`` Args: user_id (:obj:`int`): User identifier of created sticker set owner. @@ -3143,97 +4826,164 @@ def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis, must end in "_by_". is case insensitive. 1-64 characters. title (:obj:`str`): Sticker set title, 1-64 characters. - png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up - to 512 kilobytes in size, dimensions must not exceed 512px, + png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ + optional): **PNG** image with the sticker, + must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + tgs_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ + optional): **TGS** animation with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/stickers#animated-sticker-requirements for technical + requirements. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ + optional): **WEBM** video with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/stickers#video-sticker-requirements for + technical requirements. + + .. versionadded:: 13.11 + emojis (:obj:`str`): One or more emoji corresponding to the sticker. - contains_masks (:obj:`bool`, optional): Pass True, if a set of mask stickers should be - created. + contains_masks (:obj:`bool`, optional): Pass :obj:`True`, if a set of mask stickers + should be created. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/createNewStickerSet'.format(self.base_url) - - if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) - - data = {'user_id': user_id, 'name': name, 'title': title, 'png_sticker': png_sticker, - 'emojis': emojis} - + data: JSONDict = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis} + + if png_sticker is not None: + data['png_sticker'] = parse_file_input(png_sticker) + if tgs_sticker is not None: + data['tgs_sticker'] = parse_file_input(tgs_sticker) + if webm_sticker is not None: + data['webm_sticker'] = parse_file_input(webm_sticker) if contains_masks is not None: data['contains_masks'] = contains_masks if mask_position is not None: - data['mask_position'] = mask_position - data.update(kwargs) + # We need to_json() instead of to_dict() here, because we're sending a media + # message here, which isn't json dumped by utils.request + data['mask_position'] = mask_position.to_json() - result = self._request.post(url, data, timeout=timeout) + result = self._post('createNewStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def add_sticker_to_set(self, user_id, name, png_sticker, emojis, mask_position=None, - timeout=None, **kwargs): - """Use this method to add a new sticker to a set created by the bot. + def add_sticker_to_set( + self, + user_id: Union[str, int], + name: str, + emojis: str, + png_sticker: FileInput = None, + mask_position: MaskPosition = None, + timeout: DVInput[float] = DEFAULT_20, + tgs_sticker: FileInput = None, + api_kwargs: JSONDict = None, + webm_sticker: FileInput = None, + ) -> bool: + """ + Use this method to add a new sticker to a set created by the bot. + You **must** use exactly one of the fields ``png_sticker``, ``tgs_sticker`` or + ``webm_sticker``. Animated stickers can be added to animated sticker sets and only to them. + Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 + stickers. + + Warning: + As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the + arguments had to be changed. Use keyword arguments to make sure that the arguments are + passed correctly. Note: - The png_sticker argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` + The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from + disk ``open(filename, 'rb')`` Args: user_id (:obj:`int`): User identifier of created sticker set owner. + name (:obj:`str`): Sticker set name. - png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up - to 512 kilobytes in size, dimensions must not exceed 512px, + png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ + optional): **PNG** image with the sticker, + must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + tgs_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ + optional): **TGS** animation with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/stickers#animated-sticker-requirements for technical + requirements. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ + optional): **WEBM** video with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/stickers#video-sticker-requirements for + technical requirements. + + .. versionadded:: 13.11 emojis (:obj:`str`): One or more emoji corresponding to the sticker. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask - should beplaced on faces. + should be placed on faces. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/addStickerToSet'.format(self.base_url) - - if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) - - data = {'user_id': user_id, 'name': name, 'png_sticker': png_sticker, 'emojis': emojis} - + data: JSONDict = {'user_id': user_id, 'name': name, 'emojis': emojis} + + if png_sticker is not None: + data['png_sticker'] = parse_file_input(png_sticker) + if tgs_sticker is not None: + data['tgs_sticker'] = parse_file_input(tgs_sticker) + if webm_sticker is not None: + data['webm_sticker'] = parse_file_input(webm_sticker) if mask_position is not None: - data['mask_position'] = mask_position - data.update(kwargs) + # We need to_json() instead of to_dict() here, because we're sending a media + # message here, which isn't json dumped by utils.request + data['mask_position'] = mask_position.to_json() - result = self._request.post(url, data, timeout=timeout) + result = self._post('addStickerToSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_sticker_position_in_set(self, sticker, position, timeout=None, **kwargs): + def set_sticker_position_in_set( + self, + sticker: str, + position: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Use this method to move a sticker in a set created by the bot to a specific position. Args: @@ -3242,26 +4992,31 @@ def set_sticker_position_in_set(self, sticker, position, timeout=None, **kwargs) timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/setStickerPositionInSet'.format(self.base_url) + data: JSONDict = {'sticker': sticker, 'position': position} - data = {'sticker': sticker, 'position': position} - data.update(kwargs) + result = self._post( + 'setStickerPositionInSet', data, timeout=timeout, api_kwargs=api_kwargs + ) - result = self._request.post(url, data, timeout=timeout) - - return result + return result # type: ignore[return-value] @log - def delete_sticker_from_set(self, sticker, timeout=None, **kwargs): + def delete_sticker_from_set( + self, + sticker: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Use this method to delete a sticker from a set created by the bot. Args: @@ -3269,31 +5024,90 @@ def delete_sticker_from_set(self, sticker, timeout=None, **kwargs): timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url = '{0}/deleteStickerFromSet'.format(self.base_url) + data: JSONDict = {'sticker': sticker} - data = {'sticker': sticker} - data.update(kwargs) + result = self._post('deleteStickerFromSet', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url, data, timeout=timeout) + return result # type: ignore[return-value] - return result + @log + def set_sticker_set_thumb( + self, + name: str, + user_id: Union[str, int], + thumb: FileInput = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set + for animated sticker sets only. Video thumbnails can be set only for video sticker sets + only. + + Note: + The thumb can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` + + Args: + name (:obj:`str`): Sticker set name + user_id (:obj:`int`): User identifier of created sticker set owner. + thumb (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ + optional): A **PNG** image with the thumbnail, must + be up to 128 kilobytes in size and have width and height exactly 100px, or a + **TGS** animation with the thumbnail up to 32 kilobytes in size; see + https://core.telegram.org/stickers#animated-sticker-requirements for animated + sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 + kilobytes in size; see + https://core.telegram.org/stickers#video-sticker-requirements for video sticker + technical requirements. Pass a file_id as a String to send a file that + already exists on the Telegram servers, pass an HTTP URL as a String for Telegram + to get a file from the Internet, or upload a new one using multipart/form-data. + Animated sticker set thumbnails can't be uploaded via HTTP URL. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during + creation of the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'name': name, 'user_id': user_id} + + if thumb is not None: + data['thumb'] = parse_file_input(thumb) + + result = self._post('setStickerSetThumb', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] @log - def set_passport_data_errors(self, user_id, errors, timeout=None, **kwargs): + def set_passport_data_errors( + self, + user_id: Union[str, int], + errors: List[PassportElementError], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """ Informs a user that some of the Telegram Passport elements they provided contains errors. The user will not be able to re-submit their Passport to you until the errors are fixed - (the contents of the field for which you returned the error must change). Returns True - on success. + (the contents of the field for which you returned the error must change). Use this if the data submitted by the user doesn't satisfy the standards your service requires for any reason. For example, if a birthday date seems invalid, a submitted @@ -3307,158 +5121,741 @@ def set_passport_data_errors(self, user_id, errors, timeout=None, **kwargs): timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - url_ = '{0}/setPassportDataErrors'.format(self.base_url) + data: JSONDict = {'user_id': user_id, 'errors': [error.to_dict() for error in errors]} - data = {'user_id': user_id, 'errors': [error.to_dict() for error in errors]} - data.update(kwargs) + result = self._post('setPassportDataErrors', data, timeout=timeout, api_kwargs=api_kwargs) - result = self._request.post(url_, data, timeout=timeout) + return result # type: ignore[return-value] - return result + @log + def send_poll( + self, + chat_id: Union[int, str], + question: str, + options: List[str], + is_anonymous: bool = True, + type: str = Poll.REGULAR, # pylint: disable=W0622 + allows_multiple_answers: bool = False, + correct_option_id: int = None, + is_closed: bool = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + explanation: str = None, + explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, + open_period: int = None, + close_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> Message: + """ + Use this method to send a native poll. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + question (:obj:`str`): Poll question, 1-300 characters. + options (List[:obj:`str`]): List of answer options, 2-10 strings 1-100 characters each. + is_anonymous (:obj:`bool`, optional): :obj:`True`, if the poll needs to be anonymous, + defaults to :obj:`True`. + type (:obj:`str`, optional): Poll type, :attr:`telegram.Poll.QUIZ` or + :attr:`telegram.Poll.REGULAR`, defaults to :attr:`telegram.Poll.REGULAR`. + allows_multiple_answers (:obj:`bool`, optional): :obj:`True`, if the poll allows + multiple answers, ignored for polls in quiz mode, defaults to :obj:`False`. + correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer + option, required for polls in quiz mode. + explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most + 2 line feeds after entities parsing. + explanation_parse_mode (:obj:`str`, optional): Mode for parsing entities in the + explanation. See the constants in :class:`telegram.ParseMode` for the available + modes. + explanation_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in message text, which can be specified instead of + :attr:`parse_mode`. + open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active + after creation, 5-600. Can't be used together with :attr:`close_date`. + close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix + timestamp) when the poll will be automatically closed. Must be at least 5 and no + more than 600 seconds in the future. Can't be used together with + :attr:`open_period`. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be + immediately closed. This can be useful for poll preview. + disable_notification (:obj:`bool`, optional): Sends the message silently. Users will + receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the + original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A + JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove reply keyboard or to force a reply from the user. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + 'chat_id': chat_id, + 'question': question, + 'options': options, + 'explanation_parse_mode': explanation_parse_mode, + } + + if not is_anonymous: + data['is_anonymous'] = is_anonymous + if type: + data['type'] = type + if allows_multiple_answers: + data['allows_multiple_answers'] = allows_multiple_answers + if correct_option_id is not None: + data['correct_option_id'] = correct_option_id + if is_closed: + data['is_closed'] = is_closed + if explanation: + data['explanation'] = explanation + if explanation_entities: + data['explanation_entities'] = [me.to_dict() for me in explanation_entities] + if open_period: + data['open_period'] = open_period + if close_date: + if isinstance(close_date, datetime): + close_date = to_timestamp( + close_date, tzinfo=self.defaults.tzinfo if self.defaults else None + ) + data['close_date'] = close_date + + return self._message( # type: ignore[return-value] + 'sendPoll', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + @log + def stop_poll( + self, + chat_id: Union[int, str], + message_id: int, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Poll: + """ + Use this method to stop a poll which was sent by the bot. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + message_id (:obj:`int`): Identifier of the original message with the poll. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized + object for a new message inline keyboard. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.Poll`: On success, the stopped Poll with the final results is + returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'message_id': message_id} + + if reply_markup: + if isinstance(reply_markup, ReplyMarkup): + # We need to_json() instead of to_dict() here, because reply_markups may be + # attached to media messages, which aren't json dumped by utils.request + data['reply_markup'] = reply_markup.to_json() + else: + data['reply_markup'] = reply_markup + + result = self._post('stopPoll', data, timeout=timeout, api_kwargs=api_kwargs) + + return Poll.de_json(result, self) # type: ignore[return-value, arg-type] + + @log + def send_dice( + self, + chat_id: Union[int, str], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + emoji: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> Message: + """ + Use this method to send an animated emoji that will display a random value. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based. + Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, "🎳", or “🎰”. Dice can have + values 1-6 for “🎲”, “🎯” and "🎳", values 1-5 for “🏀” and “⚽”, and values 1-64 + for “🎰”. Defaults to “🎲”. + + .. versionchanged:: 13.4 + Added the "🎳" emoji. + disable_notification (:obj:`bool`, optional): Sends the message silently. Users will + receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the + original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A + JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove reply keyboard or to force a reply from the user. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id} + + if emoji: + data['emoji'] = emoji + + return self._message( # type: ignore[return-value] + 'sendDice', + data, + timeout=timeout, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + @log + def get_my_commands( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + scope: BotCommandScope = None, + language_code: str = None, + ) -> List[BotCommand]: + """ + Use this method to get the current list of the bot's commands for the given scope and user + language. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object, + describing scope of users. Defaults to :class:`telegram.BotCommandScopeDefault`. + + .. versionadded:: 13.7 + + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty + string. + + .. versionadded:: 13.7 + + Returns: + List[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty + list is returned if commands are not set. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {} + + if scope: + data['scope'] = scope.to_dict() + + if language_code: + data['language_code'] = language_code + + result = self._post('getMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) + + if (scope is None or scope.type == scope.DEFAULT) and language_code is None: + self._commands = BotCommand.de_list(result, self) # type: ignore[assignment,arg-type] + return self._commands # type: ignore[return-value] + + return BotCommand.de_list(result, self) # type: ignore[return-value,arg-type] + + @log + def set_my_commands( + self, + commands: List[Union[BotCommand, Tuple[str, str]]], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + scope: BotCommandScope = None, + language_code: str = None, + ) -> bool: + """ + Use this method to change the list of the bot's commands. See the + `Telegram docs `_ for more details about bot + commands. - def to_dict(self): - data = {'id': self.id, 'username': self.username, 'first_name': self.username} + Args: + commands (List[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A JSON-serialized list + of bot commands to be set as the list of the bot's commands. At most 100 commands + can be specified. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object, + describing scope of users for which the commands are relevant. Defaults to + :class:`telegram.BotCommandScopeDefault`. + + .. versionadded:: 13.7 + + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, for whose language + there are no dedicated commands. + + .. versionadded:: 13.7 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + cmds = [c if isinstance(c, BotCommand) else BotCommand(c[0], c[1]) for c in commands] + + data: JSONDict = {'commands': [c.to_dict() for c in cmds]} + + if scope: + data['scope'] = scope.to_dict() + + if language_code: + data['language_code'] = language_code + + result = self._post('setMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) + + # Set commands only for default scope. No need to check for outcome. + # If request failed, we won't come this far + if (scope is None or scope.type == scope.DEFAULT) and language_code is None: + self._commands = cmds + + return result # type: ignore[return-value] + + @log + def delete_my_commands( + self, + scope: BotCommandScope = None, + language_code: str = None, + api_kwargs: JSONDict = None, + timeout: ODVInput[float] = DEFAULT_NONE, + ) -> bool: + """ + Use this method to delete the list of the bot's commands for the given scope and user + language. After deletion, + `higher level commands `_ + will be shown to affected users. + + .. versionadded:: 13.7 + + Args: + scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object, + describing scope of users for which the commands are relevant. Defaults to + :class:`telegram.BotCommandScopeDefault`. + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, for whose language + there are no dedicated commands. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {} + + if scope: + data['scope'] = scope.to_dict() + + if language_code: + data['language_code'] = language_code + + result = self._post('deleteMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) + + if (scope is None or scope.type == scope.DEFAULT) and language_code is None: + self._commands = [] + + return result # type: ignore[return-value] + + @log + def log_out(self, timeout: ODVInput[float] = DEFAULT_NONE) -> bool: + """ + Use this method to log out from the cloud Bot API server before launching the bot locally. + You *must* log out the bot before running it locally, otherwise there is no guarantee that + the bot will receive updates. After a successful call, you can immediately log in on a + local server, but will not be able to log in back to the cloud Bot API server for 10 + minutes. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + + Returns: + :obj:`True`: On success + + Raises: + :class:`telegram.error.TelegramError` + + """ + return self._post('logOut', timeout=timeout) # type: ignore[return-value] + + @log + def close(self, timeout: ODVInput[float] = DEFAULT_NONE) -> bool: + """ + Use this method to close the bot instance before moving it from one local server to + another. You need to delete the webhook before calling this method to ensure that the bot + isn't launched again after server restart. The method will return error 429 in the first + 10 minutes after the bot is launched. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + + Returns: + :obj:`True`: On success + + Raises: + :class:`telegram.error.TelegramError` + + """ + return self._post('close', timeout=timeout) # type: ignore[return-value] + + @log + def copy_message( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_id: int, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> MessageId: + """ + Use this method to copy messages of any kind. Service messages and invoice messages can't + be copied. The method is analogous to the method :meth:`forward_message`, but the copied + message doesn't have a link to the original message. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format ``@channelusername``). + from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the + original message was sent (or channel username in the format ``@channelusername``). + message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id. + caption (:obj:`str`, optional): New caption for media, 0-1024 characters after + entities parsing. If not specified, the original caption is kept. + parse_mode (:obj:`str`, optional): Mode for parsing entities in the new caption. See + the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (:class:`telegram.utils.types.SLT[MessageEntity]`): List of special + entities that appear in the new caption, which can be specified instead of + parse_mode + disable_notification (:obj:`bool`, optional): Sends the message silently. Users will + receive a notification with no sound. + protect_content (:obj:`bool`, optional): Protects the contents of the sent message from + forwarding and saving. + + .. versionadded:: 13.10 + + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the + original message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. + A JSON-serialized object for an inline keyboard, custom reply keyboard, + instructions to remove reply keyboard or to force a reply from the user. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.MessageId`: On success + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + 'chat_id': chat_id, + 'from_chat_id': from_chat_id, + 'message_id': message_id, + 'parse_mode': parse_mode, + 'disable_notification': disable_notification, + 'allow_sending_without_reply': allow_sending_without_reply, + } + if caption is not None: + data['caption'] = caption + if caption_entities: + data['caption_entities'] = caption_entities + if reply_to_message_id: + data['reply_to_message_id'] = reply_to_message_id + if protect_content: + data['protect_content'] = protect_content + if reply_markup: + if isinstance(reply_markup, ReplyMarkup): + # We need to_json() instead of to_dict() here, because reply_markups may be + # attached to media messages, which aren't json dumped by utils.request + data['reply_markup'] = reply_markup.to_json() + else: + data['reply_markup'] = reply_markup + + result = self._post('copyMessage', data, timeout=timeout, api_kwargs=api_kwargs) + return MessageId.de_json(result, self) # type: ignore[return-value, arg-type] + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data: JSONDict = {'id': self.id, 'username': self.username, 'first_name': self.first_name} if self.last_name: data['last_name'] = self.last_name return data - def __reduce__(self): - return (self.__class__, (self.token, self.base_url.replace(self.token, ''), - self.base_file_url.replace(self.token, ''))) + def __eq__(self, other: object) -> bool: + return self.bot == other + + def __hash__(self) -> int: + return hash(self.bot) # camelCase aliases getMe = get_me - """Alias for :attr:`get_me`""" + """Alias for :meth:`get_me`""" sendMessage = send_message - """Alias for :attr:`send_message`""" + """Alias for :meth:`send_message`""" deleteMessage = delete_message - """Alias for :attr:`delete_message`""" + """Alias for :meth:`delete_message`""" forwardMessage = forward_message - """Alias for :attr:`forward_message`""" + """Alias for :meth:`forward_message`""" sendPhoto = send_photo - """Alias for :attr:`send_photo`""" + """Alias for :meth:`send_photo`""" sendAudio = send_audio - """Alias for :attr:`send_audio`""" + """Alias for :meth:`send_audio`""" sendDocument = send_document - """Alias for :attr:`send_document`""" + """Alias for :meth:`send_document`""" sendSticker = send_sticker - """Alias for :attr:`send_sticker`""" + """Alias for :meth:`send_sticker`""" sendVideo = send_video - """Alias for :attr:`send_video`""" + """Alias for :meth:`send_video`""" sendAnimation = send_animation - """Alias for :attr:`send_animation`""" + """Alias for :meth:`send_animation`""" sendVoice = send_voice - """Alias for :attr:`send_voice`""" + """Alias for :meth:`send_voice`""" sendVideoNote = send_video_note - """Alias for :attr:`send_video_note`""" + """Alias for :meth:`send_video_note`""" sendMediaGroup = send_media_group - """Alias for :attr:`send_media_group`""" + """Alias for :meth:`send_media_group`""" sendLocation = send_location - """Alias for :attr:`send_location`""" + """Alias for :meth:`send_location`""" editMessageLiveLocation = edit_message_live_location - """Alias for :attr:`edit_message_live_location`""" + """Alias for :meth:`edit_message_live_location`""" stopMessageLiveLocation = stop_message_live_location - """Alias for :attr:`stop_message_live_location`""" + """Alias for :meth:`stop_message_live_location`""" sendVenue = send_venue - """Alias for :attr:`send_venue`""" + """Alias for :meth:`send_venue`""" sendContact = send_contact - """Alias for :attr:`send_contact`""" + """Alias for :meth:`send_contact`""" sendGame = send_game - """Alias for :attr:`send_game`""" + """Alias for :meth:`send_game`""" sendChatAction = send_chat_action - """Alias for :attr:`send_chat_action`""" + """Alias for :meth:`send_chat_action`""" answerInlineQuery = answer_inline_query - """Alias for :attr:`answer_inline_query`""" + """Alias for :meth:`answer_inline_query`""" getUserProfilePhotos = get_user_profile_photos - """Alias for :attr:`get_user_profile_photos`""" + """Alias for :meth:`get_user_profile_photos`""" getFile = get_file - """Alias for :attr:`get_file`""" + """Alias for :meth:`get_file`""" + banChatMember = ban_chat_member + """Alias for :meth:`ban_chat_member`""" + banChatSenderChat = ban_chat_sender_chat + """Alias for :meth:`ban_chat_sender_chat`""" kickChatMember = kick_chat_member - """Alias for :attr:`kick_chat_member`""" + """Alias for :meth:`kick_chat_member`""" unbanChatMember = unban_chat_member - """Alias for :attr:`unban_chat_member`""" + """Alias for :meth:`unban_chat_member`""" + unbanChatSenderChat = unban_chat_sender_chat + """Alias for :meth:`unban_chat_sender_chat`""" answerCallbackQuery = answer_callback_query - """Alias for :attr:`answer_callback_query`""" + """Alias for :meth:`answer_callback_query`""" editMessageText = edit_message_text - """Alias for :attr:`edit_message_text`""" + """Alias for :meth:`edit_message_text`""" editMessageCaption = edit_message_caption - """Alias for :attr:`edit_message_caption`""" + """Alias for :meth:`edit_message_caption`""" editMessageMedia = edit_message_media - """Alias for :attr:`edit_message_media`""" + """Alias for :meth:`edit_message_media`""" editMessageReplyMarkup = edit_message_reply_markup - """Alias for :attr:`edit_message_reply_markup`""" + """Alias for :meth:`edit_message_reply_markup`""" getUpdates = get_updates - """Alias for :attr:`get_updates`""" + """Alias for :meth:`get_updates`""" setWebhook = set_webhook - """Alias for :attr:`set_webhook`""" + """Alias for :meth:`set_webhook`""" deleteWebhook = delete_webhook - """Alias for :attr:`delete_webhook`""" + """Alias for :meth:`delete_webhook`""" leaveChat = leave_chat - """Alias for :attr:`leave_chat`""" + """Alias for :meth:`leave_chat`""" getChat = get_chat - """Alias for :attr:`get_chat`""" + """Alias for :meth:`get_chat`""" getChatAdministrators = get_chat_administrators - """Alias for :attr:`get_chat_administrators`""" + """Alias for :meth:`get_chat_administrators`""" getChatMember = get_chat_member - """Alias for :attr:`get_chat_member`""" + """Alias for :meth:`get_chat_member`""" setChatStickerSet = set_chat_sticker_set - """Alias for :attr:`set_chat_sticker_set`""" + """Alias for :meth:`set_chat_sticker_set`""" deleteChatStickerSet = delete_chat_sticker_set - """Alias for :attr:`delete_chat_sticker_set`""" + """Alias for :meth:`delete_chat_sticker_set`""" + getChatMemberCount = get_chat_member_count + """Alias for :meth:`get_chat_member_count`""" getChatMembersCount = get_chat_members_count - """Alias for :attr:`get_chat_members_count`""" + """Alias for :meth:`get_chat_members_count`""" getWebhookInfo = get_webhook_info - """Alias for :attr:`get_webhook_info`""" + """Alias for :meth:`get_webhook_info`""" setGameScore = set_game_score - """Alias for :attr:`set_game_score`""" + """Alias for :meth:`set_game_score`""" getGameHighScores = get_game_high_scores - """Alias for :attr:`get_game_high_scores`""" + """Alias for :meth:`get_game_high_scores`""" sendInvoice = send_invoice - """Alias for :attr:`send_invoice`""" + """Alias for :meth:`send_invoice`""" answerShippingQuery = answer_shipping_query - """Alias for :attr:`answer_shipping_query`""" + """Alias for :meth:`answer_shipping_query`""" answerPreCheckoutQuery = answer_pre_checkout_query - """Alias for :attr:`answer_pre_checkout_query`""" + """Alias for :meth:`answer_pre_checkout_query`""" restrictChatMember = restrict_chat_member - """Alias for :attr:`restrict_chat_member`""" + """Alias for :meth:`restrict_chat_member`""" promoteChatMember = promote_chat_member - """Alias for :attr:`promote_chat_member`""" + """Alias for :meth:`promote_chat_member`""" + setChatPermissions = set_chat_permissions + """Alias for :meth:`set_chat_permissions`""" + setChatAdministratorCustomTitle = set_chat_administrator_custom_title + """Alias for :meth:`set_chat_administrator_custom_title`""" exportChatInviteLink = export_chat_invite_link - """Alias for :attr:`export_chat_invite_link`""" + """Alias for :meth:`export_chat_invite_link`""" + createChatInviteLink = create_chat_invite_link + """Alias for :meth:`create_chat_invite_link`""" + editChatInviteLink = edit_chat_invite_link + """Alias for :meth:`edit_chat_invite_link`""" + revokeChatInviteLink = revoke_chat_invite_link + """Alias for :meth:`revoke_chat_invite_link`""" + approveChatJoinRequest = approve_chat_join_request + """Alias for :meth:`approve_chat_join_request`""" + declineChatJoinRequest = decline_chat_join_request + """Alias for :meth:`decline_chat_join_request`""" setChatPhoto = set_chat_photo - """Alias for :attr:`set_chat_photo`""" + """Alias for :meth:`set_chat_photo`""" deleteChatPhoto = delete_chat_photo - """Alias for :attr:`delete_chat_photo`""" + """Alias for :meth:`delete_chat_photo`""" setChatTitle = set_chat_title - """Alias for :attr:`set_chat_title`""" + """Alias for :meth:`set_chat_title`""" setChatDescription = set_chat_description - """Alias for :attr:`set_chat_description`""" + """Alias for :meth:`set_chat_description`""" pinChatMessage = pin_chat_message - """Alias for :attr:`pin_chat_message`""" + """Alias for :meth:`pin_chat_message`""" unpinChatMessage = unpin_chat_message - """Alias for :attr:`unpin_chat_message`""" + """Alias for :meth:`unpin_chat_message`""" + unpinAllChatMessages = unpin_all_chat_messages + """Alias for :meth:`unpin_all_chat_messages`""" getStickerSet = get_sticker_set - """Alias for :attr:`get_sticker_set`""" + """Alias for :meth:`get_sticker_set`""" uploadStickerFile = upload_sticker_file - """Alias for :attr:`upload_sticker_file`""" + """Alias for :meth:`upload_sticker_file`""" createNewStickerSet = create_new_sticker_set - """Alias for :attr:`create_new_sticker_set`""" + """Alias for :meth:`create_new_sticker_set`""" addStickerToSet = add_sticker_to_set - """Alias for :attr:`add_sticker_to_set`""" + """Alias for :meth:`add_sticker_to_set`""" setStickerPositionInSet = set_sticker_position_in_set - """Alias for :attr:`set_sticker_position_in_set`""" + """Alias for :meth:`set_sticker_position_in_set`""" deleteStickerFromSet = delete_sticker_from_set - """Alias for :attr:`delete_sticker_from_set`""" + """Alias for :meth:`delete_sticker_from_set`""" + setStickerSetThumb = set_sticker_set_thumb + """Alias for :meth:`set_sticker_set_thumb`""" setPassportDataErrors = set_passport_data_errors - """Alias for :attr:`set_passport_data_errors`""" + """Alias for :meth:`set_passport_data_errors`""" + sendPoll = send_poll + """Alias for :meth:`send_poll`""" + stopPoll = stop_poll + """Alias for :meth:`stop_poll`""" + sendDice = send_dice + """Alias for :meth:`send_dice`""" + getMyCommands = get_my_commands + """Alias for :meth:`get_my_commands`""" + setMyCommands = set_my_commands + """Alias for :meth:`set_my_commands`""" + deleteMyCommands = delete_my_commands + """Alias for :meth:`delete_my_commands`""" + logOut = log_out + """Alias for :meth:`log_out`""" + copyMessage = copy_message + """Alias for :meth:`copy_message`""" diff --git a/telegramer/include/telegram/botcommand.py b/telegramer/include/telegram/botcommand.py new file mode 100644 index 0000000..7d0f2da --- /dev/null +++ b/telegramer/include/telegram/botcommand.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram Bot Command.""" +from typing import Any + +from telegram import TelegramObject + + +class BotCommand(TelegramObject): + """ + This object represents a bot command. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`command` and :attr:`description` are equal. + + Args: + command (:obj:`str`): Text of the command, 1-32 characters. Can contain only lowercase + English letters, digits and underscores. + description (:obj:`str`): Description of the command, 1-256 characters. + + Attributes: + command (:obj:`str`): Text of the command. + description (:obj:`str`): Description of the command. + + """ + + __slots__ = ('description', '_id_attrs', 'command') + + def __init__(self, command: str, description: str, **_kwargs: Any): + self.command = command + self.description = description + + self._id_attrs = (self.command, self.description) diff --git a/telegramer/include/telegram/botcommandscope.py b/telegramer/include/telegram/botcommandscope.py new file mode 100644 index 0000000..283f768 --- /dev/null +++ b/telegramer/include/telegram/botcommandscope.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 +"""This module contains objects representing Telegram bot command scopes.""" +from typing import Any, Union, Optional, TYPE_CHECKING, Dict, Type + +from telegram import TelegramObject, constants +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class BotCommandScope(TelegramObject): + """Base class for objects that represent the scope to which bot commands are applied. + Currently, the following 7 scopes are supported: + + * :class:`telegram.BotCommandScopeDefault` + * :class:`telegram.BotCommandScopeAllPrivateChats` + * :class:`telegram.BotCommandScopeAllGroupChats` + * :class:`telegram.BotCommandScopeAllChatAdministrators` + * :class:`telegram.BotCommandScopeChat` + * :class:`telegram.BotCommandScopeChatAdministrators` + * :class:`telegram.BotCommandScopeChatMember` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. For subclasses with additional attributes, + the notion of equality is overridden. + + Note: + Please see the `official docs`_ on how Telegram determines which commands to display. + + .. _`official docs`: https://core.telegram.org/bots/api#determining-list-of-commands + + .. versionadded:: 13.7 + + Args: + type (:obj:`str`): Scope type. + + Attributes: + type (:obj:`str`): Scope type. + """ + + __slots__ = ('type', '_id_attrs') + + DEFAULT = constants.BOT_COMMAND_SCOPE_DEFAULT + """:const:`telegram.constants.BOT_COMMAND_SCOPE_DEFAULT`""" + ALL_PRIVATE_CHATS = constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS`""" + ALL_GROUP_CHATS = constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS`""" + ALL_CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS`""" + CHAT = constants.BOT_COMMAND_SCOPE_CHAT + """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT`""" + CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS`""" + CHAT_MEMBER = constants.BOT_COMMAND_SCOPE_CHAT_MEMBER + """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_MEMBER`""" + + def __init__(self, type: str, **_kwargs: Any): + self.type = type + self._id_attrs = (self.type,) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['BotCommandScope']: + """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with this object. + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type['BotCommandScope']] = { + cls.DEFAULT: BotCommandScopeDefault, + cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats, + cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats, + cls.ALL_CHAT_ADMINISTRATORS: BotCommandScopeAllChatAdministrators, + cls.CHAT: BotCommandScopeChat, + cls.CHAT_ADMINISTRATORS: BotCommandScopeChatAdministrators, + cls.CHAT_MEMBER: BotCommandScopeChatMember, + } + + if cls is BotCommandScope: + return _class_mapping.get(data['type'], cls)(**data, bot=bot) + return cls(**data) + + +class BotCommandScopeDefault(BotCommandScope): + """Represents the default scope of bot commands. Default commands are used if no commands with + a `narrower scope`_ are specified for the user. + + .. _`narrower scope`: https://core.telegram.org/bots/api#determining-list-of-commands + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.DEFAULT`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.DEFAULT) + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + """Represents the scope of bot commands, covering all private chats. + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_PRIVATE_CHATS`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS) + + +class BotCommandScopeAllGroupChats(BotCommandScope): + """Represents the scope of bot commands, covering all group and supergroup chats. + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_GROUP_CHATS`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.ALL_GROUP_CHATS) + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + """Represents the scope of bot commands, covering all group and supergroup chat administrators. + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_CHAT_ADMINISTRATORS`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS) + + +class BotCommandScopeChat(BotCommandScope): + """Represents the scope of bot commands, covering a specific chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` and :attr:`chat_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT`. + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + """ + + __slots__ = ('chat_id',) + + def __init__(self, chat_id: Union[str, int], **_kwargs: Any): + super().__init__(type=BotCommandScope.CHAT) + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) + + +class BotCommandScopeChatAdministrators(BotCommandScope): + """Represents the scope of bot commands, covering all administrators of a specific group or + supergroup chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` and :attr:`chat_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_ADMINISTRATORS`. + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + """ + + __slots__ = ('chat_id',) + + def __init__(self, chat_id: Union[str, int], **_kwargs: Any): + super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS) + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) + + +class BotCommandScopeChatMember(BotCommandScope): + """Represents the scope of bot commands, covering a specific member of a group or supergroup + chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type`, :attr:`chat_id` and :attr:`user_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + user_id (:obj:`int`): Unique identifier of the target user. + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_MEMBER`. + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + user_id (:obj:`int`): Unique identifier of the target user. + """ + + __slots__ = ('chat_id', 'user_id') + + def __init__(self, chat_id: Union[str, int], user_id: int, **_kwargs: Any): + super().__init__(type=BotCommandScope.CHAT_MEMBER) + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id) + ) + self.user_id = int(user_id) + self._id_attrs = (self.type, self.chat_id, self.user_id) diff --git a/telegramer/include/telegram/callbackquery.py b/telegramer/include/telegram/callbackquery.py index 54e5b75..b783636 100644 --- a/telegramer/include/telegram/callbackquery.py +++ b/telegramer/include/telegram/callbackquery.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,9 +16,23 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 """This module contains an object that represents a Telegram CallbackQuery""" +from typing import TYPE_CHECKING, Any, List, Optional, Union, Tuple, ClassVar -from telegram import TelegramObject, Message, User +from telegram import Message, TelegramObject, User, Location, ReplyMarkup, constants +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput, DVInput + +if TYPE_CHECKING: + from telegram import ( + Bot, + GameHighScore, + InlineKeyboardMarkup, + MessageId, + InputMedia, + MessageEntity, + ) class CallbackQuery(TelegramObject): @@ -29,58 +43,81 @@ class CallbackQuery(TelegramObject): :attr:`message` will be present. If the button was attached to a message sent via the bot (in inline mode), the field :attr:`inline_message_id` will be present. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + Note: - * In Python `from` is a reserved word, use `from_user` instead. + * In Python ``from`` is a reserved word, use ``from_user`` instead. * Exactly one of the fields :attr:`data` or :attr:`game_short_name` will be present. + * After the user presses an inline button, Telegram clients will display a progress bar + until you call :attr:`answer`. It is, therefore, necessary to react + by calling :attr:`telegram.Bot.answer_callback_query` even if no notification to the user + is needed (e.g., without specifying any of the optional parameters). + * If you're using :attr:`Bot.arbitrary_callback_data`, :attr:`data` may be an instance + of :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data + associated with the button triggering the :class:`telegram.CallbackQuery` was already + deleted or if :attr:`data` was manipulated by a malicious client. + + .. versionadded:: 13.6 - Attributes: - id (:obj:`str`): Unique identifier for this query. - from_user (:class:`telegram.User`): Sender. - message (:class:`telegram.Message`): Optional. Message with the callback button that - originated the query. - inline_message_id (:obj:`str`): Optional. Identifier of the message sent via the bot in - inline mode, that originated the query. - chat_instance (:obj:`str`): Optional. Global identifier, uniquely corresponding to the chat - to which the message with the callback button was sent. - data (:obj:`str`): Optional. Data associated with the callback button. - game_short_name (:obj:`str`): Optional. Short name of a Game to be returned. Args: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. + chat_instance (:obj:`str`): Global identifier, uniquely corresponding to the chat to which + the message with the callback button was sent. Useful for high scores in games. message (:class:`telegram.Message`, optional): Message with the callback button that originated the query. Note that message content and message date will not be available if the message is too old. - inline_message_id (:obj:`str`, optional): Identifier of the message sent via the bot in - inline mode, that originated the query. - chat_instance (:obj:`str`, optional): Global identifier, uniquely corresponding to the chat - to which the message with the callback button was sent. Useful for high scores in - games. data (:obj:`str`, optional): Data associated with the callback button. Be aware that a bad client can send arbitrary data in this field. + inline_message_id (:obj:`str`, optional): Identifier of the message sent via the bot in + inline mode, that originated the query. game_short_name (:obj:`str`, optional): Short name of a Game to be returned, serves as the unique identifier for the game + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. - Note: - After the user presses an inline button, Telegram clients will display a progress bar - until you call :attr:`answer`. It is, therefore, necessary to react - by calling :attr:`telegram.Bot.answer_callback_query` even if no notification to the user - is needed (e.g., without specifying any of the optional parameters). + Attributes: + id (:obj:`str`): Unique identifier for this query. + from_user (:class:`telegram.User`): Sender. + chat_instance (:obj:`str`): Global identifier, uniquely corresponding to the chat to which + the message with the callback button was sent. + message (:class:`telegram.Message`): Optional. Message with the callback button that + originated the query. + data (:obj:`str` | :obj:`object`): Optional. Data associated with the callback button. + inline_message_id (:obj:`str`): Optional. Identifier of the message sent via the bot in + inline mode, that originated the query. + game_short_name (:obj:`str`): Optional. Short name of a Game to be returned. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. """ - def __init__(self, - id, - from_user, - chat_instance, - message=None, - data=None, - inline_message_id=None, - game_short_name=None, - bot=None, - **kwargs): + __slots__ = ( + 'bot', + 'game_short_name', + 'message', + 'chat_instance', + 'id', + 'from_user', + 'inline_message_id', + 'data', + '_id_attrs', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + from_user: User, + chat_instance: str, + message: Message = None, + data: str = None, + inline_message_id: str = None, + game_short_name: str = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required - self.id = id + self.id = id # pylint: disable=C0103 self.from_user = from_user self.chat_instance = chat_instance # Optionals @@ -94,96 +131,530 @@ def __init__(self, self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['CallbackQuery']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(CallbackQuery, cls).de_json(data, bot) - data['from_user'] = User.de_json(data.get('from'), bot) data['message'] = Message.de_json(data.get('message'), bot) return cls(bot=bot, **data) - def answer(self, *args, **kwargs): + def answer( + self, + text: str = None, + show_alert: bool = False, + url: str = None, + cache_time: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: bot.answer_callback_query(update.callback_query.id, *args, **kwargs) + For the documentation of the arguments, please see + :meth:`telegram.Bot.answer_callback_query`. + Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.answerCallbackQuery(self.id, *args, **kwargs) - - def edit_message_text(self, *args, **kwargs): + return self.bot.answer_callback_query( + callback_query_id=self.id, + text=text, + show_alert=show_alert, + url=url, + cache_time=cache_time, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def edit_message_text( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + ) -> Union[Message, bool]: """Shortcut for either:: - bot.edit_message_text(chat_id=update.callback_query.message.chat_id, - message_id=update.callback_query.message.message_id, - *args, **kwargs) + update.callback_query.message.edit_text(text, *args, **kwargs) or:: - bot.edit_message_text(inline_message_id=update.callback_query.inline_message_id, + bot.edit_message_text(text, inline_message_id=update.callback_query.inline_message_id, *args, **kwargs) + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_text` and :meth:`telegram.Message.edit_text`. + Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise ``True`` is returned. + edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.edit_message_text( - inline_message_id=self.inline_message_id, *args, **kwargs) - else: - return self.bot.edit_message_text( - chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs) - - def edit_message_caption(self, *args, **kwargs): + inline_message_id=self.inline_message_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + entities=entities, + chat_id=None, + message_id=None, + ) + return self.message.edit_text( + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + entities=entities, + ) + + def edit_message_caption( + self, + caption: str = None, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + ) -> Union[Message, bool]: """Shortcut for either:: - bot.edit_message_caption(chat_id=update.callback_query.message.chat_id, - message_id=update.callback_query.message.message_id, - *args, **kwargs) + update.callback_query.message.edit_caption(caption, *args, **kwargs) or:: - bot.edit_message_caption(inline_message_id=update.callback_query.inline_message_id, + bot.edit_message_caption(caption=caption + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs) + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_caption` and :meth:`telegram.Message.edit_caption`. + Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise ``True`` is returned. + edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.edit_message_caption( - inline_message_id=self.inline_message_id, *args, **kwargs) - else: - return self.bot.edit_message_caption( - chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs) - - def edit_message_reply_markup(self, *args, **kwargs): + caption=caption, + inline_message_id=self.inline_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + caption_entities=caption_entities, + chat_id=None, + message_id=None, + ) + return self.message.edit_caption( + caption=caption, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + caption_entities=caption_entities, + ) + + def edit_message_reply_markup( + self, + reply_markup: Optional['InlineKeyboardMarkup'] = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: """Shortcut for either:: - bot.edit_message_replyMarkup(chat_id=update.callback_query.message.chat_id, - message_id=update.callback_query.message.message_id, - *args, **kwargs) + update.callback_query.message.edit_reply_markup( + reply_markup=reply_markup, + *args, + **kwargs + ) or:: - bot.edit_message_reply_markup(inline_message_id=update.callback_query.inline_message_id, - *args, **kwargs) + bot.edit_message_reply_markup + inline_message_id=update.callback_query.inline_message_id, + reply_markup=reply_markup, + *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_reply_markup` and + :meth:`telegram.Message.edit_reply_markup`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise ``True`` is returned. + edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.edit_message_reply_markup( - inline_message_id=self.inline_message_id, *args, **kwargs) - else: - return self.bot.edit_message_reply_markup( - chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs) + reply_markup=reply_markup, + inline_message_id=self.inline_message_id, + timeout=timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + ) + return self.message.edit_reply_markup( + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def edit_message_media( + self, + media: 'InputMedia' = None, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: + """Shortcut for either:: + + update.callback_query.message.edit_media(*args, **kwargs) + + or:: + + bot.edit_message_media(inline_message_id=update.callback_query.inline_message_id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_media` and :meth:`telegram.Message.edit_media`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + """ + if self.inline_message_id: + return self.bot.edit_message_media( + inline_message_id=self.inline_message_id, + media=media, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + ) + return self.message.edit_media( + media=media, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def edit_message_live_location( + self, + latitude: float = None, + longitude: float = None, + location: Location = None, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + ) -> Union[Message, bool]: + """Shortcut for either:: + + update.callback_query.message.edit_live_location(*args, **kwargs) + + or:: + + bot.edit_message_live_location( + inline_message_id=update.callback_query.inline_message_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_live_location` and + :meth:`telegram.Message.edit_live_location`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + """ + if self.inline_message_id: + return self.bot.edit_message_live_location( + inline_message_id=self.inline_message_id, + latitude=latitude, + longitude=longitude, + location=location, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + chat_id=None, + message_id=None, + ) + return self.message.edit_live_location( + latitude=latitude, + longitude=longitude, + location=location, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + ) + + def stop_message_live_location( + self, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: + """Shortcut for either:: + + update.callback_query.message.stop_live_location(*args, **kwargs) + + or:: + + bot.stop_message_live_location( + inline_message_id=update.callback_query.inline_message_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.stop_message_live_location` and + :meth:`telegram.Message.stop_live_location`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + """ + if self.inline_message_id: + return self.bot.stop_message_live_location( + inline_message_id=self.inline_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + ) + return self.message.stop_live_location( + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def set_game_score( + self, + user_id: Union[int, str], + score: int, + force: bool = None, + disable_edit_message: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union[Message, bool]: + """Shortcut for either:: + + update.callback_query.message.set_game_score(*args, **kwargs) + + or:: + + bot.set_game_score(inline_message_id=update.callback_query.inline_message_id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_game_score` and :meth:`telegram.Message.set_game_score`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + """ + if self.inline_message_id: + return self.bot.set_game_score( + inline_message_id=self.inline_message_id, + user_id=user_id, + score=score, + force=force, + disable_edit_message=disable_edit_message, + timeout=timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + ) + return self.message.set_game_score( + user_id=user_id, + score=score, + force=force, + disable_edit_message=disable_edit_message, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def get_game_high_scores( + self, + user_id: Union[int, str], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> List['GameHighScore']: + """Shortcut for either:: + + update.callback_query.message.get_game_high_score(*args, **kwargs) + + or:: + + bot.get_game_high_scores(inline_message_id=update.callback_query.inline_message_id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_game_high_scores` and :meth:`telegram.Message.get_game_high_score`. + + Returns: + List[:class:`telegram.GameHighScore`] + + """ + if self.inline_message_id: + return self.bot.get_game_high_scores( + inline_message_id=self.inline_message_id, + user_id=user_id, + timeout=timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + ) + return self.message.get_game_high_scores( + user_id=user_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def delete_message( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + update.callback_query.message.delete(*args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Message.delete`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.message.delete( + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def pin_message( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + update.callback_query.message.pin(*args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Message.pin`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.message.pin( + disable_notification=disable_notification, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def unpin_message( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + update.callback_query.message.unpin(*args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Message.unpin`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.message.unpin( + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def copy_message( + self, + chat_id: Union[int, str], + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> 'MessageId': + """Shortcut for:: + + update.callback_query.message.copy( + chat_id, + from_chat_id=update.message.chat_id, + message_id=update.message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Message.copy`. + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return self.message.copy( + chat_id=chat_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + MAX_ANSWER_TEXT_LENGTH: ClassVar[int] = constants.MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH + """ + :const:`telegram.constants.MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH` + + .. versionadded:: 13.2 + """ diff --git a/telegramer/include/telegram/chat.py b/telegramer/include/telegram/chat.py index 5938c7c..b5dbfe4 100644 --- a/telegramer/include/telegram/chat.py +++ b/telegramer/include/telegram/chat.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -# pylint: disable=C0103,W0622 +# pylint: disable=W0622 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,29 +18,52 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Chat.""" - -from telegram import TelegramObject, ChatPhoto +import warnings +from datetime import datetime +from typing import TYPE_CHECKING, List, Optional, ClassVar, Union, Tuple, Any + +from telegram import ChatPhoto, TelegramObject, constants +from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput +from telegram.utils.deprecate import TelegramDeprecationWarning + +from .chatpermissions import ChatPermissions +from .chatlocation import ChatLocation +from .utils.helpers import DEFAULT_NONE, DEFAULT_20 + +if TYPE_CHECKING: + from telegram import ( + Bot, + ChatMember, + ChatInviteLink, + Message, + MessageId, + ReplyMarkup, + Contact, + InlineKeyboardMarkup, + Location, + Venue, + MessageEntity, + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + PhotoSize, + Audio, + Document, + Animation, + LabeledPrice, + Sticker, + Video, + VideoNote, + Voice, + ) class Chat(TelegramObject): """This object represents a chat. - Attributes: - id (:obj:`int`): Unique identifier for this chat. - type (:obj:`str`): Type of chat. - title (:obj:`str`): Optional. Title, for supergroups, channels and group chats. - username (:obj:`str`): Optional. Username. - first_name (:obj:`str`): Optional. First name of the other party in a private chat. - last_name (:obj:`str`): Optional. Last name of the other party in a private chat. - all_members_are_administrators (:obj:`bool`): Optional. - photo (:class:`telegram.ChatPhoto`): Optional. Chat photo. - description (:obj:`str`): Optional. Description, for supergroups and channel chats. - invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. - pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups. - Returned only in get_chat. - sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. - can_set_sticker_set (:obj:`bool`): Optional. ``True``, if the bot can change group the - sticker set. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. Args: id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits @@ -54,286 +77,1739 @@ class Chat(TelegramObject): available. first_name(:obj:`str`, optional): First name of the other party in a private chat. last_name(:obj:`str`, optional): Last name of the other party in a private chat. - all_members_are_administrators (:obj:`bool`, optional): True if a group has `All Members - Are Admins` enabled. - photo (:class:`telegram.ChatPhoto`, optional): Chat photo. Returned only in getChat. - description (:obj:`str`, optional): Description, for supergroups and channel chats. - Returned only in get_chat. - invite_link (:obj:`str`, optional): Chat invite link, for supergroups and channel chats. - Returned only in get_chat. - pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups. - Returned only in get_chat. + photo (:class:`telegram.ChatPhoto`, optional): Chat photo. + Returned only in :meth:`telegram.Bot.get_chat`. + bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. + has_private_forwards (:obj:`bool`, optional): :obj:`True`, if privacy settings of the other + party in the private chat allows to use ``tg://user?id=`` links only in chats + with the user. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.9 + description (:obj:`str`, optional): Description, for groups, supergroups and channel chats. + Returned only in :meth:`telegram.Bot.get_chat`. + invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and + channel. Returned only in :meth:`telegram.Bot.get_chat`. + pinned_message (:class:`telegram.Message`, optional): The most recent pinned message + (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. + permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, + for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. + slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between + consecutive messages sent by each unprivileged user. + Returned only in :meth:`telegram.Bot.get_chat`. + message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to + the chat will be automatically deleted; in seconds. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.4 + has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't + be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.9 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. - sticker_set_name (:obj:`str`, optional): For supergroups, name of Group sticker set. - Returned only in get_chat. - can_set_sticker_set (:obj:`bool`, optional): ``True``, if the bot can change group the - sticker set. Returned only in get_chat. + sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set. + Returned only in :meth:`telegram.Bot.get_chat`. + can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the + sticker set. Returned only in :meth:`telegram.Bot.get_chat`. + linked_chat_id (:obj:`int`, optional): Unique identifier for the linked chat, i.e. the + discussion group identifier for a channel and vice versa; for supergroups and channel + chats. Returned only in :meth:`telegram.Bot.get_chat`. + location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which + the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + id (:obj:`int`): Unique identifier for this chat. + type (:obj:`str`): Type of chat. + title (:obj:`str`): Optional. Title, for supergroups, channels and group chats. + username (:obj:`str`): Optional. Username. + first_name (:obj:`str`): Optional. First name of the other party in a private chat. + last_name (:obj:`str`): Optional. Last name of the other party in a private chat. + photo (:class:`telegram.ChatPhoto`): Optional. Chat photo. + bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. + has_private_forwards (:obj:`bool`): Optional. :obj:`True`, if privacy settings of the other + party in the private chat allows to use ``tg://user?id=`` links only in chats + with the user. + + .. versionadded:: 13.9 + description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. + invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and + channel. Returned only in :meth:`telegram.Bot.get_chat`. + pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message + (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. + permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, + for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. + slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between + consecutive messages sent by each unprivileged user. Returned only in + :meth:`telegram.Bot.get_chat`. + message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to + the chat will be automatically deleted; in seconds. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.4 + has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't + be forwarded to other chats. + + .. versionadded:: 13.9 + sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. + can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the + sticker set. + linked_chat_id (:obj:`int`): Optional. Unique identifier for the linked chat, i.e. the + discussion group identifier for a channel and vice versa; for supergroups and channel + chats. Returned only in :meth:`telegram.Bot.get_chat`. + location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which + the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. + """ - PRIVATE = 'private' - """:obj:`str`: 'private'""" - GROUP = 'group' - """:obj:`str`: 'group'""" - SUPERGROUP = 'supergroup' - """:obj:`str`: 'supergroup'""" - CHANNEL = 'channel' - """:obj:`str`: 'channel'""" - - def __init__(self, - id, - type, - title=None, - username=None, - first_name=None, - last_name=None, - all_members_are_administrators=None, - bot=None, - photo=None, - description=None, - invite_link=None, - pinned_message=None, - sticker_set_name=None, - can_set_sticker_set=None, - **kwargs): + __slots__ = ( + 'bio', + 'id', + 'type', + 'last_name', + 'bot', + 'sticker_set_name', + 'slow_mode_delay', + 'location', + 'first_name', + 'permissions', + 'invite_link', + 'pinned_message', + 'description', + 'can_set_sticker_set', + 'username', + 'title', + 'photo', + 'linked_chat_id', + 'all_members_are_administrators', + 'message_auto_delete_time', + 'has_protected_content', + 'has_private_forwards', + '_id_attrs', + ) + + SENDER: ClassVar[str] = constants.CHAT_SENDER + """:const:`telegram.constants.CHAT_SENDER` + + .. versionadded:: 13.5 + """ + PRIVATE: ClassVar[str] = constants.CHAT_PRIVATE + """:const:`telegram.constants.CHAT_PRIVATE`""" + GROUP: ClassVar[str] = constants.CHAT_GROUP + """:const:`telegram.constants.CHAT_GROUP`""" + SUPERGROUP: ClassVar[str] = constants.CHAT_SUPERGROUP + """:const:`telegram.constants.CHAT_SUPERGROUP`""" + CHANNEL: ClassVar[str] = constants.CHAT_CHANNEL + """:const:`telegram.constants.CHAT_CHANNEL`""" + + def __init__( + self, + id: int, + type: str, + title: str = None, + username: str = None, + first_name: str = None, + last_name: str = None, + bot: 'Bot' = None, + photo: ChatPhoto = None, + description: str = None, + invite_link: str = None, + pinned_message: 'Message' = None, + permissions: ChatPermissions = None, + sticker_set_name: str = None, + can_set_sticker_set: bool = None, + slow_mode_delay: int = None, + bio: str = None, + linked_chat_id: int = None, + location: ChatLocation = None, + message_auto_delete_time: int = None, + has_private_forwards: bool = None, + has_protected_content: bool = None, + **_kwargs: Any, + ): # Required - self.id = int(id) + self.id = int(id) # pylint: disable=C0103 self.type = type # Optionals self.title = title self.username = username self.first_name = first_name self.last_name = last_name - self.all_members_are_administrators = all_members_are_administrators + # TODO: Remove (also from tests), when Telegram drops this completely + self.all_members_are_administrators = _kwargs.get('all_members_are_administrators') self.photo = photo + self.bio = bio + self.has_private_forwards = has_private_forwards self.description = description self.invite_link = invite_link self.pinned_message = pinned_message + self.permissions = permissions + self.slow_mode_delay = slow_mode_delay + self.message_auto_delete_time = ( + int(message_auto_delete_time) if message_auto_delete_time is not None else None + ) + self.has_protected_content = has_protected_content self.sticker_set_name = sticker_set_name self.can_set_sticker_set = can_set_sticker_set + self.linked_chat_id = linked_chat_id + self.location = location self.bot = bot self._id_attrs = (self.id,) @property - def link(self): + def full_name(self) -> Optional[str]: + """ + :obj:`str`: Convenience property. If :attr:`first_name` is not :obj:`None` gives, + :attr:`first_name` followed by (if available) :attr:`last_name`. + + Note: + :attr:`full_name` will always be :obj:`None`, if the chat is a (super)group or + channel. + + .. versionadded:: 13.2 + """ + if not self.first_name: + return None + if self.last_name: + return f'{self.first_name} {self.last_name}' + return self.first_name + + @property + def link(self) -> Optional[str]: """:obj:`str`: Convenience property. If the chat has a :attr:`username`, returns a t.me - link of the chat.""" + link of the chat. + """ if self.username: - return "https://t.me/{}".format(self.username) + return f"https://t.me/{self.username}" return None @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Chat']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None data['photo'] = ChatPhoto.de_json(data.get('photo'), bot) - from telegram import Message + from telegram import Message # pylint: disable=C0415 + data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot) + data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot) + data['location'] = ChatLocation.de_json(data.get('location'), bot) return cls(bot=bot, **data) - def send_action(self, *args, **kwargs): + def leave(self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None) -> bool: + """Shortcut for:: + + bot.leave_chat(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.leave_chat`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.leave_chat( + chat_id=self.id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def get_administrators( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> List['ChatMember']: """Shortcut for:: - bot.send_chat_action(update.message.chat.id, *args, **kwargs) + bot.get_chat_administrators(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_chat_administrators`. Returns: - :obj:`bool`: If the action was sent successfully. + List[:class:`telegram.ChatMember`]: A list of administrators in a chat. An Array of + :class:`telegram.ChatMember` objects that contains information about all + chat administrators except other bots. If the chat is a group or a supergroup + and no administrators were appointed, only the creator will be returned. + + """ + return self.bot.get_chat_administrators( + chat_id=self.id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def get_members_count( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> int: + """ + Deprecated, use :func:`~telegram.Chat.get_member_count` instead. + .. deprecated:: 13.7 """ + warnings.warn( + '`Chat.get_members_count` is deprecated. Use `Chat.get_member_count` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + + return self.get_member_count( + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def get_member_count( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> int: + """Shortcut for:: + + bot.get_chat_member_count(update.effective_chat.id, *args, **kwargs) - return self.bot.send_chat_action(self.id, *args, **kwargs) + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_chat_member_count`. - def leave(self, *args, **kwargs): + Returns: + :obj:`int` + """ + return self.bot.get_chat_member_count( + chat_id=self.id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def get_member( + self, + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> 'ChatMember': """Shortcut for:: - bot.leave_chat(update.message.chat.id, *args, **kwargs) + bot.get_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.get_chat_member`. Returns: - :obj:`bool` If the action was sent successfully. + :class:`telegram.ChatMember` """ - return self.bot.leave_chat(self.id, *args, **kwargs) + return self.bot.get_chat_member( + chat_id=self.id, + user_id=user_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def kick_member( + self, + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + until_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + revoke_messages: bool = None, + ) -> bool: + """ + Deprecated, use :func:`~telegram.Chat.ban_member` instead. - def get_administrators(self, *args, **kwargs): + .. deprecated:: 13.7 + """ + warnings.warn( + '`Chat.kick_member` is deprecated. Use `Chat.ban_member` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + + return self.ban_member( + user_id=user_id, + timeout=timeout, + until_date=until_date, + api_kwargs=api_kwargs, + revoke_messages=revoke_messages, + ) + + def ban_member( + self, + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + until_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + revoke_messages: bool = None, + ) -> bool: """Shortcut for:: - bot.get_chat_administrators(update.message.chat.id, *args, **kwargs) + bot.ban_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_member`. Returns: - List[:class:`telegram.ChatMember`]: A list of administrators in a chat. An Array of - :class:`telegram.ChatMember` objects that contains information about all - chat administrators except other bots. If the chat is a group or a supergroup - and no administrators were appointed, only the creator will be returned + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.ban_chat_member( + chat_id=self.id, + user_id=user_id, + timeout=timeout, + until_date=until_date, + api_kwargs=api_kwargs, + revoke_messages=revoke_messages, + ) + + def ban_sender_chat( + self, + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.ban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.get_chat_administrators(self.id, *args, **kwargs) + return self.bot.ban_chat_sender_chat( + chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def ban_chat( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.ban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_sender_chat`. + + .. versionadded:: 13.9 - def get_members_count(self, *args, **kwargs): + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.ban_chat_sender_chat( + chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs + ) + + def unban_sender_chat( + self, + sender_chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: - bot.get_chat_members_count(update.message.chat.id, *args, **kwargs) + bot.unban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unban_chat_sender_chat`. + + .. versionadded:: 13.9 Returns: - :obj:`int` + :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.get_chat_members_count(self.id, *args, **kwargs) + return self.bot.unban_chat_sender_chat( + chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def unban_chat( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.unban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs) - def get_member(self, *args, **kwargs): + For the documentation of the arguments, please see + :meth:`telegram.Bot.unban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unban_chat_sender_chat( + chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs + ) + + def unban_member( + self, + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + only_if_banned: bool = None, + ) -> bool: """Shortcut for:: - bot.get_chat_member(update.message.chat.id, *args, **kwargs) + bot.unban_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.unban_chat_member`. Returns: - :class:`telegram.ChatMember` + :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.get_chat_member(self.id, *args, **kwargs) + return self.bot.unban_chat_member( + chat_id=self.id, + user_id=user_id, + timeout=timeout, + api_kwargs=api_kwargs, + only_if_banned=only_if_banned, + ) + + def promote_member( + self, + user_id: Union[str, int], + can_change_info: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_invite_users: bool = None, + can_restrict_members: bool = None, + can_pin_messages: bool = None, + can_promote_members: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + is_anonymous: bool = None, + can_manage_chat: bool = None, + can_manage_voice_chats: bool = None, + ) -> bool: + """Shortcut for:: - def kick_member(self, *args, **kwargs): + bot.promote_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.promote_chat_member`. + + .. versionadded:: 13.2 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.promote_chat_member( + chat_id=self.id, + user_id=user_id, + can_change_info=can_change_info, + can_post_messages=can_post_messages, + can_edit_messages=can_edit_messages, + can_delete_messages=can_delete_messages, + can_invite_users=can_invite_users, + can_restrict_members=can_restrict_members, + can_pin_messages=can_pin_messages, + can_promote_members=can_promote_members, + timeout=timeout, + api_kwargs=api_kwargs, + is_anonymous=is_anonymous, + can_manage_chat=can_manage_chat, + can_manage_voice_chats=can_manage_voice_chats, + ) + + def restrict_member( + self, + user_id: Union[str, int], + permissions: ChatPermissions, + until_date: Union[int, datetime] = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: - bot.kick_chat_member(update.message.chat.id, *args, **kwargs) + bot.restrict_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.restrict_chat_member`. + + .. versionadded:: 13.2 Returns: - :obj:`bool`: If the action was sent succesfully. + :obj:`bool`: On success, :obj:`True` is returned. - Note: - This method will only work if the `All Members Are Admins` setting is off in the - target group. Otherwise members may only be removed by the group's creator or by the - member that added them. + """ + return self.bot.restrict_chat_member( + chat_id=self.id, + user_id=user_id, + permissions=permissions, + until_date=until_date, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def set_permissions( + self, + permissions: ChatPermissions, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.set_chat_permissions(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_permissions`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.kick_chat_member(self.id, *args, **kwargs) + return self.bot.set_chat_permissions( + chat_id=self.id, + permissions=permissions, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def set_administrator_custom_title( + self, + user_id: Union[int, str], + custom_title: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: - def unban_member(self, *args, **kwargs): + bot.set_chat_administrator_custom_title(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_administrator_custom_title`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.set_chat_administrator_custom_title( + chat_id=self.id, + user_id=user_id, + custom_title=custom_title, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def pin_message( + self, + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: - bot.unban_chat_member(update.message.chat.id, *args, **kwargs) + bot.pin_chat_message(chat_id=update.effective_chat.id, + *args, + **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.pin_chat_message`. Returns: - :obj:`bool`: If the action was sent successfully. + :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.unban_chat_member(self.id, *args, **kwargs) + return self.bot.pin_chat_message( + chat_id=self.id, + message_id=message_id, + disable_notification=disable_notification, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def unpin_message( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + message_id: int = None, + ) -> bool: + """Shortcut for:: + + bot.unpin_chat_message(chat_id=update.effective_chat.id, + *args, + **kwargs) - def send_message(self, *args, **kwargs): + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_chat_message( + chat_id=self.id, + timeout=timeout, + api_kwargs=api_kwargs, + message_id=message_id, + ) + + def unpin_all_messages( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: - bot.send_message(Chat.id, *args, **kwargs) + bot.unpin_all_chat_messages(chat_id=update.effective_chat.id, + *args, + **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_chat_messages`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_all_chat_messages( + chat_id=self.id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def send_message( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_message(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_message(self.id, *args, **kwargs) + return self.bot.send_message( + chat_id=self.id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + ) + + def send_media_group( + self, + media: List[ + Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> List['Message']: + """Shortcut for:: - def send_photo(self, *args, **kwargs): + bot.send_media_group(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. + + Returns: + List[:class:`telegram.Message`]: On success, instance representing the message posted. + + """ + return self.bot.send_media_group( + chat_id=self.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_chat_action( + self, + action: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.send_chat_action(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.send_chat_action( + chat_id=self.id, + action=action, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + send_action = send_chat_action + """Alias for :attr:`send_chat_action`""" + + def send_photo( + self, + photo: Union[FileInput, 'PhotoSize'], + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_photo(Chat.id, *args, **kwargs) + bot.send_photo(update.effective_chat.id, *args, **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_photo(self.id, *args, **kwargs) + return self.bot.send_photo( + chat_id=self.id, + photo=photo, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_contact( + self, + phone_number: str = None, + first_name: str = None, + last_name: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + contact: 'Contact' = None, + vcard: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_contact(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. - def send_audio(self, *args, **kwargs): + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_contact( + chat_id=self.id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + contact=contact, + vcard=vcard, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_audio( + self, + audio: Union[FileInput, 'Audio'], + duration: int = None, + performer: str = None, + title: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_audio(Chat.id, *args, **kwargs) + bot.send_audio(update.effective_chat.id, *args, **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_audio(self.id, *args, **kwargs) + return self.bot.send_audio( + chat_id=self.id, + audio=audio, + duration=duration, + performer=performer, + title=title, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_document( + self, + document: Union[FileInput, 'Document'], + filename: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + disable_content_type_detection: bool = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_document(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. - def send_document(self, *args, **kwargs): + """ + return self.bot.send_document( + chat_id=self.id, + document=document, + filename=filename, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + thumb=thumb, + api_kwargs=api_kwargs, + disable_content_type_detection=disable_content_type_detection, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + protect_content=protect_content, + ) + + def send_dice( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + emoji: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_document(Chat.id, *args, **kwargs) + bot.send_dice(update.effective_chat.id, *args, **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_document(self.id, *args, **kwargs) + return self.bot.send_dice( + chat_id=self.id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + emoji=emoji, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_game( + self, + game_short_name: str, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: - def send_animation(self, *args, **kwargs): + bot.send_game(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_game( + chat_id=self.id, + game_short_name=game_short_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_invoice( + self, + title: str, + description: str, + payload: str, + provider_token: str, + currency: str, + prices: List['LabeledPrice'], + start_parameter: str = None, + photo_url: str = None, + photo_size: int = None, + photo_width: int = None, + photo_height: int = None, + need_name: bool = None, + need_phone_number: bool = None, + need_email: bool = None, + need_shipping_address: bool = None, + is_flexible: bool = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'InlineKeyboardMarkup' = None, + provider_data: Union[str, object] = None, + send_phone_number_to_provider: bool = None, + send_email_to_provider: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + max_tip_amount: int = None, + suggested_tip_amounts: List[int] = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_animation(Chat.id, *args, **kwargs) + bot.send_invoice(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. + + Warning: + As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order + of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. - Where Chat is the current instance. + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter :attr:`start_parameter` is optional. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_animation(self.id, *args, **kwargs) + return self.bot.send_invoice( + chat_id=self.id, + title=title, + description=description, + payload=payload, + provider_token=provider_token, + currency=currency, + prices=prices, + start_parameter=start_parameter, + photo_url=photo_url, + photo_size=photo_size, + photo_width=photo_width, + photo_height=photo_height, + need_name=need_name, + need_phone_number=need_phone_number, + need_email=need_email, + need_shipping_address=need_shipping_address, + is_flexible=is_flexible, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + provider_data=provider_data, + send_phone_number_to_provider=send_phone_number_to_provider, + send_email_to_provider=send_email_to_provider, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + max_tip_amount=max_tip_amount, + suggested_tip_amounts=suggested_tip_amounts, + protect_content=protect_content, + ) + + def send_location( + self, + latitude: float = None, + longitude: float = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + location: 'Location' = None, + live_period: int = None, + api_kwargs: JSONDict = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_location(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. - def send_sticker(self, *args, **kwargs): + """ + return self.bot.send_location( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + location=location, + live_period=live_period, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_animation( + self, + animation: Union[FileInput, 'Animation'], + duration: int = None, + width: int = None, + height: int = None, + thumb: FileInput = None, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_sticker(Chat.id, *args, **kwargs) + bot.send_animation(update.effective_chat.id, *args, **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_sticker(self.id, *args, **kwargs) + return self.bot.send_animation( + chat_id=self.id, + animation=animation, + duration=duration, + width=width, + height=height, + thumb=thumb, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_sticker( + self, + sticker: Union[FileInput, 'Sticker'], + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: - def send_video(self, *args, **kwargs): + bot.send_sticker(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_sticker( + chat_id=self.id, + sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_venue( + self, + latitude: float = None, + longitude: float = None, + title: str = None, + address: str = None, + foursquare_id: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + venue: 'Venue' = None, + foursquare_type: str = None, + api_kwargs: JSONDict = None, + google_place_id: str = None, + google_place_type: str = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_video(Chat.id, *args, **kwargs) + bot.send_venue(update.effective_chat.id, *args, **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_video(self.id, *args, **kwargs) + return self.bot.send_venue( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + venue=venue, + foursquare_type=foursquare_type, + api_kwargs=api_kwargs, + google_place_id=google_place_id, + google_place_type=google_place_type, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_video( + self, + video: Union[FileInput, 'Video'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + width: int = None, + height: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: bool = None, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_video(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_video( + chat_id=self.id, + video=video, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + width=width, + height=height, + parse_mode=parse_mode, + supports_streaming=supports_streaming, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_video_note( + self, + video_note: Union[FileInput, 'VideoNote'], + duration: int = None, + length: int = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_video_note(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_video_note( + chat_id=self.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + filename=filename, + protect_content=protect_content, + ) + + def send_voice( + self, + voice: Union[FileInput, 'Voice'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: - def send_video_note(self, *args, **kwargs): + bot.send_voice(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_voice( + chat_id=self.id, + voice=voice, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_poll( + self, + question: str, + options: List[str], + is_anonymous: bool = True, + # We use constant.POLL_REGULAR instead of Poll.REGULAR here to avoid circular imports + type: str = constants.POLL_REGULAR, # pylint: disable=W0622 + allows_multiple_answers: bool = False, + correct_option_id: int = None, + is_closed: bool = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + explanation: str = None, + explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, + open_period: int = None, + close_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_video_note(Chat.id, *args, **kwargs) + bot.send_poll(update.effective_chat.id, *args, **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_video_note(self.id, *args, **kwargs) + return self.bot.send_poll( + chat_id=self.id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, # pylint=pylint, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + is_closed=is_closed, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + explanation_entities=explanation_entities, + protect_content=protect_content, + ) + + def send_copy( + self, + from_chat_id: Union[str, int], + message_id: int, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(chat_id=update.effective_chat.id, *args, **kwargs) - def send_voice(self, *args, **kwargs): + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.copy_message( + chat_id=self.id, + from_chat_id=from_chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def copy_message( + self, + chat_id: Union[int, str], + message_id: int, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> 'MessageId': """Shortcut for:: - bot.send_voice(Chat.id, *args, **kwargs) + bot.copy_message(from_chat_id=update.effective_chat.id, *args, **kwargs) - Where Chat is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_voice(self.id, *args, **kwargs) + return self.bot.copy_message( + from_chat_id=self.id, + chat_id=chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def export_invite_link( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> str: + """Shortcut for:: + + bot.export_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.export_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :obj:`str`: New invite link on success. + + """ + return self.bot.export_chat_invite_link( + chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs + ) + + def create_invite_link( + self, + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, + ) -> 'ChatInviteLink': + """Shortcut for:: + + bot.create_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_chat_invite_link`. + + .. versionadded:: 13.4 + + .. versionchanged:: 13.8 + Edited signature according to the changes of + :meth:`telegram.Bot.create_chat_invite_link`. + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return self.bot.create_chat_invite_link( + chat_id=self.id, + expire_date=expire_date, + member_limit=member_limit, + timeout=timeout, + api_kwargs=api_kwargs, + name=name, + creates_join_request=creates_join_request, + ) + + def edit_invite_link( + self, + invite_link: str, + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, + ) -> 'ChatInviteLink': + """Shortcut for:: + + bot.edit_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_chat_invite_link`. + + .. versionadded:: 13.4 + + .. versionchanged:: 13.8 + Edited signature according to the changes of :meth:`telegram.Bot.edit_chat_invite_link`. + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return self.bot.edit_chat_invite_link( + chat_id=self.id, + invite_link=invite_link, + expire_date=expire_date, + member_limit=member_limit, + timeout=timeout, + api_kwargs=api_kwargs, + name=name, + creates_join_request=creates_join_request, + ) + + def revoke_invite_link( + self, + invite_link: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> 'ChatInviteLink': + """Shortcut for:: + + bot.revoke_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.revoke_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return self.bot.revoke_chat_invite_link( + chat_id=self.id, invite_link=invite_link, timeout=timeout, api_kwargs=api_kwargs + ) + + def approve_join_request( + self, + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.approve_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.approve_chat_join_request( + chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def decline_join_request( + self, + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.decline_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.decline_chat_join_request( + chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs + ) diff --git a/telegramer/include/telegram/chataction.py b/telegramer/include/telegram/chataction.py index 419abac..335ab39 100644 --- a/telegramer/include/telegram/chataction.py +++ b/telegramer/include/telegram/chataction.py @@ -2,7 +2,7 @@ # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,28 +18,57 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatAction.""" +from typing import ClassVar +from telegram import constants +from telegram.utils.deprecate import set_new_attribute_deprecated -class ChatAction(object): - """Helper class to provide constants for different chatactions.""" - - FIND_LOCATION = 'find_location' - """:obj:`str`: 'find_location'""" - RECORD_AUDIO = 'record_audio' - """:obj:`str`: 'record_audio'""" - RECORD_VIDEO = 'record_video' - """:obj:`str`: 'record_video'""" - RECORD_VIDEO_NOTE = 'record_video_note' - """:obj:`str`: 'record_video_note'""" - TYPING = 'typing' - """:obj:`str`: 'typing'""" - UPLOAD_AUDIO = 'upload_audio' - """:obj:`str`: 'upload_audio'""" - UPLOAD_DOCUMENT = 'upload_document' - """:obj:`str`: 'upload_document'""" - UPLOAD_PHOTO = 'upload_photo' - """:obj:`str`: 'upload_photo'""" - UPLOAD_VIDEO = 'upload_video' - """:obj:`str`: 'upload_video'""" - UPLOAD_VIDEO_NOTE = 'upload_video_note' - """:obj:`str`: 'upload_video_note'""" +class ChatAction: + """Helper class to provide constants for different chat actions.""" + + __slots__ = ('__dict__',) # Adding __dict__ here since it doesn't subclass TGObject + FIND_LOCATION: ClassVar[str] = constants.CHATACTION_FIND_LOCATION + """:const:`telegram.constants.CHATACTION_FIND_LOCATION`""" + RECORD_AUDIO: ClassVar[str] = constants.CHATACTION_RECORD_AUDIO + """:const:`telegram.constants.CHATACTION_RECORD_AUDIO` + + .. deprecated:: 13.5 + Deprecated by Telegram. Use :attr:`RECORD_VOICE` instead. + """ + RECORD_VOICE: ClassVar[str] = constants.CHATACTION_RECORD_VOICE + """:const:`telegram.constants.CHATACTION_RECORD_VOICE` + + .. versionadded:: 13.5 + """ + RECORD_VIDEO: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO + """:const:`telegram.constants.CHATACTION_RECORD_VIDEO`""" + RECORD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO_NOTE + """:const:`telegram.constants.CHATACTION_RECORD_VIDEO_NOTE`""" + TYPING: ClassVar[str] = constants.CHATACTION_TYPING + """:const:`telegram.constants.CHATACTION_TYPING`""" + UPLOAD_AUDIO: ClassVar[str] = constants.CHATACTION_UPLOAD_AUDIO + """:const:`telegram.constants.CHATACTION_UPLOAD_AUDIO` + + .. deprecated:: 13.5 + Deprecated by Telegram. Use :attr:`UPLOAD_VOICE` instead. + """ + UPLOAD_VOICE: ClassVar[str] = constants.CHATACTION_UPLOAD_VOICE + """:const:`telegram.constants.CHATACTION_UPLOAD_VOICE` + + .. versionadded:: 13.5 + """ + UPLOAD_DOCUMENT: ClassVar[str] = constants.CHATACTION_UPLOAD_DOCUMENT + """:const:`telegram.constants.CHATACTION_UPLOAD_DOCUMENT`""" + CHOOSE_STICKER: ClassVar[str] = constants.CHATACTION_CHOOSE_STICKER + """:const:`telegram.constants.CHOOSE_STICKER` + + .. versionadded:: 13.8""" + UPLOAD_PHOTO: ClassVar[str] = constants.CHATACTION_UPLOAD_PHOTO + """:const:`telegram.constants.CHATACTION_UPLOAD_PHOTO`""" + UPLOAD_VIDEO: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO + """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO`""" + UPLOAD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO_NOTE + """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO_NOTE`""" + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) diff --git a/telegramer/include/telegram/chatinvitelink.py b/telegramer/include/telegram/chatinvitelink.py new file mode 100644 index 0000000..6852b55 --- /dev/null +++ b/telegramer/include/telegram/chatinvitelink.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents an invite link for a chat.""" +import datetime +from typing import TYPE_CHECKING, Any, Optional + +from telegram import TelegramObject, User +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatInviteLink(TelegramObject): + """This object represents an invite link for a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`invite_link`, :attr:`creator`, :attr:`is_primary` and + :attr:`is_revoked` are equal. + + .. versionadded:: 13.4 + + Args: + invite_link (:obj:`str`): The invite link. + creator (:class:`telegram.User`): Creator of the link. + is_primary (:obj:`bool`): :obj:`True`, if the link is primary. + is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. + expire_date (:class:`datetime.datetime`, optional): Date when the link will expire or + has been expired. + member_limit (:obj:`int`, optional): Maximum number of users that can be members of the + chat simultaneously after joining the chat via this invite link; 1-99999. + name (:obj:`str`, optional): Invite link name. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat via + the link need to be approved by chat administrators. + + .. versionadded:: 13.8 + pending_join_request_count (:obj:`int`, optional): Number of pending join requests + created using this link. + + .. versionadded:: 13.8 + + Attributes: + invite_link (:obj:`str`): The invite link. If the link was created by another chat + administrator, then the second part of the link will be replaced with ``'…'``. + creator (:class:`telegram.User`): Creator of the link. + is_primary (:obj:`bool`): :obj:`True`, if the link is primary. + is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. + expire_date (:class:`datetime.datetime`): Optional. Date when the link will expire or + has been expired. + member_limit (:obj:`int`): Optional. Maximum number of users that can be members + of the chat simultaneously after joining the chat via this invite link; 1-99999. + name (:obj:`str`): Optional. Invite link name. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`): Optional. :obj:`True`, if users joining the chat via + the link need to be approved by chat administrators. + + .. versionadded:: 13.8 + pending_join_request_count (:obj:`int`): Optional. Number of pending join requests + created using this link. + + .. versionadded:: 13.8 + + """ + + __slots__ = ( + 'invite_link', + 'creator', + 'is_primary', + 'is_revoked', + 'expire_date', + 'member_limit', + 'name', + 'creates_join_request', + 'pending_join_request_count', + '_id_attrs', + ) + + def __init__( + self, + invite_link: str, + creator: User, + is_primary: bool, + is_revoked: bool, + expire_date: datetime.datetime = None, + member_limit: int = None, + name: str = None, + creates_join_request: bool = None, + pending_join_request_count: int = None, + **_kwargs: Any, + ): + # Required + self.invite_link = invite_link + self.creator = creator + self.is_primary = is_primary + self.is_revoked = is_revoked + + # Optionals + self.expire_date = expire_date + self.member_limit = int(member_limit) if member_limit is not None else None + self.name = name + self.creates_join_request = creates_join_request + self.pending_join_request_count = ( + int(pending_join_request_count) if pending_join_request_count is not None else None + ) + self._id_attrs = (self.invite_link, self.creator, self.is_primary, self.is_revoked) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatInviteLink']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['creator'] = User.de_json(data.get('creator'), bot) + data['expire_date'] = from_timestamp(data.get('expire_date', None)) + + return cls(**data) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + data['expire_date'] = to_timestamp(self.expire_date) + + return data diff --git a/telegramer/include/telegram/chatjoinrequest.py b/telegramer/include/telegram/chatjoinrequest.py new file mode 100644 index 0000000..ec89c66 --- /dev/null +++ b/telegramer/include/telegram/chatjoinrequest.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram ChatJoinRequest.""" +import datetime +from typing import TYPE_CHECKING, Any, Optional + +from telegram import TelegramObject, User, Chat, ChatInviteLink +from telegram.utils.helpers import from_timestamp, to_timestamp, DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatJoinRequest(TelegramObject): + """This object represents a join request sent to a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, :attr:`from_user` and :attr:`date` are equal. + + Note: + Since Bot API 5.5, bots are allowed to contact users who sent a join request to a chat + where the bot is an administrator with the + :attr:`~telegram.ChatMemberAdministrator.can_invite_users` administrator right – even if + the user never interacted with the bot before. + + .. versionadded:: 13.8 + + Args: + chat (:class:`telegram.Chat`): Chat to which the request was sent. + from_user (:class:`telegram.User`): User that sent the join request. + date (:class:`datetime.datetime`): Date the request was sent. + bio (:obj:`str`, optional): Bio of the user. + invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link that was used + by the user to send the join request. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + + Attributes: + chat (:class:`telegram.Chat`): Chat to which the request was sent. + from_user (:class:`telegram.User`): User that sent the join request. + date (:class:`datetime.datetime`): Date the request was sent. + bio (:obj:`str`): Optional. Bio of the user. + invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link that was used + by the user to send the join request. + + """ + + __slots__ = ( + 'chat', + 'from_user', + 'date', + 'bio', + 'invite_link', + 'bot', + '_id_attrs', + ) + + def __init__( + self, + chat: Chat, + from_user: User, + date: datetime.datetime, + bio: str = None, + invite_link: ChatInviteLink = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): + # Required + self.chat = chat + self.from_user = from_user + self.date = date + + # Optionals + self.bio = bio + self.invite_link = invite_link + + self.bot = bot + self._id_attrs = (self.chat, self.from_user, self.date) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatJoinRequest']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['chat'] = Chat.de_json(data.get('chat'), bot) + data['from_user'] = User.de_json(data.get('from'), bot) + data['date'] = from_timestamp(data.get('date', None)) + data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot) + + return cls(bot=bot, **data) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + data['date'] = to_timestamp(self.date) + + return data + + def approve( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.approve_chat_join_request(chat_id=update.effective_chat.id, + user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.approve_chat_join_request( + chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs + ) + + def decline( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.decline_chat_join_request(chat_id=update.effective_chat.id, + user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.decline_chat_join_request( + chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs + ) diff --git a/telegramer/include/telegram/chatlocation.py b/telegramer/include/telegram/chatlocation.py new file mode 100644 index 0000000..f88c708 --- /dev/null +++ b/telegramer/include/telegram/chatlocation.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a location to which a chat is connected.""" + +from typing import TYPE_CHECKING, Any, Optional + +from telegram import TelegramObject +from telegram.utils.types import JSONDict + +from .files.location import Location + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatLocation(TelegramObject): + """This object represents a location to which a chat is connected. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`location` is equal. + + Args: + location (:class:`telegram.Location`): The location to which the supergroup is connected. + Can't be a live location. + address (:obj:`str`): Location address; 1-64 characters, as defined by the chat owner + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + location (:class:`telegram.Location`): The location to which the supergroup is connected. + address (:obj:`str`): Location address, as defined by the chat owner + + """ + + __slots__ = ('location', '_id_attrs', 'address') + + def __init__( + self, + location: Location, + address: str, + **_kwargs: Any, + ): + self.location = location + self.address = address + + self._id_attrs = (self.location,) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatLocation']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['location'] = Location.de_json(data.get('location'), bot) + + return cls(bot=bot, **data) diff --git a/telegramer/include/telegram/chatmember.py b/telegramer/include/telegram/chatmember.py index c39df08..06968d4 100644 --- a/telegramer/include/telegram/chatmember.py +++ b/telegramer/include/telegram/chatmember.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,104 +17,325 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatMember.""" +import datetime +from typing import TYPE_CHECKING, Any, Optional, ClassVar, Dict, Type -from telegram import User, TelegramObject -from telegram.utils.helpers import to_timestamp, from_timestamp +from telegram import TelegramObject, User, constants +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class ChatMember(TelegramObject): - """This object contains information about one member of the chat. + """Base class for Telegram ChatMember Objects. + Currently, the following 6 types of chat members are supported: + + * :class:`telegram.ChatMemberOwner` + * :class:`telegram.ChatMemberAdministrator` + * :class:`telegram.ChatMemberMember` + * :class:`telegram.ChatMemberRestricted` + * :class:`telegram.ChatMemberLeft` + * :class:`telegram.ChatMemberBanned` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`user` and :attr:`status` are equal. + + Note: + As of Bot API 5.3, :class:`ChatMember` is nothing but the base class for the subclasses + listed above and is no longer returned directly by :meth:`~telegram.Bot.get_chat`. + Therefore, most of the arguments and attributes were deprecated and you should no longer + use :class:`ChatMember` directly. + + Args: + user (:class:`telegram.User`): Information about the user. + status (:obj:`str`): The member's status in the chat. Can be + :attr:`~telegram.ChatMember.ADMINISTRATOR`, :attr:`~telegram.ChatMember.CREATOR`, + :attr:`~telegram.ChatMember.KICKED`, :attr:`~telegram.ChatMember.LEFT`, + :attr:`~telegram.ChatMember.MEMBER` or :attr:`~telegram.ChatMember.RESTRICTED`. + custom_title (:obj:`str`, optional): Owner and administrators only. + Custom title for this user. + + .. deprecated:: 13.7 + + is_anonymous (:obj:`bool`, optional): Owner and administrators only. :obj:`True`, if the + user's presence in the chat is hidden. + + .. deprecated:: 13.7 + + until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when + restrictions will be lifted for this user. + + .. deprecated:: 13.7 + + can_be_edited (:obj:`bool`, optional): Administrators only. :obj:`True`, if the bot is + allowed to edit administrator privileges of that user. + + .. deprecated:: 13.7 + + can_manage_chat (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can access the chat event log, chat statistics, message statistics in + channels, see channel members, see anonymous administrators in supergroups and ignore + slow mode. Implied by any other administrator privilege. + + .. versionadded:: 13.4 + .. deprecated:: 13.7 + + can_manage_voice_chats (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can manage voice chats. + + .. versionadded:: 13.4 + .. deprecated:: 13.7 + + can_change_info (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, + if the user can change the chat title, photo and other settings. + + .. deprecated:: 13.7 + + can_post_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can post in the channel, channels only. + + .. deprecated:: 13.7 + + can_edit_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can edit messages of other users and can pin messages; channels only. + + .. deprecated:: 13.7 + + can_delete_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can delete messages of other users. + + .. deprecated:: 13.7 + + can_invite_users (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, + if the user can invite new users to the chat. + + .. deprecated:: 13.7 + + can_restrict_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can restrict, ban or unban chat members. + + .. deprecated:: 13.7 + + can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, + if the user can pin messages, groups and supergroups only. + + .. deprecated:: 13.7 + + can_promote_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can add new administrators with a subset of his own privileges or demote + administrators that he has promoted, directly or indirectly (promoted by administrators + that were appointed by the user). + + .. deprecated:: 13.7 + + is_member (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is a member of + the chat at the moment of the request. + + .. deprecated:: 13.7 + + can_send_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can + send text messages, contacts, locations and venues. + + .. deprecated:: 13.7 + + can_send_media_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user + can send audios, documents, photos, videos, video notes and voice notes. + + .. deprecated:: 13.7 + + can_send_polls (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is + allowed to send polls. + + .. deprecated:: 13.7 + + can_send_other_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user + can send animations, games, stickers and use inline bots. + + .. deprecated:: 13.7 + + can_add_web_page_previews (:obj:`bool`, optional): Restricted only. :obj:`True`, if user + may add web page previews to his messages. + + .. deprecated:: 13.7 Attributes: user (:class:`telegram.User`): Information about the user. status (:obj:`str`): The member's status in the chat. + custom_title (:obj:`str`): Optional. Custom title for owner and administrators. + + .. deprecated:: 13.7 + + is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is + hidden. + + .. deprecated:: 13.7 + until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted for this user. + + .. deprecated:: 13.7 + can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator privileges of that user. - can_change_info (:obj:`bool`): Optional. If the administrator can change the chat title, - photo and other settings. + + .. deprecated:: 13.7 + + can_manage_chat (:obj:`bool`): Optional. If the administrator can access the chat event + log, chat statistics, message statistics in channels, see channel members, see + anonymous administrators in supergroups and ignore slow mode. + + .. versionadded:: 13.4 + .. deprecated:: 13.7 + + can_manage_voice_chats (:obj:`bool`): Optional. if the administrator can manage + voice chats. + + .. versionadded:: 13.4 + .. deprecated:: 13.7 + + can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and + other settings. + + .. deprecated:: 13.7 + can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel. + + .. deprecated:: 13.7 + can_edit_messages (:obj:`bool`): Optional. If the administrator can edit messages of other users. + + .. deprecated:: 13.7 + can_delete_messages (:obj:`bool`): Optional. If the administrator can delete messages of other users. - can_invite_users (:obj:`bool`): Optional. If the administrator can invite new users to the - chat. + + .. deprecated:: 13.7 + + can_invite_users (:obj:`bool`): Optional. If the user can invite new users to the chat. + + .. deprecated:: 13.7 + can_restrict_members (:obj:`bool`): Optional. If the administrator can restrict, ban or unban chat members. - can_pin_messages (:obj:`bool`): Optional. If the administrator can pin messages. + + .. deprecated:: 13.7 + + can_pin_messages (:obj:`bool`): Optional. If the user can pin messages. + + .. deprecated:: 13.7 + can_promote_members (:obj:`bool`): Optional. If the administrator can add new administrators. + + .. deprecated:: 13.7 + + is_member (:obj:`bool`): Optional. Restricted only. :obj:`True`, if the user is a member of + the chat at the moment of the request. + + .. deprecated:: 13.7 + can_send_messages (:obj:`bool`): Optional. If the user can send text messages, contacts, locations and venues. + + .. deprecated:: 13.7 + can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages, implies can_send_messages. + + .. deprecated:: 13.7 + + can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to + send polls. + + .. deprecated:: 13.7 + can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games, stickers and use inline bots, implies can_send_media_messages. + + .. deprecated:: 13.7 + can_add_web_page_previews (:obj:`bool`): Optional. If user may add web page previews to his messages, implies can_send_media_messages - Args: - user (:class:`telegram.User`): Information about the user. - status (:obj:`str`): The member's status in the chat. Can be 'creator', 'administrator', - 'member', 'restricted', 'left' or 'kicked'. - until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when - restrictions will be lifted for this user. - can_be_edited (:obj:`bool`, optional): Administrators only. True, if the bot is allowed to - edit administrator privileges of that user. - can_change_info (:obj:`bool`, optional): Administrators only. True, if the administrator - can change the chat title, photo and other settings. - can_post_messages (:obj:`bool`, optional): Administrators only. True, if the administrator - can post in the channel, channels only. - can_edit_messages (:obj:`bool`, optional): Administrators only. True, if the administrator - can edit messages of other users, channels only. - can_delete_messages (:obj:`bool`, optional): Administrators only. True, if the - administrator can delete messages of other user. - can_invite_users (:obj:`bool`, optional): Administrators only. True, if the administrator - can invite new users to the chat. - can_restrict_members (:obj:`bool`, optional): Administrators only. True, if the - administrator can restrict, ban or unban chat members. - can_pin_messages (:obj:`bool`, optional): Administrators only. True, if the administrator - can pin messages, supergroups only. - can_promote_members (:obj:`bool`, optional): Administrators only. True, if the - administrator can add new administrators with a subset of his own privileges or demote - administrators that he has promoted, directly or indirectly (promoted by administrators - that were appointed by the user). - can_send_messages (:obj:`bool`, optional): Restricted only. True, if the user can send text - messages, contacts, locations and venues. - can_send_media_messages (:obj:`bool`, optional): Restricted only. True, if the user can - send audios, documents, photos, videos, video notes and voice notes, implies - can_send_messages. - can_send_other_messages (:obj:`bool`, optional): Restricted only. True, if the user can - send animations, games, stickers and use inline bots, implies can_send_media_messages. - can_add_web_page_previews (:obj:`bool`, optional): Restricted only. True, if user may add - web page previews to his messages, implies can_send_media_messages. + .. deprecated:: 13.7 """ - ADMINISTRATOR = 'administrator' - """:obj:`str`: 'administrator'""" - CREATOR = 'creator' - """:obj:`str`: 'creator'""" - KICKED = 'kicked' - """:obj:`str`: 'kicked'""" - LEFT = 'left' - """:obj:`str`: 'left'""" - MEMBER = 'member' - """:obj:`str`: 'member'""" - RESTRICTED = 'restricted' - """:obj:`str`: 'restricted'""" - - def __init__(self, user, status, until_date=None, can_be_edited=None, - can_change_info=None, can_post_messages=None, can_edit_messages=None, - can_delete_messages=None, can_invite_users=None, - can_restrict_members=None, can_pin_messages=None, - can_promote_members=None, can_send_messages=None, - can_send_media_messages=None, can_send_other_messages=None, - can_add_web_page_previews=None, **kwargs): + + __slots__ = ( + 'is_member', + 'can_restrict_members', + 'can_delete_messages', + 'custom_title', + 'can_be_edited', + 'can_post_messages', + 'can_send_messages', + 'can_edit_messages', + 'can_send_media_messages', + 'is_anonymous', + 'can_add_web_page_previews', + 'can_send_other_messages', + 'can_invite_users', + 'can_send_polls', + 'user', + 'can_promote_members', + 'status', + 'can_change_info', + 'can_pin_messages', + 'can_manage_chat', + 'can_manage_voice_chats', + 'until_date', + '_id_attrs', + ) + + ADMINISTRATOR: ClassVar[str] = constants.CHATMEMBER_ADMINISTRATOR + """:const:`telegram.constants.CHATMEMBER_ADMINISTRATOR`""" + CREATOR: ClassVar[str] = constants.CHATMEMBER_CREATOR + """:const:`telegram.constants.CHATMEMBER_CREATOR`""" + KICKED: ClassVar[str] = constants.CHATMEMBER_KICKED + """:const:`telegram.constants.CHATMEMBER_KICKED`""" + LEFT: ClassVar[str] = constants.CHATMEMBER_LEFT + """:const:`telegram.constants.CHATMEMBER_LEFT`""" + MEMBER: ClassVar[str] = constants.CHATMEMBER_MEMBER + """:const:`telegram.constants.CHATMEMBER_MEMBER`""" + RESTRICTED: ClassVar[str] = constants.CHATMEMBER_RESTRICTED + """:const:`telegram.constants.CHATMEMBER_RESTRICTED`""" + + def __init__( + self, + user: User, + status: str, + until_date: datetime.datetime = None, + can_be_edited: bool = None, + can_change_info: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_invite_users: bool = None, + can_restrict_members: bool = None, + can_pin_messages: bool = None, + can_promote_members: bool = None, + can_send_messages: bool = None, + can_send_media_messages: bool = None, + can_send_polls: bool = None, + can_send_other_messages: bool = None, + can_add_web_page_previews: bool = None, + is_member: bool = None, + custom_title: str = None, + is_anonymous: bool = None, + can_manage_chat: bool = None, + can_manage_voice_chats: bool = None, + **_kwargs: Any, + ): # Required self.user = user self.status = status + + # Optionals + self.custom_title = custom_title + self.is_anonymous = is_anonymous self.until_date = until_date self.can_be_edited = can_be_edited self.can_change_info = can_change_info @@ -127,26 +348,368 @@ def __init__(self, user, status, until_date=None, can_be_edited=None, self.can_promote_members = can_promote_members self.can_send_messages = can_send_messages self.can_send_media_messages = can_send_media_messages + self.can_send_polls = can_send_polls self.can_send_other_messages = can_send_other_messages self.can_add_web_page_previews = can_add_web_page_previews + self.is_member = is_member + self.can_manage_chat = can_manage_chat + self.can_manage_voice_chats = can_manage_voice_chats self._id_attrs = (self.user, self.status) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMember']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(ChatMember, cls).de_json(data, bot) - data['user'] = User.de_json(data.get('user'), bot) data['until_date'] = from_timestamp(data.get('until_date', None)) + _class_mapping: Dict[str, Type['ChatMember']] = { + cls.CREATOR: ChatMemberOwner, + cls.ADMINISTRATOR: ChatMemberAdministrator, + cls.MEMBER: ChatMemberMember, + cls.RESTRICTED: ChatMemberRestricted, + cls.LEFT: ChatMemberLeft, + cls.KICKED: ChatMemberBanned, + } + + if cls is ChatMember: + return _class_mapping.get(data['status'], cls)(**data, bot=bot) return cls(**data) - def to_dict(self): - data = super(ChatMember, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['until_date'] = to_timestamp(self.until_date) return data + + +class ChatMemberOwner(ChatMember): + """ + Represents a chat member that owns the chat + and has all administrator privileges. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + custom_title (:obj:`str`, optional): Custom title for this user. + is_anonymous (:obj:`bool`, optional): :obj:`True`, if the + user's presence in the chat is hidden. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.CREATOR`. + user (:class:`telegram.User`): Information about the user. + custom_title (:obj:`str`): Optional. Custom title for + this user. + is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's + presence in the chat is hidden. + """ + + __slots__ = () + + def __init__( + self, + user: User, + custom_title: str = None, + is_anonymous: bool = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.CREATOR, + user=user, + custom_title=custom_title, + is_anonymous=is_anonymous, + ) + + +class ChatMemberAdministrator(ChatMember): + """ + Represents a chat member that has some additional privileges. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + can_be_edited (:obj:`bool`, optional): :obj:`True`, if the bot + is allowed to edit administrator privileges of that user. + custom_title (:obj:`str`, optional): Custom title for this user. + is_anonymous (:obj:`bool`, optional): :obj:`True`, if the user's + presence in the chat is hidden. + can_manage_chat (:obj:`bool`, optional): :obj:`True`, if the administrator + can access the chat event log, chat statistics, message statistics in + channels, see channel members, see anonymous administrators in supergroups + and ignore slow mode. Implied by any other administrator privilege. + can_post_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can post in the channel, channels only. + can_edit_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can edit messages of other users and can pin + messages; channels only. + can_delete_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can delete messages of other users. + can_manage_voice_chats (:obj:`bool`, optional): :obj:`True`, if the + administrator can manage voice chats. + can_restrict_members (:obj:`bool`, optional): :obj:`True`, if the + administrator can restrict, ban or unban chat members. + can_promote_members (:obj:`bool`, optional): :obj:`True`, if the administrator + can add new administrators with a subset of his own privileges or demote + administrators that he has promoted, directly or indirectly (promoted by + administrators that were appointed by the user). + can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.ADMINISTRATOR`. + user (:class:`telegram.User`): Information about the user. + can_be_edited (:obj:`bool`): Optional. :obj:`True`, if the bot + is allowed to edit administrator privileges of that user. + custom_title (:obj:`str`): Optional. Custom title for this user. + is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's + presence in the chat is hidden. + can_manage_chat (:obj:`bool`): Optional. :obj:`True`, if the administrator + can access the chat event log, chat statistics, message statistics in + channels, see channel members, see anonymous administrators in supergroups + and ignore slow mode. Implied by any other administrator privilege. + can_post_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can post in the channel, channels only. + can_edit_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can edit messages of other users and can pin + messages; channels only. + can_delete_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can delete messages of other users. + can_manage_voice_chats (:obj:`bool`): Optional. :obj:`True`, if the + administrator can manage voice chats. + can_restrict_members (:obj:`bool`): Optional. :obj:`True`, if the + administrator can restrict, ban or unban chat members. + can_promote_members (:obj:`bool`): Optional. :obj:`True`, if the administrator + can add new administrators with a subset of his own privileges or demote + administrators that he has promoted, directly or indirectly (promoted by + administrators that were appointed by the user). + can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + """ + + __slots__ = () + + def __init__( + self, + user: User, + can_be_edited: bool = None, + custom_title: str = None, + is_anonymous: bool = None, + can_manage_chat: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_manage_voice_chats: bool = None, + can_restrict_members: bool = None, + can_promote_members: bool = None, + can_change_info: bool = None, + can_invite_users: bool = None, + can_pin_messages: bool = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.ADMINISTRATOR, + user=user, + can_be_edited=can_be_edited, + custom_title=custom_title, + is_anonymous=is_anonymous, + can_manage_chat=can_manage_chat, + can_post_messages=can_post_messages, + can_edit_messages=can_edit_messages, + can_delete_messages=can_delete_messages, + can_manage_voice_chats=can_manage_voice_chats, + can_restrict_members=can_restrict_members, + can_promote_members=can_promote_members, + can_change_info=can_change_info, + can_invite_users=can_invite_users, + can_pin_messages=can_pin_messages, + ) + + +class ChatMemberMember(ChatMember): + """ + Represents a chat member that has no additional + privileges or restrictions. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.MEMBER`. + user (:class:`telegram.User`): Information about the user. + + """ + + __slots__ = () + + def __init__(self, user: User, **_kwargs: Any): + super().__init__(status=ChatMember.MEMBER, user=user) + + +class ChatMemberRestricted(ChatMember): + """ + Represents a chat member that is under certain restrictions + in the chat. Supergroups only. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + is_member (:obj:`bool`, optional): :obj:`True`, if the user is a + member of the chat at the moment of the request. + can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send text messages, contacts, locations and venues. + can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send audios, documents, photos, videos, video notes and voice notes. + can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send polls. + can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is + allowed to add web page previews to their messages. + until_date (:class:`datetime.datetime`, optional): Date when restrictions + will be lifted for this user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.RESTRICTED`. + user (:class:`telegram.User`): Information about the user. + is_member (:obj:`bool`): Optional. :obj:`True`, if the user is a + member of the chat at the moment of the request. + can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send text messages, contacts, locations and venues. + can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send audios, documents, photos, videos, video notes and voice notes. + can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send polls. + can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is + allowed to add web page previews to their messages. + until_date (:class:`datetime.datetime`): Optional. Date when restrictions + will be lifted for this user. + + """ + + __slots__ = () + + def __init__( + self, + user: User, + is_member: bool = None, + can_change_info: bool = None, + can_invite_users: bool = None, + can_pin_messages: bool = None, + can_send_messages: bool = None, + can_send_media_messages: bool = None, + can_send_polls: bool = None, + can_send_other_messages: bool = None, + can_add_web_page_previews: bool = None, + until_date: datetime.datetime = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.RESTRICTED, + user=user, + is_member=is_member, + can_change_info=can_change_info, + can_invite_users=can_invite_users, + can_pin_messages=can_pin_messages, + can_send_messages=can_send_messages, + can_send_media_messages=can_send_media_messages, + can_send_polls=can_send_polls, + can_send_other_messages=can_send_other_messages, + can_add_web_page_previews=can_add_web_page_previews, + until_date=until_date, + ) + + +class ChatMemberLeft(ChatMember): + """ + Represents a chat member that isn't currently a member of the chat, + but may join it themselves. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.LEFT`. + user (:class:`telegram.User`): Information about the user. + """ + + __slots__ = () + + def __init__(self, user: User, **_kwargs: Any): + super().__init__(status=ChatMember.LEFT, user=user) + + +class ChatMemberBanned(ChatMember): + """ + Represents a chat member that was banned in the chat and + can't return to the chat or view chat messages. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`, optional): Date when restrictions + will be lifted for this user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.KICKED`. + user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`): Optional. Date when restrictions + will be lifted for this user. + + """ + + __slots__ = () + + def __init__( + self, + user: User, + until_date: datetime.datetime = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.KICKED, + user=user, + until_date=until_date, + ) diff --git a/telegramer/include/telegram/chatmemberupdated.py b/telegramer/include/telegram/chatmemberupdated.py new file mode 100644 index 0000000..cd6c76e --- /dev/null +++ b/telegramer/include/telegram/chatmemberupdated.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram ChatMemberUpdated.""" +import datetime +from typing import TYPE_CHECKING, Any, Optional, Dict, Tuple, Union + +from telegram import TelegramObject, User, Chat, ChatMember, ChatInviteLink +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatMemberUpdated(TelegramObject): + """This object represents changes in the status of a chat member. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, :attr:`from_user`, :attr:`date`, + :attr:`old_chat_member` and :attr:`new_chat_member` are equal. + + .. versionadded:: 13.4 + + Note: + In Python ``from`` is a reserved word, use ``from_user`` instead. + + Args: + chat (:class:`telegram.Chat`): Chat the user belongs to. + from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. + date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to + :class:`datetime.datetime`. + old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. + new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. + invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link, which was used + by the user to join the chat. For joining by invite link events only. + + Attributes: + chat (:class:`telegram.Chat`): Chat the user belongs to. + from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. + date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to + :class:`datetime.datetime`. + old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. + new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. + invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link, which was used + by the user to join the chat. + + """ + + __slots__ = ( + 'chat', + 'from_user', + 'date', + 'old_chat_member', + 'new_chat_member', + 'invite_link', + '_id_attrs', + ) + + def __init__( + self, + chat: Chat, + from_user: User, + date: datetime.datetime, + old_chat_member: ChatMember, + new_chat_member: ChatMember, + invite_link: ChatInviteLink = None, + **_kwargs: Any, + ): + # Required + self.chat = chat + self.from_user = from_user + self.date = date + self.old_chat_member = old_chat_member + self.new_chat_member = new_chat_member + + # Optionals + self.invite_link = invite_link + + self._id_attrs = ( + self.chat, + self.from_user, + self.date, + self.old_chat_member, + self.new_chat_member, + ) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMemberUpdated']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['chat'] = Chat.de_json(data.get('chat'), bot) + data['from_user'] = User.de_json(data.get('from'), bot) + data['date'] = from_timestamp(data.get('date')) + data['old_chat_member'] = ChatMember.de_json(data.get('old_chat_member'), bot) + data['new_chat_member'] = ChatMember.de_json(data.get('new_chat_member'), bot) + data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot) + + return cls(**data) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + # Required + data['date'] = to_timestamp(self.date) + + return data + + def difference( + self, + ) -> Dict[ + str, + Tuple[ + Union[str, bool, datetime.datetime, User], Union[str, bool, datetime.datetime, User] + ], + ]: + """Computes the difference between :attr:`old_chat_member` and :attr:`new_chat_member`. + + Example: + .. code:: python + + >>> chat_member_updated.difference() + {'custom_title': ('old title', 'new title')} + + Note: + To determine, if the :attr:`telegram.ChatMember.user` attribute has changed, *every* + attribute of the user will be checked. + + .. versionadded:: 13.5 + + Returns: + Dict[:obj:`str`, Tuple[:obj:`obj`, :obj:`obj`]]: A dictionary mapping attribute names + to tuples of the form ``(old_value, new_value)`` + """ + # we first get the names of the attributes that have changed + # user.to_dict() is unhashable, so that needs some special casing further down + old_dict = self.old_chat_member.to_dict() + old_user_dict = old_dict.pop('user') + new_dict = self.new_chat_member.to_dict() + new_user_dict = new_dict.pop('user') + + # Generator for speed: we only need to iterate over it once + # we can't directly use the values from old_dict ^ new_dict b/c that set is unordered + attributes = (entry[0] for entry in set(old_dict.items()) ^ set(new_dict.items())) + + result = { + attribute: (self.old_chat_member[attribute], self.new_chat_member[attribute]) + for attribute in attributes + } + if old_user_dict != new_user_dict: + result['user'] = (self.old_chat_member.user, self.new_chat_member.user) + + return result # type: ignore[return-value] diff --git a/telegramer/include/telegram/chatpermissions.py b/telegramer/include/telegram/chatpermissions.py new file mode 100644 index 0000000..44b989a --- /dev/null +++ b/telegramer/include/telegram/chatpermissions.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram ChatPermission.""" + +from typing import Any + +from telegram import TelegramObject + + +class ChatPermissions(TelegramObject): + """Describes actions that a non-administrator user is allowed to take in a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`, + :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`, + :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages` are equal. + + Note: + Though not stated explicitly in the official docs, Telegram changes not only the + permissions that are set, but also sets all the others to :obj:`False`. However, since not + documented, this behaviour may change unbeknown to PTB. + + Args: + can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text + messages, contacts, locations and venues. + can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to + send audios, documents, photos, videos, video notes and voice notes, implies + :attr:`can_send_messages`. + can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send polls, + implies :attr:`can_send_messages`. + can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to + send animations, games, stickers and use inline bots, implies + :attr:`can_send_media_messages`. + can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is allowed to + add web page previews to their messages, implies :attr:`can_send_media_messages`. + can_change_info (:obj:`bool`, optional): :obj:`True`, if the user is allowed to change the + chat title, photo and other settings. Ignored in public supergroups. + can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user is allowed to invite new + users to the chat. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin + messages. Ignored in public supergroups. + + Attributes: + can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text + messages, contacts, locations and venues. + can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to + send audios, documents, photos, videos, video notes and voice notes, implies + :attr:`can_send_messages`. + can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send polls, + implies :attr:`can_send_messages`. + can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to + send animations, games, stickers and use inline bots, implies + :attr:`can_send_media_messages`. + can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to + add web page previews to their messages, implies :attr:`can_send_media_messages`. + can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to change the + chat title, photo and other settings. Ignored in public supergroups. + can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to invite + new users to the chat. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin + messages. Ignored in public supergroups. + + """ + + __slots__ = ( + 'can_send_other_messages', + 'can_invite_users', + 'can_send_polls', + '_id_attrs', + 'can_send_messages', + 'can_send_media_messages', + 'can_change_info', + 'can_pin_messages', + 'can_add_web_page_previews', + ) + + def __init__( + self, + can_send_messages: bool = None, + can_send_media_messages: bool = None, + can_send_polls: bool = None, + can_send_other_messages: bool = None, + can_add_web_page_previews: bool = None, + can_change_info: bool = None, + can_invite_users: bool = None, + can_pin_messages: bool = None, + **_kwargs: Any, + ): + # Required + self.can_send_messages = can_send_messages + self.can_send_media_messages = can_send_media_messages + self.can_send_polls = can_send_polls + self.can_send_other_messages = can_send_other_messages + self.can_add_web_page_previews = can_add_web_page_previews + self.can_change_info = can_change_info + self.can_invite_users = can_invite_users + self.can_pin_messages = can_pin_messages + + self._id_attrs = ( + self.can_send_messages, + self.can_send_media_messages, + self.can_send_polls, + self.can_send_other_messages, + self.can_add_web_page_previews, + self.can_change_info, + self.can_invite_users, + self.can_pin_messages, + ) diff --git a/telegramer/include/telegram/choseninlineresult.py b/telegramer/include/telegram/choseninlineresult.py index f25f913..b931553 100644 --- a/telegramer/include/telegram/choseninlineresult.py +++ b/telegramer/include/telegram/choseninlineresult.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -# pylint: disable=R0902,R0912,R0913 +# pylint: disable=R0902,R0913 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,7 +19,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChosenInlineResult.""" -from telegram import TelegramObject, User, Location +from typing import TYPE_CHECKING, Any, Optional + +from telegram import Location, TelegramObject, User +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class ChosenInlineResult(TelegramObject): @@ -27,15 +33,13 @@ class ChosenInlineResult(TelegramObject): Represents a result of an inline query that was chosen by the user and sent to their chat partner. - Note: - In Python `from` is a reserved word, use `from_user` instead. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`result_id` is equal. - Attributes: - result_id (:obj:`str`): The unique identifier for the result that was chosen. - from_user (:class:`telegram.User`): The user that chose the result. - location (:class:`telegram.Location`): Optional. Sender location. - inline_message_id (:obj:`str`): Optional. Identifier of the sent inline message. - query (:obj:`str`): The query that was used to obtain the result. + Note: + * In Python ``from`` is a reserved word, use ``from_user`` instead. + * It is necessary to enable inline feedback via `@Botfather `_ in + order to receive these objects in updates. Args: result_id (:obj:`str`): The unique identifier for the result that was chosen. @@ -48,15 +52,26 @@ class ChosenInlineResult(TelegramObject): query (:obj:`str`): The query that was used to obtain the result. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + result_id (:obj:`str`): The unique identifier for the result that was chosen. + from_user (:class:`telegram.User`): The user that chose the result. + location (:class:`telegram.Location`): Optional. Sender location. + inline_message_id (:obj:`str`): Optional. Identifier of the sent inline message. + query (:obj:`str`): The query that was used to obtain the result. + """ - def __init__(self, - result_id, - from_user, - query, - location=None, - inline_message_id=None, - **kwargs): + __slots__ = ('location', 'result_id', 'from_user', 'inline_message_id', '_id_attrs', 'query') + + def __init__( + self, + result_id: str, + from_user: User, + query: str, + location: Location = None, + inline_message_id: str = None, + **_kwargs: Any, + ): # Required self.result_id = result_id self.from_user = from_user @@ -68,11 +83,13 @@ def __init__(self, self._id_attrs = (self.result_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChosenInlineResult']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(ChosenInlineResult, cls).de_json(data, bot) # Required data['from_user'] = User.de_json(data.pop('from'), bot) # Optionals diff --git a/telegramer/include/telegram/constants.py b/telegramer/include/telegram/constants.py index d386382..b7600a7 100644 --- a/telegramer/include/telegram/constants.py +++ b/telegramer/include/telegram/constants.py @@ -1,5 +1,5 @@ # python-telegram-bot - a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # by the python-telegram-bot contributors # # This program is free software: you can redistribute it and/or modify @@ -17,38 +17,382 @@ """Constants in the Telegram network. The following constants were extracted from the -`Telegram Bots FAQ `_. +`Telegram Bots FAQ `_ and +`Telegram Bots API `_. Attributes: + BOT_API_VERSION (:obj:`str`): `5.7`. Telegram Bot API version supported by this + version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. + + .. versionadded:: 13.4 MAX_MESSAGE_LENGTH (:obj:`int`): 4096 - MAX_CAPTION_LENGTH (:obj:`int`): 200 + MAX_CAPTION_LENGTH (:obj:`int`): 1024 SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443] MAX_FILESIZE_DOWNLOAD (:obj:`int`): In bytes (20MB) MAX_FILESIZE_UPLOAD (:obj:`int`): In bytes (50MB) + MAX_PHOTOSIZE_UPLOAD (:obj:`int`): In bytes (10MB) MAX_MESSAGES_PER_SECOND_PER_CHAT (:obj:`int`): `1`. Telegram may allow short bursts that go over this limit, but eventually you'll begin receiving 429 errors. MAX_MESSAGES_PER_SECOND (:obj:`int`): 30 MAX_MESSAGES_PER_MINUTE_PER_GROUP (:obj:`int`): 20 MAX_INLINE_QUERY_RESULTS (:obj:`int`): 50 + MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH (:obj:`int`): 200 + + .. versionadded:: 13.2 The following constant have been found by experimentation: Attributes: MAX_MESSAGE_ENTITIES (:obj:`int`): 100 (Beyond this cap telegram will simply ignore further formatting styles) + ANONYMOUS_ADMIN_ID (:obj:`int`): ``1087968824`` (User id in groups for anonymous admin) + SERVICE_CHAT_ID (:obj:`int`): ``777000`` (Telegram service chat, that also acts as sender of + channel posts forwarded to discussion groups) + FAKE_CHANNEL_ID (:obj:`int`): ``136817688`` (User id in groups when message is sent on behalf + of a channel). + + .. versionadded:: 13.9 + +The following constants are related to specific classes and are also available +as attributes of those classes: + +:class:`telegram.Chat`: + +Attributes: + CHAT_PRIVATE (:obj:`str`): ``'private'`` + CHAT_GROUP (:obj:`str`): ``'group'`` + CHAT_SUPERGROUP (:obj:`str`): ``'supergroup'`` + CHAT_CHANNEL (:obj:`str`): ``'channel'`` + CHAT_SENDER (:obj:`str`): ``'sender'``. Only relevant for + :attr:`telegram.InlineQuery.chat_type`. + + .. versionadded:: 13.5 + +:class:`telegram.ChatAction`: + +Attributes: + CHATACTION_FIND_LOCATION (:obj:`str`): ``'find_location'`` + CHATACTION_RECORD_AUDIO (:obj:`str`): ``'record_audio'`` + + .. deprecated:: 13.5 + Deprecated by Telegram. Use :const:`CHATACTION_RECORD_VOICE` instead. + CHATACTION_RECORD_VOICE (:obj:`str`): ``'record_voice'`` + + .. versionadded:: 13.5 + CHATACTION_RECORD_VIDEO (:obj:`str`): ``'record_video'`` + CHATACTION_RECORD_VIDEO_NOTE (:obj:`str`): ``'record_video_note'`` + CHATACTION_TYPING (:obj:`str`): ``'typing'`` + CHATACTION_UPLOAD_AUDIO (:obj:`str`): ``'upload_audio'`` + + .. deprecated:: 13.5 + Deprecated by Telegram. Use :const:`CHATACTION_UPLOAD_VOICE` instead. + CHATACTION_UPLOAD_VOICE (:obj:`str`): ``'upload_voice'`` + + .. versionadded:: 13.5 + CHATACTION_UPLOAD_DOCUMENT (:obj:`str`): ``'upload_document'`` + CHATACTION_CHOOSE_STICKER (:obj:`str`): ``'choose_sticker'`` + + .. versionadded:: 13.8 + CHATACTION_UPLOAD_PHOTO (:obj:`str`): ``'upload_photo'`` + CHATACTION_UPLOAD_VIDEO (:obj:`str`): ``'upload_video'`` + CHATACTION_UPLOAD_VIDEO_NOTE (:obj:`str`): ``'upload_video_note'`` + +:class:`telegram.ChatMember`: + +Attributes: + CHATMEMBER_ADMINISTRATOR (:obj:`str`): ``'administrator'`` + CHATMEMBER_CREATOR (:obj:`str`): ``'creator'`` + CHATMEMBER_KICKED (:obj:`str`): ``'kicked'`` + CHATMEMBER_LEFT (:obj:`str`): ``'left'`` + CHATMEMBER_MEMBER (:obj:`str`): ``'member'`` + CHATMEMBER_RESTRICTED (:obj:`str`): ``'restricted'`` + +:class:`telegram.Dice`: + +Attributes: + DICE_DICE (:obj:`str`): ``'🎲'`` + DICE_DARTS (:obj:`str`): ``'🎯'`` + DICE_BASKETBALL (:obj:`str`): ``'🏀'`` + DICE_FOOTBALL (:obj:`str`): ``'⚽'`` + DICE_SLOT_MACHINE (:obj:`str`): ``'🎰'`` + DICE_BOWLING (:obj:`str`): ``'🎳'`` + + .. versionadded:: 13.4 + DICE_ALL_EMOJI (List[:obj:`str`]): List of all supported base emoji. + + .. versionchanged:: 13.4 + Added :attr:`DICE_BOWLING` + +:class:`telegram.MessageEntity`: + +Attributes: + MESSAGEENTITY_MENTION (:obj:`str`): ``'mention'`` + MESSAGEENTITY_HASHTAG (:obj:`str`): ``'hashtag'`` + MESSAGEENTITY_CASHTAG (:obj:`str`): ``'cashtag'`` + MESSAGEENTITY_PHONE_NUMBER (:obj:`str`): ``'phone_number'`` + MESSAGEENTITY_BOT_COMMAND (:obj:`str`): ``'bot_command'`` + MESSAGEENTITY_URL (:obj:`str`): ``'url'`` + MESSAGEENTITY_EMAIL (:obj:`str`): ``'email'`` + MESSAGEENTITY_BOLD (:obj:`str`): ``'bold'`` + MESSAGEENTITY_ITALIC (:obj:`str`): ``'italic'`` + MESSAGEENTITY_CODE (:obj:`str`): ``'code'`` + MESSAGEENTITY_PRE (:obj:`str`): ``'pre'`` + MESSAGEENTITY_TEXT_LINK (:obj:`str`): ``'text_link'`` + MESSAGEENTITY_TEXT_MENTION (:obj:`str`): ``'text_mention'`` + MESSAGEENTITY_UNDERLINE (:obj:`str`): ``'underline'`` + MESSAGEENTITY_STRIKETHROUGH (:obj:`str`): ``'strikethrough'`` + MESSAGEENTITY_SPOILER (:obj:`str`): ``'spoiler'`` + + .. versionadded:: 13.10 + MESSAGEENTITY_ALL_TYPES (List[:obj:`str`]): List of all the types of message entity. + +:class:`telegram.ParseMode`: + +Attributes: + PARSEMODE_MARKDOWN (:obj:`str`): ``'Markdown'`` + PARSEMODE_MARKDOWN_V2 (:obj:`str`): ``'MarkdownV2'`` + PARSEMODE_HTML (:obj:`str`): ``'HTML'`` + +:class:`telegram.Poll`: + +Attributes: + POLL_REGULAR (:obj:`str`): ``'regular'`` + POLL_QUIZ (:obj:`str`): ``'quiz'`` + MAX_POLL_QUESTION_LENGTH (:obj:`int`): 300 + MAX_POLL_OPTION_LENGTH (:obj:`int`): 100 + +:class:`telegram.MaskPosition`: + +Attributes: + STICKER_FOREHEAD (:obj:`str`): ``'forehead'`` + STICKER_EYES (:obj:`str`): ``'eyes'`` + STICKER_MOUTH (:obj:`str`): ``'mouth'`` + STICKER_CHIN (:obj:`str`): ``'chin'`` + +:class:`telegram.Update`: + +Attributes: + UPDATE_MESSAGE (:obj:`str`): ``'message'`` + + .. versionadded:: 13.5 + UPDATE_EDITED_MESSAGE (:obj:`str`): ``'edited_message'`` + + .. versionadded:: 13.5 + UPDATE_CHANNEL_POST (:obj:`str`): ``'channel_post'`` + + .. versionadded:: 13.5 + UPDATE_EDITED_CHANNEL_POST (:obj:`str`): ``'edited_channel_post'`` + + .. versionadded:: 13.5 + UPDATE_INLINE_QUERY (:obj:`str`): ``'inline_query'`` + + .. versionadded:: 13.5 + UPDATE_CHOSEN_INLINE_RESULT (:obj:`str`): ``'chosen_inline_result'`` + + .. versionadded:: 13.5 + UPDATE_CALLBACK_QUERY (:obj:`str`): ``'callback_query'`` + + .. versionadded:: 13.5 + UPDATE_SHIPPING_QUERY (:obj:`str`): ``'shipping_query'`` + + .. versionadded:: 13.5 + UPDATE_PRE_CHECKOUT_QUERY (:obj:`str`): ``'pre_checkout_query'`` + + .. versionadded:: 13.5 + UPDATE_POLL (:obj:`str`): ``'poll'`` + + .. versionadded:: 13.5 + UPDATE_POLL_ANSWER (:obj:`str`): ``'poll_answer'`` + + .. versionadded:: 13.5 + UPDATE_MY_CHAT_MEMBER (:obj:`str`): ``'my_chat_member'`` + + .. versionadded:: 13.5 + UPDATE_CHAT_MEMBER (:obj:`str`): ``'chat_member'`` + + .. versionadded:: 13.5 + UPDATE_CHAT_JOIN_REQUEST (:obj:`str`): ``'chat_join_request'`` + + .. versionadded:: 13.8 + UPDATE_ALL_TYPES (List[:obj:`str`]): List of all update types. + + .. versionadded:: 13.5 + .. versionchanged:: 13.8 + +:class:`telegram.BotCommandScope`: + +Attributes: + BOT_COMMAND_SCOPE_DEFAULT (:obj:`str`): ``'default'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS (:obj:`str`): ``'all_private_chats'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_ALL_GROUP_CHATS (:obj:`str`): ``'all_group_chats'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS (:obj:`str`): ``'all_chat_administrators'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_CHAT (:obj:`str`): ``'chat'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS (:obj:`str`): ``'chat_administrators'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_CHAT_MEMBER (:obj:`str`): ``'chat_member'`` + + ..versionadded:: 13.7 """ +from typing import List -MAX_MESSAGE_LENGTH = 4096 -MAX_CAPTION_LENGTH = 200 +BOT_API_VERSION: str = '5.7' +MAX_MESSAGE_LENGTH: int = 4096 +MAX_CAPTION_LENGTH: int = 1024 +ANONYMOUS_ADMIN_ID: int = 1087968824 +SERVICE_CHAT_ID: int = 777000 +FAKE_CHANNEL_ID: int = 136817688 # constants above this line are tested -SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443] -MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB) -MAX_FILESIZE_UPLOAD = int(50E6) # (50MB) -MAX_MESSAGES_PER_SECOND_PER_CHAT = 1 -MAX_MESSAGES_PER_SECOND = 30 -MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20 -MAX_MESSAGE_ENTITIES = 100 -MAX_INLINE_QUERY_RESULTS = 50 +SUPPORTED_WEBHOOK_PORTS: List[int] = [443, 80, 88, 8443] +MAX_FILESIZE_DOWNLOAD: int = int(20e6) # (20MB) +MAX_FILESIZE_UPLOAD: int = int(50e6) # (50MB) +MAX_PHOTOSIZE_UPLOAD: int = int(10e6) # (10MB) +MAX_MESSAGES_PER_SECOND_PER_CHAT: int = 1 +MAX_MESSAGES_PER_SECOND: int = 30 +MAX_MESSAGES_PER_MINUTE_PER_GROUP: int = 20 +MAX_MESSAGE_ENTITIES: int = 100 +MAX_INLINE_QUERY_RESULTS: int = 50 +MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH: int = 200 + +CHAT_SENDER: str = 'sender' +CHAT_PRIVATE: str = 'private' +CHAT_GROUP: str = 'group' +CHAT_SUPERGROUP: str = 'supergroup' +CHAT_CHANNEL: str = 'channel' + +CHATACTION_FIND_LOCATION: str = 'find_location' +CHATACTION_RECORD_AUDIO: str = 'record_audio' +CHATACTION_RECORD_VOICE: str = 'record_voice' +CHATACTION_RECORD_VIDEO: str = 'record_video' +CHATACTION_RECORD_VIDEO_NOTE: str = 'record_video_note' +CHATACTION_TYPING: str = 'typing' +CHATACTION_UPLOAD_AUDIO: str = 'upload_audio' +CHATACTION_UPLOAD_VOICE: str = 'upload_voice' +CHATACTION_UPLOAD_DOCUMENT: str = 'upload_document' +CHATACTION_CHOOSE_STICKER: str = 'choose_sticker' +CHATACTION_UPLOAD_PHOTO: str = 'upload_photo' +CHATACTION_UPLOAD_VIDEO: str = 'upload_video' +CHATACTION_UPLOAD_VIDEO_NOTE: str = 'upload_video_note' + +CHATMEMBER_ADMINISTRATOR: str = 'administrator' +CHATMEMBER_CREATOR: str = 'creator' +CHATMEMBER_KICKED: str = 'kicked' +CHATMEMBER_LEFT: str = 'left' +CHATMEMBER_MEMBER: str = 'member' +CHATMEMBER_RESTRICTED: str = 'restricted' + +DICE_DICE: str = '🎲' +DICE_DARTS: str = '🎯' +DICE_BASKETBALL: str = '🏀' +DICE_FOOTBALL: str = '⚽' +DICE_SLOT_MACHINE: str = '🎰' +DICE_BOWLING: str = '🎳' +DICE_ALL_EMOJI: List[str] = [ + DICE_DICE, + DICE_DARTS, + DICE_BASKETBALL, + DICE_FOOTBALL, + DICE_SLOT_MACHINE, + DICE_BOWLING, +] + +MESSAGEENTITY_MENTION: str = 'mention' +MESSAGEENTITY_HASHTAG: str = 'hashtag' +MESSAGEENTITY_CASHTAG: str = 'cashtag' +MESSAGEENTITY_PHONE_NUMBER: str = 'phone_number' +MESSAGEENTITY_BOT_COMMAND: str = 'bot_command' +MESSAGEENTITY_URL: str = 'url' +MESSAGEENTITY_EMAIL: str = 'email' +MESSAGEENTITY_BOLD: str = 'bold' +MESSAGEENTITY_ITALIC: str = 'italic' +MESSAGEENTITY_CODE: str = 'code' +MESSAGEENTITY_PRE: str = 'pre' +MESSAGEENTITY_TEXT_LINK: str = 'text_link' +MESSAGEENTITY_TEXT_MENTION: str = 'text_mention' +MESSAGEENTITY_UNDERLINE: str = 'underline' +MESSAGEENTITY_STRIKETHROUGH: str = 'strikethrough' +MESSAGEENTITY_SPOILER: str = 'spoiler' +MESSAGEENTITY_ALL_TYPES: List[str] = [ + MESSAGEENTITY_MENTION, + MESSAGEENTITY_HASHTAG, + MESSAGEENTITY_CASHTAG, + MESSAGEENTITY_PHONE_NUMBER, + MESSAGEENTITY_BOT_COMMAND, + MESSAGEENTITY_URL, + MESSAGEENTITY_EMAIL, + MESSAGEENTITY_BOLD, + MESSAGEENTITY_ITALIC, + MESSAGEENTITY_CODE, + MESSAGEENTITY_PRE, + MESSAGEENTITY_TEXT_LINK, + MESSAGEENTITY_TEXT_MENTION, + MESSAGEENTITY_UNDERLINE, + MESSAGEENTITY_STRIKETHROUGH, + MESSAGEENTITY_SPOILER, +] + +PARSEMODE_MARKDOWN: str = 'Markdown' +PARSEMODE_MARKDOWN_V2: str = 'MarkdownV2' +PARSEMODE_HTML: str = 'HTML' + +POLL_REGULAR: str = 'regular' +POLL_QUIZ: str = 'quiz' +MAX_POLL_QUESTION_LENGTH: int = 300 +MAX_POLL_OPTION_LENGTH: int = 100 + +STICKER_FOREHEAD: str = 'forehead' +STICKER_EYES: str = 'eyes' +STICKER_MOUTH: str = 'mouth' +STICKER_CHIN: str = 'chin' + +UPDATE_MESSAGE = 'message' +UPDATE_EDITED_MESSAGE = 'edited_message' +UPDATE_CHANNEL_POST = 'channel_post' +UPDATE_EDITED_CHANNEL_POST = 'edited_channel_post' +UPDATE_INLINE_QUERY = 'inline_query' +UPDATE_CHOSEN_INLINE_RESULT = 'chosen_inline_result' +UPDATE_CALLBACK_QUERY = 'callback_query' +UPDATE_SHIPPING_QUERY = 'shipping_query' +UPDATE_PRE_CHECKOUT_QUERY = 'pre_checkout_query' +UPDATE_POLL = 'poll' +UPDATE_POLL_ANSWER = 'poll_answer' +UPDATE_MY_CHAT_MEMBER = 'my_chat_member' +UPDATE_CHAT_MEMBER = 'chat_member' +UPDATE_CHAT_JOIN_REQUEST = 'chat_join_request' +UPDATE_ALL_TYPES = [ + UPDATE_MESSAGE, + UPDATE_EDITED_MESSAGE, + UPDATE_CHANNEL_POST, + UPDATE_EDITED_CHANNEL_POST, + UPDATE_INLINE_QUERY, + UPDATE_CHOSEN_INLINE_RESULT, + UPDATE_CALLBACK_QUERY, + UPDATE_SHIPPING_QUERY, + UPDATE_PRE_CHECKOUT_QUERY, + UPDATE_POLL, + UPDATE_POLL_ANSWER, + UPDATE_MY_CHAT_MEMBER, + UPDATE_CHAT_MEMBER, + UPDATE_CHAT_JOIN_REQUEST, +] + +BOT_COMMAND_SCOPE_DEFAULT = 'default' +BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS = 'all_private_chats' +BOT_COMMAND_SCOPE_ALL_GROUP_CHATS = 'all_group_chats' +BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS = 'all_chat_administrators' +BOT_COMMAND_SCOPE_CHAT = 'chat' +BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS = 'chat_administrators' +BOT_COMMAND_SCOPE_CHAT_MEMBER = 'chat_member' diff --git a/telegramer/include/telegram/dice.py b/telegramer/include/telegram/dice.py new file mode 100644 index 0000000..8836529 --- /dev/null +++ b/telegramer/include/telegram/dice.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram Dice.""" +from typing import Any, List, ClassVar + +from telegram import TelegramObject, constants + + +class Dice(TelegramObject): + """ + This object represents an animated emoji with a random value for currently supported base + emoji. (The singular form of "dice" is "die". However, PTB mimics the Telegram API, which uses + the term "dice".) + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`value` and :attr:`emoji` are equal. + + Note: + If :attr:`emoji` is "🎯", a value of 6 currently represents a bullseye, while a value of 1 + indicates that the dartboard was missed. However, this behaviour is undocumented and might + be changed by Telegram. + + If :attr:`emoji` is "🏀", a value of 4 or 5 currently score a basket, while a value of 1 to + 3 indicates that the basket was missed. However, this behaviour is undocumented and might + be changed by Telegram. + + If :attr:`emoji` is "⚽", a value of 4 to 5 currently scores a goal, while a value of 1 to + 3 indicates that the goal was missed. However, this behaviour is undocumented and might + be changed by Telegram. + + If :attr:`emoji` is "🎳", a value of 6 knocks all the pins, while a value of 1 means all + the pins were missed. However, this behaviour is undocumented and might be changed by + Telegram. + + If :attr:`emoji` is "🎰", each value corresponds to a unique combination of symbols, which + can be found at our `wiki `_. However, this behaviour is undocumented + and might be changed by Telegram. + + Args: + value (:obj:`int`): Value of the dice. 1-6 for dice, darts and bowling balls, 1-5 for + basketball and football/soccer ball, 1-64 for slot machine. + emoji (:obj:`str`): Emoji on which the dice throw animation is based. + + Attributes: + value (:obj:`int`): Value of the dice. + emoji (:obj:`str`): Emoji on which the dice throw animation is based. + + """ + + __slots__ = ('emoji', 'value', '_id_attrs') + + def __init__(self, value: int, emoji: str, **_kwargs: Any): + self.value = value + self.emoji = emoji + + self._id_attrs = (self.value, self.emoji) + + DICE: ClassVar[str] = constants.DICE_DICE # skipcq: PTC-W0052 + """:const:`telegram.constants.DICE_DICE`""" + DARTS: ClassVar[str] = constants.DICE_DARTS + """:const:`telegram.constants.DICE_DARTS`""" + BASKETBALL: ClassVar[str] = constants.DICE_BASKETBALL + """:const:`telegram.constants.DICE_BASKETBALL`""" + FOOTBALL: ClassVar[str] = constants.DICE_FOOTBALL + """:const:`telegram.constants.DICE_FOOTBALL`""" + SLOT_MACHINE: ClassVar[str] = constants.DICE_SLOT_MACHINE + """:const:`telegram.constants.DICE_SLOT_MACHINE`""" + BOWLING: ClassVar[str] = constants.DICE_BOWLING + """ + :const:`telegram.constants.DICE_BOWLING` + + .. versionadded:: 13.4 + """ + ALL_EMOJI: ClassVar[List[str]] = constants.DICE_ALL_EMOJI + """:const:`telegram.constants.DICE_ALL_EMOJI`""" diff --git a/telegramer/include/telegram/error.py b/telegramer/include/telegram/error.py index 1ea6ec1..3cf41d5 100644 --- a/telegramer/include/telegram/error.py +++ b/telegramer/include/telegram/error.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,29 +16,36 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0115 """This module contains an object that represents Telegram errors.""" +from typing import Tuple -def _lstrip_str(in_s, lstr): +def _lstrip_str(in_s: str, lstr: str) -> str: """ Args: in_s (:obj:`str`): in string lstr (:obj:`str`): substr to strip from left side Returns: - str: + :obj:`str`: The stripped string. """ if in_s.startswith(lstr): - res = in_s[len(lstr):] + res = in_s[len(lstr) :] else: res = in_s return res class TelegramError(Exception): - def __init__(self, message): - super(TelegramError, self).__init__() + """Base class for Telegram errors.""" + + # Apparently the base class Exception already has __dict__ in it, so its not included here + __slots__ = ('message',) + + def __init__(self, message: str): + super().__init__() msg = _lstrip_str(message, 'Error: ') msg = _lstrip_str(msg, '[Error]: ') @@ -48,55 +55,97 @@ def __init__(self, message): msg = msg.capitalize() self.message = msg - def __str__(self): - return '%s' % (self.message) + def __str__(self) -> str: + return '%s' % self.message + + def __reduce__(self) -> Tuple[type, Tuple[str]]: + return self.__class__, (self.message,) class Unauthorized(TelegramError): - pass + """Raised when the bot has not enough rights to perform the requested action.""" + + __slots__ = () class InvalidToken(TelegramError): + """Raised when the token is invalid.""" + + __slots__ = () - def __init__(self): - super(InvalidToken, self).__init__('Invalid token') + def __init__(self) -> None: + super().__init__('Invalid token') + + def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override] + return self.__class__, () class NetworkError(TelegramError): - pass + """Base class for exceptions due to networking errors.""" + + __slots__ = () class BadRequest(NetworkError): - pass + """Raised when Telegram could not process the request correctly.""" + + __slots__ = () class TimedOut(NetworkError): + """Raised when a request took too long to finish.""" + + __slots__ = () + + def __init__(self) -> None: + super().__init__('Timed out') - def __init__(self): - super(TimedOut, self).__init__('Timed out') + def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override] + return self.__class__, () class ChatMigrated(TelegramError): """ + Raised when the requested group chat migrated to supergroup and has a new chat id. + Args: - new_chat_id (:obj:`int`): + new_chat_id (:obj:`int`): The new chat id of the group. """ - def __init__(self, new_chat_id): - super(ChatMigrated, - self).__init__('Group migrated to supergroup. New chat id: {}'.format(new_chat_id)) + __slots__ = ('new_chat_id',) + + def __init__(self, new_chat_id: int): + super().__init__(f'Group migrated to supergroup. New chat id: {new_chat_id}') self.new_chat_id = new_chat_id + def __reduce__(self) -> Tuple[type, Tuple[int]]: # type: ignore[override] + return self.__class__, (self.new_chat_id,) + class RetryAfter(TelegramError): """ + Raised when flood limits where exceeded. + Args: - retry_after (:obj:`int`): + retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request. """ - def __init__(self, retry_after): - super(RetryAfter, - self).__init__('Flood control exceeded. Retry in {} seconds'.format(retry_after)) + __slots__ = ('retry_after',) + + def __init__(self, retry_after: int): + super().__init__(f'Flood control exceeded. Retry in {float(retry_after)} seconds') self.retry_after = float(retry_after) + + def __reduce__(self) -> Tuple[type, Tuple[float]]: # type: ignore[override] + return self.__class__, (self.retry_after,) + + +class Conflict(TelegramError): + """Raised when a long poll or webhook conflicts with another one.""" + + __slots__ = () + + def __reduce__(self) -> Tuple[type, Tuple[str]]: + return self.__class__, (self.message,) diff --git a/telegramer/include/telegram/ext/__init__.py b/telegramer/include/telegram/ext/__init__.py index 1c69d0b..1d3a8fe 100644 --- a/telegramer/include/telegram/ext/__init__.py +++ b/telegramer/include/telegram/ext/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,18 +16,37 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0413 """Extensions over the Telegram Bot API to facilitate bot making""" +from .extbot import ExtBot +from .basepersistence import BasePersistence +from .picklepersistence import PicklePersistence +from .dictpersistence import DictPersistence +from .handler import Handler +from .callbackcontext import CallbackContext +from .contexttypes import ContextTypes from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async + +# https://bugs.python.org/issue41451, fixed on 3.7+, doesn't actually remove slots +# try-except is just here in case the __init__ is called twice (like in the tests) +# this block is also the reason for the pylint-ignore at the top of the file +try: + del Dispatcher.__slots__ +except AttributeError as exc: + if str(exc) == '__slots__': + pass + else: + raise exc + from .jobqueue import JobQueue, Job from .updater import Updater from .callbackqueryhandler import CallbackQueryHandler from .choseninlineresulthandler import ChosenInlineResultHandler -from .commandhandler import CommandHandler -from .handler import Handler from .inlinequeryhandler import InlineQueryHandler +from .filters import BaseFilter, MessageFilter, UpdateFilter, Filters from .messagehandler import MessageHandler -from .filters import BaseFilter, Filters +from .commandhandler import CommandHandler, PrefixHandler from .regexhandler import RegexHandler from .stringcommandhandler import StringCommandHandler from .stringregexhandler import StringRegexHandler @@ -37,10 +56,51 @@ from .shippingqueryhandler import ShippingQueryHandler from .messagequeue import MessageQueue from .messagequeue import DelayQueue +from .pollanswerhandler import PollAnswerHandler +from .pollhandler import PollHandler +from .chatmemberhandler import ChatMemberHandler +from .chatjoinrequesthandler import ChatJoinRequestHandler +from .defaults import Defaults +from .callbackdatacache import CallbackDataCache, InvalidCallbackData -__all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler', - 'ChosenInlineResultHandler', 'CommandHandler', 'Handler', 'InlineQueryHandler', - 'MessageHandler', 'BaseFilter', 'Filters', 'RegexHandler', 'StringCommandHandler', - 'StringRegexHandler', 'TypeHandler', 'ConversationHandler', - 'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue', - 'DispatcherHandlerStop', 'run_async') +__all__ = ( + 'BaseFilter', + 'BasePersistence', + 'CallbackContext', + 'CallbackDataCache', + 'CallbackQueryHandler', + 'ChatJoinRequestHandler', + 'ChatMemberHandler', + 'ChosenInlineResultHandler', + 'CommandHandler', + 'ContextTypes', + 'ConversationHandler', + 'Defaults', + 'DelayQueue', + 'DictPersistence', + 'Dispatcher', + 'DispatcherHandlerStop', + 'ExtBot', + 'Filters', + 'Handler', + 'InlineQueryHandler', + 'InvalidCallbackData', + 'Job', + 'JobQueue', + 'MessageFilter', + 'MessageHandler', + 'MessageQueue', + 'PicklePersistence', + 'PollAnswerHandler', + 'PollHandler', + 'PreCheckoutQueryHandler', + 'PrefixHandler', + 'RegexHandler', + 'ShippingQueryHandler', + 'StringCommandHandler', + 'StringRegexHandler', + 'TypeHandler', + 'UpdateFilter', + 'Updater', + 'run_async', +) diff --git a/telegramer/include/telegram/ext/basepersistence.py b/telegramer/include/telegram/ext/basepersistence.py new file mode 100644 index 0000000..20afa1d --- /dev/null +++ b/telegramer/include/telegram/ext/basepersistence.py @@ -0,0 +1,566 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the BasePersistence class.""" +import warnings +from sys import version_info as py_ver +from abc import ABC, abstractmethod +from copy import copy +from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, DefaultDict + +from telegram.utils.deprecate import set_new_attribute_deprecated + +from telegram import Bot +import telegram.ext.extbot + +from telegram.ext.utils.types import UD, CD, BD, ConversationDict, CDCData + + +class BasePersistence(Generic[UD, CD, BD], ABC): + """Interface class for adding persistence to your bot. + Subclass this object for different implementations of a persistent bot. + + All relevant methods must be overwritten. This includes: + + * :meth:`get_bot_data` + * :meth:`update_bot_data` + * :meth:`refresh_bot_data` + * :meth:`get_chat_data` + * :meth:`update_chat_data` + * :meth:`refresh_chat_data` + * :meth:`get_user_data` + * :meth:`update_user_data` + * :meth:`refresh_user_data` + * :meth:`get_callback_data` + * :meth:`update_callback_data` + * :meth:`get_conversations` + * :meth:`update_conversation` + * :meth:`flush` + + If you don't actually need one of those methods, a simple ``pass`` is enough. For example, if + ``store_bot_data=False``, you don't need :meth:`get_bot_data`, :meth:`update_bot_data` or + :meth:`refresh_bot_data`. + + Warning: + Persistence will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and + insert the bot set with :meth:`set_bot` upon loading of the data. This is to ensure that + changes to the bot apply to the saved objects, too. If you change the bots token, this may + lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see + :meth:`replace_bot` and :meth:`insert_bot`. + + Note: + :meth:`replace_bot` and :meth:`insert_bot` are used *independently* of the implementation + of the :meth:`update/get_*` methods, i.e. you don't need to worry about it while + implementing a custom persistence subclass. + + Args: + store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this + persistence class. Default is :obj:`True`. + store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this + persistence class. Default is :obj:`True` . + store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this + persistence class. Default is :obj:`True`. + store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this + persistence class. Default is :obj:`False`. + + .. versionadded:: 13.6 + + Attributes: + store_user_data (:obj:`bool`): Optional, Whether user_data should be saved by this + persistence class. + store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this + persistence class. + store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this + persistence class. + store_callback_data (:obj:`bool`): Optional. Whether callback_data should be saved by this + persistence class. + + .. versionadded:: 13.6 + """ + + # Apparently Py 3.7 and below have '__dict__' in ABC + if py_ver < (3, 7): + __slots__ = ( + 'store_user_data', + 'store_chat_data', + 'store_bot_data', + 'store_callback_data', + 'bot', + ) + else: + __slots__ = ( + 'store_user_data', # type: ignore[assignment] + 'store_chat_data', + 'store_bot_data', + 'store_callback_data', + 'bot', + '__dict__', + ) + + def __new__( + cls, *args: object, **kwargs: object # pylint: disable=W0613 + ) -> 'BasePersistence': + """This overrides the get_* and update_* methods to use insert/replace_bot. + That has the side effect that we always pass deepcopied data to those methods, so in + Pickle/DictPersistence we don't have to worry about copying the data again. + + Note: This doesn't hold for second tuple-entry of callback_data. That's a Dict[str, str], + so no bots to replace anyway. + """ + instance = super().__new__(cls) + get_user_data = instance.get_user_data + get_chat_data = instance.get_chat_data + get_bot_data = instance.get_bot_data + get_callback_data = instance.get_callback_data + update_user_data = instance.update_user_data + update_chat_data = instance.update_chat_data + update_bot_data = instance.update_bot_data + update_callback_data = instance.update_callback_data + + def get_user_data_insert_bot() -> DefaultDict[int, UD]: + return instance.insert_bot(get_user_data()) + + def get_chat_data_insert_bot() -> DefaultDict[int, CD]: + return instance.insert_bot(get_chat_data()) + + def get_bot_data_insert_bot() -> BD: + return instance.insert_bot(get_bot_data()) + + def get_callback_data_insert_bot() -> Optional[CDCData]: + cdc_data = get_callback_data() + if cdc_data is None: + return None + return instance.insert_bot(cdc_data[0]), cdc_data[1] + + def update_user_data_replace_bot(user_id: int, data: UD) -> None: + return update_user_data(user_id, instance.replace_bot(data)) + + def update_chat_data_replace_bot(chat_id: int, data: CD) -> None: + return update_chat_data(chat_id, instance.replace_bot(data)) + + def update_bot_data_replace_bot(data: BD) -> None: + return update_bot_data(instance.replace_bot(data)) + + def update_callback_data_replace_bot(data: CDCData) -> None: + obj_data, queue = data + return update_callback_data((instance.replace_bot(obj_data), queue)) + + # We want to ignore TGDeprecation warnings so we use obj.__setattr__. Adds to __dict__ + object.__setattr__(instance, 'get_user_data', get_user_data_insert_bot) + object.__setattr__(instance, 'get_chat_data', get_chat_data_insert_bot) + object.__setattr__(instance, 'get_bot_data', get_bot_data_insert_bot) + object.__setattr__(instance, 'get_callback_data', get_callback_data_insert_bot) + object.__setattr__(instance, 'update_user_data', update_user_data_replace_bot) + object.__setattr__(instance, 'update_chat_data', update_chat_data_replace_bot) + object.__setattr__(instance, 'update_bot_data', update_bot_data_replace_bot) + object.__setattr__(instance, 'update_callback_data', update_callback_data_replace_bot) + return instance + + def __init__( + self, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + store_callback_data: bool = False, + ): + self.store_user_data = store_user_data + self.store_chat_data = store_chat_data + self.store_bot_data = store_bot_data + self.store_callback_data = store_callback_data + self.bot: Bot = None # type: ignore[assignment] + + def __setattr__(self, key: str, value: object) -> None: + # Allow user defined subclasses to have custom attributes. + if issubclass(self.__class__, BasePersistence) and self.__class__.__name__ not in { + 'DictPersistence', + 'PicklePersistence', + }: + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + + def set_bot(self, bot: Bot) -> None: + """Set the Bot to be used by this persistence instance. + + Args: + bot (:class:`telegram.Bot`): The bot. + """ + if self.store_callback_data and not isinstance(bot, telegram.ext.extbot.ExtBot): + raise TypeError('store_callback_data can only be used with telegram.ext.ExtBot.') + + self.bot = bot + + @classmethod + def replace_bot(cls, obj: object) -> object: + """ + Replaces all instances of :class:`telegram.Bot` that occur within the passed object with + :attr:`REPLACED_BOT`. Currently, this handles objects of type ``list``, ``tuple``, ``set``, + ``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or + ``__slots__`` attribute, excluding classes and objects that can't be copied with + ``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and + the error will be logged. + + Args: + obj (:obj:`object`): The object + + Returns: + :obj:`obj`: Copy of the object with Bot instances replaced. + """ + return cls._replace_bot(obj, {}) + + @classmethod + def _replace_bot(cls, obj: object, memo: Dict[int, object]) -> object: # pylint: disable=R0911 + obj_id = id(obj) + if obj_id in memo: + return memo[obj_id] + + if isinstance(obj, Bot): + memo[obj_id] = cls.REPLACED_BOT + return cls.REPLACED_BOT + if isinstance(obj, (list, set)): + # We copy the iterable here for thread safety, i.e. make sure the object we iterate + # over doesn't change its length during the iteration + temp_iterable = obj.copy() + new_iterable = obj.__class__(cls._replace_bot(item, memo) for item in temp_iterable) + memo[obj_id] = new_iterable + return new_iterable + if isinstance(obj, (tuple, frozenset)): + # tuples and frozensets are immutable so we don't need to worry about thread safety + new_immutable = obj.__class__(cls._replace_bot(item, memo) for item in obj) + memo[obj_id] = new_immutable + return new_immutable + if isinstance(obj, type): + # classes usually do have a __dict__, but it's not writable + warnings.warn( + 'BasePersistence.replace_bot does not handle classes. See ' + 'the docs of BasePersistence.replace_bot for more information.', + RuntimeWarning, + ) + return obj + + try: + new_obj = copy(obj) + memo[obj_id] = new_obj + except Exception: + warnings.warn( + 'BasePersistence.replace_bot does not handle objects that can not be copied. See ' + 'the docs of BasePersistence.replace_bot for more information.', + RuntimeWarning, + ) + memo[obj_id] = obj + return obj + + if isinstance(obj, dict): + # We handle dicts via copy(obj) so we don't have to make a + # difference between dict and defaultdict + new_obj = cast(dict, new_obj) + # We can't iterate over obj.items() due to thread safety, i.e. the dicts length may + # change during the iteration + temp_dict = new_obj.copy() + new_obj.clear() + for k, val in temp_dict.items(): + new_obj[cls._replace_bot(k, memo)] = cls._replace_bot(val, memo) + memo[obj_id] = new_obj + return new_obj + try: + if hasattr(obj, '__slots__'): + for attr_name in new_obj.__slots__: + setattr( + new_obj, + attr_name, + cls._replace_bot( + cls._replace_bot(getattr(new_obj, attr_name), memo), memo + ), + ) + if '__dict__' in obj.__slots__: + # In this case, we have already covered the case that obj has __dict__ + # Note that obj may have a __dict__ even if it's not in __slots__! + memo[obj_id] = new_obj + return new_obj + if hasattr(obj, '__dict__'): + for attr_name, attr in new_obj.__dict__.items(): + setattr(new_obj, attr_name, cls._replace_bot(attr, memo)) + memo[obj_id] = new_obj + return new_obj + except Exception as exception: + warnings.warn( + f'Parsing of an object failed with the following exception: {exception}. ' + f'See the docs of BasePersistence.replace_bot for more information.', + RuntimeWarning, + ) + + memo[obj_id] = obj + return obj + + def insert_bot(self, obj: object) -> object: + """ + Replaces all instances of :attr:`REPLACED_BOT` that occur within the passed object with + :attr:`bot`. Currently, this handles objects of type ``list``, ``tuple``, ``set``, + ``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or + ``__slots__`` attribute, excluding classes and objects that can't be copied with + ``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and + the error will be logged. + + Args: + obj (:obj:`object`): The object + + Returns: + :obj:`obj`: Copy of the object with Bot instances inserted. + """ + return self._insert_bot(obj, {}) + + def _insert_bot(self, obj: object, memo: Dict[int, object]) -> object: # pylint: disable=R0911 + obj_id = id(obj) + if obj_id in memo: + return memo[obj_id] + + if isinstance(obj, Bot): + memo[obj_id] = self.bot + return self.bot + if isinstance(obj, str) and obj == self.REPLACED_BOT: + memo[obj_id] = self.bot + return self.bot + if isinstance(obj, (list, set)): + # We copy the iterable here for thread safety, i.e. make sure the object we iterate + # over doesn't change its length during the iteration + temp_iterable = obj.copy() + new_iterable = obj.__class__(self._insert_bot(item, memo) for item in temp_iterable) + memo[obj_id] = new_iterable + return new_iterable + if isinstance(obj, (tuple, frozenset)): + # tuples and frozensets are immutable so we don't need to worry about thread safety + new_immutable = obj.__class__(self._insert_bot(item, memo) for item in obj) + memo[obj_id] = new_immutable + return new_immutable + if isinstance(obj, type): + # classes usually do have a __dict__, but it's not writable + warnings.warn( + 'BasePersistence.insert_bot does not handle classes. See ' + 'the docs of BasePersistence.insert_bot for more information.', + RuntimeWarning, + ) + return obj + + try: + new_obj = copy(obj) + except Exception: + warnings.warn( + 'BasePersistence.insert_bot does not handle objects that can not be copied. See ' + 'the docs of BasePersistence.insert_bot for more information.', + RuntimeWarning, + ) + memo[obj_id] = obj + return obj + + if isinstance(obj, dict): + # We handle dicts via copy(obj) so we don't have to make a + # difference between dict and defaultdict + new_obj = cast(dict, new_obj) + # We can't iterate over obj.items() due to thread safety, i.e. the dicts length may + # change during the iteration + temp_dict = new_obj.copy() + new_obj.clear() + for k, val in temp_dict.items(): + new_obj[self._insert_bot(k, memo)] = self._insert_bot(val, memo) + memo[obj_id] = new_obj + return new_obj + try: + if hasattr(obj, '__slots__'): + for attr_name in obj.__slots__: + setattr( + new_obj, + attr_name, + self._insert_bot( + self._insert_bot(getattr(new_obj, attr_name), memo), memo + ), + ) + if '__dict__' in obj.__slots__: + # In this case, we have already covered the case that obj has __dict__ + # Note that obj may have a __dict__ even if it's not in __slots__! + memo[obj_id] = new_obj + return new_obj + if hasattr(obj, '__dict__'): + for attr_name, attr in new_obj.__dict__.items(): + setattr(new_obj, attr_name, self._insert_bot(attr, memo)) + memo[obj_id] = new_obj + return new_obj + except Exception as exception: + warnings.warn( + f'Parsing of an object failed with the following exception: {exception}. ' + f'See the docs of BasePersistence.insert_bot for more information.', + RuntimeWarning, + ) + + memo[obj_id] = obj + return obj + + @abstractmethod + def get_user_data(self) -> DefaultDict[int, UD]: + """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a + persistence object. It should return the ``user_data`` if stored, or an empty + :obj:`defaultdict(telegram.ext.utils.types.UD)` with integer keys. + + Returns: + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data. + """ + + @abstractmethod + def get_chat_data(self) -> DefaultDict[int, CD]: + """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a + persistence object. It should return the ``chat_data`` if stored, or an empty + :obj:`defaultdict(telegram.ext.utils.types.CD)` with integer keys. + + Returns: + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data. + """ + + @abstractmethod + def get_bot_data(self) -> BD: + """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a + persistence object. It should return the ``bot_data`` if stored, or an empty + :class:`telegram.ext.utils.types.BD`. + + Returns: + :class:`telegram.ext.utils.types.BD`: The restored bot data. + """ + + def get_callback_data(self) -> Optional[CDCData]: + """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a + persistence object. If callback data was stored, it should be returned. + + .. versionadded:: 13.6 + + Returns: + Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or + :obj:`None`, if no data was stored. + """ + raise NotImplementedError + + @abstractmethod + def get_conversations(self, name: str) -> ConversationDict: + """Will be called by :class:`telegram.ext.Dispatcher` when a + :class:`telegram.ext.ConversationHandler` is added if + :attr:`telegram.ext.ConversationHandler.persistent` is :obj:`True`. + It should return the conversations for the handler with `name` or an empty :obj:`dict` + + Args: + name (:obj:`str`): The handlers name. + + Returns: + :obj:`dict`: The restored conversations for the handler. + """ + + @abstractmethod + def update_conversation( + self, name: str, key: Tuple[int, ...], new_state: Optional[object] + ) -> None: + """Will be called when a :class:`telegram.ext.ConversationHandler` changes states. + This allows the storage of the new state in the persistence. + + Args: + name (:obj:`str`): The handler's name. + key (:obj:`tuple`): The key the state is changed for. + new_state (:obj:`tuple` | :obj:`any`): The new state for the given key. + """ + + @abstractmethod + def update_user_data(self, user_id: int, data: UD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has + handled an update. + + Args: + user_id (:obj:`int`): The user the data might have been changed for. + data (:class:`telegram.ext.utils.types.UD`): The + :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``. + """ + + @abstractmethod + def update_chat_data(self, chat_id: int, data: CD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has + handled an update. + + Args: + chat_id (:obj:`int`): The chat the data might have been changed for. + data (:class:`telegram.ext.utils.types.CD`): The + :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``. + """ + + @abstractmethod + def update_bot_data(self, data: BD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has + handled an update. + + Args: + data (:class:`telegram.ext.utils.types.BD`): The + :attr:`telegram.ext.Dispatcher.bot_data`. + """ + + def refresh_user_data(self, user_id: int, user_data: UD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` before passing the + :attr:`user_data` to a callback. Can be used to update data stored in :attr:`user_data` + from an external source. + + .. versionadded:: 13.6 + + Args: + user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with. + user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user. + """ + + def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` before passing the + :attr:`chat_data` to a callback. Can be used to update data stored in :attr:`chat_data` + from an external source. + + .. versionadded:: 13.6 + + Args: + chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with. + chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat. + """ + + def refresh_bot_data(self, bot_data: BD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` before passing the + :attr:`bot_data` to a callback. Can be used to update data stored in :attr:`bot_data` + from an external source. + + .. versionadded:: 13.6 + + Args: + bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``. + """ + + def update_callback_data(self, data: CDCData) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has + handled an update. + + .. versionadded:: 13.6 + + Args: + data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore + :class:`telegram.ext.CallbackDataCache`. + """ + raise NotImplementedError + + def flush(self) -> None: + """Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the + persistence a chance to finish up saving or close a database connection gracefully. + """ + + REPLACED_BOT: ClassVar[str] = 'bot_instance_replaced_by_ptb_persistence' + """:obj:`str`: Placeholder for :class:`telegram.Bot` instances replaced in saved data.""" diff --git a/telegramer/include/telegram/ext/callbackcontext.py b/telegramer/include/telegram/ext/callbackcontext.py new file mode 100644 index 0000000..607ca92 --- /dev/null +++ b/telegramer/include/telegram/ext/callbackcontext.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=R0201 +"""This module contains the CallbackContext class.""" +from queue import Queue +from typing import ( + TYPE_CHECKING, + Dict, + List, + Match, + NoReturn, + Optional, + Tuple, + Union, + Generic, + Type, + TypeVar, +) + +from telegram import Update, CallbackQuery +from telegram.ext import ExtBot +from telegram.ext.utils.types import UD, CD, BD + +if TYPE_CHECKING: + from telegram import Bot + from telegram.ext import Dispatcher, Job, JobQueue + +CC = TypeVar('CC', bound='CallbackContext') + + +class CallbackContext(Generic[UD, CD, BD]): + """ + This is a context object passed to the callback called by :class:`telegram.ext.Handler` + or by the :class:`telegram.ext.Dispatcher` in an error handler added by + :attr:`telegram.ext.Dispatcher.add_error_handler` or to the callback of a + :class:`telegram.ext.Job`. + + Note: + :class:`telegram.ext.Dispatcher` will create a single context for an entire update. This + means that if you got 2 handlers in different groups and they both get called, they will + get passed the same `CallbackContext` object (of course with proper attributes like + `.matches` differing). This allows you to add custom attributes in a lower handler group + callback, and then subsequently access those attributes in a higher handler group callback. + Note that the attributes on `CallbackContext` might change in the future, so make sure to + use a fairly unique name for the attributes. + + Warning: + Do not combine custom attributes and ``@run_async``/ + :meth:`telegram.ext.Disptacher.run_async`. Due to how ``run_async`` works, it will + almost certainly execute the callbacks for an update out of order, and the attributes + that you think you added will not be present. + + Args: + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context. + + Attributes: + matches (List[:obj:`re match object`]): Optional. If the associated update originated from + a regex-supported handler or had a :class:`Filters.regex`, this will contain a list of + match objects for every pattern where ``re.search(pattern, string)`` returned a match. + Note that filters short circuit, so combined regex filters will not always + be evaluated. + args (List[:obj:`str`]): Optional. Arguments passed to a command if the associated update + is handled by :class:`telegram.ext.CommandHandler`, :class:`telegram.ext.PrefixHandler` + or :class:`telegram.ext.StringCommandHandler`. It contains a list of the words in the + text after the command, using any whitespace string as a delimiter. + error (:obj:`Exception`): Optional. The error that was raised. Only present when passed + to a error handler registered with :attr:`telegram.ext.Dispatcher.add_error_handler`. + async_args (List[:obj:`object`]): Optional. Positional arguments of the function that + raised the error. Only present when the raising function was run asynchronously using + :meth:`telegram.ext.Dispatcher.run_async`. + async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the function + that raised the error. Only present when the raising function was run asynchronously + using :meth:`telegram.ext.Dispatcher.run_async`. + job (:class:`telegram.ext.Job`): Optional. The job which originated this callback. + Only present when passed to the callback of :class:`telegram.ext.Job`. + + """ + + __slots__ = ( + '_dispatcher', + '_chat_id_and_data', + '_user_id_and_data', + 'args', + 'matches', + 'error', + 'job', + 'async_args', + 'async_kwargs', + '__dict__', + ) + + def __init__(self, dispatcher: 'Dispatcher'): + """ + Args: + dispatcher (:class:`telegram.ext.Dispatcher`): + """ + if not dispatcher.use_context: + raise ValueError( + 'CallbackContext should not be used with a non context aware ' 'dispatcher!' + ) + self._dispatcher = dispatcher + self._chat_id_and_data: Optional[Tuple[int, CD]] = None + self._user_id_and_data: Optional[Tuple[int, UD]] = None + self.args: Optional[List[str]] = None + self.matches: Optional[List[Match]] = None + self.error: Optional[Exception] = None + self.job: Optional['Job'] = None + self.async_args: Optional[Union[List, Tuple]] = None + self.async_kwargs: Optional[Dict[str, object]] = None + + @property + def dispatcher(self) -> 'Dispatcher': + """:class:`telegram.ext.Dispatcher`: The dispatcher associated with this context.""" + return self._dispatcher + + @property + def bot_data(self) -> BD: + """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each + update it will be the same ``dict``. + """ + return self.dispatcher.bot_data + + @bot_data.setter + def bot_data(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to bot_data, see https://git.io/Jt6ic" + ) + + @property + def chat_data(self) -> Optional[CD]: + """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each + update from the same chat id it will be the same ``dict``. + + Warning: + When a group chat migrates to a supergroup, its chat id will change and the + ``chat_data`` needs to be transferred. For details see our `wiki page + `_. + """ + if self._chat_id_and_data: + return self._chat_id_and_data[1] + return None + + @chat_data.setter + def chat_data(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to chat_data, see https://git.io/Jt6ic" + ) + + @property + def user_data(self) -> Optional[UD]: + """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each + update from the same user it will be the same ``dict``. + """ + if self._user_id_and_data: + return self._user_id_and_data[1] + return None + + @user_data.setter + def user_data(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to user_data, see https://git.io/Jt6ic" + ) + + def refresh_data(self) -> None: + """If :attr:`dispatcher` uses persistence, calls + :meth:`telegram.ext.BasePersistence.refresh_bot_data` on :attr:`bot_data`, + :meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data` and + :meth:`telegram.ext.BasePersistence.refresh_user_data` on :attr:`user_data`, if + appropriate. + + .. versionadded:: 13.6 + """ + if self.dispatcher.persistence: + if self.dispatcher.persistence.store_bot_data: + self.dispatcher.persistence.refresh_bot_data(self.bot_data) + if self.dispatcher.persistence.store_chat_data and self._chat_id_and_data is not None: + self.dispatcher.persistence.refresh_chat_data(*self._chat_id_and_data) + if self.dispatcher.persistence.store_user_data and self._user_id_and_data is not None: + self.dispatcher.persistence.refresh_user_data(*self._user_id_and_data) + + def drop_callback_data(self, callback_query: CallbackQuery) -> None: + """ + Deletes the cached data for the specified callback query. + + .. versionadded:: 13.6 + + Note: + Will *not* raise exceptions in case the data is not found in the cache. + *Will* raise :class:`KeyError` in case the callback query can not be found in the + cache. + + Args: + callback_query (:class:`telegram.CallbackQuery`): The callback query. + + Raises: + KeyError | RuntimeError: :class:`KeyError`, if the callback query can not be found in + the cache and :class:`RuntimeError`, if the bot doesn't allow for arbitrary + callback data. + """ + if isinstance(self.bot, ExtBot): + if not self.bot.arbitrary_callback_data: + raise RuntimeError( + 'This telegram.ext.ExtBot instance does not use arbitrary callback data.' + ) + self.bot.callback_data_cache.drop_data(callback_query) + else: + raise RuntimeError('telegram.Bot does not allow for arbitrary callback data.') + + @classmethod + def from_error( + cls: Type[CC], + update: object, + error: Exception, + dispatcher: 'Dispatcher', + async_args: Union[List, Tuple] = None, + async_kwargs: Dict[str, object] = None, + ) -> CC: + """ + Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error + handlers. + + .. seealso:: :meth:`telegram.ext.Dispatcher.add_error_handler` + + Args: + update (:obj:`object` | :class:`telegram.Update`): The update associated with the + error. May be :obj:`None`, e.g. for errors in job callbacks. + error (:obj:`Exception`): The error. + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this + context. + async_args (List[:obj:`object`]): Optional. Positional arguments of the function that + raised the error. Pass only when the raising function was run asynchronously using + :meth:`telegram.ext.Dispatcher.run_async`. + async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the + function that raised the error. Pass only when the raising function was run + asynchronously using :meth:`telegram.ext.Dispatcher.run_async`. + + Returns: + :class:`telegram.ext.CallbackContext` + """ + self = cls.from_update(update, dispatcher) + self.error = error + self.async_args = async_args + self.async_kwargs = async_kwargs + return self + + @classmethod + def from_update(cls: Type[CC], update: object, dispatcher: 'Dispatcher') -> CC: + """ + Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the + handlers. + + .. seealso:: :meth:`telegram.ext.Dispatcher.add_handler` + + Args: + update (:obj:`object` | :class:`telegram.Update`): The update. + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this + context. + + Returns: + :class:`telegram.ext.CallbackContext` + """ + self = cls(dispatcher) + + if update is not None and isinstance(update, Update): + chat = update.effective_chat + user = update.effective_user + + if chat: + self._chat_id_and_data = ( + chat.id, + dispatcher.chat_data[chat.id], # pylint: disable=W0212 + ) + if user: + self._user_id_and_data = ( + user.id, + dispatcher.user_data[user.id], # pylint: disable=W0212 + ) + return self + + @classmethod + def from_job(cls: Type[CC], job: 'Job', dispatcher: 'Dispatcher') -> CC: + """ + Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a + job callback. + + .. seealso:: :meth:`telegram.ext.JobQueue` + + Args: + job (:class:`telegram.ext.Job`): The job. + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this + context. + + Returns: + :class:`telegram.ext.CallbackContext` + """ + self = cls(dispatcher) + self.job = job + return self + + def update(self, data: Dict[str, object]) -> None: + """Updates ``self.__slots__`` with the passed data. + + Args: + data (Dict[:obj:`str`, :obj:`object`]): The data. + """ + for key, value in data.items(): + setattr(self, key, value) + + @property + def bot(self) -> 'Bot': + """:class:`telegram.Bot`: The bot associated with this context.""" + return self._dispatcher.bot + + @property + def job_queue(self) -> Optional['JobQueue']: + """ + :class:`telegram.ext.JobQueue`: The ``JobQueue`` used by the + :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater` + associated with this context. + + """ + return self._dispatcher.job_queue + + @property + def update_queue(self) -> Queue: + """ + :class:`queue.Queue`: The ``Queue`` instance used by the + :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater` + associated with this context. + + """ + return self._dispatcher.update_queue + + @property + def match(self) -> Optional[Match[str]]: + """ + `Regex match type`: The first match from :attr:`matches`. + Useful if you are only filtering using a single regex filter. + Returns `None` if :attr:`matches` is empty. + """ + try: + return self.matches[0] # type: ignore[index] # pylint: disable=unsubscriptable-object + except (IndexError, TypeError): + return None diff --git a/telegramer/include/telegram/ext/callbackdatacache.py b/telegramer/include/telegram/ext/callbackdatacache.py new file mode 100644 index 0000000..df64005 --- /dev/null +++ b/telegramer/include/telegram/ext/callbackdatacache.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the CallbackDataCache class.""" +import logging +import time +from datetime import datetime +from threading import Lock +from typing import Dict, Tuple, Union, Optional, MutableMapping, TYPE_CHECKING, cast +from uuid import uuid4 + +from cachetools import LRUCache # pylint: disable=E0401 + +from telegram import ( + InlineKeyboardMarkup, + InlineKeyboardButton, + TelegramError, + CallbackQuery, + Message, + User, +) +from telegram.utils.helpers import to_float_timestamp +from telegram.ext.utils.types import CDCData + +if TYPE_CHECKING: + from telegram.ext import ExtBot + + +class InvalidCallbackData(TelegramError): + """ + Raised when the received callback data has been tempered with or deleted from cache. + + .. versionadded:: 13.6 + + Args: + callback_data (:obj:`int`, optional): The button data of which the callback data could not + be found. + + Attributes: + callback_data (:obj:`int`): Optional. The button data of which the callback data could not + be found. + """ + + __slots__ = ('callback_data',) + + def __init__(self, callback_data: str = None) -> None: + super().__init__( + 'The object belonging to this callback_data was deleted or the callback_data was ' + 'manipulated.' + ) + self.callback_data = callback_data + + def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override] + return self.__class__, (self.callback_data,) + + +class _KeyboardData: + __slots__ = ('keyboard_uuid', 'button_data', 'access_time') + + def __init__( + self, keyboard_uuid: str, access_time: float = None, button_data: Dict[str, object] = None + ): + self.keyboard_uuid = keyboard_uuid + self.button_data = button_data or {} + self.access_time = access_time or time.time() + + def update_access_time(self) -> None: + """Updates the access time with the current time.""" + self.access_time = time.time() + + def to_tuple(self) -> Tuple[str, float, Dict[str, object]]: + """Gives a tuple representation consisting of the keyboard uuid, the access time and the + button data. + """ + return self.keyboard_uuid, self.access_time, self.button_data + + +class CallbackDataCache: + """A custom cache for storing the callback data of a :class:`telegram.ext.ExtBot`. Internally, + it keeps two mappings with fixed maximum size: + + * One for mapping the data received in callback queries to the cached objects + * One for mapping the IDs of received callback queries to the cached objects + + The second mapping allows to manually drop data that has been cached for keyboards of messages + sent via inline mode. + If necessary, will drop the least recently used items. + + .. versionadded:: 13.6 + + Args: + bot (:class:`telegram.ext.ExtBot`): The bot this cache is for. + maxsize (:obj:`int`, optional): Maximum number of items in each of the internal mappings. + Defaults to 1024. + persistent_data (:obj:`telegram.ext.utils.types.CDCData`, optional): Data to initialize + the cache with, as returned by :meth:`telegram.ext.BasePersistence.get_callback_data`. + + Attributes: + bot (:class:`telegram.ext.ExtBot`): The bot this cache is for. + maxsize (:obj:`int`): maximum size of the cache. + + """ + + __slots__ = ('bot', 'maxsize', '_keyboard_data', '_callback_queries', '__lock', 'logger') + + def __init__( + self, + bot: 'ExtBot', + maxsize: int = 1024, + persistent_data: CDCData = None, + ): + self.logger = logging.getLogger(__name__) + + self.bot = bot + self.maxsize = maxsize + self._keyboard_data: MutableMapping[str, _KeyboardData] = LRUCache(maxsize=maxsize) + self._callback_queries: MutableMapping[str, str] = LRUCache(maxsize=maxsize) + self.__lock = Lock() + + if persistent_data: + keyboard_data, callback_queries = persistent_data + for key, value in callback_queries.items(): + self._callback_queries[key] = value + for uuid, access_time, data in keyboard_data: + self._keyboard_data[uuid] = _KeyboardData( + keyboard_uuid=uuid, access_time=access_time, button_data=data + ) + + @property + def persistence_data(self) -> CDCData: + """:obj:`telegram.ext.utils.types.CDCData`: The data that needs to be persisted to allow + caching callback data across bot reboots. + """ + # While building a list/dict from the LRUCaches has linear runtime (in the number of + # entries), the runtime is bounded by maxsize and it has the big upside of not throwing a + # highly customized data structure at users trying to implement a custom persistence class + with self.__lock: + return [data.to_tuple() for data in self._keyboard_data.values()], dict( + self._callback_queries.items() + ) + + def process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup: + """Registers the reply markup to the cache. If any of the buttons have + :attr:`callback_data`, stores that data and builds a new keyboard with the correspondingly + replaced buttons. Otherwise does nothing and returns the original reply markup. + + Args: + reply_markup (:class:`telegram.InlineKeyboardMarkup`): The keyboard. + + Returns: + :class:`telegram.InlineKeyboardMarkup`: The keyboard to be passed to Telegram. + + """ + with self.__lock: + return self.__process_keyboard(reply_markup) + + def __process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup: + keyboard_uuid = uuid4().hex + keyboard_data = _KeyboardData(keyboard_uuid) + + # Built a new nested list of buttons by replacing the callback data if needed + buttons = [ + [ + # We create a new button instead of replacing callback_data in case the + # same object is used elsewhere + InlineKeyboardButton( + btn.text, + callback_data=self.__put_button(btn.callback_data, keyboard_data), + ) + if btn.callback_data + else btn + for btn in column + ] + for column in reply_markup.inline_keyboard + ] + + if not keyboard_data.button_data: + # If we arrive here, no data had to be replaced and we can return the input + return reply_markup + + self._keyboard_data[keyboard_uuid] = keyboard_data + return InlineKeyboardMarkup(buttons) + + @staticmethod + def __put_button(callback_data: object, keyboard_data: _KeyboardData) -> str: + """Stores the data for a single button in :attr:`keyboard_data`. + Returns the string that should be passed instead of the callback_data, which is + ``keyboard_uuid + button_uuids``. + """ + uuid = uuid4().hex + keyboard_data.button_data[uuid] = callback_data + return f'{keyboard_data.keyboard_uuid}{uuid}' + + def __get_keyboard_uuid_and_button_data( + self, callback_data: str + ) -> Union[Tuple[str, object], Tuple[None, InvalidCallbackData]]: + keyboard, button = self.extract_uuids(callback_data) + try: + # we get the values before calling update() in case KeyErrors are raised + # we don't want to update in that case + keyboard_data = self._keyboard_data[keyboard] + button_data = keyboard_data.button_data[button] + # Update the timestamp for the LRU + keyboard_data.update_access_time() + return keyboard, button_data + except KeyError: + return None, InvalidCallbackData(callback_data) + + @staticmethod + def extract_uuids(callback_data: str) -> Tuple[str, str]: + """Extracts the keyboard uuid and the button uuid from the given ``callback_data``. + + Args: + callback_data (:obj:`str`): The ``callback_data`` as present in the button. + + Returns: + (:obj:`str`, :obj:`str`): Tuple of keyboard and button uuid + + """ + # Extract the uuids as put in __put_button + return callback_data[:32], callback_data[32:] + + def process_message(self, message: Message) -> None: + """Replaces the data in the inline keyboard attached to the message with the cached + objects, if necessary. If the data could not be found, + :class:`telegram.ext.InvalidCallbackData` will be inserted. + + Note: + Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check + if the reply markup (if any) was actually sent by this caches bot. If it was not, the + message will be returned unchanged. + + Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is + :obj:`None` for those! In the corresponding reply markups the callback data will be + replaced by :class:`telegram.ext.InvalidCallbackData`. + + Warning: + * Does *not* consider :attr:`telegram.Message.reply_to_message` and + :attr:`telegram.Message.pinned_message`. Pass them to these method separately. + * *In place*, i.e. the passed :class:`telegram.Message` will be changed! + + Args: + message (:class:`telegram.Message`): The message. + + """ + with self.__lock: + self.__process_message(message) + + def __process_message(self, message: Message) -> Optional[str]: + """As documented in process_message, but returns the uuid of the attached keyboard, if any, + which is relevant for process_callback_query. + + **IN PLACE** + """ + if not message.reply_markup: + return None + + if message.via_bot: + sender: Optional[User] = message.via_bot + elif message.from_user: + sender = message.from_user + else: + sender = None + + if sender is not None and sender != self.bot.bot: + return None + + keyboard_uuid = None + + for row in message.reply_markup.inline_keyboard: + for button in row: + if button.callback_data: + button_data = cast(str, button.callback_data) + keyboard_id, callback_data = self.__get_keyboard_uuid_and_button_data( + button_data + ) + # update_callback_data makes sure that the _id_attrs are updated + button.update_callback_data(callback_data) + + # This is lazy loaded. The firsts time we find a button + # we load the associated keyboard - afterwards, there is + if not keyboard_uuid and not isinstance(callback_data, InvalidCallbackData): + keyboard_uuid = keyboard_id + + return keyboard_uuid + + def process_callback_query(self, callback_query: CallbackQuery) -> None: + """Replaces the data in the callback query and the attached messages keyboard with the + cached objects, if necessary. If the data could not be found, + :class:`telegram.ext.InvalidCallbackData` will be inserted. + If :attr:`callback_query.data` or :attr:`callback_query.message` is present, this also + saves the callback queries ID in order to be able to resolve it to the stored data. + + Note: + Also considers inserts data into the buttons of + :attr:`telegram.Message.reply_to_message` and :attr:`telegram.Message.pinned_message` + if necessary. + + Warning: + *In place*, i.e. the passed :class:`telegram.CallbackQuery` will be changed! + + Args: + callback_query (:class:`telegram.CallbackQuery`): The callback query. + + """ + with self.__lock: + mapped = False + + if callback_query.data: + data = callback_query.data + + # Get the cached callback data for the CallbackQuery + keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data) + callback_query.data = button_data # type: ignore[assignment] + + # Map the callback queries ID to the keyboards UUID for later use + if not mapped and not isinstance(button_data, InvalidCallbackData): + self._callback_queries[callback_query.id] = keyboard_uuid # type: ignore + mapped = True + + # Get the cached callback data for the inline keyboard attached to the + # CallbackQuery. + if callback_query.message: + self.__process_message(callback_query.message) + for message in ( + callback_query.message.pinned_message, + callback_query.message.reply_to_message, + ): + if message: + self.__process_message(message) + + def drop_data(self, callback_query: CallbackQuery) -> None: + """Deletes the data for the specified callback query. + + Note: + Will *not* raise exceptions in case the callback data is not found in the cache. + *Will* raise :class:`KeyError` in case the callback query can not be found in the + cache. + + Args: + callback_query (:class:`telegram.CallbackQuery`): The callback query. + + Raises: + KeyError: If the callback query can not be found in the cache + """ + with self.__lock: + try: + keyboard_uuid = self._callback_queries.pop(callback_query.id) + self.__drop_keyboard(keyboard_uuid) + except KeyError as exc: + raise KeyError('CallbackQuery was not found in cache.') from exc + + def __drop_keyboard(self, keyboard_uuid: str) -> None: + try: + self._keyboard_data.pop(keyboard_uuid) + except KeyError: + return + + def clear_callback_data(self, time_cutoff: Union[float, datetime] = None) -> None: + """Clears the stored callback data. + + Args: + time_cutoff (:obj:`float` | :obj:`datetime.datetime`, optional): Pass a UNIX timestamp + or a :obj:`datetime.datetime` to clear only entries which are older. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + + """ + with self.__lock: + self.__clear(self._keyboard_data, time_cutoff=time_cutoff) + + def clear_callback_queries(self) -> None: + """Clears the stored callback query IDs.""" + with self.__lock: + self.__clear(self._callback_queries) + + def __clear(self, mapping: MutableMapping, time_cutoff: Union[float, datetime] = None) -> None: + if not time_cutoff: + mapping.clear() + return + + if isinstance(time_cutoff, datetime): + effective_cutoff = to_float_timestamp( + time_cutoff, tzinfo=self.bot.defaults.tzinfo if self.bot.defaults else None + ) + else: + effective_cutoff = time_cutoff + + # We need a list instead of a generator here, as the list doesn't change it's size + # during the iteration + to_drop = [key for key, data in mapping.items() if data.access_time < effective_cutoff] + for key in to_drop: + mapping.pop(key) diff --git a/telegramer/include/telegram/ext/callbackqueryhandler.py b/telegramer/include/telegram/ext/callbackqueryhandler.py index 3a8ca7a..bb19fa7 100644 --- a/telegramer/include/telegram/ext/callbackqueryhandler.py +++ b/telegramer/include/telegram/ext/callbackqueryhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,134 +19,218 @@ """This module contains the CallbackQueryHandler class.""" import re - -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass - -try: - string_types -except NameError: - string_types = str +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Match, + Optional, + Pattern, + TypeVar, + Union, + cast, +) from telegram import Update +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE + from .handler import Handler +from .utils.types import CCT + +if TYPE_CHECKING: + from telegram.ext import Dispatcher + +RT = TypeVar('RT') -class CallbackQueryHandler(Handler): +class CallbackQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram callback queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. - Attributes: - callback (:obj:`callable`): The callback function for this handler. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pattern (:obj:`str` | `Pattern`): Optional. Regex pattern to test - :attr:`telegram.CallbackQuery.data` against. - pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the - callback function. - pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. - Note: - :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you - can use to keep any data in will be sent to the :attr:`callback` function. Related to - either the user or the chat that the update was sent in. For each update from the same user - or in the same chat, it will be the same ``dict``. + * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same + user or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + * If your bot allows arbitrary objects as ``callback_data``, it may happen that the + original ``callback_data`` for the incoming :class:`telegram.CallbackQuery`` can not be + found. This is the case when either a malicious client tempered with the + ``callback_data`` or the data was simply dropped from cache or not persisted. In these + cases, an instance of :class:`telegram.ext.InvalidCallbackData` will be set as + ``callback_data``. + + .. versionadded:: 13.6 + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not ``None``, ``re.match`` - is used on :attr:`telegram.CallbackQuery.data` to determine if an update should be - handled by this handler. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pattern (:obj:`str` | `Pattern` | :obj:`callable` | :obj:`type`, optional): + Pattern to test :attr:`telegram.CallbackQuery.data` against. If a string or a regex + pattern is passed, :meth:`re.match` is used on :attr:`telegram.CallbackQuery.data` to + determine if an update should be handled by this handler. If your bot allows arbitrary + objects as ``callback_data``, non-strings will be accepted. To filter arbitrary + objects you may pass + + * a callable, accepting exactly one argument, namely the + :attr:`telegram.CallbackQuery.data`. It must return :obj:`True` or + :obj:`False`/:obj:`None` to indicate, whether the update should be handled. + * a :obj:`type`. If :attr:`telegram.CallbackQuery.data` is an instance of that type + (or a subclass), the update will be handled. + + If :attr:`telegram.CallbackQuery.data` is :obj:`None`, the + :class:`telegram.CallbackQuery` update will not be handled. + + .. versionchanged:: 13.6 + Added support for arbitrary callback data. pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. - Default is ``False`` + Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. - Default is ``False`` - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pattern (`Pattern` | :obj:`callable` | :obj:`type`): Optional. Regex pattern, callback or + type to test :attr:`telegram.CallbackQuery.data` against. + + .. versionchanged:: 13.6 + Added support for arbitrary callback data. + pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the + callback function. + pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ - def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pattern=None, - pass_groups=False, - pass_groupdict=False, - pass_user_data=False, - pass_chat_data=False): - super(CallbackQueryHandler, self).__init__( + __slots__ = ('pattern', 'pass_groups', 'pass_groupdict') + + def __init__( + self, + callback: Callable[[Update, CCT], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pattern: Union[str, Pattern, type, Callable[[object], Optional[bool]]] = None, + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) + pass_chat_data=pass_chat_data, + run_async=run_async, + ) - if isinstance(pattern, string_types): + if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern = pattern self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - def check_update(self, update): + def check_update(self, update: object) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.callback_query: + callback_data = update.callback_query.data if self.pattern: - if update.callback_query.data: - match = re.match(self.pattern, update.callback_query.data) - return bool(match) + if callback_data is None: + return False + if isinstance(self.pattern, type): + return isinstance(callback_data, self.pattern) + if callable(self.pattern): + return self.pattern(callback_data) + match = re.match(self.pattern, callback_data) + if match: + return match else: return True - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. - + return None + + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: Update = None, + check_result: Union[bool, Match] = None, + ) -> Dict[str, object]: + """Pass the results of ``re.match(pattern, data).{groups(), groupdict()}`` to the + callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if + needed. """ - optional_args = self.collect_optional_args(dispatcher, update) - if self.pattern: - match = re.match(self.pattern, update.callback_query.data) - + optional_args = super().collect_optional_args(dispatcher, update, check_result) + if self.pattern and not callable(self.pattern): + check_result = cast(Match, check_result) if self.pass_groups: - optional_args['groups'] = match.groups() + optional_args['groups'] = check_result.groups() if self.pass_groupdict: - optional_args['groupdict'] = match.groupdict() - - return self.callback(dispatcher.bot, update, **optional_args) + optional_args['groupdict'] = check_result.groupdict() + return optional_args + + def collect_additional_context( + self, + context: CCT, + update: Update, + dispatcher: 'Dispatcher', + check_result: Union[bool, Match], + ) -> None: + """Add the result of ``re.match(pattern, update.callback_query.data)`` to + :attr:`CallbackContext.matches` as list with one element. + """ + if self.pattern: + check_result = cast(Match, check_result) + context.matches = [check_result] diff --git a/telegramer/include/telegram/ext/chatjoinrequesthandler.py b/telegramer/include/telegram/ext/chatjoinrequesthandler.py new file mode 100644 index 0000000..aafed54 --- /dev/null +++ b/telegramer/include/telegram/ext/chatjoinrequesthandler.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the ChatJoinRequestHandler class.""" + + +from telegram import Update + +from .handler import Handler +from .utils.types import CCT + + +class ChatJoinRequestHandler(Handler[Update, CCT]): + """Handler class to handle Telegram updates that contain a chat join request. + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + + .. versionadded:: 13.8 + + Args: + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + + """ + + __slots__ = () + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + return isinstance(update, Update) and bool(update.chat_join_request) diff --git a/telegramer/include/telegram/ext/chatmemberhandler.py b/telegramer/include/telegram/ext/chatmemberhandler.py new file mode 100644 index 0000000..eb9d91b --- /dev/null +++ b/telegramer/include/telegram/ext/chatmemberhandler.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the ChatMemberHandler classes.""" +from typing import ClassVar, TypeVar, Union, Callable + +from telegram import Update +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from .handler import Handler +from .utils.types import CCT + +RT = TypeVar('RT') + + +class ChatMemberHandler(Handler[Update, CCT]): + """Handler class to handle Telegram updates that contain a chat member update. + + .. versionadded:: 13.4 + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + + Args: + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + chat_member_types (:obj:`int`, optional): Pass one of :attr:`MY_CHAT_MEMBER`, + :attr:`CHAT_MEMBER` or :attr:`ANY_CHAT_MEMBER` to specify if this handler should handle + only updates with :attr:`telegram.Update.my_chat_member`, + :attr:`telegram.Update.chat_member` or both. Defaults to :attr:`MY_CHAT_MEMBER`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + chat_member_types (:obj:`int`, optional): Specifies if this handler should handle + only updates with :attr:`telegram.Update.my_chat_member`, + :attr:`telegram.Update.chat_member` or both. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + + """ + + __slots__ = ('chat_member_types',) + MY_CHAT_MEMBER: ClassVar[int] = -1 + """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`.""" + CHAT_MEMBER: ClassVar[int] = 0 + """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_member`.""" + ANY_CHAT_MEMBER: ClassVar[int] = 1 + """:obj:`int`: Used as a constant to handle bot :attr:`telegram.Update.my_chat_member` + and :attr:`telegram.Update.chat_member`.""" + + def __init__( + self, + callback: Callable[[Update, CCT], RT], + chat_member_types: int = MY_CHAT_MEMBER, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + super().__init__( + callback, + pass_update_queue=pass_update_queue, + pass_job_queue=pass_job_queue, + pass_user_data=pass_user_data, + pass_chat_data=pass_chat_data, + run_async=run_async, + ) + + self.chat_member_types = chat_member_types + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + if isinstance(update, Update): + if not (update.my_chat_member or update.chat_member): + return False + if self.chat_member_types == self.ANY_CHAT_MEMBER: + return True + if self.chat_member_types == self.CHAT_MEMBER: + return bool(update.chat_member) + return bool(update.my_chat_member) + return False diff --git a/telegramer/include/telegram/ext/choseninlineresulthandler.py b/telegramer/include/telegram/ext/choseninlineresulthandler.py index 0b02456..1d94b79 100644 --- a/telegramer/include/telegram/ext/choseninlineresulthandler.py +++ b/telegramer/include/telegram/ext/choseninlineresulthandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,25 +17,23 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the ChosenInlineResultHandler class.""" +import re +from typing import Optional, TypeVar, Union, Callable, TYPE_CHECKING, Pattern, Match, cast -from .handler import Handler from telegram import Update -from telegram.utils.deprecate import deprecate +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from .handler import Handler +from .utils.types import CCT -class ChosenInlineResultHandler(Handler): - """Handler class to handle Telegram updates that contain a chosen inline result. +RT = TypeVar('RT') - Attributes: - callback (:obj:`callable`): The callback function for this handler. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + + +class ChosenInlineResultHandler(Handler[Update, CCT]): + """Handler class to handle Telegram updates that contain a chosen inline result. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you @@ -43,63 +41,120 @@ class ChosenInlineResultHandler(Handler): either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + Args: - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not :obj:`None`, ``re.match`` + is used on :attr:`telegram.ChosenInlineResult.result_id` to determine if an update + should be handled by this handler. This is accessible in the callback as + :attr:`telegram.ext.CallbackContext.matches`. + + .. versionadded:: 13.6 + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + pattern (`Pattern`): Optional. Regex pattern to test + :attr:`telegram.ChosenInlineResult.result_id` against. + + .. versionadded:: 13.6 """ - def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False): - super(ChosenInlineResultHandler, self).__init__( + __slots__ = ('pattern',) + + def __init__( + self, + callback: Callable[[Update, 'CallbackContext'], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + pattern: Union[str, Pattern] = None, + ): + super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) + pass_chat_data=pass_chat_data, + run_async=run_async, + ) - def check_update(self, update): + if isinstance(pattern, str): + pattern = re.compile(pattern) + + self.pattern = pattern + + def check_update(self, update: object) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ - return isinstance(update, Update) and update.chosen_inline_result - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. - + if isinstance(update, Update) and update.chosen_inline_result: + if self.pattern: + match = re.match(self.pattern, update.chosen_inline_result.result_id) + if match: + return match + else: + return True + return None + + def collect_additional_context( + self, + context: 'CallbackContext', + update: Update, + dispatcher: 'Dispatcher', + check_result: Union[bool, Match], + ) -> None: + """This function adds the matched regex pattern result to + :attr:`telegram.ext.CallbackContext.matches`. """ - optional_args = self.collect_optional_args(dispatcher, update) - - return self.callback(dispatcher.bot, update, **optional_args) - - # old non-PEP8 Handler methods - m = "telegram.ChosenInlineResultHandler." - checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update") - handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update") + if self.pattern: + check_result = cast(Match, check_result) + context.matches = [check_result] diff --git a/telegramer/include/telegram/ext/commandhandler.py b/telegramer/include/telegram/ext/commandhandler.py index 5ee5c62..6f53d23 100644 --- a/telegramer/include/telegram/ext/commandhandler.py +++ b/telegramer/include/telegram/ext/commandhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,167 +16,441 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -"""This module contains the CommandHandler class.""" +"""This module contains the CommandHandler and PrefixHandler classes.""" +import re import warnings +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, TypeVar, Union -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass - -try: - string_types -except NameError: - string_types = str +from telegram import MessageEntity, Update +from telegram.ext import BaseFilter, Filters +from telegram.utils.deprecate import TelegramDeprecationWarning +from telegram.utils.types import SLT +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from .utils.types import CCT from .handler import Handler -from telegram import Update +if TYPE_CHECKING: + from telegram.ext import Dispatcher + +RT = TypeVar('RT') -class CommandHandler(Handler): + +class CommandHandler(Handler[Update, CCT]): """Handler class to handle Telegram commands. Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the - bot's name and/or some additional text. + bot's name and/or some additional text. The handler will add a ``list`` to the + :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings, + which is the text following the command split on single or consecutive whitespace characters. - Attributes: - command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler - should listen for. - callback (:obj:`callable`): The callback function for this handler. - filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these - Filters. - allow_edited (:obj:`bool`): Optional. Determines Whether the handler should also accept - edited messages. - pass_args (:obj:`bool`): Optional. Determines whether the handler should be passed - ``args``. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. + By default the handler listens to messages as well as edited messages. To change this behavior + use ``~Filters.update.edited_message`` in the filter argument. Note: - :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you - can use to keep any data in will be sent to the :attr:`callback` function. Related to - either the user or the chat that the update was sent in. For each update from the same user - or in the same chat, it will be the same ``dict``. + * :class:`CommandHandler` does *not* handle (edited) channel posts. + * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a :obj:`dict` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same + user or in the same chat, it will be the same :obj:`dict`. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: - command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler - should listen for. - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. + command (:class:`telegram.utils.types.SLT[str]`): + The command or list of commands this handler should listen for. + Limitations are the same as described here https://core.telegram.org/bots#commands + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). allow_edited (:obj:`bool`, optional): Determines whether the handler should also accept - edited messages. Default is ``False``. + edited messages. Default is :obj:`False`. + DEPRECATED: Edited is allowed by default. To change this behavior use + ``~Filters.update.edited_message``. pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the arguments passed to the command as a keyword argument called ``args``. It will contain a list of strings, which is the text following the command split on single or - consecutive whitespace characters. Default is ``False`` - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + consecutive whitespace characters. Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Raises: + ValueError: when command is too long or has illegal chars. + Attributes: + command (:class:`telegram.utils.types.SLT[str]`): + The command or list of commands this handler should listen for. + Limitations are the same as described here https://core.telegram.org/bots#commands + callback (:obj:`callable`): The callback function for this handler. + filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these + Filters. + allow_edited (:obj:`bool`): Determines whether the handler should also accept + edited messages. + pass_args (:obj:`bool`): Determines whether the handler should be passed + ``args``. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ - def __init__(self, - command, - callback, - filters=None, - allow_edited=False, - pass_args=False, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False): - super(CommandHandler, self).__init__( + __slots__ = ('command', 'filters', 'pass_args') + + def __init__( + self, + command: SLT[str], + callback: Callable[[Update, CCT], RT], + filters: BaseFilter = None, + allow_edited: bool = None, + pass_args: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) + pass_chat_data=pass_chat_data, + run_async=run_async, + ) - if isinstance(command, string_types): + if isinstance(command, str): self.command = [command.lower()] else: self.command = [x.lower() for x in command] - self.filters = filters - self.allow_edited = allow_edited - self.pass_args = pass_args + for comm in self.command: + if not re.match(r'^[\da-z_]{1,32}$', comm): + raise ValueError('Command is not a valid bot command') - # We put this up here instead of with the rest of checking code - # in check_update since we don't wanna spam a ton - if isinstance(self.filters, list): - warnings.warn('Using a list of filters in MessageHandler is getting ' - 'deprecated, please use bitwise operators (& and |) ' - 'instead. More info: https://git.io/vPTbc.') + if filters: + self.filters = Filters.update.messages & filters + else: + self.filters = Filters.update.messages - def check_update(self, update): + if allow_edited is not None: + warnings.warn( + 'allow_edited is deprecated. See https://git.io/fxJuV for more info', + TelegramDeprecationWarning, + stacklevel=2, + ) + if not allow_edited: + self.filters &= ~Filters.update.edited_message + self.pass_args = pass_args + + def check_update( + self, update: object + ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: - :obj:`bool` + :obj:`list`: The list of args for the handler. + + """ + if isinstance(update, Update) and update.effective_message: + message = update.effective_message + + if ( + message.entities + and message.entities[0].type == MessageEntity.BOT_COMMAND + and message.entities[0].offset == 0 + and message.text + and message.bot + ): + command = message.text[1 : message.entities[0].length] + args = message.text.split()[1:] + command_parts = command.split('@') + command_parts.append(message.bot.username) + + if not ( + command_parts[0].lower() in self.command + and command_parts[1].lower() == message.bot.username.lower() + ): + return None + + filter_result = self.filters(update) + if filter_result: + return args, filter_result + return False + return None + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: Update = None, + check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]] = None, + ) -> Dict[str, object]: + """Provide text after the command to the callback the ``args`` argument as list, split on + single whitespaces. """ - if (isinstance(update, Update) - and (update.message or update.edited_message and self.allow_edited)): - message = update.message or update.edited_message + optional_args = super().collect_optional_args(dispatcher, update) + if self.pass_args and isinstance(check_result, tuple): + optional_args['args'] = check_result[0] + return optional_args - if message.text and message.text.startswith('/') and len(message.text) > 1: - first_word = message.text_html.split(None, 1)[0] - if len(first_word) > 1 and first_word.startswith('/'): - command = first_word[1:].split('@') - command.append( - message.bot.username) # in case the command was sent without a username + def collect_additional_context( + self, + context: CCT, + update: Update, + dispatcher: 'Dispatcher', + check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]], + ) -> None: + """Add text after the command to :attr:`CallbackContext.args` as list, split on single + whitespaces and add output of data filters to :attr:`CallbackContext` as well. + """ + if isinstance(check_result, tuple): + context.args = check_result[0] + if isinstance(check_result[1], dict): + context.update(check_result[1]) - if not (command[0].lower() in self.command - and command[1].lower() == message.bot.username.lower()): - return False - if self.filters is None: - res = True - elif isinstance(self.filters, list): - res = any(func(message) for func in self.filters) - else: - res = self.filters(message) +class PrefixHandler(CommandHandler): + """Handler class to handle custom prefix commands. - return res + This is a intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`. + It supports configurable commands with the same options as CommandHandler. It will respond to + every combination of :attr:`prefix` and :attr:`command`. It will add a ``list`` to the + :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings, + which is the text following the command split on single or consecutive whitespace characters. - return False + Examples: - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. + Single prefix and command: - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. + .. code:: python + + PrefixHandler('!', 'test', callback) # will respond to '!test'. + + Multiple prefixes, single command: + + .. code:: python + + PrefixHandler(['!', '#'], 'test', callback) # will respond to '!test' and '#test'. + + Multiple prefixes and commands: + + .. code:: python + + PrefixHandler(['!', '#'], ['test', 'help'], callback) # will respond to '!test', \ + '#test', '!help' and '#help'. + + + By default the handler listens to messages as well as edited messages. To change this behavior + use ``~Filters.update.edited_message``. + + Note: + * :class:`PrefixHandler` does *not* handle (edited) channel posts. + * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a :obj:`dict` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same + user or in the same chat, it will be the same :obj:`dict`. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + + Args: + prefix (:class:`telegram.utils.types.SLT[str]`): + The prefix(es) that will precede :attr:`command`. + command (:class:`telegram.utils.types.SLT[str]`): + The command or list of commands this handler should listen for. + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from + :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in + :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise + operators (& for and, | for or, ~ for not). + pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the + arguments passed to the command as a keyword argument called ``args``. It will contain + a list of strings, which is the text following the command split on single or + consecutive whitespace characters. Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these + Filters. + pass_args (:obj:`bool`): Determines whether the handler should be passed + ``args``. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + + """ + + # 'prefix' is a class property, & 'command' is included in the superclass, so they're left out. + __slots__ = ('_prefix', '_command', '_commands') + def __init__( + self, + prefix: SLT[str], + command: SLT[str], + callback: Callable[[Update, CCT], RT], + filters: BaseFilter = None, + pass_args: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + + self._prefix: List[str] = [] + self._command: List[str] = [] + self._commands: List[str] = [] + + super().__init__( + 'nocommand', + callback, + filters=filters, + allow_edited=None, + pass_args=pass_args, + pass_update_queue=pass_update_queue, + pass_job_queue=pass_job_queue, + pass_user_data=pass_user_data, + pass_chat_data=pass_chat_data, + run_async=run_async, + ) + + self.prefix = prefix # type: ignore[assignment] + self.command = command # type: ignore[assignment] + self._build_commands() + + @property + def prefix(self) -> List[str]: + """ + The prefixes that will precede :attr:`command`. + + Returns: + List[:obj:`str`] """ - optional_args = self.collect_optional_args(dispatcher, update) + return self._prefix - message = update.message or update.edited_message + @prefix.setter + def prefix(self, prefix: Union[str, List[str]]) -> None: + if isinstance(prefix, str): + self._prefix = [prefix.lower()] + else: + self._prefix = prefix + self._build_commands() + + @property # type: ignore[override] + def command(self) -> List[str]: # type: ignore[override] + """ + The list of commands this handler should listen for. - if self.pass_args: - optional_args['args'] = message.text.split()[1:] + Returns: + List[:obj:`str`] + """ + return self._command + + @command.setter + def command(self, command: Union[str, List[str]]) -> None: + if isinstance(command, str): + self._command = [command.lower()] + else: + self._command = command + self._build_commands() + + def _build_commands(self) -> None: + self._commands = [x.lower() + y.lower() for x in self.prefix for y in self.command] + + def check_update( + self, update: object + ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]: + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`list`: The list of args for the handler. + + """ + if isinstance(update, Update) and update.effective_message: + message = update.effective_message - return self.callback(dispatcher.bot, update, **optional_args) + if message.text: + text_list = message.text.split() + if text_list[0].lower() not in self._commands: + return None + filter_result = self.filters(update) + if filter_result: + return text_list[1:], filter_result + return False + return None diff --git a/telegramer/include/telegram/ext/contexttypes.py b/telegramer/include/telegram/ext/contexttypes.py new file mode 100644 index 0000000..ee03037 --- /dev/null +++ b/telegramer/include/telegram/ext/contexttypes.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=R0201 +"""This module contains the auxiliary class ContextTypes.""" +from typing import Type, Generic, overload, Dict # pylint: disable=W0611 + +from telegram.ext.callbackcontext import CallbackContext +from telegram.ext.utils.types import CCT, UD, CD, BD + + +class ContextTypes(Generic[CCT, UD, CD, BD]): + """ + Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext` + interface. + + .. versionadded:: 13.6 + + Args: + context (:obj:`type`, optional): Determines the type of the ``context`` argument of all + (error-)handler callbacks and job callbacks. Must be a subclass of + :class:`telegram.ext.CallbackContext`. Defaults to + :class:`telegram.ext.CallbackContext`. + bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments. + chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments. + user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments. + + """ + + __slots__ = ('_context', '_bot_data', '_chat_data', '_user_data') + + # overload signatures generated with https://git.io/JtJPj + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]', + ): + ... + + @overload + def __init__(self: 'ContextTypes[CCT, Dict, Dict, Dict]', context: Type[CCT]): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[UD, Dict, Dict], UD, Dict, Dict]', user_data: Type[UD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[Dict, CD, Dict], Dict, CD, Dict]', chat_data: Type[CD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[Dict, Dict, BD], Dict, Dict, BD]', bot_data: Type[BD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, Dict, Dict]', context: Type[CCT], user_data: Type[UD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, Dict, CD, Dict]', context: Type[CCT], chat_data: Type[CD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, Dict, Dict, BD]', context: Type[CCT], bot_data: Type[BD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[UD, CD, Dict], UD, CD, Dict]', + user_data: Type[UD], + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[UD, Dict, BD], UD, Dict, BD]', + user_data: Type[UD], + bot_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[Dict, CD, BD], Dict, CD, BD]', + chat_data: Type[CD], + bot_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, CD, Dict]', + context: Type[CCT], + user_data: Type[UD], + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, Dict, BD]', + context: Type[CCT], + user_data: Type[UD], + bot_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, Dict, CD, BD]', + context: Type[CCT], + chat_data: Type[CD], + bot_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext[UD, CD, BD], UD, CD, BD]', + user_data: Type[UD], + chat_data: Type[CD], + bot_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, CD, BD]', + context: Type[CCT], + user_data: Type[UD], + chat_data: Type[CD], + bot_data: Type[BD], + ): + ... + + def __init__( # type: ignore[no-untyped-def] + self, + context=CallbackContext, + bot_data=dict, + chat_data=dict, + user_data=dict, + ): + if not issubclass(context, CallbackContext): + raise ValueError('context must be a subclass of CallbackContext.') + + # We make all those only accessible via properties because we don't currently support + # changing this at runtime, so overriding the attributes doesn't make sense + self._context = context + self._bot_data = bot_data + self._chat_data = chat_data + self._user_data = user_data + + @property + def context(self) -> Type[CCT]: + return self._context + + @property + def bot_data(self) -> Type[BD]: + return self._bot_data + + @property + def chat_data(self) -> Type[CD]: + return self._chat_data + + @property + def user_data(self) -> Type[UD]: + return self._user_data diff --git a/telegramer/include/telegram/ext/conversationhandler.py b/telegramer/include/telegram/ext/conversationhandler.py index 0cc6633..23edf2f 100644 --- a/telegramer/include/telegram/ext/conversationhandler.py +++ b/telegramer/include/telegram/ext/conversationhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,30 +16,81 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=R0201 """This module contains the ConversationHandler.""" import logging +import warnings +import functools +import datetime +from threading import Lock +from typing import TYPE_CHECKING, Dict, List, NoReturn, Optional, Union, Tuple, cast, ClassVar from telegram import Update -from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler, - ChosenInlineResultHandler) -from telegram.utils.promise import Promise +from telegram.ext import ( + BasePersistence, + CallbackContext, + CallbackQueryHandler, + ChosenInlineResultHandler, + DispatcherHandlerStop, + Handler, + InlineQueryHandler, +) +from telegram.ext.utils.promise import Promise +from telegram.ext.utils.types import ConversationDict +from telegram.ext.utils.types import CCT + +if TYPE_CHECKING: + from telegram.ext import Dispatcher, Job +CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]] + + +class _ConversationTimeoutContext: + # '__dict__' is not included since this a private class + __slots__ = ('conversation_key', 'update', 'dispatcher', 'callback_context') + + def __init__( + self, + conversation_key: Tuple[int, ...], + update: Update, + dispatcher: 'Dispatcher', + callback_context: Optional[CallbackContext], + ): + self.conversation_key = conversation_key + self.update = update + self.dispatcher = dispatcher + self.callback_context = callback_context + + +class ConversationHandler(Handler[Update, CCT]): + """ + A handler to hold a conversation with a single or multiple users through Telegram updates by + managing four collections of other handlers. + Note: + ``ConversationHandler`` will only accept updates that are (subclass-)instances of + :class:`telegram.Update`. This is, because depending on the :attr:`per_user` and + :attr:`per_chat` ``ConversationHandler`` relies on + :attr:`telegram.Update.effective_user` and/or :attr:`telegram.Update.effective_chat` in + order to determine which conversation an update should belong to. For ``per_message=True``, + ``ConversationHandler`` uses ``update.callback_query.message.message_id`` when + ``per_chat=True`` and ``update.callback_query.inline_message_id`` when ``per_chat=False``. + For a more detailed explanation, please see our `FAQ`_. -class ConversationHandler(Handler): - """ - A handler to hold a conversation with a single user by managing four collections of other - handlers. Note that neither posts in Telegram Channels, nor group interactions with multiple - users are managed by instances of this class. + Finally, ``ConversationHandler``, does *not* handle (edited) channel posts. + + .. _`FAQ`: https://git.io/JtcyU The first collection, a ``list`` named :attr:`entry_points`, is used to initiate the conversation, for example with a :class:`telegram.ext.CommandHandler` or - :class:`telegram.ext.RegexHandler`. + :class:`telegram.ext.MessageHandler`. The second collection, a ``dict`` named :attr:`states`, contains the different conversation steps and one or more associated handlers that should be used if the user sends a message when - the conversation with them is currently in that state. You will probably use mostly - :class:`telegram.ext.MessageHandler` and :class:`telegram.ext.RegexHandler` here. + the conversation with them is currently in that state. Here you can also define a state for + :attr:`TIMEOUT` to define the behavior when :attr:`conversation_timeout` is exceeded, and a + state for :attr:`WAITING` to define behavior when a new update is received while the previous + ``@run_async`` decorated handler is not finished. The third collection, a ``list`` named :attr:`fallbacks`, is used if the user is currently in a conversation but the state has either no associated handler or the handler that is associated @@ -47,108 +98,161 @@ class ConversationHandler(Handler): a regular text message is expected. You could use this for a ``/cancel`` command or to let the user know their message was not recognized. - The fourth, optional collection of handlers, a ``list`` named :attr:`timed_out_behavior` is - used if the wait for ``run_async`` takes longer than defined in :attr:`run_async_timeout`. - For example, you can let the user know that they should wait for a bit before they can - continue. - To change the state of conversation, the callback function of a handler must return the new - state after responding to the user. If it does not return anything (returning ``None`` by - default), the state will not change. To end the conversation, the callback function must - return :attr:`END` or ``-1``. - - Attributes: - entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can - trigger the start of the conversation. - states (Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]): A :obj:`dict` that - defines the different states of conversation a user can be in and one or more - associated ``Handler`` objects that should be used in that state. - fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if - the user is in a conversation, but every handler for their current state returned - ``False`` on :attr:`check_update`. - allow_reentry (:obj:`bool`): Optional. Determines if a user can restart a conversation with - an entry point. - run_async_timeout (:obj:`float`): Optional. The time-out for ``run_async`` decorated - Handlers. - timed_out_behavior (List[:class:`telegram.ext.Handler`]): Optional. A list of handlers that - might be used if the wait for ``run_async`` timed out. - per_chat (:obj:`bool`): Optional. If the conversationkey should contain the Chat's ID. - per_user (:obj:`bool`): Optional. If the conversationkey should contain the User's ID. - per_message (:obj:`bool`): Optional. If the conversationkey should contain the Message's - ID. - conversation_timeout (:obj:`float`|:obj:`datetime.timedelta`): Optional. When this handler - is inactive more than this timeout (in seconds), it will be automatically ended. If - this value is 0 (default), there will be no timeout. + state after responding to the user. If it does not return anything (returning :obj:`None` by + default), the state will not change. If an entry point callback function returns :obj:`None`, + the conversation ends immediately after the execution of this callback function. + To end the conversation, the callback function must return :attr:`END` or ``-1``. To + handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``. + Finally, :class:`telegram.ext.DispatcherHandlerStop` can be used in conversations as described + in the corresponding documentation. + + Note: + In each of the described collections of handlers, a handler may in turn be a + :class:`ConversationHandler`. In that case, the nested :class:`ConversationHandler` should + have the attribute :attr:`map_to_parent` which allows to return to the parent conversation + at specified states within the nested conversation. + + Note that the keys in :attr:`map_to_parent` must not appear as keys in :attr:`states` + attribute or else the latter will be ignored. You may map :attr:`END` to one of the parents + states to continue the parent conversation after this has ended or even map a state to + :attr:`END` to end the *parent* conversation from within the nested one. For an example on + nested :class:`ConversationHandler` s, see our `examples`_. + + .. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples Args: entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can trigger the start of the conversation. The first handler which :attr:`check_update` - method returns ``True`` will be used. If all return ``False``, the update is not + method returns :obj:`True` will be used. If all return :obj:`False`, the update is not handled. states (Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]): A :obj:`dict` that defines the different states of conversation a user can be in and one or more associated ``Handler`` objects that should be used in that state. The first handler - which :attr:`check_update` method returns ``True`` will be used. + which :attr:`check_update` method returns :obj:`True` will be used. fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if the user is in a conversation, but every handler for their current state returned - ``False`` on :attr:`check_update`. The first handler which :attr:`check_update` method - returns ``True`` will be used. If all return ``False``, the update is not handled. - allow_reentry (:obj:`bool`, optional): If set to ``True``, a user that is currently in a + :obj:`False` on :attr:`check_update`. The first handler which :attr:`check_update` + method returns :obj:`True` will be used. If all return :obj:`False`, the update is not + handled. + allow_reentry (:obj:`bool`, optional): If set to :obj:`True`, a user that is currently in a conversation can restart the conversation by triggering one of the entry points. - run_async_timeout (:obj:`float`, optional): If the previous handler for this user was - running asynchronously using the ``run_async`` decorator, it might not be finished when - the next message arrives. This timeout defines how long the conversation handler should - wait for the next state to be computed. The default is ``None`` which means it will - wait indefinitely. - timed_out_behavior (List[:class:`telegram.ext.Handler`], optional): A list of handlers that - might be used if the wait for ``run_async`` timed out. The first handler which - :attr:`check_update` method returns ``True`` will be used. If all return ``False``, - the update is not handled. per_chat (:obj:`bool`, optional): If the conversationkey should contain the Chat's ID. - Default is ``True``. + Default is :obj:`True`. per_user (:obj:`bool`, optional): If the conversationkey should contain the User's ID. - Default is ``True``. + Default is :obj:`True`. per_message (:obj:`bool`, optional): If the conversationkey should contain the Message's - ID. Default is ``False``. - conversation_timeout (:obj:`float`|:obj:`datetime.timedelta`, optional): When this handler - is inactive more than this timeout (in seconds), it will be automatically ended. If - this value is 0 or None (default), there will be no timeout. + ID. Default is :obj:`False`. + conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`, optional): When this + handler is inactive more than this timeout (in seconds), it will be automatically + ended. If this value is 0 or :obj:`None` (default), there will be no timeout. The last + received update and the corresponding ``context`` will be handled by ALL the handler's + who's :attr:`check_update` method returns :obj:`True` that are in the state + :attr:`ConversationHandler.TIMEOUT`. + + Note: + Using `conversation_timeout` with nested conversations is currently not + supported. You can still try to use it, but it will likely behave differently + from what you expect. + + + name (:obj:`str`, optional): The name for this conversationhandler. Required for + persistence. + persistent (:obj:`bool`, optional): If the conversations dict for this handler should be + saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater` + map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be + used to instruct a nested conversationhandler to transition into a mapped state on + its parent conversationhandler in place of a specified nested state. + run_async (:obj:`bool`, optional): Pass :obj:`True` to *override* the + :attr:`Handler.run_async` setting of all handlers (in :attr:`entry_points`, + :attr:`states` and :attr:`fallbacks`). + + Note: + If set to :obj:`True`, you should not pass a handler instance, that needs to be + run synchronously in another context. + + .. versionadded:: 13.2 Raises: ValueError + Attributes: + persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be + saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater` + run_async (:obj:`bool`): If :obj:`True`, will override the + :attr:`Handler.run_async` setting of all internal handlers on initialization. + + .. versionadded:: 13.2 + """ - END = -1 - """:obj:`int`: Used as a constant to return when a conversation is ended.""" - def __init__(self, - entry_points, - states, - fallbacks, - allow_reentry=False, - run_async_timeout=None, - timed_out_behavior=None, - per_chat=True, - per_user=True, - per_message=False, - conversation_timeout=None): - - self.entry_points = entry_points - self.states = states - self.fallbacks = fallbacks - - self.allow_reentry = allow_reentry - self.run_async_timeout = run_async_timeout - self.timed_out_behavior = timed_out_behavior - self.per_user = per_user - self.per_chat = per_chat - self.per_message = per_message - self.conversation_timeout = conversation_timeout - - self.timeout_jobs = dict() - self.conversations = dict() - self.current_conversation = None - self.current_handler = None + __slots__ = ( + '_entry_points', + '_states', + '_fallbacks', + '_allow_reentry', + '_per_user', + '_per_chat', + '_per_message', + '_conversation_timeout', + '_name', + 'persistent', + '_persistence', + '_map_to_parent', + 'timeout_jobs', + '_timeout_jobs_lock', + '_conversations', + '_conversations_lock', + 'logger', + ) + + END: ClassVar[int] = -1 + """:obj:`int`: Used as a constant to return when a conversation is ended.""" + TIMEOUT: ClassVar[int] = -2 + """:obj:`int`: Used as a constant to handle state when a conversation is timed out.""" + WAITING: ClassVar[int] = -3 + """:obj:`int`: Used as a constant to handle state when a conversation is still waiting on the + previous ``@run_sync`` decorated running handler to finish.""" + # pylint: disable=W0231 + def __init__( + self, + entry_points: List[Handler[Update, CCT]], + states: Dict[object, List[Handler[Update, CCT]]], + fallbacks: List[Handler[Update, CCT]], + allow_reentry: bool = False, + per_chat: bool = True, + per_user: bool = True, + per_message: bool = False, + conversation_timeout: Union[float, datetime.timedelta] = None, + name: str = None, + persistent: bool = False, + map_to_parent: Dict[object, object] = None, + run_async: bool = False, + ): + self.run_async = run_async + + self._entry_points = entry_points + self._states = states + self._fallbacks = fallbacks + + self._allow_reentry = allow_reentry + self._per_user = per_user + self._per_chat = per_chat + self._per_message = per_message + self._conversation_timeout = conversation_timeout + self._name = name + if persistent and not self.name: + raise ValueError("Conversations can't be persistent when handler is unnamed.") + self.persistent: bool = persistent + self._persistence: Optional[BasePersistence] = None + """:obj:`telegram.ext.BasePersistence`: The persistence used to store conversations. + Set by dispatcher""" + self._map_to_parent = map_to_parent + + self.timeout_jobs: Dict[Tuple[int, ...], 'Job'] = {} + self._timeout_jobs_lock = Lock() + self._conversations: ConversationDict = {} + self._conversations_lock = Lock() self.logger = logging.getLogger(__name__) @@ -156,10 +260,12 @@ def __init__(self, raise ValueError("'per_user', 'per_chat' and 'per_message' can't all be 'False'") if self.per_message and not self.per_chat: - logging.warning("If 'per_message=True' is used, 'per_chat=True' should also be used, " - "since message IDs are not globally unique.") + warnings.warn( + "If 'per_message=True' is used, 'per_chat=True' should also be used, " + "since message IDs are not globally unique." + ) - all_handlers = list() + all_handlers: List[Handler] = [] all_handlers.extend(entry_points) all_handlers.extend(fallbacks) @@ -169,165 +275,451 @@ def __init__(self, if self.per_message: for handler in all_handlers: if not isinstance(handler, CallbackQueryHandler): - logging.warning("If 'per_message=True', all entry points and state handlers" - " must be 'CallbackQueryHandler', since no other handlers " - "have a message context.") + warnings.warn( + "If 'per_message=True', all entry points and state handlers" + " must be 'CallbackQueryHandler', since no other handlers " + "have a message context." + ) + break else: for handler in all_handlers: if isinstance(handler, CallbackQueryHandler): - logging.warning("If 'per_message=False', 'CallbackQueryHandler' will not be " - "tracked for every message.") + warnings.warn( + "If 'per_message=False', 'CallbackQueryHandler' will not be " + "tracked for every message." + ) + break if self.per_chat: for handler in all_handlers: if isinstance(handler, (InlineQueryHandler, ChosenInlineResultHandler)): - logging.warning("If 'per_chat=True', 'InlineQueryHandler' can not be used, " - "since inline queries have no chat context.") + warnings.warn( + "If 'per_chat=True', 'InlineQueryHandler' can not be used, " + "since inline queries have no chat context." + ) + break + + if self.conversation_timeout: + for handler in all_handlers: + if isinstance(handler, self.__class__): + warnings.warn( + "Using `conversation_timeout` with nested conversations is currently not " + "supported. You can still try to use it, but it will likely behave " + "differently from what you expect." + ) + break + + if self.run_async: + for handler in all_handlers: + handler.run_async = True + + @property + def entry_points(self) -> List[Handler]: + """List[:class:`telegram.ext.Handler`]: A list of ``Handler`` objects that can trigger the + start of the conversation. + """ + return self._entry_points + + @entry_points.setter + def entry_points(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to entry_points after initialization.') + + @property + def states(self) -> Dict[object, List[Handler]]: + """Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]: A :obj:`dict` that + defines the different states of conversation a user can be in and one or more + associated ``Handler`` objects that should be used in that state. + """ + return self._states - def _get_key(self, update): + @states.setter + def states(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to states after initialization.') + + @property + def fallbacks(self) -> List[Handler]: + """List[:class:`telegram.ext.Handler`]: A list of handlers that might be used if + the user is in a conversation, but every handler for their current state returned + :obj:`False` on :attr:`check_update`. + """ + return self._fallbacks + + @fallbacks.setter + def fallbacks(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to fallbacks after initialization.') + + @property + def allow_reentry(self) -> bool: + """:obj:`bool`: Determines if a user can restart a conversation with an entry point.""" + return self._allow_reentry + + @allow_reentry.setter + def allow_reentry(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to allow_reentry after initialization.') + + @property + def per_user(self) -> bool: + """:obj:`bool`: If the conversation key should contain the User's ID.""" + return self._per_user + + @per_user.setter + def per_user(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to per_user after initialization.') + + @property + def per_chat(self) -> bool: + """:obj:`bool`: If the conversation key should contain the Chat's ID.""" + return self._per_chat + + @per_chat.setter + def per_chat(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to per_chat after initialization.') + + @property + def per_message(self) -> bool: + """:obj:`bool`: If the conversation key should contain the message's ID.""" + return self._per_message + + @per_message.setter + def per_message(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to per_message after initialization.') + + @property + def conversation_timeout( + self, + ) -> Optional[Union[float, datetime.timedelta]]: + """:obj:`float` | :obj:`datetime.timedelta`: Optional. When this + handler is inactive more than this timeout (in seconds), it will be automatically + ended. + """ + return self._conversation_timeout + + @conversation_timeout.setter + def conversation_timeout(self, value: object) -> NoReturn: + raise ValueError( + 'You can not assign a new value to conversation_timeout after initialization.' + ) + + @property + def name(self) -> Optional[str]: + """:obj:`str`: Optional. The name for this :class:`ConversationHandler`.""" + return self._name + + @name.setter + def name(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to name after initialization.') + + @property + def map_to_parent(self) -> Optional[Dict[object, object]]: + """Dict[:obj:`object`, :obj:`object`]: Optional. A :obj:`dict` that can be + used to instruct a nested :class:`ConversationHandler` to transition into a mapped state on + its parent :class:`ConversationHandler` in place of a specified nested state. + """ + return self._map_to_parent + + @map_to_parent.setter + def map_to_parent(self, value: object) -> NoReturn: + raise ValueError('You can not assign a new value to map_to_parent after initialization.') + + @property + def persistence(self) -> Optional[BasePersistence]: + """The persistence class as provided by the :class:`Dispatcher`.""" + return self._persistence + + @persistence.setter + def persistence(self, persistence: BasePersistence) -> None: + self._persistence = persistence + # Set persistence for nested conversations + for handlers in self.states.values(): + for handler in handlers: + if isinstance(handler, ConversationHandler): + handler.persistence = self.persistence + + @property + def conversations(self) -> ConversationDict: # skipcq: PY-D0003 + return self._conversations + + @conversations.setter + def conversations(self, value: ConversationDict) -> None: + self._conversations = value + # Set conversations for nested conversations + for handlers in self.states.values(): + for handler in handlers: + if isinstance(handler, ConversationHandler) and self.persistence and handler.name: + handler.conversations = self.persistence.get_conversations(handler.name) + + def _get_key(self, update: Update) -> Tuple[int, ...]: chat = update.effective_chat user = update.effective_user - key = list() + key = [] if self.per_chat: - key.append(chat.id) + key.append(chat.id) # type: ignore[union-attr] if self.per_user and user is not None: key.append(user.id) if self.per_message: - key.append(update.callback_query.inline_message_id - or update.callback_query.message.message_id) + key.append( + update.callback_query.inline_message_id # type: ignore[union-attr] + or update.callback_query.message.message_id # type: ignore[union-attr] + ) return tuple(key) - def check_update(self, update): + def _resolve_promise(self, state: Tuple) -> object: + old_state, new_state = state + try: + res = new_state.result(0) + res = res if res is not None else old_state + except Exception as exc: + self.logger.exception("Promise function raised exception") + self.logger.exception("%s", exc) + res = old_state + finally: + if res is None and old_state is None: + res = self.END + return res + + def _schedule_job( + self, + new_state: object, + dispatcher: 'Dispatcher', + update: Update, + context: Optional[CallbackContext], + conversation_key: Tuple[int, ...], + ) -> None: + if new_state != self.END: + try: + # both job_queue & conversation_timeout are checked before calling _schedule_job + j_queue = dispatcher.job_queue + self.timeout_jobs[conversation_key] = j_queue.run_once( # type: ignore[union-attr] + self._trigger_timeout, + self.conversation_timeout, # type: ignore[arg-type] + context=_ConversationTimeoutContext( + conversation_key, update, dispatcher, context + ), + ) + except Exception as exc: + self.logger.exception( + "Failed to schedule timeout job due to the following exception:" + ) + self.logger.exception("%s", exc) + + def check_update(self, update: object) -> CheckUpdateType: # pylint: disable=R0911 """ Determines whether an update should be handled by this conversationhandler, and if so in which state the conversation currently is. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ + if not isinstance(update, Update): + return None # Ignore messages in channels - if (not isinstance(update, Update) or - update.channel_post or - self.per_chat and not update.effective_chat or - self.per_message and not update.callback_query or - update.callback_query and self.per_chat and not update.callback_query.message): - return False + if update.channel_post or update.edited_channel_post: + return None + if self.per_chat and not update.effective_chat: + return None + if self.per_message and not update.callback_query: + return None + if update.callback_query and self.per_chat and not update.callback_query.message: + return None key = self._get_key(update) - state = self.conversations.get(key) + with self._conversations_lock: + state = self.conversations.get(key) # Resolve promises - if isinstance(state, tuple) and len(state) is 2 and isinstance(state[1], Promise): + if isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], Promise): self.logger.debug('waiting for promise...') - old_state, new_state = state - error = False - try: - res = new_state.result(timeout=self.run_async_timeout) - except Exception as exc: - self.logger.exception("Promise function raised exception") - self.logger.exception("{}".format(exc)) - error = True - - if not error and new_state.done.is_set(): - self.update_state(res, key) - state = self.conversations.get(key) + # check if promise is finished or not + if state[1].done.wait(0): + res = self._resolve_promise(state) + self._update_state(res, key) + with self._conversations_lock: + state = self.conversations.get(key) + # if not then handle WAITING state instead else: - for candidate in (self.timed_out_behavior or []): - if candidate.check_update(update): - # Save the current user and the selected handler for handle_update - self.current_conversation = key - self.current_handler = candidate + hdlrs = self.states.get(self.WAITING, []) + for hdlr in hdlrs: + check = hdlr.check_update(update) + if check is not None and check is not False: + return key, hdlr, check + return None - return True - - else: - return False - - self.logger.debug('selecting conversation %s with state %s' % (str(key), str(state))) + self.logger.debug('selecting conversation %s with state %s', str(key), str(state)) handler = None # Search entry points for a match if state is None or self.allow_reentry: for entry_point in self.entry_points: - if entry_point.check_update(update): + check = entry_point.check_update(update) + if check is not None and check is not False: handler = entry_point break else: if state is None: - return False + return None # Get the handler list for current state, if we didn't find one yet and we're still here if state is not None and not handler: handlers = self.states.get(state) - for candidate in (handlers or []): - if candidate.check_update(update): + for candidate in handlers or []: + check = candidate.check_update(update) + if check is not None and check is not False: handler = candidate break # Find a fallback handler if all other handlers fail else: for fallback in self.fallbacks: - if fallback.check_update(update): + check = fallback.check_update(update) + if check is not None and check is not False: handler = fallback break else: - return False - - # Save the current user and the selected handler for handle_update - self.current_conversation = key - self.current_handler = handler + return None - return True + return key, handler, check # type: ignore[return-value] - def handle_update(self, update, dispatcher): + def handle_update( # type: ignore[override] + self, + update: Update, + dispatcher: 'Dispatcher', + check_result: CheckUpdateType, + context: CallbackContext = None, + ) -> Optional[object]: """Send the update to the callback for the current state and Handler Args: + check_result: The result from check_update. For this handler it's a tuple of key, + handler, and the handler's check result. update (:class:`telegram.Update`): Incoming telegram update. dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. + context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by + the dispatcher. """ - new_state = self.current_handler.handle_update(update, dispatcher) - timeout_job = self.timeout_jobs.pop(self.current_conversation, None) - - if timeout_job is not None: - timeout_job.schedule_removal() - if self.conversation_timeout and new_state != self.END: - self.timeout_jobs[self.current_conversation] = dispatcher.job_queue.run_once( - self._trigger_timeout, self.conversation_timeout, - context=self.current_conversation - ) - - self.update_state(new_state, self.current_conversation) - - def update_state(self, new_state, key): + update = cast(Update, update) # for mypy + conversation_key, handler, check_result = check_result # type: ignore[assignment,misc] + raise_dp_handler_stop = False + + with self._timeout_jobs_lock: + # Remove the old timeout job (if present) + timeout_job = self.timeout_jobs.pop(conversation_key, None) + + if timeout_job is not None: + timeout_job.schedule_removal() + try: + new_state = handler.handle_update(update, dispatcher, check_result, context) + except DispatcherHandlerStop as exception: + new_state = exception.state + raise_dp_handler_stop = True + with self._timeout_jobs_lock: + if self.conversation_timeout: + if dispatcher.job_queue is not None: + # Add the new timeout job + if isinstance(new_state, Promise): + new_state.add_done_callback( + functools.partial( + self._schedule_job, + dispatcher=dispatcher, + update=update, + context=context, + conversation_key=conversation_key, + ) + ) + elif new_state != self.END: + self._schedule_job( + new_state, dispatcher, update, context, conversation_key + ) + else: + self.logger.warning( + "Ignoring `conversation_timeout` because the Dispatcher has no JobQueue." + ) + + if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent: + self._update_state(self.END, conversation_key) + if raise_dp_handler_stop: + raise DispatcherHandlerStop(self.map_to_parent.get(new_state)) + return self.map_to_parent.get(new_state) + + self._update_state(new_state, conversation_key) + if raise_dp_handler_stop: + # Don't pass the new state here. If we're in a nested conversation, the parent is + # expecting None as return value. + raise DispatcherHandlerStop() + return None + + def _update_state(self, new_state: object, key: Tuple[int, ...]) -> None: if new_state == self.END: - if key in self.conversations: - del self.conversations[key] - else: - pass + with self._conversations_lock: + if key in self.conversations: + # If there is no key in conversations, nothing is done. + del self.conversations[key] + if self.persistent and self.persistence and self.name: + self.persistence.update_conversation(self.name, key, None) elif isinstance(new_state, Promise): - self.conversations[key] = (self.conversations.get(key), new_state) + with self._conversations_lock: + self.conversations[key] = (self.conversations.get(key), new_state) + if self.persistent and self.persistence and self.name: + self.persistence.update_conversation( + self.name, key, (self.conversations.get(key), new_state) + ) elif new_state is not None: - self.conversations[key] = new_state - - def _trigger_timeout(self, bot, job): - del self.timeout_jobs[job.context] - self.update_state(self.END, job.context) + if new_state not in self.states: + warnings.warn( + f"Handler returned state {new_state} which is unknown to the " + f"ConversationHandler{' ' + self.name if self.name is not None else ''}." + ) + with self._conversations_lock: + self.conversations[key] = new_state + if self.persistent and self.persistence and self.name: + self.persistence.update_conversation(self.name, key, new_state) + + def _trigger_timeout(self, context: CallbackContext, job: 'Job' = None) -> None: + self.logger.debug('conversation timeout was triggered!') + + # Backward compatibility with bots that do not use CallbackContext + if isinstance(context, CallbackContext): + job = context.job + ctxt = cast(_ConversationTimeoutContext, job.context) # type: ignore[union-attr] + else: + ctxt = cast(_ConversationTimeoutContext, job.context) + + callback_context = ctxt.callback_context + + with self._timeout_jobs_lock: + found_job = self.timeout_jobs[ctxt.conversation_key] + if found_job is not job: + # The timeout has been cancelled in handle_update + return + del self.timeout_jobs[ctxt.conversation_key] + + handlers = self.states.get(self.TIMEOUT, []) + for handler in handlers: + check = handler.check_update(ctxt.update) + if check is not None and check is not False: + try: + handler.handle_update(ctxt.update, ctxt.dispatcher, check, callback_context) + except DispatcherHandlerStop: + self.logger.warning( + 'DispatcherHandlerStop in TIMEOUT state of ' + 'ConversationHandler has no effect. Ignoring.' + ) + + self._update_state(self.END, ctxt.conversation_key) diff --git a/telegramer/include/telegram/ext/defaults.py b/telegramer/include/telegram/ext/defaults.py new file mode 100644 index 0000000..1e08255 --- /dev/null +++ b/telegramer/include/telegram/ext/defaults.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=R0201 +"""This module contains the class Defaults, which allows to pass default values to Updater.""" +from typing import NoReturn, Optional, Dict, Any + +import pytz + +from telegram.utils.deprecate import set_new_attribute_deprecated +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + + +class Defaults: + """Convenience Class to gather all parameters with a (user defined) default value + + Parameters: + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or URLs in your bot's message. + disable_notification (:obj:`bool`, optional): Sends the message silently. Users will + receive a notification with no sound. + disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this + message. + allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the + read timeout from the server (instead of the one specified during creation of the + connection pool). + + Note: + Will *not* be used for :meth:`telegram.Bot.get_updates`! + quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply + to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will + be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. + tzinfo (:obj:`tzinfo`, optional): A timezone to be used for all date(time) inputs + appearing throughout PTB, i.e. if a timezone naive date(time) object is passed + somewhere, it will be assumed to be in ``tzinfo``. Must be a timezone provided by the + ``pytz`` module. Defaults to UTC. + run_async (:obj:`bool`, optional): Default setting for the ``run_async`` parameter of + handlers and error handlers registered through :meth:`Dispatcher.add_handler` and + :meth:`Dispatcher.add_error_handler`. Defaults to :obj:`False`. + """ + + __slots__ = ( + '_timeout', + '_tzinfo', + '_disable_web_page_preview', + '_run_async', + '_quote', + '_disable_notification', + '_allow_sending_without_reply', + '_parse_mode', + '_api_defaults', + '__dict__', + ) + + def __init__( + self, + parse_mode: str = None, + disable_notification: bool = None, + disable_web_page_preview: bool = None, + # Timeout needs special treatment, since the bot methods have two different + # default values for timeout (None and 20s) + timeout: ODVInput[float] = DEFAULT_NONE, + quote: bool = None, + tzinfo: pytz.BaseTzInfo = pytz.utc, + run_async: bool = False, + allow_sending_without_reply: bool = None, + ): + self._parse_mode = parse_mode + self._disable_notification = disable_notification + self._disable_web_page_preview = disable_web_page_preview + self._allow_sending_without_reply = allow_sending_without_reply + self._timeout = timeout + self._quote = quote + self._tzinfo = tzinfo + self._run_async = run_async + + # Gather all defaults that actually have a default value + self._api_defaults = {} + for kwarg in ( + 'parse_mode', + 'explanation_parse_mode', + 'disable_notification', + 'disable_web_page_preview', + 'allow_sending_without_reply', + ): + value = getattr(self, kwarg) + if value not in [None, DEFAULT_NONE]: + self._api_defaults[kwarg] = value + # Special casing, as None is a valid default value + if self._timeout != DEFAULT_NONE: + self._api_defaults['timeout'] = self._timeout + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + + @property + def api_defaults(self) -> Dict[str, Any]: # skip-cq: PY-D0003 + return self._api_defaults + + @property + def parse_mode(self) -> Optional[str]: + """:obj:`str`: Optional. Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or URLs in your bot's message. + """ + return self._parse_mode + + @parse_mode.setter + def parse_mode(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def explanation_parse_mode(self) -> Optional[str]: + """:obj:`str`: Optional. Alias for :attr:`parse_mode`, used for + the corresponding parameter of :meth:`telegram.Bot.send_poll`. + """ + return self._parse_mode + + @explanation_parse_mode.setter + def explanation_parse_mode(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def disable_notification(self) -> Optional[bool]: + """:obj:`bool`: Optional. Sends the message silently. Users will + receive a notification with no sound. + """ + return self._disable_notification + + @disable_notification.setter + def disable_notification(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def disable_web_page_preview(self) -> Optional[bool]: + """:obj:`bool`: Optional. Disables link previews for links in this + message. + """ + return self._disable_web_page_preview + + @disable_web_page_preview.setter + def disable_web_page_preview(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def allow_sending_without_reply(self) -> Optional[bool]: + """:obj:`bool`: Optional. Pass :obj:`True`, if the message + should be sent even if the specified replied-to message is not found. + """ + return self._allow_sending_without_reply + + @allow_sending_without_reply.setter + def allow_sending_without_reply(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def timeout(self) -> ODVInput[float]: + """:obj:`int` | :obj:`float`: Optional. If this value is specified, use it as the + read timeout from the server (instead of the one specified during creation of the + connection pool). + """ + return self._timeout + + @timeout.setter + def timeout(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def quote(self) -> Optional[bool]: + """:obj:`bool`: Optional. If set to :obj:`True`, the reply is sent as an actual reply + to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will + be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. + """ + return self._quote + + @quote.setter + def quote(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def tzinfo(self) -> pytz.BaseTzInfo: + """:obj:`tzinfo`: A timezone to be used for all date(time) objects appearing + throughout PTB. + """ + return self._tzinfo + + @tzinfo.setter + def tzinfo(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + @property + def run_async(self) -> bool: + """:obj:`bool`: Optional. Default setting for the ``run_async`` parameter of + handlers and error handlers registered through :meth:`Dispatcher.add_handler` and + :meth:`Dispatcher.add_error_handler`. + """ + return self._run_async + + @run_async.setter + def run_async(self, value: object) -> NoReturn: + raise AttributeError( + "You can not assign a new value to defaults after because it would " + "not have any effect." + ) + + def __hash__(self) -> int: + return hash( + ( + self._parse_mode, + self._disable_notification, + self._disable_web_page_preview, + self._allow_sending_without_reply, + self._timeout, + self._quote, + self._tzinfo, + self._run_async, + ) + ) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Defaults): + return all(getattr(self, attr) == getattr(other, attr) for attr in self.__slots__) + return False + + def __ne__(self, other: object) -> bool: + return not self == other diff --git a/telegramer/include/telegram/ext/dictpersistence.py b/telegramer/include/telegram/ext/dictpersistence.py new file mode 100644 index 0000000..e307123 --- /dev/null +++ b/telegramer/include/telegram/ext/dictpersistence.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the DictPersistence class.""" + +from typing import DefaultDict, Dict, Optional, Tuple, cast +from collections import defaultdict + +from telegram.utils.helpers import ( + decode_conversations_from_json, + decode_user_chat_data_from_json, + encode_conversations_to_json, +) +from telegram.ext import BasePersistence +from telegram.ext.utils.types import ConversationDict, CDCData + +try: + import ujson as json +except ImportError: + import json # type: ignore[no-redef] + + +class DictPersistence(BasePersistence): + """Using Python's :obj:`dict` and ``json`` for making your bot persistent. + + Note: + This class does *not* implement a :meth:`flush` method, meaning that data managed by + ``DictPersistence`` is in-memory only and will be lost when the bot shuts down. This is, + because ``DictPersistence`` is mainly intended as starting point for custom persistence + classes that need to JSON-serialize the stored data before writing them to file/database. + + Warning: + :class:`DictPersistence` will try to replace :class:`telegram.Bot` instances by + :attr:`REPLACED_BOT` and insert the bot set with + :meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure + that changes to the bot apply to the saved objects, too. If you change the bots token, this + may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see + :meth:`telegram.ext.BasePersistence.replace_bot` and + :meth:`telegram.ext.BasePersistence.insert_bot`. + + Args: + store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this + persistence class. Default is :obj:`True`. + store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this + persistence class. Default is :obj:`True`. + store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this + persistence class. Default is :obj:`True`. + store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this + persistence class. Default is :obj:`False`. + + .. versionadded:: 13.6 + user_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct + user_data on creating this persistence. Default is ``""``. + chat_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct + chat_data on creating this persistence. Default is ``""``. + bot_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct + bot_data on creating this persistence. Default is ``""``. + callback_data_json (:obj:`str`, optional): Json string that will be used to reconstruct + callback_data on creating this persistence. Default is ``""``. + + .. versionadded:: 13.6 + conversations_json (:obj:`str`, optional): JSON string that will be used to reconstruct + conversation on creating this persistence. Default is ``""``. + + Attributes: + store_user_data (:obj:`bool`): Whether user_data should be saved by this + persistence class. + store_chat_data (:obj:`bool`): Whether chat_data should be saved by this + persistence class. + store_bot_data (:obj:`bool`): Whether bot_data should be saved by this + persistence class. + store_callback_data (:obj:`bool`): Whether callback_data be saved by this + persistence class. + + .. versionadded:: 13.6 + """ + + __slots__ = ( + '_user_data', + '_chat_data', + '_bot_data', + '_callback_data', + '_conversations', + '_user_data_json', + '_chat_data_json', + '_bot_data_json', + '_callback_data_json', + '_conversations_json', + ) + + def __init__( + self, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + user_data_json: str = '', + chat_data_json: str = '', + bot_data_json: str = '', + conversations_json: str = '', + store_callback_data: bool = False, + callback_data_json: str = '', + ): + super().__init__( + store_user_data=store_user_data, + store_chat_data=store_chat_data, + store_bot_data=store_bot_data, + store_callback_data=store_callback_data, + ) + self._user_data = None + self._chat_data = None + self._bot_data = None + self._callback_data = None + self._conversations = None + self._user_data_json = None + self._chat_data_json = None + self._bot_data_json = None + self._callback_data_json = None + self._conversations_json = None + if user_data_json: + try: + self._user_data = decode_user_chat_data_from_json(user_data_json) + self._user_data_json = user_data_json + except (ValueError, AttributeError) as exc: + raise TypeError("Unable to deserialize user_data_json. Not valid JSON") from exc + if chat_data_json: + try: + self._chat_data = decode_user_chat_data_from_json(chat_data_json) + self._chat_data_json = chat_data_json + except (ValueError, AttributeError) as exc: + raise TypeError("Unable to deserialize chat_data_json. Not valid JSON") from exc + if bot_data_json: + try: + self._bot_data = json.loads(bot_data_json) + self._bot_data_json = bot_data_json + except (ValueError, AttributeError) as exc: + raise TypeError("Unable to deserialize bot_data_json. Not valid JSON") from exc + if not isinstance(self._bot_data, dict): + raise TypeError("bot_data_json must be serialized dict") + if callback_data_json: + try: + data = json.loads(callback_data_json) + except (ValueError, AttributeError) as exc: + raise TypeError( + "Unable to deserialize callback_data_json. Not valid JSON" + ) from exc + # We are a bit more thorough with the checking of the format here, because it's + # more complicated than for the other things + try: + if data is None: + self._callback_data = None + else: + self._callback_data = cast( + CDCData, + ([(one, float(two), three) for one, two, three in data[0]], data[1]), + ) + self._callback_data_json = callback_data_json + except (ValueError, IndexError) as exc: + raise TypeError("callback_data_json is not in the required format") from exc + if self._callback_data is not None and ( + not all( + isinstance(entry[2], dict) and isinstance(entry[0], str) + for entry in self._callback_data[0] + ) + or not isinstance(self._callback_data[1], dict) + ): + raise TypeError("callback_data_json is not in the required format") + + if conversations_json: + try: + self._conversations = decode_conversations_from_json(conversations_json) + self._conversations_json = conversations_json + except (ValueError, AttributeError) as exc: + raise TypeError( + "Unable to deserialize conversations_json. Not valid JSON" + ) from exc + + @property + def user_data(self) -> Optional[DefaultDict[int, Dict]]: + """:obj:`dict`: The user_data as a dict.""" + return self._user_data + + @property + def user_data_json(self) -> str: + """:obj:`str`: The user_data serialized as a JSON-string.""" + if self._user_data_json: + return self._user_data_json + return json.dumps(self.user_data) + + @property + def chat_data(self) -> Optional[DefaultDict[int, Dict]]: + """:obj:`dict`: The chat_data as a dict.""" + return self._chat_data + + @property + def chat_data_json(self) -> str: + """:obj:`str`: The chat_data serialized as a JSON-string.""" + if self._chat_data_json: + return self._chat_data_json + return json.dumps(self.chat_data) + + @property + def bot_data(self) -> Optional[Dict]: + """:obj:`dict`: The bot_data as a dict.""" + return self._bot_data + + @property + def bot_data_json(self) -> str: + """:obj:`str`: The bot_data serialized as a JSON-string.""" + if self._bot_data_json: + return self._bot_data_json + return json.dumps(self.bot_data) + + @property + def callback_data(self) -> Optional[CDCData]: + """:class:`telegram.ext.utils.types.CDCData`: The meta data on the stored callback data. + + .. versionadded:: 13.6 + """ + return self._callback_data + + @property + def callback_data_json(self) -> str: + """:obj:`str`: The meta data on the stored callback data as a JSON-string. + + .. versionadded:: 13.6 + """ + if self._callback_data_json: + return self._callback_data_json + return json.dumps(self.callback_data) + + @property + def conversations(self) -> Optional[Dict[str, ConversationDict]]: + """:obj:`dict`: The conversations as a dict.""" + return self._conversations + + @property + def conversations_json(self) -> str: + """:obj:`str`: The conversations serialized as a JSON-string.""" + if self._conversations_json: + return self._conversations_json + return encode_conversations_to_json(self.conversations) # type: ignore[arg-type] + + def get_user_data(self) -> DefaultDict[int, Dict[object, object]]: + """Returns the user_data created from the ``user_data_json`` or an empty + :obj:`defaultdict`. + + Returns: + :obj:`defaultdict`: The restored user data. + """ + if self.user_data is None: + self._user_data = defaultdict(dict) + return self.user_data # type: ignore[return-value] + + def get_chat_data(self) -> DefaultDict[int, Dict[object, object]]: + """Returns the chat_data created from the ``chat_data_json`` or an empty + :obj:`defaultdict`. + + Returns: + :obj:`defaultdict`: The restored chat data. + """ + if self.chat_data is None: + self._chat_data = defaultdict(dict) + return self.chat_data # type: ignore[return-value] + + def get_bot_data(self) -> Dict[object, object]: + """Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`. + + Returns: + :obj:`dict`: The restored bot data. + """ + if self.bot_data is None: + self._bot_data = {} + return self.bot_data # type: ignore[return-value] + + def get_callback_data(self) -> Optional[CDCData]: + """Returns the callback_data created from the ``callback_data_json`` or :obj:`None`. + + .. versionadded:: 13.6 + + Returns: + Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or + :obj:`None`, if no data was stored. + """ + if self.callback_data is None: + self._callback_data = None + return None + return self.callback_data[0], self.callback_data[1].copy() + + def get_conversations(self, name: str) -> ConversationDict: + """Returns the conversations created from the ``conversations_json`` or an empty + :obj:`dict`. + + Returns: + :obj:`dict`: The restored conversations data. + """ + if self.conversations is None: + self._conversations = {} + return self.conversations.get(name, {}).copy() # type: ignore[union-attr] + + def update_conversation( + self, name: str, key: Tuple[int, ...], new_state: Optional[object] + ) -> None: + """Will update the conversations for the given handler. + + Args: + name (:obj:`str`): The handler's name. + key (:obj:`tuple`): The key the state is changed for. + new_state (:obj:`tuple` | :obj:`any`): The new state for the given key. + """ + if not self._conversations: + self._conversations = {} + if self._conversations.setdefault(name, {}).get(key) == new_state: + return + self._conversations[name][key] = new_state + self._conversations_json = None + + def update_user_data(self, user_id: int, data: Dict) -> None: + """Will update the user_data (if changed). + + Args: + user_id (:obj:`int`): The user the data might have been changed for. + data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``. + """ + if self._user_data is None: + self._user_data = defaultdict(dict) + if self._user_data.get(user_id) == data: + return + self._user_data[user_id] = data + self._user_data_json = None + + def update_chat_data(self, chat_id: int, data: Dict) -> None: + """Will update the chat_data (if changed). + + Args: + chat_id (:obj:`int`): The chat the data might have been changed for. + data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``. + """ + if self._chat_data is None: + self._chat_data = defaultdict(dict) + if self._chat_data.get(chat_id) == data: + return + self._chat_data[chat_id] = data + self._chat_data_json = None + + def update_bot_data(self, data: Dict) -> None: + """Will update the bot_data (if changed). + + Args: + data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.bot_data`. + """ + if self._bot_data == data: + return + self._bot_data = data + self._bot_data_json = None + + def update_callback_data(self, data: CDCData) -> None: + """Will update the callback_data (if changed). + + .. versionadded:: 13.6 + + Args: + data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore + :class:`telegram.ext.CallbackDataCache`. + """ + if self._callback_data == data: + return + self._callback_data = (data[0], data[1].copy()) + self._callback_data_json = None + + def refresh_user_data(self, user_id: int, user_data: Dict) -> None: + """Does nothing. + + .. versionadded:: 13.6 + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` + """ + + def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None: + """Does nothing. + + .. versionadded:: 13.6 + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` + """ + + def refresh_bot_data(self, bot_data: Dict) -> None: + """Does nothing. + + .. versionadded:: 13.6 + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` + """ diff --git a/telegramer/include/telegram/ext/dispatcher.py b/telegramer/include/telegram/ext/dispatcher.py index 0f43fdb..af24188 100644 --- a/telegramer/include/telegram/ext/dispatcher.py +++ b/telegramer/include/telegram/ext/dispatcher.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,58 +19,112 @@ """This module contains the Dispatcher class.""" import logging +import warnings import weakref +from collections import defaultdict from functools import wraps -from threading import Thread, Lock, Event, current_thread, BoundedSemaphore +from queue import Empty, Queue +from threading import BoundedSemaphore, Event, Lock, Thread, current_thread from time import sleep +from typing import ( + TYPE_CHECKING, + Callable, + DefaultDict, + Dict, + List, + Optional, + Set, + Union, + Generic, + TypeVar, + overload, + cast, +) from uuid import uuid4 -from collections import defaultdict -# REMREM from queue import Queue, Empty -from Queue import Queue, Empty +from telegram import TelegramError, Update +from telegram.ext import BasePersistence, ContextTypes +from telegram.ext.callbackcontext import CallbackContext +from telegram.ext.handler import Handler +import telegram.ext.extbot +from telegram.ext.callbackdatacache import CallbackDataCache +from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated +from telegram.ext.utils.promise import Promise +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from telegram.ext.utils.types import CCT, UD, CD, BD -from future.builtins import range +if TYPE_CHECKING: + from telegram import Bot + from telegram.ext import JobQueue -from telegram import TelegramError -from telegram.ext.handler import Handler -from telegram.utils.promise import Promise +DEFAULT_GROUP: int = 0 -logging.getLogger(__name__).addHandler(logging.NullHandler()) -DEFAULT_GROUP = 0 +UT = TypeVar('UT') -def run_async(func): - """Function decorator that will run the function in a new thread. +def run_async( + func: Callable[[Update, CallbackContext], object] +) -> Callable[[Update, CallbackContext], object]: + """ + Function decorator that will run the function in a new thread. Will run :attr:`telegram.ext.Dispatcher.run_async`. Using this decorator is only possible when only a single Dispatcher exist in the system. - Note: Use this decorator to run handlers asynchronously. + Note: + DEPRECATED. Use :attr:`telegram.ext.Dispatcher.run_async` directly instead or the + :attr:`Handler.run_async` parameter. + Warning: + If you're using ``@run_async`` you cannot rely on adding custom attributes to + :class:`telegram.ext.CallbackContext`. See its docs for more info. """ + @wraps(func) - def async_func(*args, **kwargs): - return Dispatcher.get_instance().run_async(func, *args, **kwargs) + def async_func(*args: object, **kwargs: object) -> object: + warnings.warn( + 'The @run_async decorator is deprecated. Use the `run_async` parameter of ' + 'your Handler or `Dispatcher.run_async` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + return Dispatcher.get_instance()._run_async( # pylint: disable=W0212 + func, *args, update=None, error_handling=False, **kwargs + ) return async_func class DispatcherHandlerStop(Exception): - """Raise this in handler to prevent execution any other handler (even in different group).""" - pass + """ + Raise this in handler to prevent execution of any other handler (even in different group). + In order to use this exception in a :class:`telegram.ext.ConversationHandler`, pass the + optional ``state`` parameter instead of returning the next state: -class Dispatcher(object): - """This class dispatches all kinds of updates to its registered handlers. + .. code-block:: python + + def callback(update, context): + ... + raise DispatcherHandlerStop(next_state) Attributes: - bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. - update_queue (:obj:`Queue`): The synchronized queue that will contain the updates. - job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue` - instance to pass onto handler callbacks. - workers (:obj:`int`): Number of maximum concurrent worker threads for the ``@run_async`` - decorator. + state (:obj:`object`): Optional. The next state of the conversation. + + Args: + state (:obj:`object`, optional): The next state of the conversation. + """ + + __slots__ = ('state',) + + def __init__(self, state: object = None) -> None: + super().__init__() + self.state = state + + +class Dispatcher(Generic[CCT, UD, CD, BD]): + """This class dispatches all kinds of updates to its registered handlers. Args: bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. @@ -78,62 +132,216 @@ class Dispatcher(object): job_queue (:class:`telegram.ext.JobQueue`, optional): The :class:`telegram.ext.JobQueue` instance to pass onto handler callbacks. workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the - ``@run_async`` decorator. defaults to 4. + ``@run_async`` decorator and :meth:`run_async`. Defaults to 4. + persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to + store data that should be persistent over restarts. + use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback + API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. + **New users**: set this to :obj:`True`. + context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance + of :class:`telegram.ext.ContextTypes` to customize the types used in the + ``context`` interface. If not passed, the defaults documented in + :class:`telegram.ext.ContextTypes` will be used. + + .. versionadded:: 13.6 + + Attributes: + bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. + update_queue (:obj:`Queue`): The synchronized queue that will contain the updates. + job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue` + instance to pass onto handler callbacks. + workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the + ``@run_async`` decorator and :meth:`run_async`. + user_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the user. + chat_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the chat. + bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot. + persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to + store data that should be persistent over restarts. + context_types (:class:`telegram.ext.ContextTypes`): Container for the types used + in the ``context`` interface. + + .. versionadded:: 13.6 """ + # Allowing '__weakref__' creation here since we need it for the singleton + __slots__ = ( + 'workers', + 'persistence', + 'use_context', + 'update_queue', + 'job_queue', + 'user_data', + 'chat_data', + 'bot_data', + '_update_persistence_lock', + 'handlers', + 'groups', + 'error_handlers', + 'running', + '__stop_event', + '__exception_event', + '__async_queue', + '__async_threads', + 'bot', + '__dict__', + '__weakref__', + 'context_types', + ) + __singleton_lock = Lock() __singleton_semaphore = BoundedSemaphore() __singleton = None logger = logging.getLogger(__name__) - def __init__(self, bot, update_queue, workers=4, exception_event=None, job_queue=None): + @overload + def __init__( + self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]', + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True, + ): + ... + + @overload + def __init__( + self: 'Dispatcher[CCT, UD, CD, BD]', + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True, + context_types: ContextTypes[CCT, UD, CD, BD] = None, + ): + ... + + def __init__( + self, + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True, + context_types: ContextTypes[CCT, UD, CD, BD] = None, + ): self.bot = bot self.update_queue = update_queue self.job_queue = job_queue self.workers = workers + self.use_context = use_context + self.context_types = cast(ContextTypes[CCT, UD, CD, BD], context_types or ContextTypes()) + + if not use_context: + warnings.warn( + 'Old Handler API is deprecated - see https://git.io/fxJuV for details', + TelegramDeprecationWarning, + stacklevel=3, + ) + + if self.workers < 1: + warnings.warn( + 'Asynchronous callbacks can not be processed without at least one worker thread.' + ) + + self.user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data) + self.chat_data: DefaultDict[int, CD] = defaultdict(self.context_types.chat_data) + self.bot_data = self.context_types.bot_data() + self.persistence: Optional[BasePersistence] = None + self._update_persistence_lock = Lock() + if persistence: + if not isinstance(persistence, BasePersistence): + raise TypeError("persistence must be based on telegram.ext.BasePersistence") + self.persistence = persistence + self.persistence.set_bot(self.bot) + if self.persistence.store_user_data: + self.user_data = self.persistence.get_user_data() + if not isinstance(self.user_data, defaultdict): + raise ValueError("user_data must be of type defaultdict") + if self.persistence.store_chat_data: + self.chat_data = self.persistence.get_chat_data() + if not isinstance(self.chat_data, defaultdict): + raise ValueError("chat_data must be of type defaultdict") + if self.persistence.store_bot_data: + self.bot_data = self.persistence.get_bot_data() + if not isinstance(self.bot_data, self.context_types.bot_data): + raise ValueError( + f"bot_data must be of type {self.context_types.bot_data.__name__}" + ) + if self.persistence.store_callback_data: + self.bot = cast(telegram.ext.extbot.ExtBot, self.bot) + persistent_data = self.persistence.get_callback_data() + if persistent_data is not None: + if not isinstance(persistent_data, tuple) and len(persistent_data) != 2: + raise ValueError('callback_data must be a 2-tuple') + self.bot.callback_data_cache = CallbackDataCache( + self.bot, + self.bot.callback_data_cache.maxsize, + persistent_data=persistent_data, + ) + else: + self.persistence = None - self.user_data = defaultdict(dict) - """:obj:`dict`: A dictionary handlers can use to store data for the user.""" - self.chat_data = defaultdict(dict) - """:obj:`dict`: A dictionary handlers can use to store data for the chat.""" - self.handlers = {} + self.handlers: Dict[int, List[Handler]] = {} """Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group.""" - self.groups = [] + self.groups: List[int] = [] """List[:obj:`int`]: A list with all groups.""" - self.error_handlers = [] - """List[:obj:`callable`]: A list of errorHandlers.""" + self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {} + """Dict[:obj:`callable`, :obj:`bool`]: A dict, where the keys are error handlers and the + values indicate whether they are to be run asynchronously.""" self.running = False """:obj:`bool`: Indicates if this dispatcher is running.""" self.__stop_event = Event() self.__exception_event = exception_event or Event() - self.__async_queue = Queue() - self.__async_threads = set() + self.__async_queue: Queue = Queue() + self.__async_threads: Set[Thread] = set() # For backward compatibility, we allow a "singleton" mode for the dispatcher. When there's # only one instance of Dispatcher, it will be possible to use the `run_async` decorator. with self.__singleton_lock: - if self.__singleton_semaphore.acquire(blocking=0): + if self.__singleton_semaphore.acquire(blocking=False): # pylint: disable=R1732 self._set_singleton(self) else: self._set_singleton(None) - def _init_async_threads(self, base_name, workers): - base_name = '{}_'.format(base_name) if base_name else '' + def __setattr__(self, key: str, value: object) -> None: + # Mangled names don't automatically apply in __setattr__ (see + # https://docs.python.org/3/tutorial/classes.html#private-variables), so we have to make + # it mangled so they don't raise TelegramDeprecationWarning unnecessarily + if key.startswith('__'): + key = f"_{self.__class__.__name__}{key}" + if issubclass(self.__class__, Dispatcher) and self.__class__ is not Dispatcher: + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + + @property + def exception_event(self) -> Event: # skipcq: PY-D0003 + return self.__exception_event + + def _init_async_threads(self, base_name: str, workers: int) -> None: + base_name = f'{base_name}_' if base_name else '' for i in range(workers): - thread = Thread(target=self._pooled, name='{}{}'.format(base_name, i)) + thread = Thread(target=self._pooled, name=f'Bot:{self.bot.id}:worker:{base_name}{i}') self.__async_threads.add(thread) thread.start() @classmethod - def _set_singleton(cls, val): + def _set_singleton(cls, val: Optional['Dispatcher']) -> None: cls.logger.debug('Setting singleton dispatcher as %s', val) cls.__singleton = weakref.ref(val) if val else None @classmethod - def get_instance(cls): + def get_instance(cls) -> 'Dispatcher': """Get the singleton instance of this class. Returns: @@ -144,47 +352,95 @@ def get_instance(cls): """ if cls.__singleton is not None: - return cls.__singleton() # pylint: disable=not-callable - else: - raise RuntimeError('{} not initialized or multiple instances exist'.format( - cls.__name__)) + return cls.__singleton() # type: ignore[return-value] # pylint: disable=not-callable + raise RuntimeError(f'{cls.__name__} not initialized or multiple instances exist') - def _pooled(self): - thr_name = current_thread().getName() + def _pooled(self) -> None: + thr_name = current_thread().name while 1: promise = self.__async_queue.get() # If unpacking fails, the thread pool is being closed from Updater._join_async_threads if not isinstance(promise, Promise): - self.logger.debug("Closing run_async thread %s/%d", thr_name, - len(self.__async_threads)) + self.logger.debug( + "Closing run_async thread %s/%d", thr_name, len(self.__async_threads) + ) break promise.run() + + if not promise.exception: + self.update_persistence(update=promise.update) + continue + if isinstance(promise.exception, DispatcherHandlerStop): self.logger.warning( 'DispatcherHandlerStop is not supported with async functions; func: %s', - promise.pooled_function.__name__) + promise.pooled_function.__name__, + ) + continue - def run_async(self, func, *args, **kwargs): - """Queue a function (with given args/kwargs) to be run asynchronously. + # Avoid infinite recursion of error handlers. + if promise.pooled_function in self.error_handlers: + self.logger.error('An uncaught error was raised while handling the error.') + continue + + # Don't perform error handling for a `Promise` with deactivated error handling. This + # should happen only via the deprecated `@run_async` decorator or `Promises` created + # within error handlers + if not promise.error_handling: + self.logger.error('A promise with deactivated error handling raised an error.') + continue + + # If we arrive here, an exception happened in the promise and was neither + # DispatcherHandlerStop nor raised by an error handler. So we can and must handle it + try: + self.dispatch_error(promise.update, promise.exception, promise=promise) + except Exception: + self.logger.exception('An uncaught error was raised while handling the error.') + + def run_async( + self, func: Callable[..., object], *args: object, update: object = None, **kwargs: object + ) -> Promise: + """ + Queue a function (with given args/kwargs) to be run asynchronously. Exceptions raised + by the function will be handled by the error handlers registered with + :meth:`add_error_handler`. + + Warning: + * If you're using ``@run_async``/:meth:`run_async` you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + * Calling a function through :meth:`run_async` from within an error handler can lead to + an infinite error handling loop. Args: func (:obj:`callable`): The function to run in the thread. - *args (:obj:`tuple`, optional): Arguments to `func`. - **kwargs (:obj:`dict`, optional): Keyword arguments to `func`. + *args (:obj:`tuple`, optional): Arguments to ``func``. + update (:class:`telegram.Update` | :obj:`object`, optional): The update associated with + the functions call. If passed, it will be available in the error handlers, in case + an exception is raised by :attr:`func`. + **kwargs (:obj:`dict`, optional): Keyword arguments to ``func``. Returns: Promise """ - # TODO: handle exception in async threads - # set a threading.Event to notify caller thread - promise = Promise(func, args, kwargs) + return self._run_async(func, *args, update=update, error_handling=True, **kwargs) + + def _run_async( + self, + func: Callable[..., object], + *args: object, + update: object = None, + error_handling: bool = True, + **kwargs: object, + ) -> Promise: + # TODO: Remove error_handling parameter once we drop the @run_async decorator + promise = Promise(func, args, kwargs, update=update, error_handling=error_handling) self.__async_queue.put(promise) return promise - def start(self, ready=None): + def start(self, ready: Event = None) -> None: """Thread target of thread 'dispatcher'. Runs in background and processes the update queue. @@ -205,7 +461,7 @@ def start(self, ready=None): self.logger.error(msg) raise TelegramError(msg) - self._init_async_threads(uuid4(), self.workers) + self._init_async_threads(str(uuid4()), self.workers) self.running = True self.logger.debug('Dispatcher started') @@ -220,18 +476,19 @@ def start(self, ready=None): if self.__stop_event.is_set(): self.logger.debug('orderly stopping') break - elif self.__exception_event.is_set(): + if self.__exception_event.is_set(): self.logger.critical('stopping due to exception in another thread') break continue - self.logger.debug('Processing Update: %s' % update) + self.logger.debug('Processing Update: %s', update) self.process_update(update) + self.update_queue.task_done() self.running = False self.logger.debug('Dispatcher thread stopped') - def stop(self): + def stop(self) -> None: """Stops the thread.""" if self.running: self.__stop_event.set() @@ -249,20 +506,27 @@ def stop(self): self.__async_queue.put(None) for i, thr in enumerate(threads): - self.logger.debug('Waiting for async thread {0}/{1} to end'.format(i + 1, total)) + self.logger.debug('Waiting for async thread %s/%s to end', i + 1, total) thr.join() self.__async_threads.remove(thr) - self.logger.debug('async thread {0}/{1} has ended'.format(i + 1, total)) + self.logger.debug('async thread %s/%s has ended', i + 1, total) @property - def has_running_threads(self): + def has_running_threads(self) -> bool: # skipcq: PY-D0003 return self.running or bool(self.__async_threads) - def process_update(self, update): - """Processes a single update. + def process_update(self, update: object) -> None: + """Processes a single update and updates the persistence. + + Note: + If the update is handled by least one synchronously running handlers (i.e. + ``run_async=False``), :meth:`update_persistence` is called *once* after all handlers + synchronous handlers are done. Each asynchronously running handler will trigger + :meth:`update_persistence` on its own. Args: - update (:obj:`str` | :class:`telegram.Update` | :class:`telegram.TelegramError`): + update (:class:`telegram.Update` | :obj:`object` | \ + :class:`telegram.error.TelegramError`): The update to process. """ @@ -271,40 +535,58 @@ def process_update(self, update): try: self.dispatch_error(None, update) except Exception: - self.logger.exception('An uncaught error was raised while handling the error') + self.logger.exception('An uncaught error was raised while handling the error.') return + context = None + handled = False + sync_modes = [] + for group in self.groups: try: - for handler in (x for x in self.handlers[group] if x.check_update(update)): - handler.handle_update(update, self) - break + for handler in self.handlers[group]: + check = handler.check_update(update) + if check is not None and check is not False: + if not context and self.use_context: + context = self.context_types.context.from_update(update, self) + context.refresh_data() + handled = True + sync_modes.append(handler.run_async) + handler.handle_update(update, self, check, context) + break # Stop processing with any other handler. except DispatcherHandlerStop: self.logger.debug('Stopping further handlers due to DispatcherHandlerStop') + self.update_persistence(update=update) break # Dispatch any error. - except TelegramError as te: - self.logger.warning('A TelegramError was raised while processing the Update') - + except Exception as exc: try: - self.dispatch_error(update, te) + self.dispatch_error(update, exc) except DispatcherHandlerStop: self.logger.debug('Error handler stopped further handlers') break + # Errors should not stop the thread. except Exception: - self.logger.exception('An uncaught error was raised while handling the error') - - # Errors should not stop the thread. - except Exception: - self.logger.exception('An uncaught error was raised while processing the update') - - def add_handler(self, handler, group=DEFAULT_GROUP): + self.logger.exception('An uncaught error was raised while handling the error.') + + # Update persistence, if handled + handled_only_async = all(sync_modes) + if handled: + # Respect default settings + if all(mode is DEFAULT_FALSE for mode in sync_modes) and self.bot.defaults: + handled_only_async = self.bot.defaults.run_async + # If update was only handled by async handlers, we don't need to update here + if not handled_only_async: + self.update_persistence(update=update) + + def add_handler(self, handler: Handler[UT, CCT], group: int = DEFAULT_GROUP) -> None: """Register a handler. - TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. + TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of + update with :class:`telegram.ext.DispatcherHandlerStop`. A handler must be an instance of a subclass of :class:`telegram.ext.Handler`. All handlers are organized in groups with a numeric value. The default group is 0. All groups will be @@ -325,20 +607,38 @@ def add_handler(self, handler, group=DEFAULT_GROUP): group (:obj:`int`, optional): The group identifier. Default is 0. """ + # Unfortunately due to circular imports this has to be here + from .conversationhandler import ConversationHandler # pylint: disable=C0415 if not isinstance(handler, Handler): - raise TypeError('handler is not an instance of {0}'.format(Handler.__name__)) + raise TypeError(f'handler is not an instance of {Handler.__name__}') if not isinstance(group, int): raise TypeError('group is not int') + # For some reason MyPy infers the type of handler is here, + # so for now we just ignore all the errors + if ( + isinstance(handler, ConversationHandler) + and handler.persistent # type: ignore[attr-defined] + and handler.name # type: ignore[attr-defined] + ): + if not self.persistence: + raise ValueError( + f"ConversationHandler {handler.name} " # type: ignore[attr-defined] + f"can not be persistent if dispatcher has no persistence" + ) + handler.persistence = self.persistence # type: ignore[attr-defined] + handler.conversations = ( # type: ignore[attr-defined] + self.persistence.get_conversations(handler.name) # type: ignore[attr-defined] + ) if group not in self.handlers: - self.handlers[group] = list() + self.handlers[group] = [] self.groups.append(group) self.groups = sorted(self.groups) self.handlers[group].append(handler) - def remove_handler(self, handler, group=DEFAULT_GROUP): + def remove_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None: """Remove a handler from the specified group. Args: @@ -352,38 +652,169 @@ def remove_handler(self, handler, group=DEFAULT_GROUP): del self.handlers[group] self.groups.remove(group) - def add_error_handler(self, callback): - """Registers an error handler in the Dispatcher. + def update_persistence(self, update: object = None) -> None: + """Update :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`. + + Args: + update (:class:`telegram.Update`, optional): The update to process. If passed, only the + corresponding ``user_data`` and ``chat_data`` will be updated. + """ + with self._update_persistence_lock: + self.__update_persistence(update) + + def __update_persistence(self, update: object = None) -> None: + if self.persistence: + # We use list() here in order to decouple chat_ids from self.chat_data, as dict view + # objects will change, when the dict does and we want to loop over chat_ids + chat_ids = list(self.chat_data.keys()) + user_ids = list(self.user_data.keys()) + + if isinstance(update, Update): + if update.effective_chat: + chat_ids = [update.effective_chat.id] + else: + chat_ids = [] + if update.effective_user: + user_ids = [update.effective_user.id] + else: + user_ids = [] + + if self.persistence.store_callback_data: + self.bot = cast(telegram.ext.extbot.ExtBot, self.bot) + try: + self.persistence.update_callback_data( + self.bot.callback_data_cache.persistence_data + ) + except Exception as exc: + try: + self.dispatch_error(update, exc) + except Exception: + message = ( + 'Saving callback data raised an error and an ' + 'uncaught error was raised while handling ' + 'the error with an error_handler' + ) + self.logger.exception(message) + if self.persistence.store_bot_data: + try: + self.persistence.update_bot_data(self.bot_data) + except Exception as exc: + try: + self.dispatch_error(update, exc) + except Exception: + message = ( + 'Saving bot data raised an error and an ' + 'uncaught error was raised while handling ' + 'the error with an error_handler' + ) + self.logger.exception(message) + if self.persistence.store_chat_data: + for chat_id in chat_ids: + try: + self.persistence.update_chat_data(chat_id, self.chat_data[chat_id]) + except Exception as exc: + try: + self.dispatch_error(update, exc) + except Exception: + message = ( + 'Saving chat data raised an error and an ' + 'uncaught error was raised while handling ' + 'the error with an error_handler' + ) + self.logger.exception(message) + if self.persistence.store_user_data: + for user_id in user_ids: + try: + self.persistence.update_user_data(user_id, self.user_data[user_id]) + except Exception as exc: + try: + self.dispatch_error(update, exc) + except Exception: + message = ( + 'Saving user data raised an error and an ' + 'uncaught error was raised while handling ' + 'the error with an error_handler' + ) + self.logger.exception(message) + + def add_error_handler( + self, + callback: Callable[[object, CCT], None], + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621 + ) -> None: + """Registers an error handler in the Dispatcher. This handler will receive every error + which happens in your bot. + + Note: + Attempts to add the same callback multiple times will be ignored. + + Warning: + The errors handled within these handlers won't show up in the logger, so you + need to make sure that you reraise the error. Args: - callback (:obj:`callable`): A function that takes ``Bot, Update, TelegramError`` as - arguments. + callback (:obj:`callable`): The callback function for this error handler. Will be + called when an error is raised. Callback signature for context based API: + ``def callback(update: object, context: CallbackContext)`` + + The error that happened will be present in context.error. + run_async (:obj:`bool`, optional): Whether this handlers callback should be run + asynchronously using :meth:`run_async`. Defaults to :obj:`False`. + + Note: + See https://git.io/fxJuV for more info about switching to context based API. """ - self.error_handlers.append(callback) + if callback in self.error_handlers: + self.logger.debug('The callback is already registered as an error handler. Ignoring.') + return - def remove_error_handler(self, callback): + if run_async is DEFAULT_FALSE and self.bot.defaults and self.bot.defaults.run_async: + run_async = True + + self.error_handlers[callback] = run_async + + def remove_error_handler(self, callback: Callable[[object, CCT], None]) -> None: """Removes an error handler. Args: callback (:obj:`callable`): The error handler to remove. """ - if callback in self.error_handlers: - self.error_handlers.remove(callback) + self.error_handlers.pop(callback, None) - def dispatch_error(self, update, error): + def dispatch_error( + self, update: Optional[object], error: Exception, promise: Promise = None + ) -> None: """Dispatches an error. Args: - update (:obj:`str` | :class:`telegram.Update` | None): The update that caused the error - error (:class:`telegram.TelegramError`): The Telegram error that was raised. + update (:obj:`object` | :class:`telegram.Update`): The update that caused the error. + error (:obj:`Exception`): The error that was raised. + promise (:class:`telegram.utils.Promise`, optional): The promise whose pooled function + raised the error. """ + async_args = None if not promise else promise.args + async_kwargs = None if not promise else promise.kwargs + if self.error_handlers: - for callback in self.error_handlers: - callback(self.bot, update, error) + for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621 + if self.use_context: + context = self.context_types.context.from_error( + update, error, self, async_args=async_args, async_kwargs=async_kwargs + ) + if run_async: + self.run_async(callback, update, context, update=update) + else: + callback(update, context) + else: + if run_async: + self.run_async(callback, self.bot, update, error, update=update) + else: + callback(self.bot, update, error) else: self.logger.exception( - 'No error handlers are registered, logging exception.', exc_info=error) + 'No error handlers are registered, logging exception.', exc_info=error + ) diff --git a/telegramer/include/telegram/ext/extbot.py b/telegramer/include/telegram/ext/extbot.py new file mode 100644 index 0000000..f026191 --- /dev/null +++ b/telegramer/include/telegram/ext/extbot.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python +# pylint: disable=E0611,E0213,E1102,C0103,E1101,R0913,R0904 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram Bot with convenience extensions.""" +from copy import copy +from typing import Union, cast, List, Callable, Optional, Tuple, TypeVar, TYPE_CHECKING, Sequence + +import telegram.bot +from telegram import ( + ReplyMarkup, + Message, + InlineKeyboardMarkup, + Poll, + MessageId, + Update, + Chat, + CallbackQuery, +) + +from telegram.ext.callbackdatacache import CallbackDataCache +from telegram.utils.types import JSONDict, ODVInput, DVInput +from ..utils.helpers import DEFAULT_NONE + +if TYPE_CHECKING: + from telegram import InlineQueryResult, MessageEntity + from telegram.utils.request import Request + from .defaults import Defaults + +HandledTypes = TypeVar('HandledTypes', bound=Union[Message, CallbackQuery, Chat]) + + +class ExtBot(telegram.bot.Bot): + """This object represents a Telegram Bot with convenience extensions. + + Warning: + Not to be confused with :class:`telegram.Bot`. + + For the documentation of the arguments, methods and attributes, please see + :class:`telegram.Bot`. + + .. versionadded:: 13.6 + + Args: + defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to + be used if not set explicitly in the bot methods. + arbitrary_callback_data (:obj:`bool` | :obj:`int`, optional): Whether to + allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`. + Pass an integer to specify the maximum number of objects cached in memory. For more + details, please see our `wiki `_. Defaults to :obj:`False`. + + Attributes: + arbitrary_callback_data (:obj:`bool` | :obj:`int`): Whether this bot instance + allows to use arbitrary objects as callback data for + :class:`telegram.InlineKeyboardButton`. + callback_data_cache (:class:`telegram.ext.CallbackDataCache`): The cache for objects passed + as callback data for :class:`telegram.InlineKeyboardButton`. + + """ + + __slots__ = ('arbitrary_callback_data', 'callback_data_cache') + + # The ext_bot argument is a little hack to get warnings handled correctly. + # It's not very clean, but the warnings will be dropped at some point anyway. + def __setattr__(self, key: str, value: object, ext_bot: bool = True) -> None: + if issubclass(self.__class__, ExtBot) and self.__class__ is not ExtBot: + object.__setattr__(self, key, value) + return + super().__setattr__(key, value, ext_bot=ext_bot) # type: ignore[call-arg] + + def __init__( + self, + token: str, + base_url: str = None, + base_file_url: str = None, + request: 'Request' = None, + private_key: bytes = None, + private_key_password: bytes = None, + defaults: 'Defaults' = None, + arbitrary_callback_data: Union[bool, int] = False, + ): + super().__init__( + token=token, + base_url=base_url, + base_file_url=base_file_url, + request=request, + private_key=private_key, + private_key_password=private_key_password, + ) + # We don't pass this to super().__init__ to avoid the deprecation warning + self.defaults = defaults + + # set up callback_data + if not isinstance(arbitrary_callback_data, bool): + maxsize = cast(int, arbitrary_callback_data) + self.arbitrary_callback_data = True + else: + maxsize = 1024 + self.arbitrary_callback_data = arbitrary_callback_data + self.callback_data_cache: CallbackDataCache = CallbackDataCache(bot=self, maxsize=maxsize) + + def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]: + # If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the + # CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input + if isinstance(reply_markup, InlineKeyboardMarkup) and self.arbitrary_callback_data: + return self.callback_data_cache.process_keyboard(reply_markup) + + return reply_markup + + def insert_callback_data(self, update: Update) -> None: + """If this bot allows for arbitrary callback data, this inserts the cached data into all + corresponding buttons within this update. + + Note: + Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check + if the reply markup (if any) was actually sent by this caches bot. If it was not, the + message will be returned unchanged. + + Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is + :obj:`None` for those! In the corresponding reply markups the callback data will be + replaced by :class:`telegram.ext.InvalidCallbackData`. + + Warning: + *In place*, i.e. the passed :class:`telegram.Message` will be changed! + + Args: + update (:class`telegram.Update`): The update. + + """ + # The only incoming updates that can directly contain a message sent by the bot itself are: + # * CallbackQueries + # * Messages where the pinned_message is sent by the bot + # * Messages where the reply_to_message is sent by the bot + # * Messages where via_bot is the bot + # Finally there is effective_chat.pinned message, but that's only returned in get_chat + if update.callback_query: + self._insert_callback_data(update.callback_query) + # elif instead of if, as effective_message includes callback_query.message + # and that has already been processed + elif update.effective_message: + self._insert_callback_data(update.effective_message) + + def _insert_callback_data(self, obj: HandledTypes) -> HandledTypes: + if not self.arbitrary_callback_data: + return obj + + if isinstance(obj, CallbackQuery): + self.callback_data_cache.process_callback_query(obj) + return obj # type: ignore[return-value] + + if isinstance(obj, Message): + if obj.reply_to_message: + # reply_to_message can't contain further reply_to_messages, so no need to check + self.callback_data_cache.process_message(obj.reply_to_message) + if obj.reply_to_message.pinned_message: + # pinned messages can't contain reply_to_message, no need to check + self.callback_data_cache.process_message(obj.reply_to_message.pinned_message) + if obj.pinned_message: + # pinned messages can't contain reply_to_message, no need to check + self.callback_data_cache.process_message(obj.pinned_message) + + # Finally, handle the message itself + self.callback_data_cache.process_message(message=obj) + return obj # type: ignore[return-value] + + if isinstance(obj, Chat) and obj.pinned_message: + self.callback_data_cache.process_message(obj.pinned_message) + + return obj + + def _message( + self, + endpoint: str, + data: JSONDict, + reply_to_message_id: int = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: ReplyMarkup = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> Union[bool, Message]: + # We override this method to call self._replace_keyboard and self._insert_callback_data. + # This covers most methods that have a reply_markup + result = super()._message( + endpoint=endpoint, + data=data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=self._replace_keyboard(reply_markup), + allow_sending_without_reply=allow_sending_without_reply, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + if isinstance(result, Message): + self._insert_callback_data(result) + return result + + def get_updates( + self, + offset: int = None, + limit: int = 100, + timeout: float = 0, + read_latency: float = 2.0, + allowed_updates: List[str] = None, + api_kwargs: JSONDict = None, + ) -> List[Update]: + updates = super().get_updates( + offset=offset, + limit=limit, + timeout=timeout, + read_latency=read_latency, + allowed_updates=allowed_updates, + api_kwargs=api_kwargs, + ) + + for update in updates: + self.insert_callback_data(update) + + return updates + + def _effective_inline_results( # pylint: disable=R0201 + self, + results: Union[ + Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] + ], + next_offset: str = None, + current_offset: str = None, + ) -> Tuple[Sequence['InlineQueryResult'], Optional[str]]: + """ + This method is called by Bot.answer_inline_query to build the actual results list. + Overriding this to call self._replace_keyboard suffices + """ + effective_results, next_offset = super()._effective_inline_results( + results=results, next_offset=next_offset, current_offset=current_offset + ) + + # Process arbitrary callback + if not self.arbitrary_callback_data: + return effective_results, next_offset + results = [] + for result in effective_results: + # All currently existingInlineQueryResults have a reply_markup, but future ones + # might not have. Better be save than sorry + if not hasattr(result, 'reply_markup'): + results.append(result) + else: + # We build a new result in case the user wants to use the same object in + # different places + new_result = copy(result) + markup = self._replace_keyboard(result.reply_markup) # type: ignore[attr-defined] + new_result.reply_markup = markup + results.append(new_result) + + return results, next_offset + + def stop_poll( + self, + chat_id: Union[int, str], + message_id: int, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Poll: + # We override this method to call self._replace_keyboard + return super().stop_poll( + chat_id=chat_id, + message_id=message_id, + reply_markup=self._replace_keyboard(reply_markup), + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def copy_message( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_id: int, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> MessageId: + # We override this method to call self._replace_keyboard + return super().copy_message( + chat_id=chat_id, + from_chat_id=from_chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=self._replace_keyboard(reply_markup), + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def get_chat( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Chat: + # We override this method to call self._insert_callback_data + result = super().get_chat(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs) + return self._insert_callback_data(result) + + # updated camelCase aliases + getChat = get_chat + """Alias for :meth:`get_chat`""" + copyMessage = copy_message + """Alias for :meth:`copy_message`""" + getUpdates = get_updates + """Alias for :meth:`get_updates`""" + stopPoll = stop_poll + """Alias for :meth:`stop_poll`""" diff --git a/telegramer/include/telegram/ext/filters.py b/telegramer/include/telegram/ext/filters.py index cbef3eb..519c73a 100644 --- a/telegramer/include/telegram/ext/filters.py +++ b/telegramer/include/telegram/ext/filters.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,26 +16,51 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0112, C0103, W0221 """This module contains the Filters for use with the MessageHandler class.""" import re -from telegram import Chat -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass - -try: - string_types -except NameError: - string_types = str - - -class BaseFilter(object): - """Base class for all Message Filters. - - Subclassing from this class filters to be combined using bitwise operators: +import warnings + +from abc import ABC, abstractmethod +from sys import version_info as py_ver +from threading import Lock +from typing import ( + Dict, + FrozenSet, + List, + Match, + Optional, + Pattern, + Set, + Tuple, + Union, + cast, + NoReturn, +) + +from telegram import Chat, Message, MessageEntity, Update, User + +__all__ = [ + 'Filters', + 'BaseFilter', + 'MessageFilter', + 'UpdateFilter', + 'InvertedFilter', + 'MergedFilter', + 'XORFilter', +] + +from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated +from telegram.utils.types import SLT + +DataDict = Dict[str, list] + + +class BaseFilter(ABC): + """Base class for all Filters. + + Filters subclassing from this class can combined using bitwise operators: And: @@ -45,6 +70,10 @@ class BaseFilter(object): >>> (Filters.audio | Filters.video) + Exclusive Or: + + >>> (Filters.regex('To Be') ^ Filters.regex('Not 2B')) + Not: >>> ~ Filters.command @@ -54,55 +83,169 @@ class BaseFilter(object): >>> (Filters.text & (Filters.entity(URL) | Filters.entity(TEXT_LINK))) >>> Filters.text & (~ Filters.forwarded) - If you want to create your own filters create a class inheriting from this class and implement - a `filter` method that returns a boolean: `True` if the message should be handled, `False` - otherwise. Note that the filters work only as class instances, not actual class objects - (so remember to initialize your filter classes). + Note: + Filters use the same short circuiting logic as python's `and`, `or` and `not`. + This means that for example: + + >>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)') + + With ``message.text == x``, will only ever return the matches for the first filter, + since the second one is never evaluated. + + + If you want to create your own filters create a class inheriting from either + :class:`MessageFilter` or :class:`UpdateFilter` and implement a :meth:`filter` method that + returns a boolean: :obj:`True` if the message should be + handled, :obj:`False` otherwise. + Note that the filters work only as class instances, not + actual class objects (so remember to + initialize your filter classes). By default the filters name (what will get printed when converted to a string for display) - will be the class name. If you want to overwrite this assign a better name to the `name` + will be the class name. If you want to overwrite this assign a better name to the :attr:`name` class variable. Attributes: name (:obj:`str`): Name for this filter. Defaults to the type of filter. - + data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should + return a dict with lists. The dict will be merged with + :class:`telegram.ext.CallbackContext`'s internal dict in most cases + (depends on the handler). """ - name = None + if py_ver < (3, 7): + __slots__ = ('_name', '_data_filter') + else: + __slots__ = ('_name', '_data_filter', '__dict__') # type: ignore[assignment] + + def __new__(cls, *args: object, **kwargs: object) -> 'BaseFilter': # pylint: disable=W0613 + instance = super().__new__(cls) + instance._name = None + instance._data_filter = False + + return instance - def __call__(self, message): - return self.filter(message) + @abstractmethod + def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: + ... - def __and__(self, other): + def __and__(self, other: 'BaseFilter') -> 'BaseFilter': return MergedFilter(self, and_filter=other) - def __or__(self, other): + def __or__(self, other: 'BaseFilter') -> 'BaseFilter': return MergedFilter(self, or_filter=other) - def __invert__(self): + def __xor__(self, other: 'BaseFilter') -> 'BaseFilter': + return XORFilter(self, other) + + def __invert__(self) -> 'BaseFilter': return InvertedFilter(self) - def __repr__(self): + def __setattr__(self, key: str, value: object) -> None: + # Allow setting custom attributes w/o warning for user defined custom filters. + # To differentiate between a custom and a PTB filter, we use this hacky but + # simple way of checking the module name where the class is defined from. + if ( + issubclass(self.__class__, (UpdateFilter, MessageFilter)) + and self.__class__.__module__ != __name__ + ): # __name__ is telegram.ext.filters + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + + @property + def data_filter(self) -> bool: + return self._data_filter + + @data_filter.setter + def data_filter(self, value: bool) -> None: + self._data_filter = value + + @property + def name(self) -> Optional[str]: + return self._name + + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name # pylint: disable=E0237 + + def __repr__(self) -> str: # We do this here instead of in a __init__ so filter don't have to call __init__ or super() if self.name is None: self.name = self.__class__.__name__ return self.name - def filter(self, message): + +class MessageFilter(BaseFilter): + """Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed + to :meth:`filter` is ``update.effective_message``. + + Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom + filters. + + Attributes: + name (:obj:`str`): Name for this filter. Defaults to the type of filter. + data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should + return a dict with lists. The dict will be merged with + :class:`telegram.ext.CallbackContext`'s internal dict in most cases + (depends on the handler). + + """ + + __slots__ = () + + def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: + return self.filter(update.effective_message) + + @abstractmethod + def filter(self, message: Message) -> Optional[Union[bool, DataDict]]: """This method must be overwritten. Args: message (:class:`telegram.Message`): The message that is tested. Returns: - :obj:`bool` + :obj:`dict` or :obj:`bool` """ - raise NotImplementedError +class UpdateFilter(BaseFilter): + """Base class for all Update Filters. In contrast to :class:`MessageFilter`, the object + passed to :meth:`filter` is ``update``, which allows to create filters like + :attr:`Filters.update.edited_message`. + + Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom + filters. -class InvertedFilter(BaseFilter): + Attributes: + name (:obj:`str`): Name for this filter. Defaults to the type of filter. + data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should + return a dict with lists. The dict will be merged with + :class:`telegram.ext.CallbackContext`'s internal dict in most cases + (depends on the handler). + + """ + + __slots__ = () + + def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: + return self.filter(update) + + @abstractmethod + def filter(self, update: Update) -> Optional[Union[bool, DataDict]]: + """This method must be overwritten. + + Args: + update (:class:`telegram.Update`): The update that is tested. + + Returns: + :obj:`dict` or :obj:`bool`. + + """ + + +class InvertedFilter(UpdateFilter): """Represents a filter that has been inverted. Args: @@ -110,44 +253,183 @@ class InvertedFilter(BaseFilter): """ - def __init__(self, f): + __slots__ = ('f',) + + def __init__(self, f: BaseFilter): self.f = f - def filter(self, message): - return not self.f(message) + def filter(self, update: Update) -> bool: + return not bool(self.f(update)) + + @property + def name(self) -> str: + return f"" - def __repr__(self): - return "".format(self.f) + @name.setter + def name(self, name: str) -> NoReturn: + raise RuntimeError('Cannot set name for InvertedFilter') -class MergedFilter(BaseFilter): +class MergedFilter(UpdateFilter): """Represents a filter consisting of two other filters. Args: - base_filter: Filter 1 of the merged filter + base_filter: Filter 1 of the merged filter. and_filter: Optional filter to "and" with base_filter. Mutually exclusive with or_filter. or_filter: Optional filter to "or" with base_filter. Mutually exclusive with and_filter. """ - def __init__(self, base_filter, and_filter=None, or_filter=None): + __slots__ = ('base_filter', 'and_filter', 'or_filter') + + def __init__( + self, base_filter: BaseFilter, and_filter: BaseFilter = None, or_filter: BaseFilter = None + ): self.base_filter = base_filter + if self.base_filter.data_filter: + self.data_filter = True self.and_filter = and_filter + if ( + self.and_filter + and not isinstance(self.and_filter, bool) + and self.and_filter.data_filter + ): + self.data_filter = True self.or_filter = or_filter - - def filter(self, message): + if self.or_filter and not isinstance(self.and_filter, bool) and self.or_filter.data_filter: + self.data_filter = True + + @staticmethod + def _merge(base_output: Union[bool, Dict], comp_output: Union[bool, Dict]) -> DataDict: + base = base_output if isinstance(base_output, dict) else {} + comp = comp_output if isinstance(comp_output, dict) else {} + for k in comp.keys(): + # Make sure comp values are lists + comp_value = comp[k] if isinstance(comp[k], list) else [] + try: + # If base is a list then merge + if isinstance(base[k], list): + base[k] += comp_value + else: + base[k] = [base[k]] + comp_value + except KeyError: + base[k] = comp_value + return base + + def filter(self, update: Update) -> Union[bool, DataDict]: # pylint: disable=R0911 + base_output = self.base_filter(update) + # We need to check if the filters are data filters and if so return the merged data. + # If it's not a data filter or an or_filter but no matches return bool if self.and_filter: - return self.base_filter(message) and self.and_filter(message) + # And filter needs to short circuit if base is falsey + if base_output: + comp_output = self.and_filter(update) + if comp_output: + if self.data_filter: + merged = self._merge(base_output, comp_output) + if merged: + return merged + return True elif self.or_filter: - return self.base_filter(message) or self.or_filter(message) + # Or filter needs to short circuit if base is truthey + if base_output: + if self.data_filter: + return base_output + return True + + comp_output = self.or_filter(update) + if comp_output: + if self.data_filter: + return comp_output + return True + return False + + @property + def name(self) -> str: + return ( + f"<{self.base_filter} {'and' if self.and_filter else 'or'} " + f"{self.and_filter or self.or_filter}>" + ) + + @name.setter + def name(self, name: str) -> NoReturn: + raise RuntimeError('Cannot set name for MergedFilter') + + +class XORFilter(UpdateFilter): + """Convenience filter acting as wrapper for :class:`MergedFilter` representing the an XOR gate + for two filters. + + Args: + base_filter: Filter 1 of the merged filter. + xor_filter: Filter 2 of the merged filter. + + """ - def __repr__(self): - return "<{} {} {}>".format(self.base_filter, "and" if self.and_filter else "or", - self.and_filter or self.or_filter) + __slots__ = ('base_filter', 'xor_filter', 'merged_filter') + def __init__(self, base_filter: BaseFilter, xor_filter: BaseFilter): + self.base_filter = base_filter + self.xor_filter = xor_filter + self.merged_filter = (base_filter & ~xor_filter) | (~base_filter & xor_filter) + + def filter(self, update: Update) -> Optional[Union[bool, DataDict]]: + return self.merged_filter(update) + + @property + def name(self) -> str: + return f'<{self.base_filter} xor {self.xor_filter}>' + + @name.setter + def name(self, name: str) -> NoReturn: + raise RuntimeError('Cannot set name for XORFilter') + + +class _DiceEmoji(MessageFilter): + __slots__ = ('emoji',) + + def __init__(self, emoji: str = None, name: str = None): + self.name = f'Filters.dice.{name}' if name else 'Filters.dice' + self.emoji = emoji + + class _DiceValues(MessageFilter): + __slots__ = ('values', 'emoji') + + def __init__( + self, + values: SLT[int], + name: str, + emoji: str = None, + ): + self.values = [values] if isinstance(values, int) else values + self.emoji = emoji + self.name = f'{name}({values})' + + def filter(self, message: Message) -> bool: + if message.dice and message.dice.value in self.values: + if self.emoji: + return message.dice.emoji == self.emoji + return True + return False + + def __call__( # type: ignore[override] + self, update: Union[Update, List[int], Tuple[int]] + ) -> Union[bool, '_DiceValues']: + if isinstance(update, Update): + return self.filter(update.effective_message) + return self._DiceValues(update, self.name, emoji=self.emoji) + + def filter(self, message: Message) -> bool: + if bool(message.dice): + if self.emoji: + return message.dice.emoji == self.emoji + return True + return False -class Filters(object): - """Predefined filters for use as the `filter` argument of :class:`telegram.ext.MessageHandler`. + +class Filters: + """Predefined filters for use as the ``filter`` argument of + :class:`telegram.ext.MessageHandler`. Examples: Use ``MessageHandler(Filters.video, callback_method)`` to filter all video @@ -155,85 +437,272 @@ class Filters(object): """ - class _All(BaseFilter): + __slots__ = ('__dict__',) + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + + class _All(MessageFilter): + __slots__ = () name = 'Filters.all' - def filter(self, message): + def filter(self, message: Message) -> bool: return True all = _All() - """:obj:`Filter`: All Messages.""" + """All Messages.""" - class _Text(BaseFilter): + class _Text(MessageFilter): + __slots__ = () name = 'Filters.text' - def filter(self, message): - return bool(message.text and not message.text.startswith('/')) + class _TextStrings(MessageFilter): + __slots__ = ('strings',) + + def __init__(self, strings: Union[List[str], Tuple[str]]): + self.strings = strings + self.name = f'Filters.text({strings})' + + def filter(self, message: Message) -> bool: + if message.text: + return message.text in self.strings + return False + + def __call__( # type: ignore[override] + self, update: Union[Update, List[str], Tuple[str]] + ) -> Union[bool, '_TextStrings']: + if isinstance(update, Update): + return self.filter(update.effective_message) + return self._TextStrings(update) + + def filter(self, message: Message) -> bool: + return bool(message.text) text = _Text() - """:obj:`Filter`: Text Messages.""" + """Text Messages. If a list of strings is passed, it filters messages to only allow those + whose text is appearing in the given list. + + Examples: + To allow any text message, simply use + ``MessageHandler(Filters.text, callback_method)``. + + A simple use case for passing a list is to allow only messages that were sent by a + custom :class:`telegram.ReplyKeyboardMarkup`:: + + buttons = ['Start', 'Settings', 'Back'] + markup = ReplyKeyboardMarkup.from_column(buttons) + ... + MessageHandler(Filters.text(buttons), callback_method) + + Note: + * Dice messages don't have text. If you want to filter either text or dice messages, use + ``Filters.text | Filters.dice``. + * Messages containing a command are accepted by this filter. Use + ``Filters.text & (~Filters.command)``, if you want to filter only text messages without + commands. + + Args: + update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only + exact matches are allowed. If not specified, will allow any text message. + """ + + class _Caption(MessageFilter): + __slots__ = () + name = 'Filters.caption' + + class _CaptionStrings(MessageFilter): + __slots__ = ('strings',) + + def __init__(self, strings: Union[List[str], Tuple[str]]): + self.strings = strings + self.name = f'Filters.caption({strings})' + + def filter(self, message: Message) -> bool: + if message.caption: + return message.caption in self.strings + return False + + def __call__( # type: ignore[override] + self, update: Union[Update, List[str], Tuple[str]] + ) -> Union[bool, '_CaptionStrings']: + if isinstance(update, Update): + return self.filter(update.effective_message) + return self._CaptionStrings(update) + + def filter(self, message: Message) -> bool: + return bool(message.caption) - class _Command(BaseFilter): + caption = _Caption() + """Messages with a caption. If a list of strings is passed, it filters messages to only + allow those whose caption is appearing in the given list. + + Examples: + ``MessageHandler(Filters.caption, callback_method)`` + + Args: + update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which captions to allow. Only + exact matches are allowed. If not specified, will allow any message with a caption. + """ + + class _Command(MessageFilter): + __slots__ = () name = 'Filters.command' - def filter(self, message): - return bool(message.text and message.text.startswith('/')) + class _CommandOnlyStart(MessageFilter): + __slots__ = ('only_start',) + + def __init__(self, only_start: bool): + self.only_start = only_start + self.name = f'Filters.command({only_start})' + + def filter(self, message: Message) -> bool: + return bool( + message.entities + and any(e.type == MessageEntity.BOT_COMMAND for e in message.entities) + ) + + def __call__( # type: ignore[override] + self, update: Union[bool, Update] + ) -> Union[bool, '_CommandOnlyStart']: + if isinstance(update, Update): + return self.filter(update.effective_message) + return self._CommandOnlyStart(update) + + def filter(self, message: Message) -> bool: + return bool( + message.entities + and message.entities[0].type == MessageEntity.BOT_COMMAND + and message.entities[0].offset == 0 + ) command = _Command() - """:obj:`Filter`: Messages starting with ``/``.""" + """ + Messages with a :attr:`telegram.MessageEntity.BOT_COMMAND`. By default only allows + messages `starting` with a bot command. Pass :obj:`False` to also allow messages that contain a + bot command `anywhere` in the text. + + Examples:: + + MessageHandler(Filters.command, command_at_start_callback) + MessageHandler(Filters.command(False), command_anywhere_callback) + + Note: + ``Filters.text`` also accepts messages containing a command. + + Args: + update (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot + command. Defaults to :obj:`True`. + """ - class regex(BaseFilter): + class regex(MessageFilter): """ - Filters updates by searching for an occurence of ``pattern`` in the message text. - The ``re.search`` function is used to determine whether an update should be filtered. + Filters updates by searching for an occurrence of ``pattern`` in the message text. + The ``re.search()`` function is used to determine whether an update should be filtered. + Refer to the documentation of the ``re`` module for more information. - Note: Does not allow passing groups or a groupdict like the ``RegexHandler`` yet, - but this will probably be implemented in a future update, gradually phasing out the - RegexHandler (see https://github.com/python-telegram-bot/python-telegram-bot/issues/835). + To get the groups and groupdict matched, see :attr:`telegram.ext.CallbackContext.matches`. Examples: - Example ``CommandHandler("start", deep_linked_callback, Filters.regex('parameter'))`` + Use ``MessageHandler(Filters.regex(r'help'), callback)`` to capture all messages that + contain the word 'help'. You can also use + ``MessageHandler(Filters.regex(re.compile(r'help', re.IGNORECASE)), callback)`` if + you want your pattern to be case insensitive. This approach is recommended + if you need to specify flags on your pattern. + + Note: + Filters use the same short circuiting logic as python's `and`, `or` and `not`. + This means that for example: + + >>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)') + + With a message.text of `x`, will only ever return the matches for the first filter, + since the second one is never evaluated. Args: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. """ - def __init__(self, pattern): - self.pattern = re.compile(pattern) - self.name = 'Filters.regex({})'.format(self.pattern) + __slots__ = ('pattern',) + data_filter = True - # TODO: Once the callback revamp (#1026) is done, the regex filter should be able to pass - # the matched groups and groupdict to the context object. + def __init__(self, pattern: Union[str, Pattern]): + if isinstance(pattern, str): + pattern = re.compile(pattern) + pattern = cast(Pattern, pattern) + self.pattern: Pattern = pattern + self.name = f'Filters.regex({self.pattern})' - def filter(self, message): + def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: + """""" # remove method from docs if message.text: - return bool(self.pattern.search(message.text)) - return False + match = self.pattern.search(message.text) + if match: + return {'matches': [match]} + return {} + + class caption_regex(MessageFilter): + """ + Filters updates by searching for an occurrence of ``pattern`` in the message caption. + + This filter works similarly to :class:`Filters.regex`, with the only exception being that + it applies to the message caption instead of the text. + + Examples: + Use ``MessageHandler(Filters.photo & Filters.caption_regex(r'help'), callback)`` + to capture all photos with caption containing the word 'help'. + + Note: + This filter will not work on simple text messages, but only on media with caption. - class _Reply(BaseFilter): + Args: + pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. + """ + + __slots__ = ('pattern',) + data_filter = True + + def __init__(self, pattern: Union[str, Pattern]): + if isinstance(pattern, str): + pattern = re.compile(pattern) + pattern = cast(Pattern, pattern) + self.pattern: Pattern = pattern + self.name = f'Filters.caption_regex({self.pattern})' + + def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: + """""" # remove method from docs + if message.caption: + match = self.pattern.search(message.caption) + if match: + return {'matches': [match]} + return {} + + class _Reply(MessageFilter): + __slots__ = () name = 'Filters.reply' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.reply_to_message) reply = _Reply() - """:obj:`Filter`: Messages that are a reply to another message.""" + """Messages that are a reply to another message.""" - class _Audio(BaseFilter): + class _Audio(MessageFilter): + __slots__ = () name = 'Filters.audio' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.audio) audio = _Audio() - """:obj:`Filter`: Messages that contain :class:`telegram.Audio`.""" + """Messages that contain :class:`telegram.Audio`.""" - class _Document(BaseFilter): + class _Document(MessageFilter): + __slots__ = () name = 'Filters.document' - class category(BaseFilter): - """This Filter filters documents by their category in the mime-type attribute + class category(MessageFilter): + """Filters documents by their category in the mime-type attribute. Note: This Filter only filters by the mime_type of the document, @@ -241,22 +710,27 @@ class category(BaseFilter): The user can manipulate the mime-type of a message and send media with wrong types that don't fit to this handler. - Examples: - Filters.documents.category('audio/') returnes `True` for all types - of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav' + Example: + Filters.document.category('audio/') returns :obj:`True` for all types + of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. """ - def __init__(self, category): + __slots__ = ('_category',) + + def __init__(self, category: Optional[str]): """Initialize the category you want to filter Args: - category (str, optional): category of the media you want to filter""" - self.category = category - self.name = "Filters.document.category('{}')".format(self.category) + category (str, optional): category of the media you want to filter + """ + self._category = category + self.name = f"Filters.document.category('{self._category}')" - def filter(self, message): + def filter(self, message: Message) -> bool: + """""" # remove method from docs if message.document: - return message.document.mime_type.startswith(self.category) + return message.document.mime_type.startswith(self._category) + return False application = category('application/') audio = category('audio/') @@ -264,7 +738,7 @@ def filter(self, message): video = category('video/') text = category('text/') - class mime_type(BaseFilter): + class mime_type(MessageFilter): """This Filter filters documents by their mime-type attribute Note: @@ -273,21 +747,21 @@ class mime_type(BaseFilter): The user can manipulate the mime-type of a message and send media with wrong types that don't fit to this handler. - Examples: - Filters.documents.mime_type('audio/mpeg') filters all audio in mp3 format. + Example: + ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format. """ - def __init__(self, mimetype): - """Initialize the category you want to filter + __slots__ = ('mimetype',) - Args: - filetype (str, optional): mime_type of the media you want to filter""" + def __init__(self, mimetype: Optional[str]): self.mimetype = mimetype - self.name = "Filters.document.mime_type('{}')".format(self.mimetype) + self.name = f"Filters.document.mime_type('{self.mimetype}')" - def filter(self, message): + def filter(self, message: Message) -> bool: + """""" # remove method from docs if message.document: return message.document.mime_type == self.mimetype + return False apk = mime_type('application/vnd.android.package-archive') doc = mime_type('application/msword') @@ -305,94 +779,242 @@ def filter(self, message): xml = mime_type('application/xml') zip = mime_type('application/zip') - def filter(self, message): + class file_extension(MessageFilter): + """This filter filters documents by their file ending/extension. + + Note: + * This Filter only filters by the file ending/extension of the document, + it doesn't check the validity of document. + * The user can manipulate the file extension of a document and + send media with wrong types that don't fit to this handler. + * Case insensitive by default, + you may change this with the flag ``case_sensitive=True``. + * Extension should be passed without leading dot + unless it's a part of the extension. + * Pass :obj:`None` to filter files with no extension, + i.e. without a dot in the filename. + + Example: + * ``Filters.document.file_extension("jpg")`` + filters files with extension ``".jpg"``. + * ``Filters.document.file_extension(".jpg")`` + filters files with extension ``"..jpg"``. + * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)`` + filters files with extension ``".Dockerfile"`` minding the case. + * ``Filters.document.file_extension(None)`` + filters files without a dot in the filename. + """ + + __slots__ = ('_file_extension', 'is_case_sensitive') + + def __init__(self, file_extension: Optional[str], case_sensitive: bool = False): + """Initialize the extension you want to filter. + + Args: + file_extension (:obj:`str` | :obj:`None`): + media file extension you want to filter. + case_sensitive (:obj:bool, optional): + pass :obj:`True` to make the filter case sensitive. + Default: :obj:`False`. + """ + self.is_case_sensitive = case_sensitive + if file_extension is None: + self._file_extension = None + self.name = "Filters.document.file_extension(None)" + elif self.is_case_sensitive: + self._file_extension = f".{file_extension}" + self.name = ( + f"Filters.document.file_extension({file_extension!r}," + " case_sensitive=True)" + ) + else: + self._file_extension = f".{file_extension}".lower() + self.name = f"Filters.document.file_extension({file_extension.lower()!r})" + + def filter(self, message: Message) -> bool: + """""" # remove method from docs + if message.document is None: + return False + if self._file_extension is None: + return "." not in message.document.file_name + if self.is_case_sensitive: + filename = message.document.file_name + else: + filename = message.document.file_name.lower() + return filename.endswith(self._file_extension) + + def filter(self, message: Message) -> bool: return bool(message.document) document = _Document() - """:obj:`Filter`: Messages that contain :class:`telegram.Document`.""" + """ + Subset for messages containing a document/file. + + Examples: + Use these filters like: ``Filters.document.mp3``, + ``Filters.document.mime_type("text/plain")`` etc. Or use just + ``Filters.document`` for all document messages. + + Attributes: + category: Filters documents by their category in the mime-type attribute - class _Animation(BaseFilter): + Note: + This Filter only filters by the mime_type of the document, + it doesn't check the validity of the document. + The user can manipulate the mime-type of a message and + send media with wrong types that don't fit to this handler. + + Example: + ``Filters.document.category('audio/')`` filters all types + of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. + application: Same as ``Filters.document.category("application")``. + audio: Same as ``Filters.document.category("audio")``. + image: Same as ``Filters.document.category("image")``. + video: Same as ``Filters.document.category("video")``. + text: Same as ``Filters.document.category("text")``. + mime_type: Filters documents by their mime-type attribute + + Note: + This Filter only filters by the mime_type of the document, + it doesn't check the validity of document. + + The user can manipulate the mime-type of a message and + send media with wrong types that don't fit to this handler. + + Example: + ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format. + apk: Same as ``Filters.document.mime_type("application/vnd.android.package-archive")``. + doc: Same as ``Filters.document.mime_type("application/msword")``. + docx: Same as ``Filters.document.mime_type("application/vnd.openxmlformats-\ +officedocument.wordprocessingml.document")``. + exe: Same as ``Filters.document.mime_type("application/x-ms-dos-executable")``. + gif: Same as ``Filters.document.mime_type("video/mp4")``. + jpg: Same as ``Filters.document.mime_type("image/jpeg")``. + mp3: Same as ``Filters.document.mime_type("audio/mpeg")``. + pdf: Same as ``Filters.document.mime_type("application/pdf")``. + py: Same as ``Filters.document.mime_type("text/x-python")``. + svg: Same as ``Filters.document.mime_type("image/svg+xml")``. + txt: Same as ``Filters.document.mime_type("text/plain")``. + targz: Same as ``Filters.document.mime_type("application/x-compressed-tar")``. + wav: Same as ``Filters.document.mime_type("audio/x-wav")``. + xml: Same as ``Filters.document.mime_type("application/xml")``. + zip: Same as ``Filters.document.mime_type("application/zip")``. + file_extension: This filter filters documents by their file ending/extension. + + Note: + * This Filter only filters by the file ending/extension of the document, + it doesn't check the validity of document. + * The user can manipulate the file extension of a document and + send media with wrong types that don't fit to this handler. + * Case insensitive by default, + you may change this with the flag ``case_sensitive=True``. + * Extension should be passed without leading dot + unless it's a part of the extension. + * Pass :obj:`None` to filter files with no extension, + i.e. without a dot in the filename. + + Example: + * ``Filters.document.file_extension("jpg")`` + filters files with extension ``".jpg"``. + * ``Filters.document.file_extension(".jpg")`` + filters files with extension ``"..jpg"``. + * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)`` + filters files with extension ``".Dockerfile"`` minding the case. + * ``Filters.document.file_extension(None)`` + filters files without a dot in the filename. + """ + + class _Animation(MessageFilter): + __slots__ = () name = 'Filters.animation' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.animation) animation = _Animation() - """:obj:`Filter`: Messages that contain :class:`telegram.Animation`.""" + """Messages that contain :class:`telegram.Animation`.""" - class _Photo(BaseFilter): + class _Photo(MessageFilter): + __slots__ = () name = 'Filters.photo' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.photo) photo = _Photo() - """:obj:`Filter`: Messages that contain :class:`telegram.PhotoSize`.""" + """Messages that contain :class:`telegram.PhotoSize`.""" - class _Sticker(BaseFilter): + class _Sticker(MessageFilter): + __slots__ = () name = 'Filters.sticker' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.sticker) sticker = _Sticker() - """:obj:`Filter`: Messages that contain :class:`telegram.Sticker`.""" + """Messages that contain :class:`telegram.Sticker`.""" - class _Video(BaseFilter): + class _Video(MessageFilter): + __slots__ = () name = 'Filters.video' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.video) video = _Video() - """:obj:`Filter`: Messages that contain :class:`telegram.Video`.""" + """Messages that contain :class:`telegram.Video`.""" - class _Voice(BaseFilter): + class _Voice(MessageFilter): + __slots__ = () name = 'Filters.voice' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.voice) voice = _Voice() - """:obj:`Filter`: Messages that contain :class:`telegram.Voice`.""" + """Messages that contain :class:`telegram.Voice`.""" - class _VideoNote(BaseFilter): + class _VideoNote(MessageFilter): + __slots__ = () name = 'Filters.video_note' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.video_note) video_note = _VideoNote() - """:obj:`Filter`: Messages that contain :class:`telegram.VideoNote`.""" + """Messages that contain :class:`telegram.VideoNote`.""" - class _Contact(BaseFilter): + class _Contact(MessageFilter): + __slots__ = () name = 'Filters.contact' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.contact) contact = _Contact() - """:obj:`Filter`: Messages that contain :class:`telegram.Contact`.""" + """Messages that contain :class:`telegram.Contact`.""" - class _Location(BaseFilter): + class _Location(MessageFilter): + __slots__ = () name = 'Filters.location' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.location) location = _Location() - """:obj:`Filter`: Messages that contain :class:`telegram.Location`.""" + """Messages that contain :class:`telegram.Location`.""" - class _Venue(BaseFilter): + class _Venue(MessageFilter): + __slots__ = () name = 'Filters.venue' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.venue) venue = _Venue() - """:obj:`Filter`: Messages that contain :class:`telegram.Venue`.""" + """Messages that contain :class:`telegram.Venue`.""" - class _StatusUpdate(BaseFilter): + class _StatusUpdate(UpdateFilter): """Subset for messages containing a status update. Examples: @@ -401,99 +1023,185 @@ class _StatusUpdate(BaseFilter): """ - class _NewChatMembers(BaseFilter): + __slots__ = () + + class _NewChatMembers(MessageFilter): + __slots__ = () name = 'Filters.status_update.new_chat_members' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.new_chat_members) new_chat_members = _NewChatMembers() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.new_chat_members`.""" + """Messages that contain :attr:`telegram.Message.new_chat_members`.""" - class _LeftChatMember(BaseFilter): + class _LeftChatMember(MessageFilter): + __slots__ = () name = 'Filters.status_update.left_chat_member' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.left_chat_member) left_chat_member = _LeftChatMember() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.left_chat_member`.""" + """Messages that contain :attr:`telegram.Message.left_chat_member`.""" - class _NewChatTitle(BaseFilter): + class _NewChatTitle(MessageFilter): + __slots__ = () name = 'Filters.status_update.new_chat_title' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.new_chat_title) new_chat_title = _NewChatTitle() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.new_chat_title`.""" + """Messages that contain :attr:`telegram.Message.new_chat_title`.""" - class _NewChatPhoto(BaseFilter): + class _NewChatPhoto(MessageFilter): + __slots__ = () name = 'Filters.status_update.new_chat_photo' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.new_chat_photo) new_chat_photo = _NewChatPhoto() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.new_chat_photo`.""" + """Messages that contain :attr:`telegram.Message.new_chat_photo`.""" - class _DeleteChatPhoto(BaseFilter): + class _DeleteChatPhoto(MessageFilter): + __slots__ = () name = 'Filters.status_update.delete_chat_photo' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.delete_chat_photo) delete_chat_photo = _DeleteChatPhoto() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" + """Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" - class _ChatCreated(BaseFilter): + class _ChatCreated(MessageFilter): + __slots__ = () name = 'Filters.status_update.chat_created' - def filter(self, message): - return bool(message.group_chat_created or message.supergroup_chat_created or - message.channel_chat_created) + def filter(self, message: Message) -> bool: + return bool( + message.group_chat_created + or message.supergroup_chat_created + or message.channel_chat_created + ) chat_created = _ChatCreated() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.group_chat_created`, + """Messages that contain :attr:`telegram.Message.group_chat_created`, :attr: `telegram.Message.supergroup_chat_created` or :attr: `telegram.Message.channel_chat_created`.""" - class _Migrate(BaseFilter): + class _MessageAutoDeleteTimerChanged(MessageFilter): + __slots__ = () + name = 'MessageAutoDeleteTimerChanged' + + def filter(self, message: Message) -> bool: + return bool(message.message_auto_delete_timer_changed) + + message_auto_delete_timer_changed = _MessageAutoDeleteTimerChanged() + """Messages that contain :attr:`message_auto_delete_timer_changed`""" + + class _Migrate(MessageFilter): + __slots__ = () name = 'Filters.status_update.migrate' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.migrate_from_chat_id or message.migrate_to_chat_id) migrate = _Migrate() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or - :attr: `telegram.Message.migrate_to_chat_id`.""" + """Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or + :attr:`telegram.Message.migrate_to_chat_id`.""" - class _PinnedMessage(BaseFilter): + class _PinnedMessage(MessageFilter): + __slots__ = () name = 'Filters.status_update.pinned_message' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.pinned_message) pinned_message = _PinnedMessage() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.pinned_message`.""" + """Messages that contain :attr:`telegram.Message.pinned_message`.""" - class _ConnectedWebsite(BaseFilter): + class _ConnectedWebsite(MessageFilter): + __slots__ = () name = 'Filters.status_update.connected_website' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.connected_website) connected_website = _ConnectedWebsite() - """:obj:`Filter`: Messages that contain :attr:`telegram.Message.connected_website`.""" + """Messages that contain :attr:`telegram.Message.connected_website`.""" + + class _ProximityAlertTriggered(MessageFilter): + __slots__ = () + name = 'Filters.status_update.proximity_alert_triggered' + + def filter(self, message: Message) -> bool: + return bool(message.proximity_alert_triggered) + + proximity_alert_triggered = _ProximityAlertTriggered() + """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" + + class _VoiceChatScheduled(MessageFilter): + __slots__ = () + name = 'Filters.status_update.voice_chat_scheduled' + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_scheduled) + + voice_chat_scheduled = _VoiceChatScheduled() + """Messages that contain :attr:`telegram.Message.voice_chat_scheduled`.""" + + class _VoiceChatStarted(MessageFilter): + __slots__ = () + name = 'Filters.status_update.voice_chat_started' + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_started) + + voice_chat_started = _VoiceChatStarted() + """Messages that contain :attr:`telegram.Message.voice_chat_started`.""" + + class _VoiceChatEnded(MessageFilter): + __slots__ = () + name = 'Filters.status_update.voice_chat_ended' + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_ended) + + voice_chat_ended = _VoiceChatEnded() + """Messages that contain :attr:`telegram.Message.voice_chat_ended`.""" + + class _VoiceChatParticipantsInvited(MessageFilter): + __slots__ = () + name = 'Filters.status_update.voice_chat_participants_invited' + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_participants_invited) + + voice_chat_participants_invited = _VoiceChatParticipantsInvited() + """Messages that contain :attr:`telegram.Message.voice_chat_participants_invited`.""" name = 'Filters.status_update' - def filter(self, message): - return bool(self.new_chat_members(message) or self.left_chat_member(message) or - self.new_chat_title(message) or self.new_chat_photo(message) or - self.delete_chat_photo(message) or self.chat_created(message) or - self.migrate(message) or self.pinned_message(message) or - self.connected_website(message)) + def filter(self, message: Update) -> bool: + return bool( + self.new_chat_members(message) + or self.left_chat_member(message) + or self.new_chat_title(message) + or self.new_chat_photo(message) + or self.delete_chat_photo(message) + or self.chat_created(message) + or self.message_auto_delete_timer_changed(message) + or self.migrate(message) + or self.pinned_message(message) + or self.connected_website(message) + or self.proximity_alert_triggered(message) + or self.voice_chat_scheduled(message) + or self.voice_chat_started(message) + or self.voice_chat_ended(message) + or self.voice_chat_participants_invited(message) + ) status_update = _StatusUpdate() """Subset for messages containing a status update. @@ -503,46 +1211,73 @@ def filter(self, message): ``Filters.status_update`` for all status update messages. Attributes: - chat_created (:obj:`Filter`): Messages that contain + chat_created: Messages that contain :attr:`telegram.Message.group_chat_created`, :attr:`telegram.Message.supergroup_chat_created` or :attr:`telegram.Message.channel_chat_created`. - delete_chat_photo (:obj:`Filter`): Messages that contain + connected_website: Messages that contain + :attr:`telegram.Message.connected_website`. + delete_chat_photo: Messages that contain :attr:`telegram.Message.delete_chat_photo`. - left_chat_member (:obj:`Filter`): Messages that contain + left_chat_member: Messages that contain :attr:`telegram.Message.left_chat_member`. - migrate (:obj:`Filter`): Messages that contain - :attr:`telegram.Message.migrate_from_chat_id` or - :attr: `telegram.Message.migrate_from_chat_id`. - new_chat_members (:obj:`Filter`): Messages that contain + migrate: Messages that contain + :attr:`telegram.Message.migrate_to_chat_id` or + :attr:`telegram.Message.migrate_from_chat_id`. + new_chat_members: Messages that contain :attr:`telegram.Message.new_chat_members`. - new_chat_photo (:obj:`Filter`): Messages that contain + new_chat_photo: Messages that contain :attr:`telegram.Message.new_chat_photo`. - new_chat_title (:obj:`Filter`): Messages that contain + new_chat_title: Messages that contain :attr:`telegram.Message.new_chat_title`. - pinned_message (:obj:`Filter`): Messages that contain + message_auto_delete_timer_changed: Messages that contain + :attr:`message_auto_delete_timer_changed`. + + .. versionadded:: 13.4 + pinned_message: Messages that contain :attr:`telegram.Message.pinned_message`. + proximity_alert_triggered: Messages that contain + :attr:`telegram.Message.proximity_alert_triggered`. + voice_chat_scheduled: Messages that contain + :attr:`telegram.Message.voice_chat_scheduled`. + + .. versionadded:: 13.5 + voice_chat_started: Messages that contain + :attr:`telegram.Message.voice_chat_started`. + + .. versionadded:: 13.4 + voice_chat_ended: Messages that contain + :attr:`telegram.Message.voice_chat_ended`. + + .. versionadded:: 13.4 + voice_chat_participants_invited: Messages that contain + :attr:`telegram.Message.voice_chat_participants_invited`. + + .. versionadded:: 13.4 + """ - class _Forwarded(BaseFilter): + class _Forwarded(MessageFilter): + __slots__ = () name = 'Filters.forwarded' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.forward_date) forwarded = _Forwarded() - """:obj:`Filter`: Messages that are forwarded.""" + """Messages that are forwarded.""" - class _Game(BaseFilter): + class _Game(MessageFilter): + __slots__ = () name = 'Filters.game' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.game) game = _Game() - """:obj:`Filter`: Messages that contain :class:`telegram.Game`.""" + """Messages that contain :class:`telegram.Game`.""" - class entity(BaseFilter): + class entity(MessageFilter): """ Filters messages to only allow those which have a :class:`telegram.MessageEntity` where their `type` matches `entity_type`. @@ -556,14 +1291,17 @@ class entity(BaseFilter): """ - def __init__(self, entity_type): + __slots__ = ('entity_type',) + + def __init__(self, entity_type: str): self.entity_type = entity_type - self.name = 'Filters.entity({})'.format(self.entity_type) + self.name = f'Filters.entity({self.entity_type})' - def filter(self, message): + def filter(self, message: Message) -> bool: + """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.entities) - class caption_entity(BaseFilter): + class caption_entity(MessageFilter): """ Filters media messages to only allow those which have a :class:`telegram.MessageEntity` where their `type` matches `entity_type`. @@ -577,156 +1315,1028 @@ class caption_entity(BaseFilter): """ - def __init__(self, entity_type): + __slots__ = ('entity_type',) + + def __init__(self, entity_type: str): self.entity_type = entity_type - self.name = 'Filters.caption_entity({})'.format(self.entity_type) + self.name = f'Filters.caption_entity({self.entity_type})' - def filter(self, message): + def filter(self, message: Message) -> bool: + """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.caption_entities) - class _Private(BaseFilter): + class _Private(MessageFilter): + __slots__ = () name = 'Filters.private' - def filter(self, message): + def filter(self, message: Message) -> bool: + warnings.warn( + 'Filters.private is deprecated. Use Filters.chat_type.private instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) return message.chat.type == Chat.PRIVATE private = _Private() - """:obj:`Filter`: Messages sent in a private chat.""" + """ + Messages sent in a private chat. - class _Group(BaseFilter): + Note: + DEPRECATED. Use + :attr:`telegram.ext.Filters.chat_type.private` instead. + """ + + class _Group(MessageFilter): + __slots__ = () name = 'Filters.group' - def filter(self, message): + def filter(self, message: Message) -> bool: + warnings.warn( + 'Filters.group is deprecated. Use Filters.chat_type.groups instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP] group = _Group() - """:obj:`Filter`: Messages sent in a group chat.""" + """ + Messages sent in a group or a supergroup chat. + + Note: + DEPRECATED. Use + :attr:`telegram.ext.Filters.chat_type.groups` instead. + """ + + class _ChatType(MessageFilter): + __slots__ = () + name = 'Filters.chat_type' + + class _Channel(MessageFilter): + __slots__ = () + name = 'Filters.chat_type.channel' + + def filter(self, message: Message) -> bool: + return message.chat.type == Chat.CHANNEL + + channel = _Channel() + + class _Group(MessageFilter): + __slots__ = () + name = 'Filters.chat_type.group' + + def filter(self, message: Message) -> bool: + return message.chat.type == Chat.GROUP + + group = _Group() + + class _SuperGroup(MessageFilter): + __slots__ = () + name = 'Filters.chat_type.supergroup' + + def filter(self, message: Message) -> bool: + return message.chat.type == Chat.SUPERGROUP + + supergroup = _SuperGroup() - class user(BaseFilter): - """Filters messages to allow only those which are from specified user ID. + class _Groups(MessageFilter): + __slots__ = () + name = 'Filters.chat_type.groups' + + def filter(self, message: Message) -> bool: + return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP] + + groups = _Groups() + + class _Private(MessageFilter): + __slots__ = () + name = 'Filters.chat_type.private' + + def filter(self, message: Message) -> bool: + return message.chat.type == Chat.PRIVATE + + private = _Private() + + def filter(self, message: Message) -> bool: + return bool(message.chat.type) + + chat_type = _ChatType() + """Subset for filtering the type of chat. + + Examples: + Use these filters like: ``Filters.chat_type.channel`` or + ``Filters.chat_type.supergroup`` etc. Or use just ``Filters.chat_type`` for all + chat types. + + Attributes: + channel: Updates from channel + group: Updates from group + supergroup: Updates from supergroup + groups: Updates from group *or* supergroup + private: Updates sent in private chat + """ + + class _ChatUserBaseFilter(MessageFilter, ABC): + __slots__ = ( + 'chat_id_name', + 'username_name', + 'allow_empty', + '__lock', + '_chat_ids', + '_usernames', + ) + + def __init__( + self, + chat_id: SLT[int] = None, + username: SLT[str] = None, + allow_empty: bool = False, + ): + self.chat_id_name = 'chat_id' + self.username_name = 'username' + self.allow_empty = allow_empty + self.__lock = Lock() + + self._chat_ids: Set[int] = set() + self._usernames: Set[str] = set() + + self._set_chat_ids(chat_id) + self._set_usernames(username) + + @abstractmethod + def get_chat_or_user(self, message: Message) -> Union[Chat, User, None]: + ... + + @staticmethod + def _parse_chat_id(chat_id: SLT[int]) -> Set[int]: + if chat_id is None: + return set() + if isinstance(chat_id, int): + return {chat_id} + return set(chat_id) + + @staticmethod + def _parse_username(username: SLT[str]) -> Set[str]: + if username is None: + return set() + if isinstance(username, str): + return {username[1:] if username.startswith('@') else username} + return {chat[1:] if chat.startswith('@') else chat for chat in username} + + def _set_chat_ids(self, chat_id: SLT[int]) -> None: + with self.__lock: + if chat_id and self._usernames: + raise RuntimeError( + f"Can't set {self.chat_id_name} in conjunction with (already set) " + f"{self.username_name}s." + ) + self._chat_ids = self._parse_chat_id(chat_id) + + def _set_usernames(self, username: SLT[str]) -> None: + with self.__lock: + if username and self._chat_ids: + raise RuntimeError( + f"Can't set {self.username_name} in conjunction with (already set) " + f"{self.chat_id_name}s." + ) + self._usernames = self._parse_username(username) + + @property + def chat_ids(self) -> FrozenSet[int]: + with self.__lock: + return frozenset(self._chat_ids) + + @chat_ids.setter + def chat_ids(self, chat_id: SLT[int]) -> None: + self._set_chat_ids(chat_id) + + @property + def usernames(self) -> FrozenSet[str]: + with self.__lock: + return frozenset(self._usernames) + + @usernames.setter + def usernames(self, username: SLT[str]) -> None: + self._set_usernames(username) + + def add_usernames(self, username: SLT[str]) -> None: + with self.__lock: + if self._chat_ids: + raise RuntimeError( + f"Can't set {self.username_name} in conjunction with (already set) " + f"{self.chat_id_name}s." + ) + + parsed_username = self._parse_username(username) + self._usernames |= parsed_username + + def add_chat_ids(self, chat_id: SLT[int]) -> None: + with self.__lock: + if self._usernames: + raise RuntimeError( + f"Can't set {self.chat_id_name} in conjunction with (already set) " + f"{self.username_name}s." + ) + + parsed_chat_id = self._parse_chat_id(chat_id) + + self._chat_ids |= parsed_chat_id + + def remove_usernames(self, username: SLT[str]) -> None: + with self.__lock: + if self._chat_ids: + raise RuntimeError( + f"Can't set {self.username_name} in conjunction with (already set) " + f"{self.chat_id_name}s." + ) + + parsed_username = self._parse_username(username) + self._usernames -= parsed_username + + def remove_chat_ids(self, chat_id: SLT[int]) -> None: + with self.__lock: + if self._usernames: + raise RuntimeError( + f"Can't set {self.chat_id_name} in conjunction with (already set) " + f"{self.username_name}s." + ) + parsed_chat_id = self._parse_chat_id(chat_id) + self._chat_ids -= parsed_chat_id + + def filter(self, message: Message) -> bool: + """""" # remove method from docs + chat_or_user = self.get_chat_or_user(message) + if chat_or_user: + if self.chat_ids: + return chat_or_user.id in self.chat_ids + if self.usernames: + return bool(chat_or_user.username and chat_or_user.username in self.usernames) + return self.allow_empty + return False + + @property + def name(self) -> str: + return ( + f'Filters.{self.__class__.__name__}(' + f'{", ".join(str(s) for s in (self.usernames or self.chat_ids))})' + ) + + @name.setter + def name(self, name: str) -> NoReturn: + raise RuntimeError(f'Cannot set name for Filters.{self.__class__.__name__}') + + class user(_ChatUserBaseFilter): + # pylint: disable=W0235 + """Filters messages to allow only those which are from specified user ID(s) or + username(s). Examples: ``MessageHandler(Filters.user(1234), callback_method)`` + Warning: + :attr:`user_ids` will give a *copy* of the saved user ids as :class:`frozenset`. This + is to ensure thread safety. To add/remove a user, you should use :meth:`add_usernames`, + :meth:`add_user_ids`, :meth:`remove_usernames` and :meth:`remove_user_ids`. Only update + the entire set by ``filter.user_ids/usernames = new_set``, if you are entirely sure + that it is not causing race conditions, as this will complete replace the current set + of allowed users. + Args: - user_id(:obj:`int` | List[:obj:`int`], optional): Which user ID(s) to allow through. - username(:obj:`str` | List[:obj:`str`], optional): Which username(s) to allow through. - If username starts with '@' symbol, it will be ignored. + user_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which user ID(s) to allow through. + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. Leading ``'@'`` s in usernames will be + discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user + is specified in :attr:`user_ids` and :attr:`usernames`. Defaults to :obj:`False` Raises: - ValueError: If chat_id and username are both present, or neither is. + RuntimeError: If user_id and username are both present. + + Attributes: + user_ids(set(:obj:`int`), optional): Which user ID(s) to allow through. + usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to + allow through. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user + is specified in :attr:`user_ids` and :attr:`usernames`. """ - def __init__(self, user_id=None, username=None): - if not (bool(user_id) ^ bool(username)): - raise ValueError('One and only one of user_id or username must be used') - if user_id is not None and isinstance(user_id, int): - self.user_ids = [user_id] - else: - self.user_ids = user_id - if username is None: - self.usernames = username - elif isinstance(username, string_types): - self.usernames = [username.replace('@', '')] - else: - self.usernames = [user.replace('@', '') for user in username] + __slots__ = () - def filter(self, message): - if self.user_ids is not None: - return bool(message.from_user and message.from_user.id in self.user_ids) - else: - # self.usernames is not None - return bool(message.from_user and message.from_user.username and - message.from_user.username in self.usernames) + def __init__( + self, + user_id: SLT[int] = None, + username: SLT[str] = None, + allow_empty: bool = False, + ): + super().__init__(chat_id=user_id, username=username, allow_empty=allow_empty) + self.chat_id_name = 'user_id' - class chat(BaseFilter): - """Filters messages to allow only those which are from specified chat ID. + def get_chat_or_user(self, message: Message) -> Optional[User]: + return message.from_user + + @property + def user_ids(self) -> FrozenSet[int]: + return self.chat_ids + + @user_ids.setter + def user_ids(self, user_id: SLT[int]) -> None: + self.chat_ids = user_id # type: ignore[assignment] + + def add_usernames(self, username: SLT[str]) -> None: + """ + Add one or more users to the allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().add_usernames(username) + + def add_user_ids(self, user_id: SLT[int]) -> None: + """ + Add one or more users to the allowed user ids. + + Args: + user_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which user ID(s) to allow through. + """ + return super().add_chat_ids(user_id) + + def remove_usernames(self, username: SLT[str]) -> None: + """ + Remove one or more users from allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to disallow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().remove_usernames(username) + + def remove_user_ids(self, user_id: SLT[int]) -> None: + """ + Remove one or more users from allowed user ids. + + Args: + user_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which user ID(s) to disallow through. + """ + return super().remove_chat_ids(user_id) + + class via_bot(_ChatUserBaseFilter): + # pylint: disable=W0235 + """Filters messages to allow only those which are from specified via_bot ID(s) or + username(s). + + Examples: + ``MessageHandler(Filters.via_bot(1234), callback_method)`` + + Warning: + :attr:`bot_ids` will give a *copy* of the saved bot ids as :class:`frozenset`. This + is to ensure thread safety. To add/remove a bot, you should use :meth:`add_usernames`, + :meth:`add_bot_ids`, :meth:`remove_usernames` and :meth:`remove_bot_ids`. Only update + the entire set by ``filter.bot_ids/usernames = new_set``, if you are entirely sure + that it is not causing race conditions, as this will complete replace the current set + of allowed bots. + + Args: + bot_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which bot ID(s) to allow through. + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. Leading ``'@'`` s in usernames will be + discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user + is specified in :attr:`bot_ids` and :attr:`usernames`. Defaults to :obj:`False` + + Raises: + RuntimeError: If bot_id and username are both present. + + Attributes: + bot_ids(set(:obj:`int`), optional): Which bot ID(s) to allow through. + usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to + allow through. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no bot + is specified in :attr:`bot_ids` and :attr:`usernames`. + + """ + + __slots__ = () + + def __init__( + self, + bot_id: SLT[int] = None, + username: SLT[str] = None, + allow_empty: bool = False, + ): + super().__init__(chat_id=bot_id, username=username, allow_empty=allow_empty) + self.chat_id_name = 'bot_id' + + def get_chat_or_user(self, message: Message) -> Optional[User]: + return message.via_bot + + @property + def bot_ids(self) -> FrozenSet[int]: + return self.chat_ids + + @bot_ids.setter + def bot_ids(self, bot_id: SLT[int]) -> None: + self.chat_ids = bot_id # type: ignore[assignment] + + def add_usernames(self, username: SLT[str]) -> None: + """ + Add one or more users to the allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().add_usernames(username) + + def add_bot_ids(self, bot_id: SLT[int]) -> None: + """ + + Add one or more users to the allowed user ids. + + Args: + bot_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which bot ID(s) to allow through. + """ + return super().add_chat_ids(bot_id) + + def remove_usernames(self, username: SLT[str]) -> None: + """ + Remove one or more users from allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to disallow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().remove_usernames(username) + + def remove_bot_ids(self, bot_id: SLT[int]) -> None: + """ + Remove one or more users from allowed user ids. + + Args: + bot_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which bot ID(s) to disallow through. + """ + return super().remove_chat_ids(bot_id) + + class chat(_ChatUserBaseFilter): + # pylint: disable=W0235 + """Filters messages to allow only those which are from a specified chat ID or username. Examples: ``MessageHandler(Filters.chat(-1234), callback_method)`` + Warning: + :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This + is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`, + :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update + the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure + that it is not causing race conditions, as this will complete replace the current set + of allowed chats. + Args: - chat_id(:obj:`int` | List[:obj:`int`], optional): Which chat ID(s) to allow through. - username(:obj:`str` | List[:obj:`str`], optional): Which username(s) to allow through. - If username start swith '@' symbol, it will be ignored. + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which chat ID(s) to allow through. + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False` Raises: - ValueError: If chat_id and username are both present, or neither is. + RuntimeError: If chat_id and username are both present. + + Attributes: + chat_ids(set(:obj:`int`), optional): Which chat ID(s) to allow through. + usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to + allow through. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. """ - def __init__(self, chat_id=None, username=None): - if not (bool(chat_id) ^ bool(username)): - raise ValueError('One and only one of chat_id or username must be used') - if chat_id is not None and isinstance(chat_id, int): - self.chat_ids = [chat_id] - else: - self.chat_ids = chat_id - if username is None: - self.usernames = username - elif isinstance(username, string_types): - self.usernames = [username.replace('@', '')] - else: - self.usernames = [chat.replace('@', '') for chat in username] + __slots__ = () - def filter(self, message): - if self.chat_ids is not None: - return bool(message.chat_id in self.chat_ids) - else: - # self.usernames is not None - return bool(message.chat.username and message.chat.username in self.usernames) + def get_chat_or_user(self, message: Message) -> Optional[Chat]: + return message.chat + + def add_usernames(self, username: SLT[str]) -> None: + """ + Add one or more chats to the allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().add_usernames(username) + + def add_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Add one or more chats to the allowed chat ids. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which chat ID(s) to allow through. + """ + return super().add_chat_ids(chat_id) + + def remove_usernames(self, username: SLT[str]) -> None: + """ + Remove one or more chats from allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to disallow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().remove_usernames(username) + + def remove_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Remove one or more chats from allowed chat ids. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which chat ID(s) to disallow through. + """ + return super().remove_chat_ids(chat_id) + + class forwarded_from(_ChatUserBaseFilter): + # pylint: disable=W0235 + """Filters messages to allow only those which are forwarded from the specified chat ID(s) + or username(s) based on :attr:`telegram.Message.forward_from` and + :attr:`telegram.Message.forward_from_chat`. + + .. versionadded:: 13.5 + + Examples: + ``MessageHandler(Filters.forwarded_from(chat_id=1234), callback_method)`` + + Note: + When a user has disallowed adding a link to their account while forwarding their + messages, this filter will *not* work since both + :attr:`telegram.Message.forwarded_from` and + :attr:`telegram.Message.forwarded_from_chat` are :obj:`None`. However, this behaviour + is undocumented and might be changed by Telegram. + + Warning: + :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This + is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`, + :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update + the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure + that it is not causing race conditions, as this will complete replace the current set + of allowed chats. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which chat/user ID(s) to allow through. + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. Leading ``'@'`` s in usernames will be + discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. + + Raises: + RuntimeError: If both chat_id and username are present. + + Attributes: + chat_ids(set(:obj:`int`), optional): Which chat/user ID(s) to allow through. + usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to + allow through. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. + """ + + __slots__ = () + + def get_chat_or_user(self, message: Message) -> Union[User, Chat, None]: + return message.forward_from or message.forward_from_chat + + def add_usernames(self, username: SLT[str]) -> None: + """ + Add one or more chats to the allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().add_usernames(username) + + def add_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Add one or more chats to the allowed chat ids. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which chat/user ID(s) to allow through. + """ + return super().add_chat_ids(chat_id) + + def remove_usernames(self, username: SLT[str]) -> None: + """ + Remove one or more chats from allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which username(s) to disallow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().remove_usernames(username) + + def remove_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Remove one or more chats from allowed chat ids. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which chat/user ID(s) to disallow through. + """ + return super().remove_chat_ids(chat_id) + + class sender_chat(_ChatUserBaseFilter): + # pylint: disable=W0235 + """Filters messages to allow only those which are from a specified sender chat's chat ID or + username. + + Examples: + * To filter for messages sent to a group by a channel with ID + ``-1234``, use ``MessageHandler(Filters.sender_chat(-1234), callback_method)``. + * To filter for messages of anonymous admins in a super group with username + ``@anonymous``, use + ``MessageHandler(Filters.sender_chat(username='anonymous'), callback_method)``. + * To filter for messages sent to a group by *any* channel, use + ``MessageHandler(Filters.sender_chat.channel, callback_method)``. + * To filter for messages of anonymous admins in *any* super group, use + ``MessageHandler(Filters.sender_chat.super_group, callback_method)``. + + Note: + Remember, ``sender_chat`` is also set for messages in a channel as the channel itself, + so when your bot is an admin in a channel and the linked discussion group, you would + receive the message twice (once from inside the channel, once inside the discussion + group). Since v13.9, the field :attr:`telegram.Message.is_automatic_forward` will be + :obj:`True` for the discussion group message. + + .. seealso:: :attr:`Filters.is_automatic_forward` + + Warning: + :attr:`chat_ids` will return a *copy* of the saved chat ids as :class:`frozenset`. This + is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`, + :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update + the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure + that it is not causing race conditions, as this will complete replace the current set + of allowed chats. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which sender chat chat ID(s) to allow through. + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which sender chat username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender + chat is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to + :obj:`False` + + Raises: + RuntimeError: If both chat_id and username are present. + + Attributes: + chat_ids(set(:obj:`int`), optional): Which sender chat chat ID(s) to allow through. + usernames(set(:obj:`str`), optional): Which sender chat username(s) (without leading + ``'@'``) to allow through. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender + chat is specified in :attr:`chat_ids` and :attr:`usernames`. + super_group: Messages whose sender chat is a super group. + + Examples: + ``Filters.sender_chat.supergroup`` + channel: Messages whose sender chat is a channel. + + Examples: + ``Filters.sender_chat.channel`` + + """ + + __slots__ = () + + def get_chat_or_user(self, message: Message) -> Optional[Chat]: + return message.sender_chat + + def add_usernames(self, username: SLT[str]) -> None: + """ + Add one or more sender chats to the allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which sender chat username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().add_usernames(username) + + def add_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Add one or more sender chats to the allowed chat ids. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which sender chat ID(s) to allow through. + """ + return super().add_chat_ids(chat_id) - class _Invoice(BaseFilter): + def remove_usernames(self, username: SLT[str]) -> None: + """ + Remove one or more sender chats from allowed usernames. + + Args: + username(:class:`telegram.utils.types.SLT[str]`, optional): + Which sender chat username(s) to disallow through. + Leading ``'@'`` s in usernames will be discarded. + """ + return super().remove_usernames(username) + + def remove_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Remove one or more sender chats from allowed chat ids. + + Args: + chat_id(:class:`telegram.utils.types.SLT[int]`, optional): + Which sender chat ID(s) to disallow through. + """ + return super().remove_chat_ids(chat_id) + + class _SuperGroup(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + if message.sender_chat: + return message.sender_chat.type == Chat.SUPERGROUP + return False + + class _Channel(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + if message.sender_chat: + return message.sender_chat.type == Chat.CHANNEL + return False + + super_group = _SuperGroup() + channel = _Channel() + + class _IsAutomaticForward(MessageFilter): + __slots__ = () + name = 'Filters.is_automatic_forward' + + def filter(self, message: Message) -> bool: + return bool(message.is_automatic_forward) + + is_automatic_forward = _IsAutomaticForward() + """Messages that contain :attr:`telegram.Message.is_automatic_forward`. + + .. versionadded:: 13.9 + """ + + class _HasProtectedContent(MessageFilter): + __slots__ = () + name = 'Filters.has_protected_content' + + def filter(self, message: Message) -> bool: + return bool(message.has_protected_content) + + has_protected_content = _HasProtectedContent() + """Messages that contain :attr:`telegram.Message.has_protected_content`. + + .. versionadded:: 13.9 + """ + + class _Invoice(MessageFilter): + __slots__ = () name = 'Filters.invoice' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.invoice) invoice = _Invoice() - """:obj:`Filter`: Messages that contain :class:`telegram.Invoice`.""" + """Messages that contain :class:`telegram.Invoice`.""" - class _SuccessfulPayment(BaseFilter): + class _SuccessfulPayment(MessageFilter): + __slots__ = () name = 'Filters.successful_payment' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.successful_payment) successful_payment = _SuccessfulPayment() - """:obj:`Filter`: Messages that confirm a :class:`telegram.SuccessfulPayment`.""" + """Messages that confirm a :class:`telegram.SuccessfulPayment`.""" - class _PassportData(BaseFilter): + class _PassportData(MessageFilter): + __slots__ = () name = 'Filters.passport_data' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.passport_data) passport_data = _PassportData() - """:obj:`Filter`: Messages that contain a :class:`telegram.PassportData`""" + """Messages that contain a :class:`telegram.PassportData`""" + + class _Poll(MessageFilter): + __slots__ = () + name = 'Filters.poll' - class language(BaseFilter): + def filter(self, message: Message) -> bool: + return bool(message.poll) + + poll = _Poll() + """Messages that contain a :class:`telegram.Poll`.""" + + class _Dice(_DiceEmoji): + __slots__ = () + dice = _DiceEmoji('🎲', 'dice') + darts = _DiceEmoji('🎯', 'darts') + basketball = _DiceEmoji('🏀', 'basketball') + football = _DiceEmoji('⚽') + slot_machine = _DiceEmoji('🎰') + bowling = _DiceEmoji('🎳', 'bowling') + + dice = _Dice() + """Dice Messages. If an integer or a list of integers is passed, it filters messages to only + allow those whose dice value is appearing in the given list. + + Examples: + To allow any dice message, simply use + ``MessageHandler(Filters.dice, callback_method)``. + + To allow only dice messages with the emoji 🎲, but any value, use + ``MessageHandler(Filters.dice.dice, callback_method)``. + + To allow only dice messages with the emoji 🎯 and with value 6, use + ``MessageHandler(Filters.dice.darts(6), callback_method)``. + + To allow only dice messages with the emoji ⚽ and with value 5 `or` 6, use + ``MessageHandler(Filters.dice.football([5, 6]), callback_method)``. + + Note: + Dice messages don't have text. If you want to filter either text or dice messages, use + ``Filters.text | Filters.dice``. + + Args: + update (:class:`telegram.utils.types.SLT[int]`, optional): + Which values to allow. If not specified, will allow any dice message. + + Attributes: + dice: Dice messages with the emoji 🎲. Passing a list of integers is supported just as for + :attr:`Filters.dice`. + darts: Dice messages with the emoji 🎯. Passing a list of integers is supported just as for + :attr:`Filters.dice`. + basketball: Dice messages with the emoji 🏀. Passing a list of integers is supported just + as for :attr:`Filters.dice`. + football: Dice messages with the emoji ⚽. Passing a list of integers is supported just + as for :attr:`Filters.dice`. + slot_machine: Dice messages with the emoji 🎰. Passing a list of integers is supported just + as for :attr:`Filters.dice`. + bowling: Dice messages with the emoji 🎳. Passing a list of integers is supported just + as for :attr:`Filters.dice`. + + .. versionadded:: 13.4 + + """ + + class language(MessageFilter): """Filters messages to only allow those which are from users with a certain language code. - Note: According to telegrams documentation, every single user does not have the - `language_code` attribute. + Note: + According to official Telegram API documentation, not every single user has the + `language_code` attribute. Do not count on this filter working on all users. Examples: ``MessageHandler(Filters.language("en"), callback_method)`` Args: - lang (:obj:`str` | List[:obj:`str`]): Which language code(s) to allow through. This - will be matched using ``.startswith`` meaning that 'en' will match both 'en_US' - and 'en_GB'. + lang (:class:`telegram.utils.types.SLT[str]`): + Which language code(s) to allow through. + This will be matched using ``.startswith`` meaning that + 'en' will match both 'en_US' and 'en_GB'. """ - def __init__(self, lang): - if isinstance(lang, string_types): + __slots__ = ('lang',) + + def __init__(self, lang: SLT[str]): + if isinstance(lang, str): + lang = cast(str, lang) self.lang = [lang] else: + lang = cast(List[str], lang) self.lang = lang - self.name = 'Filters.language({})'.format(self.lang) + self.name = f'Filters.language({self.lang})' + + def filter(self, message: Message) -> bool: + """""" # remove method from docs + return bool( + message.from_user.language_code + and any(message.from_user.language_code.startswith(x) for x in self.lang) + ) + + class _Attachment(MessageFilter): + __slots__ = () + + name = 'Filters.attachment' + + def filter(self, message: Message) -> bool: + return bool(message.effective_attachment) + + attachment = _Attachment() + """Messages that contain :meth:`telegram.Message.effective_attachment`. + + + .. versionadded:: 13.6""" + + class _UpdateType(UpdateFilter): + __slots__ = () + name = 'Filters.update' - def filter(self, message): - return message.from_user.language_code and any( - [message.from_user.language_code.startswith(x) for x in self.lang]) + class _Message(UpdateFilter): + __slots__ = () + name = 'Filters.update.message' + + def filter(self, update: Update) -> bool: + return update.message is not None + + message = _Message() + + class _EditedMessage(UpdateFilter): + __slots__ = () + name = 'Filters.update.edited_message' + + def filter(self, update: Update) -> bool: + return update.edited_message is not None + + edited_message = _EditedMessage() + + class _Messages(UpdateFilter): + __slots__ = () + name = 'Filters.update.messages' + + def filter(self, update: Update) -> bool: + return update.message is not None or update.edited_message is not None + + messages = _Messages() + + class _ChannelPost(UpdateFilter): + __slots__ = () + name = 'Filters.update.channel_post' + + def filter(self, update: Update) -> bool: + return update.channel_post is not None + + channel_post = _ChannelPost() + + class _EditedChannelPost(UpdateFilter): + __slots__ = () + name = 'Filters.update.edited_channel_post' + + def filter(self, update: Update) -> bool: + return update.edited_channel_post is not None + + edited_channel_post = _EditedChannelPost() + + class _ChannelPosts(UpdateFilter): + __slots__ = () + name = 'Filters.update.channel_posts' + + def filter(self, update: Update) -> bool: + return update.channel_post is not None or update.edited_channel_post is not None + + channel_posts = _ChannelPosts() + + def filter(self, update: Update) -> bool: + return bool(self.messages(update) or self.channel_posts(update)) + + update = _UpdateType() + """Subset for filtering the type of update. + + Examples: + Use these filters like: ``Filters.update.message`` or + ``Filters.update.channel_posts`` etc. Or use just ``Filters.update`` for all + types. + + Attributes: + message: Updates with :attr:`telegram.Update.message` + edited_message: Updates with :attr:`telegram.Update.edited_message` + messages: Updates with either :attr:`telegram.Update.message` or + :attr:`telegram.Update.edited_message` + channel_post: Updates with :attr:`telegram.Update.channel_post` + edited_channel_post: Updates with + :attr:`telegram.Update.edited_channel_post` + channel_posts: Updates with either :attr:`telegram.Update.channel_post` or + :attr:`telegram.Update.edited_channel_post` + """ diff --git a/telegramer/include/telegram/ext/handler.py b/telegramer/include/telegram/ext/handler.py index 5a3b6dc..b6e3a63 100644 --- a/telegramer/include/telegram/ext/handler.py +++ b/telegramer/include/telegram/ext/handler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,21 +17,26 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the base class for handlers as used by the Dispatcher.""" +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, Union, Generic +from sys import version_info as py_ver +from telegram.utils.deprecate import set_new_attribute_deprecated -class Handler(object): - """The base class for all update handlers. Create custom handlers by inheriting from it. +from telegram import Update +from telegram.ext.utils.promise import Promise +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from telegram.ext.utils.types import CCT - Attributes: - callback (:obj:`callable`): The callback function for this handler. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. +if TYPE_CHECKING: + from telegram.ext import Dispatcher + +RT = TypeVar('RT') +UT = TypeVar('UT') + + +class Handler(Generic[UT, CCT], ABC): + """The base class for all update handlers. Create custom handlers by inheriting from it. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you @@ -39,87 +44,217 @@ class Handler(object): either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + Args: - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ - def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False): + # Apparently Py 3.7 and below have '__dict__' in ABC + if py_ver < (3, 7): + __slots__ = ( + 'callback', + 'pass_update_queue', + 'pass_job_queue', + 'pass_user_data', + 'pass_chat_data', + 'run_async', + ) + else: + __slots__ = ( + 'callback', # type: ignore[assignment] + 'pass_update_queue', + 'pass_job_queue', + 'pass_user_data', + 'pass_chat_data', + 'run_async', + '__dict__', + ) + + def __init__( + self, + callback: Callable[[UT, CCT], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): self.callback = callback self.pass_update_queue = pass_update_queue self.pass_job_queue = pass_job_queue self.pass_user_data = pass_user_data self.pass_chat_data = pass_chat_data + self.run_async = run_async + + def __setattr__(self, key: str, value: object) -> None: + # See comment on BaseFilter to know why this was done. + if key.startswith('__'): + key = f"_{self.__class__.__name__}{key}" + if issubclass(self.__class__, Handler) and not self.__class__.__module__.startswith( + 'telegram.ext.' + ): + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) - def check_update(self, update): + @abstractmethod + def check_update(self, update: object) -> Optional[Union[bool, object]]: """ This method is called to determine if an update should be handled by this handler instance. It should always be overridden. + Note: + Custom updates types can be handled by the dispatcher. Therefore, an implementation of + this method should always check the type of :attr:`update`. + Args: update (:obj:`str` | :class:`telegram.Update`): The update to be tested. Returns: - :obj:`bool` + Either :obj:`None` or :obj:`False` if the update should not be handled. Otherwise an + object that will be passed to :meth:`handle_update` and + :meth:`collect_additional_context` when the update gets handled. """ - raise NotImplementedError - def handle_update(self, update, dispatcher): + def handle_update( + self, + update: UT, + dispatcher: 'Dispatcher', + check_result: object, + context: CCT = None, + ) -> Union[RT, Promise]: """ This method is called if it was determined that an update should indeed - be handled by this instance. It should also be overridden, but in most - cases call ``self.callback(dispatcher.bot, update)``, possibly along with - optional arguments. To work with the ``ConversationHandler``, this method should return the - value returned from ``self.callback`` + be handled by this instance. Calls :attr:`callback` along with its respectful + arguments. To work with the :class:`telegram.ext.ConversationHandler`, this method + returns the value returned from :attr:`callback`. + Note that it can be overridden if needed by the subclassing handler. Args: update (:obj:`str` | :class:`telegram.Update`): The update to be handled. - dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher to collect optional args. + dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher. + check_result (:obj:`obj`): The result from :attr:`check_update`. + context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by + the dispatcher. """ - raise NotImplementedError + run_async = self.run_async + if ( + self.run_async is DEFAULT_FALSE + and dispatcher.bot.defaults + and dispatcher.bot.defaults.run_async + ): + run_async = True + + if context: + self.collect_additional_context(context, update, dispatcher, check_result) + if run_async: + return dispatcher.run_async(self.callback, update, context, update=update) + return self.callback(update, context) + + optional_args = self.collect_optional_args(dispatcher, update, check_result) + if run_async: + return dispatcher.run_async( + self.callback, dispatcher.bot, update, update=update, **optional_args + ) + return self.callback(dispatcher.bot, update, **optional_args) # type: ignore - def collect_optional_args(self, dispatcher, update=None): - """Prepares the optional arguments that are the same for all types of handlers. + def collect_additional_context( + self, + context: CCT, + update: UT, + dispatcher: 'Dispatcher', + check_result: Any, + ) -> None: + """Prepares additional arguments for the context. Override if needed. + + Args: + context (:class:`telegram.ext.CallbackContext`): The context object. + update (:class:`telegram.Update`): The update to gather chat/user id from. + dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher. + check_result: The result (return value) from :attr:`check_update`. + + """ + + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: UT = None, + check_result: Any = None, # pylint: disable=W0613 + ) -> Dict[str, object]: + """ + Prepares the optional arguments. If the handler has additional optional args, + it should subclass this method, but remember to call this super method. + + DEPRECATED: This method is being replaced by new context based callbacks. Please see + https://git.io/fxJuV for more info. Args: dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher. + update (:class:`telegram.Update`): The update to gather chat/user id from. + check_result: The result from check_update """ - optional_args = dict() + optional_args: Dict[str, object] = {} if self.pass_update_queue: optional_args['update_queue'] = dispatcher.update_queue if self.pass_job_queue: optional_args['job_queue'] = dispatcher.job_queue - if self.pass_user_data or self.pass_chat_data: - chat = update.effective_chat + if self.pass_user_data and isinstance(update, Update): user = update.effective_user - - if self.pass_user_data: - optional_args['user_data'] = dispatcher.user_data[user.id if user else None] - - if self.pass_chat_data: - optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None] + optional_args['user_data'] = dispatcher.user_data[ + user.id if user else None # type: ignore[index] + ] + if self.pass_chat_data and isinstance(update, Update): + chat = update.effective_chat + optional_args['chat_data'] = dispatcher.chat_data[ + chat.id if chat else None # type: ignore[index] + ] return optional_args diff --git a/telegramer/include/telegram/ext/inlinequeryhandler.py b/telegramer/include/telegram/ext/inlinequeryhandler.py index 157df93..de43431 100644 --- a/telegramer/include/telegram/ext/inlinequeryhandler.py +++ b/telegramer/include/telegram/ext/inlinequeryhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,143 +16,206 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -""" This module contains the InlineQueryHandler class """ +"""This module contains the InlineQueryHandler class.""" import re - -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass - -try: - string_types -except NameError: - string_types = str +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Match, + Optional, + Pattern, + TypeVar, + Union, + cast, + List, +) from telegram import Update -from telegram.utils.deprecate import deprecate +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE + from .handler import Handler +from .utils.types import CCT +if TYPE_CHECKING: + from telegram.ext import Dispatcher -class InlineQueryHandler(Handler): +RT = TypeVar('RT') + + +class InlineQueryHandler(Handler[Update, CCT]): """ Handler class to handle Telegram inline queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. - Attributes: - callback (:obj:`callable`): The callback function for this handler. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pattern (:obj:`str` | :obj:`Pattern`): Optional. Regex pattern to test - :attr:`telegram.InlineQuery.query` against. - pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the - callback function. - pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. - Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + * When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + * :attr:`telegram.InlineQuery.chat_type` will not be set for inline queries from secret + chats and may not be set for inline queries coming from third-party clients. These + updates won't be handled, if :attr:`chat_types` is passed. + Args: - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not ``None``, + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not :obj:`None`, ``re.match`` is used on :attr:`telegram.InlineQuery.query` to determine if an update should be handled by this handler. + chat_types (List[:obj:`str`], optional): List of allowed chat types. If passed, will only + handle inline queries with the appropriate :attr:`telegram.InlineQuery.chat_type`. + + .. versionadded:: 13.5 pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. - Default is ``False`` + Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. - Default is ``False`` - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pattern (:obj:`str` | :obj:`Pattern`): Optional. Regex pattern to test + :attr:`telegram.InlineQuery.query` against. + chat_types (List[:obj:`str`], optional): List of allowed chat types. + + .. versionadded:: 13.5 + pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the + callback function. + pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + """ - def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pattern=None, - pass_groups=False, - pass_groupdict=False, - pass_user_data=False, - pass_chat_data=False): - super(InlineQueryHandler, self).__init__( + __slots__ = ('pattern', 'chat_types', 'pass_groups', 'pass_groupdict') + + def __init__( + self, + callback: Callable[[Update, CCT], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pattern: Union[str, Pattern] = None, + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + chat_types: List[str] = None, + ): + super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) + pass_chat_data=pass_chat_data, + run_async=run_async, + ) - if isinstance(pattern, string_types): + if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern = pattern + self.chat_types = chat_types self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - def check_update(self, update): + def check_update(self, update: object) -> Optional[Union[bool, Match]]: """ Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` - """ + """ if isinstance(update, Update) and update.inline_query: + if (self.chat_types is not None) and ( + update.inline_query.chat_type not in self.chat_types + ): + return False if self.pattern: if update.inline_query.query: match = re.match(self.pattern, update.inline_query.query) - return bool(match) + if match: + return match else: return True - - def handle_update(self, update, dispatcher): - """ - Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. + return None + + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: Update = None, + check_result: Optional[Union[bool, Match]] = None, + ) -> Dict[str, object]: + """Pass the results of ``re.match(pattern, query).{groups(), groupdict()}`` to the + callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if + needed. """ - - optional_args = self.collect_optional_args(dispatcher, update) + optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pattern: - match = re.match(self.pattern, update.inline_query.query) - + check_result = cast(Match, check_result) if self.pass_groups: - optional_args['groups'] = match.groups() + optional_args['groups'] = check_result.groups() if self.pass_groupdict: - optional_args['groupdict'] = match.groupdict() - - return self.callback(dispatcher.bot, update, **optional_args) - - # old non-PEP8 Handler methods - m = "telegram.InlineQueryHandler." - checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update") - handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update") + optional_args['groupdict'] = check_result.groupdict() + return optional_args + + def collect_additional_context( + self, + context: CCT, + update: Update, + dispatcher: 'Dispatcher', + check_result: Optional[Union[bool, Match]], + ) -> None: + """Add the result of ``re.match(pattern, update.inline_query.query)`` to + :attr:`CallbackContext.matches` as list with one element. + """ + if self.pattern: + check_result = cast(Match, check_result) + context.matches = [check_result] diff --git a/telegramer/include/telegram/ext/jobqueue.py b/telegramer/include/telegram/ext/jobqueue.py index 1ce8490..f0c1fba 100644 --- a/telegramer/include/telegram/ext/jobqueue.py +++ b/telegramer/include/telegram/ext/jobqueue.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,81 +18,145 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes JobQueue and Job.""" -import logging -import time import datetime -import weakref -from numbers import Number -from threading import Thread, Lock, Event -# REMREM from queue import PriorityQueue, Empty -from Queue import PriorityQueue, Empty +import logging +from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union, cast, overload +import pytz +from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, JobEvent +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.combining import OrTrigger +from apscheduler.triggers.cron import CronTrigger +from apscheduler.job import Job as APSJob -class Days(object): - MON, TUE, WED, THU, FRI, SAT, SUN = range(7) - EVERY_DAY = tuple(range(7)) +from telegram.ext.callbackcontext import CallbackContext +from telegram.utils.types import JSONDict +from telegram.utils.deprecate import set_new_attribute_deprecated +if TYPE_CHECKING: + from telegram import Bot + from telegram.ext import Dispatcher + import apscheduler.job # noqa: F401 -class JobQueue(object): - """This class allows you to periodically perform tasks with the bot. - Attributes: - _queue (:obj:`PriorityQueue`): The queue that holds the Jobs. - bot (:class:`telegram.Bot`): Bot that's send to the handlers. +class JobQueue: + """This class allows you to periodically perform tasks with the bot. It is a convenience + wrapper for the APScheduler library. - Args: + Attributes: + scheduler (:class:`apscheduler.schedulers.background.BackgroundScheduler`): The APScheduler bot (:class:`telegram.Bot`): The bot instance that should be passed to the jobs. + DEPRECATED: Use :attr:`set_dispatcher` instead. """ - def __init__(self, bot): - self._queue = PriorityQueue() - self.bot = bot - self.logger = logging.getLogger(self.__class__.__name__) - self.__start_lock = Lock() - self.__next_peek_lock = Lock() # to protect self._next_peek & self.__tick - self.__tick = Event() - self.__thread = None - self._next_peek = None - self._running = False - - def _put(self, job, next_t=None, last_t=None): - if next_t is None: - next_t = job.interval - if next_t is None: - raise ValueError('next_t is None') - - if isinstance(next_t, datetime.datetime): - next_t = (next_t - datetime.datetime.now()).total_seconds() - - elif isinstance(next_t, datetime.time): - next_datetime = datetime.datetime.combine(datetime.date.today(), next_t) - - if datetime.datetime.now().time() > next_t: - next_datetime += datetime.timedelta(days=1) - - next_t = (next_datetime - datetime.datetime.now()).total_seconds() - - elif isinstance(next_t, datetime.timedelta): - next_t = next_t.total_seconds() - - next_t += last_t or time.time() - - self.logger.debug('Putting job %s with t=%f', job.name, next_t) + __slots__ = ('_dispatcher', 'logger', 'scheduler', '__dict__') - self._queue.put((next_t, job)) + def __init__(self) -> None: + self._dispatcher: 'Dispatcher' = None # type: ignore[assignment] + self.logger = logging.getLogger(self.__class__.__name__) + self.scheduler = BackgroundScheduler(timezone=pytz.utc) + self.scheduler.add_listener( + self._update_persistence, mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR + ) + + # Dispatch errors and don't log them in the APS logger + def aps_log_filter(record): # type: ignore + return 'raised an exception' not in record.msg + + logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter) + self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR) + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + + def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]: + if self._dispatcher.use_context: + return [self._dispatcher.context_types.context.from_job(job, self._dispatcher)] + return [self._dispatcher.bot, job] + + def _tz_now(self) -> datetime.datetime: + return datetime.datetime.now(self.scheduler.timezone) + + def _update_persistence(self, _: JobEvent) -> None: + self._dispatcher.update_persistence() + + def _dispatch_error(self, event: JobEvent) -> None: + try: + self._dispatcher.dispatch_error(None, event.exception) + # Errors should not stop the thread. + except Exception: + self.logger.exception( + 'An error was raised while processing the job and an ' + 'uncaught error was raised while handling the error ' + 'with an error_handler.' + ) + + @overload + def _parse_time_input(self, time: None, shift_day: bool = False) -> None: + ... + + @overload + def _parse_time_input( + self, + time: Union[float, int, datetime.timedelta, datetime.datetime, datetime.time], + shift_day: bool = False, + ) -> datetime.datetime: + ... + + def _parse_time_input( + self, + time: Union[float, int, datetime.timedelta, datetime.datetime, datetime.time, None], + shift_day: bool = False, + ) -> Optional[datetime.datetime]: + if time is None: + return None + if isinstance(time, (int, float)): + return self._tz_now() + datetime.timedelta(seconds=time) + if isinstance(time, datetime.timedelta): + return self._tz_now() + time + if isinstance(time, datetime.time): + date_time = datetime.datetime.combine( + datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time + ) + if date_time.tzinfo is None: + date_time = self.scheduler.timezone.localize(date_time) + if shift_day and date_time <= datetime.datetime.now(pytz.utc): + date_time += datetime.timedelta(days=1) + return date_time + # isinstance(time, datetime.datetime): + return time + + def set_dispatcher(self, dispatcher: 'Dispatcher') -> None: + """Set the dispatcher to be used by this JobQueue. Use this instead of passing a + :class:`telegram.Bot` to the JobQueue, which is deprecated. - # Wake up the loop if this job should be executed next - self._set_next_peek(next_t) + Args: + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher. - def run_once(self, callback, when, context=None, name=None): + """ + self._dispatcher = dispatcher + if dispatcher.bot.defaults: + self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc) + + def run_once( + self, + callback: Callable[['CallbackContext'], None], + when: Union[float, datetime.timedelta, datetime.datetime, datetime.time], + context: object = None, + name: str = None, + job_kwargs: JSONDict = None, + ) -> 'Job': """Creates a new ``Job`` that runs once and adds it to the queue. Args: callback (:obj:`callable`): The callback function that should be executed by the new - job. It should take ``bot, job`` as parameters, where ``job`` is the - :class:`telegram.ext.Job` instance. It can be used to access it's - ``job.context`` or change it to a repeating job. + job. Callback signature for context based API: + + ``def callback(CallbackContext)`` + + ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access + its ``job.context`` or change it to a repeating job. when (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ :obj:`datetime.datetime` | :obj:`datetime.time`): Time in or at which the job should run. This parameter will be interpreted @@ -103,33 +167,71 @@ def run_once(self, callback, when, context=None, name=None): * :obj:`datetime.timedelta` will be interpreted as "time from now" in which the job should run. * :obj:`datetime.datetime` will be interpreted as a specific date and time at - which the job should run. + which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`, + the default timezone of the bot will be used. * :obj:`datetime.time` will be interpreted as a specific time of day at which the job should run. This could be either today or, if the time has already passed, - tomorrow. + tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the + default timezone of the bot will be used. context (:obj:`object`, optional): Additional data needed for the callback function. - Can be accessed through ``job.context`` in the callback. Defaults to ``None``. + Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. + job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the + ``scheduler.add_job()``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ - job = Job(callback, repeat=False, context=context, name=name, job_queue=self) - self._put(job, next_t=when) + if not job_kwargs: + job_kwargs = {} + + name = name or callback.__name__ + job = Job(callback, context, name, self) + date_time = self._parse_time_input(when, shift_day=True) + + j = self.scheduler.add_job( + callback, + name=name, + trigger='date', + run_date=date_time, + args=self._build_args(job), + timezone=date_time.tzinfo or self.scheduler.timezone, + **job_kwargs, + ) + + job.job = j return job - def run_repeating(self, callback, interval, first=None, context=None, name=None): - """Creates a new ``Job`` that runs once and adds it to the queue. + def run_repeating( + self, + callback: Callable[['CallbackContext'], None], + interval: Union[float, datetime.timedelta], + first: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None, + last: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None, + context: object = None, + name: str = None, + job_kwargs: JSONDict = None, + ) -> 'Job': + """Creates a new ``Job`` that runs at specified intervals and adds it to the queue. + + Note: + For a note about DST, please see the documentation of `APScheduler`_. + + .. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html + #daylight-saving-time-behavior Args: callback (:obj:`callable`): The callback function that should be executed by the new - job. It should take ``bot, job`` as parameters, where ``job`` is the - :class:`telegram.ext.Job` instance. It can be used to access it's - ``Job.context`` or change it to a repeating job. + job. Callback signature for context based API: + + ``def callback(CallbackContext)`` + + ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access + its ``job.context`` or change it to a repeating job. interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted as seconds. @@ -143,331 +245,415 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None) * :obj:`datetime.timedelta` will be interpreted as "time from now" in which the job should run. * :obj:`datetime.datetime` will be interpreted as a specific date and time at - which the job should run. + which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`, + the default timezone of the bot will be used. * :obj:`datetime.time` will be interpreted as a specific time of day at which the job should run. This could be either today or, if the time has already passed, - tomorrow. + tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the + default timezone of the bot will be used. Defaults to ``interval`` + last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ + :obj:`datetime.datetime` | :obj:`datetime.time`, optional): + Latest possible time for the job to run. This parameter will be interpreted + depending on its type. See ``first`` for details. + + If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type + and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be + assumed. + + Defaults to :obj:`None`. context (:obj:`object`, optional): Additional data needed for the callback function. - Can be accessed through ``job.context`` in the callback. Defaults to ``None``. + Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. + job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the + ``scheduler.add_job()``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ - job = Job(callback, - interval=interval, - repeat=True, - context=context, - name=name, - job_queue=self) - self._put(job, next_t=first) + if not job_kwargs: + job_kwargs = {} + + name = name or callback.__name__ + job = Job(callback, context, name, self) + + dt_first = self._parse_time_input(first) + dt_last = self._parse_time_input(last) + + if dt_last and dt_first and dt_last < dt_first: + raise ValueError("'last' must not be before 'first'!") + + if isinstance(interval, datetime.timedelta): + interval = interval.total_seconds() + + j = self.scheduler.add_job( + callback, + trigger='interval', + args=self._build_args(job), + start_date=dt_first, + end_date=dt_last, + seconds=interval, + name=name, + **job_kwargs, + ) + + job.job = j return job - def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None): - """Creates a new ``Job`` that runs once and adds it to the queue. + def run_monthly( + self, + callback: Callable[['CallbackContext'], None], + when: datetime.time, + day: int, + context: object = None, + name: str = None, + day_is_strict: bool = True, + job_kwargs: JSONDict = None, + ) -> 'Job': + """Creates a new ``Job`` that runs on a monthly basis and adds it to the queue. Args: - callback (:obj:`callable`): The callback function that should be executed by the new - job. It should take ``bot, job`` as parameters, where ``job`` is the - :class:`telegram.ext.Job` instance. It can be used to access it's ``Job.context`` - or change it to a repeating job. - time (:obj:`datetime.time`): Time of day at which the job should run. - days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should - run. Defaults to ``EVERY_DAY`` + callback (:obj:`callable`): The callback function that should be executed by the new + job. Callback signature for context based API: + + ``def callback(CallbackContext)`` + + ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access + its ``job.context`` or change it to a repeating job. + when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone + (``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used. + day (:obj:`int`): Defines the day of the month whereby the job would run. It should + be within the range of 1 and 31, inclusive. context (:obj:`object`, optional): Additional data needed for the callback function. - Can be accessed through ``job.context`` in the callback. Defaults to ``None``. + Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. + day_is_strict (:obj:`bool`, optional): If :obj:`False` and day > month.days, will pick + the last day in the month. Defaults to :obj:`True`. + job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the + ``scheduler.add_job()``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ - job = Job(callback, - interval=datetime.timedelta(days=1), - repeat=True, - days=days, - context=context, - name=name, - job_queue=self) - self._put(job, next_t=time) + if not job_kwargs: + job_kwargs = {} + + name = name or callback.__name__ + job = Job(callback, context, name, self) + + if day_is_strict: + j = self.scheduler.add_job( + callback, + trigger='cron', + args=self._build_args(job), + name=name, + day=day, + hour=when.hour, + minute=when.minute, + second=when.second, + timezone=when.tzinfo or self.scheduler.timezone, + **job_kwargs, + ) + else: + trigger = OrTrigger( + [ + CronTrigger( + day=day, + hour=when.hour, + minute=when.minute, + second=when.second, + timezone=when.tzinfo, + **job_kwargs, + ), + CronTrigger( + day='last', + hour=when.hour, + minute=when.minute, + second=when.second, + timezone=when.tzinfo or self.scheduler.timezone, + **job_kwargs, + ), + ] + ) + j = self.scheduler.add_job( + callback, trigger=trigger, args=self._build_args(job), name=name, **job_kwargs + ) + + job.job = j return job - def _set_next_peek(self, t): - # """ - # Set next peek if not defined or `t` is before next peek. - # In case the next peek was set, also trigger the `self.__tick` event. - # """ - with self.__next_peek_lock: - if not self._next_peek or self._next_peek > t: - self._next_peek = t - self.__tick.set() + def run_daily( + self, + callback: Callable[['CallbackContext'], None], + time: datetime.time, + days: Tuple[int, ...] = tuple(range(7)), + context: object = None, + name: str = None, + job_kwargs: JSONDict = None, + ) -> 'Job': + """Creates a new ``Job`` that runs on a daily basis and adds it to the queue. - def tick(self): - """Run all jobs that are due and re-enqueue them with their interval.""" - now = time.time() + Note: + For a note about DST, please see the documentation of `APScheduler`_. - self.logger.debug('Ticking jobs with t=%f', now) + .. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html + #daylight-saving-time-behavior - while True: - try: - t, job = self._queue.get(False) - except Empty: - break - - self.logger.debug('Peeked at %s with t=%f', job.name, t) - - if t > now: - # We can get here in two conditions: - # 1. At the second or later pass of the while loop, after we've already - # processed the job(s) we were supposed to at this time. - # 2. At the first iteration of the loop only if `self.put()` had triggered - # `self.__tick` because `self._next_peek` wasn't set - self.logger.debug("Next task isn't due yet. Finished!") - self._queue.put((t, job)) - self._set_next_peek(t) - break - - if job.removed: - self.logger.debug('Removing job %s', job.name) - continue - - if job.enabled: - try: - current_week_day = datetime.datetime.now().weekday() - if any(day == current_week_day for day in job.days): - self.logger.debug('Running job %s', job.name) - job.run(self.bot) - - except Exception: - self.logger.exception('An uncaught error was raised while executing job %s', - job.name) - else: - self.logger.debug('Skipping disabled job %s', job.name) + Args: + callback (:obj:`callable`): The callback function that should be executed by the new + job. Callback signature for context based API: - if job.repeat and not job.removed: - self._put(job, last_t=t) - else: - self.logger.debug('Dropping non-repeating or removed job %s', job.name) + ``def callback(CallbackContext)`` - def start(self): - """Starts the job_queue thread.""" - self.__start_lock.acquire() - - if not self._running: - self._running = True - self.__start_lock.release() - self.__thread = Thread(target=self._main_loop, name="job_queue") - self.__thread.start() - self.logger.debug('%s thread started', self.__class__.__name__) - else: - self.__start_lock.release() + ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access + its ``job.context`` or change it to a repeating job. + time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone + (``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used. + days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should + run (where ``0-6`` correspond to monday - sunday). Defaults to ``EVERY_DAY`` + context (:obj:`object`, optional): Additional data needed for the callback function. + Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. + name (:obj:`str`, optional): The name of the new job. Defaults to + ``callback.__name__``. + job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the + ``scheduler.add_job()``. - def _main_loop(self): - """ - Thread target of thread ``job_queue``. Runs in background and performs ticks on the job - queue. + Returns: + :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job + queue. """ - while self._running: - # self._next_peek may be (re)scheduled during self.tick() or self.put() - with self.__next_peek_lock: - tmout = self._next_peek - time.time() if self._next_peek else None - self._next_peek = None - self.__tick.clear() + if not job_kwargs: + job_kwargs = {} + + name = name or callback.__name__ + job = Job(callback, context, name, self) + + j = self.scheduler.add_job( + callback, + name=name, + args=self._build_args(job), + trigger='cron', + day_of_week=','.join([str(d) for d in days]), + hour=time.hour, + minute=time.minute, + second=time.second, + timezone=time.tzinfo or self.scheduler.timezone, + **job_kwargs, + ) + + job.job = j + return job - self.__tick.wait(tmout) + def run_custom( + self, + callback: Callable[['CallbackContext'], None], + job_kwargs: JSONDict, + context: object = None, + name: str = None, + ) -> 'Job': + """Creates a new customly defined ``Job``. - # If we were woken up by self.stop(), just bail out - if not self._running: - break + Args: + callback (:obj:`callable`): The callback function that should be executed by the new + job. Callback signature for context based API: - self.tick() + ``def callback(CallbackContext)`` - self.logger.debug('%s thread stopped', self.__class__.__name__) + ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access + its ``job.context`` or change it to a repeating job. + job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for + ``scheduler.add_job``. + context (:obj:`object`, optional): Additional data needed for the callback function. + Can be accessed through ``job.context`` in the callback. Defaults to ``None``. + name (:obj:`str`, optional): The name of the new job. Defaults to + ``callback.__name__``. - def stop(self): - """Stops the thread.""" - with self.__start_lock: - self._running = False + Returns: + :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job + queue. - self.__tick.set() - if self.__thread is not None: - self.__thread.join() + """ + name = name or callback.__name__ + job = Job(callback, context, name, self) - def jobs(self): - """Returns a tuple of all jobs that are currently in the ``JobQueue``.""" - with self._queue.mutex: - return tuple(job[1] for job in self._queue.queue if job) + j = self.scheduler.add_job(callback, args=self._build_args(job), name=name, **job_kwargs) - def get_jobs_by_name(self, name): - """Returns a tuple of jobs with the given name that are currently in the ``JobQueue``""" - with self._queue.mutex: - return tuple(job[1] for job in self._queue.queue if job and job[1].name == name) + job.job = j + return job + def start(self) -> None: + """Starts the job_queue thread.""" + if not self.scheduler.running: + self.scheduler.start() -class Job(object): - """This class encapsulates a Job. + def stop(self) -> None: + """Stops the thread.""" + if self.scheduler.running: + self.scheduler.shutdown() + + def jobs(self) -> Tuple['Job', ...]: + """Returns a tuple of all *scheduled* jobs that are currently in the ``JobQueue``.""" + return tuple( + Job._from_aps_job(job, self) # pylint: disable=W0212 + for job in self.scheduler.get_jobs() + ) + + def get_jobs_by_name(self, name: str) -> Tuple['Job', ...]: + """Returns a tuple of all *pending/scheduled* jobs with the given name that are currently + in the ``JobQueue``. + """ + return tuple(job for job in self.jobs() if job.name == name) - Attributes: - callback (:obj:`callable`): The callback function that should be executed by the new job. - context (:obj:`object`): Optional. Additional data needed for the callback function. - name (:obj:`str`): Optional. The name of the new job. + +class Job: + """This class is a convenience wrapper for the jobs held in a :class:`telegram.ext.JobQueue`. + With the current backend APScheduler, :attr:`job` holds a :class:`apscheduler.job.Job` + instance. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Note: + * All attributes and instance methods of :attr:`job` are also directly available as + attributes/methods of the corresponding :class:`telegram.ext.Job` object. + * Two instances of :class:`telegram.ext.Job` are considered equal, if their corresponding + ``job`` attributes have the same ``id``. + * If :attr:`job` isn't passed on initialization, it must be set manually afterwards for + this :class:`telegram.ext.Job` to be useful. Args: callback (:obj:`callable`): The callback function that should be executed by the new job. - It should take ``bot, job`` as parameters, where ``job`` is the - :class:`telegram.ext.Job` instance. It can be used to access it's :attr:`context` - or change it to a repeating job. - interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`, optional): The interval in - which the job will run. If it is an :obj:`int` or a :obj:`float`, it will be - interpreted as seconds. If you don't set this value, you must set :attr:`repeat` to - ``False`` and specify :attr:`next_t` when you put the job into the job queue. - repeat (:obj:`bool`, optional): If this job should be periodically execute its callback - function (``True``) or only once (``False``). Defaults to ``True``. + Callback signature for context based API: + + ``def callback(CallbackContext)`` + + a ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access + its ``job.context`` or change it to a repeating job. context (:obj:`object`, optional): Additional data needed for the callback function. Can be - accessed through ``job.context`` in the callback. Defaults to ``None``. + accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. - days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run. - Defaults to ``Days.EVERY_DAY`` job_queue (:class:`telegram.ext.JobQueue`, optional): The ``JobQueue`` this job belongs to. Only optional for backward compatibility with ``JobQueue.put()``. + job (:class:`apscheduler.job.Job`, optional): The APS Job this job is a wrapper for. + Attributes: + callback (:obj:`callable`): The callback function that should be executed by the new job. + context (:obj:`object`): Optional. Additional data needed for the callback function. + name (:obj:`str`): Optional. The name of the new job. + job_queue (:class:`telegram.ext.JobQueue`): Optional. The ``JobQueue`` this job belongs to. + job (:class:`apscheduler.job.Job`): Optional. The APS Job this job is a wrapper for. """ - def __init__(self, - callback, - interval=None, - repeat=True, - context=None, - days=Days.EVERY_DAY, - name=None, - job_queue=None): + __slots__ = ( + 'callback', + 'context', + 'name', + 'job_queue', + '_removed', + '_enabled', + 'job', + '__dict__', + ) + + def __init__( + self, + callback: Callable[['CallbackContext'], None], + context: object = None, + name: str = None, + job_queue: JobQueue = None, + job: APSJob = None, + ): self.callback = callback self.context = context self.name = name or callback.__name__ + self.job_queue = job_queue - self._repeat = repeat - self._interval = None - self.interval = interval - self.repeat = repeat + self._removed = False + self._enabled = False - self._days = None - self.days = days + self.job = cast(APSJob, job) # skipcq: PTC-W0052 - self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) - self._remove = Event() - self._enabled = Event() - self._enabled.set() - - def run(self, bot): - """Executes the callback function.""" - self.callback(bot, self) - - def schedule_removal(self): + def run(self, dispatcher: 'Dispatcher') -> None: + """Executes the callback function independently of the jobs schedule.""" + try: + if dispatcher.use_context: + self.callback(dispatcher.context_types.context.from_job(self, dispatcher)) + else: + self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg] + except Exception as exc: + try: + dispatcher.dispatch_error(None, exc) + # Errors should not stop the thread. + except Exception: + dispatcher.logger.exception( + 'An error was raised while processing the job and an ' + 'uncaught error was raised while handling the error ' + 'with an error_handler.' + ) + + def schedule_removal(self) -> None: """ Schedules this job for removal from the ``JobQueue``. It will be removed without executing its callback function again. - """ - self._remove.set() + self.job.remove() + self._removed = True @property - def removed(self): + def removed(self) -> bool: """:obj:`bool`: Whether this job is due to be removed.""" - return self._remove.is_set() + return self._removed @property - def enabled(self): + def enabled(self) -> bool: """:obj:`bool`: Whether this job is enabled.""" - return self._enabled.is_set() + return self._enabled @enabled.setter - def enabled(self, status): + def enabled(self, status: bool) -> None: if status: - self._enabled.set() + self.job.resume() else: - self._enabled.clear() + self.job.pause() + self._enabled = status @property - def interval(self): + def next_t(self) -> Optional[datetime.datetime]: """ - :obj:`int` | :obj:`float` | :obj:`datetime.timedelta`: Optional. The interval in which the - job will run. - + :obj:`datetime.datetime`: Datetime for the next job execution. + Datetime is localized according to :attr:`tzinfo`. + If job is removed or already ran it equals to :obj:`None`. """ - return self._interval - - @interval.setter - def interval(self, interval): - if interval is None and self.repeat: - raise ValueError("The 'interval' can not be 'None' when 'repeat' is set to 'True'") - - if not (interval is None or isinstance(interval, (Number, datetime.timedelta))): - raise ValueError("The 'interval' must be of type 'datetime.timedelta'," - " 'int' or 'float'") - - self._interval = interval + return self.job.next_run_time - @property - def interval_seconds(self): - """:obj:`int`: The interval for this job in seconds.""" - interval = self.interval - if isinstance(interval, datetime.timedelta): - return interval.total_seconds() + @classmethod + def _from_aps_job(cls, job: APSJob, job_queue: JobQueue) -> 'Job': + # context based callbacks + if len(job.args) == 1: + context = job.args[0].job.context else: - return interval - - @property - def repeat(self): - """:obj:`bool`: Optional. If this job should periodically execute its callback function.""" - return self._repeat - - @repeat.setter - def repeat(self, repeat): - if self.interval is None and repeat: - raise ValueError("'repeat' can not be set to 'True' when no 'interval' is set") - self._repeat = repeat - - @property - def days(self): - """Tuple[:obj:`int`]: Optional. Defines on which days of the week the job should run.""" - return self._days + context = job.args[1].context + return cls(job.func, context=context, name=job.name, job_queue=job_queue, job=job) - @days.setter - def days(self, days): - if not isinstance(days, tuple): - raise ValueError("The 'days' argument should be of type 'tuple'") + def __getattr__(self, item: str) -> object: + return getattr(self.job, item) - if not all(isinstance(day, int) for day in days): - raise ValueError("The elements of the 'days' argument should be of type 'int'") - - if not all(0 <= day <= 6 for day in days): - raise ValueError("The elements of the 'days' argument should be from 0 up to and " - "including 6") - - self._days = days - - @property - def job_queue(self): - """:class:`telegram.ext.JobQueue`: Optional. The ``JobQueue`` this job belongs to.""" - return self._job_queue - - @job_queue.setter - def job_queue(self, job_queue): - # Property setter for backward compatibility with JobQueue.put() - if not self._job_queue: - self._job_queue = weakref.proxy(job_queue) - else: - raise RuntimeError("The 'job_queue' attribute can only be set once.") + def __lt__(self, other: object) -> bool: + return False - def __lt__(self, other): + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.id == other.id return False diff --git a/telegramer/include/telegram/ext/messagehandler.py b/telegramer/include/telegram/ext/messagehandler.py index dadbee5..57faa3f 100644 --- a/telegramer/include/telegram/ext/messagehandler.py +++ b/telegramer/include/telegram/ext/messagehandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,34 +19,24 @@ # TODO: Remove allow_edited """This module contains the MessageHandler class.""" import warnings +from typing import TYPE_CHECKING, Callable, Dict, Optional, TypeVar, Union from telegram import Update +from telegram.ext import BaseFilter, Filters +from telegram.utils.deprecate import TelegramDeprecationWarning +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE + from .handler import Handler +from .utils.types import CCT +if TYPE_CHECKING: + from telegram.ext import Dispatcher -class MessageHandler(Handler): - """Handler class to handle telegram messages. They might contain text, media or status updates. +RT = TypeVar('RT') - Attributes: - filters (:obj:`Filter`): Only allow updates with these Filters. See - :mod:`telegram.ext.filters` for a full list of all available filters. - callback (:obj:`callable`): The callback function for this handler. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. - message_updates (:obj:`bool`): Optional. Should "normal" message updates be handled? - Default is ``True``. - channel_post_updates (:obj:`bool`): Optional. Should channel posts updates be handled? - Default is ``True``. - edited_updates (:obj:`bool`): Optional. Should "edited" message updates be handled? - Default is ``False``. - allow_edited (:obj:`bool`): Optional. If the handler should also accept edited messages. - Default is ``False`` - Deprecated. use edited_updates instead. + +class MessageHandler(Handler[Update, CCT]): + """Handler class to handle telegram messages. They might contain text, media or status updates. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you @@ -54,116 +44,165 @@ class MessageHandler(Handler): either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + Args: filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise - operators (& for and, | for or, ~ for not). - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + operators (& for and, | for or, ~ for not). Default is + :attr:`telegram.ext.filters.Filters.update`. This defaults to all message_type updates + being: ``message``, ``edited_message``, ``channel_post`` and ``edited_channel_post``. + If you don't want or need any of those pass ``~Filters.update.*`` in the filter + argument. + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. message_updates (:obj:`bool`, optional): Should "normal" message updates be handled? - Default is ``True``. + Default is :obj:`None`. + DEPRECATED: Please switch to filters for update filtering. channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled? - Default is ``True``. + Default is :obj:`None`. + DEPRECATED: Please switch to filters for update filtering. edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default - is ``False``. - allow_edited (:obj:`bool`, optional): If the handler should also accept edited messages. - Default is ``False`` - Deprecated. use edited_updates instead. + is :obj:`None`. + DEPRECATED: Please switch to filters for update filtering. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. Raises: ValueError - """ + Attributes: + filters (:obj:`Filter`): Only allow updates with these Filters. See + :mod:`telegram.ext.filters` for a full list of all available filters. + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + message_updates (:obj:`bool`): Should "normal" message updates be handled? + Default is :obj:`None`. + channel_post_updates (:obj:`bool`): Should channel posts updates be handled? + Default is :obj:`None`. + edited_updates (:obj:`bool`): Should "edited" message updates be handled? + Default is :obj:`None`. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. - def __init__(self, - filters, - callback, - allow_edited=False, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False, - message_updates=True, - channel_post_updates=True, - edited_updates=False): - if not message_updates and not channel_post_updates and not edited_updates: - raise ValueError( - 'message_updates, channel_post_updates and edited_updates are all False') - if allow_edited: - warnings.warn('allow_edited is getting deprecated, please use edited_updates instead') - edited_updates = allow_edited + """ - super(MessageHandler, self).__init__( + __slots__ = ('filters',) + + def __init__( + self, + filters: BaseFilter, + callback: Callable[[Update, CCT], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + message_updates: bool = None, + channel_post_updates: bool = None, + edited_updates: bool = None, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + + super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) - self.filters = filters - self.message_updates = message_updates - self.channel_post_updates = channel_post_updates - self.edited_updates = edited_updates - - # We put this up here instead of with the rest of checking code - # in check_update since we don't wanna spam a ton - if isinstance(self.filters, list): - warnings.warn('Using a list of filters in MessageHandler is getting ' - 'deprecated, please use bitwise operators (& and |) ' - 'instead. More info: https://git.io/vPTbc.') - - def _is_allowed_update(self, update): - return any([self.message_updates and update.message, - self.edited_updates and (update.edited_message or update.edited_channel_post), - self.channel_post_updates and update.channel_post]) - - def check_update(self, update): + pass_chat_data=pass_chat_data, + run_async=run_async, + ) + if message_updates is False and channel_post_updates is False and edited_updates is False: + raise ValueError( + 'message_updates, channel_post_updates and edited_updates are all False' + ) + if filters is not None: + self.filters = Filters.update & filters + else: + self.filters = Filters.update + if message_updates is not None: + warnings.warn( + 'message_updates is deprecated. See https://git.io/fxJuV for more info', + TelegramDeprecationWarning, + stacklevel=2, + ) + if message_updates is False: + self.filters &= ~Filters.update.message + + if channel_post_updates is not None: + warnings.warn( + 'channel_post_updates is deprecated. See https://git.io/fxJuV ' 'for more info', + TelegramDeprecationWarning, + stacklevel=2, + ) + if channel_post_updates is False: + self.filters &= ~Filters.update.channel_post + + if edited_updates is not None: + warnings.warn( + 'edited_updates is deprecated. See https://git.io/fxJuV for more info', + TelegramDeprecationWarning, + stacklevel=2, + ) + if edited_updates is False: + self.filters &= ~( + Filters.update.edited_message | Filters.update.edited_channel_post + ) + + def check_update(self, update: object) -> Optional[Union[bool, Dict[str, list]]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ - if isinstance(update, Update) and self._is_allowed_update(update): - - if not self.filters: - res = True - - else: - message = update.effective_message - if isinstance(self.filters, list): - res = any(func(message) for func in self.filters) - else: - res = self.filters(message) - - else: - res = False - - return res - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. - - """ - optional_args = self.collect_optional_args(dispatcher, update) - - return self.callback(dispatcher.bot, update, **optional_args) + if isinstance(update, Update) and update.effective_message: + return self.filters(update) + return None + + def collect_additional_context( + self, + context: CCT, + update: Update, + dispatcher: 'Dispatcher', + check_result: Optional[Union[bool, Dict[str, object]]], + ) -> None: + """Adds possible output of data filters to the :class:`CallbackContext`.""" + if isinstance(check_result, dict): + context.update(check_result) diff --git a/telegramer/include/telegram/ext/messagequeue.py b/telegramer/include/telegram/ext/messagequeue.py index 946a8de..da2a734 100644 --- a/telegramer/include/telegram/ext/messagequeue.py +++ b/telegramer/include/telegram/ext/messagequeue.py @@ -4,7 +4,7 @@ # Tymofii A. Khodniev (thodnev) # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -20,32 +20,27 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/] """A throughput-limiting message processor for Telegram bots.""" -from telegram.utils import promise - import functools -import sys -import time +import queue as q import threading -if sys.version_info.major > 2: - import queue as q -else: - import Queue as q +import time +import warnings +from typing import TYPE_CHECKING, Callable, List, NoReturn + +from telegram.ext.utils.promise import Promise +from telegram.utils.deprecate import TelegramDeprecationWarning + +if TYPE_CHECKING: + from telegram import Bot # We need to count < 1s intervals, so the most accurate timer is needed -# Starting from Python 3.3 we have time.perf_counter which is the clock -# with the highest resolution available to the system, so let's use it there. -# In Python 2.7, there's no perf_counter yet, so fallback on what we have: -# on Windows, the best available is time.clock while time.time is on -# another platforms (M. Lutz, "Learning Python," 4ed, p.630-634) -if sys.version_info.major == 3 and sys.version_info.minor >= 3: - curtime = time.perf_counter # pylint: disable=E1101 -else: - curtime = time.clock if sys.platform[:3] == 'win' else time.time +curtime = time.perf_counter class DelayQueueError(RuntimeError): """Indicates processing errors.""" - pass + + __slots__ = () class DelayQueue(threading.Thread): @@ -53,13 +48,9 @@ class DelayQueue(threading.Thread): Processes callbacks from queue with specified throughput limits. Creates a separate thread to process callbacks with delays. - Attributes: - burst_limit (:obj:`int`): Number of maximum callbacks to process per time-window. - time_limit (:obj:`int`): Defines width of time-window used when each processing limit is - calculated. - exc_route (:obj:`callable`): A callable, accepting 1 positional argument; used to route - exceptions from processor thread to main thread; - name (:obj:`str`): Thread's name. + .. deprecated:: 13.3 + :class:`telegram.ext.DelayQueue` in its current form is deprecated and will be reinvented + in a future release. See `this thread `_ for a list of known bugs. Args: queue (:obj:`Queue`, optional): Used to pass callbacks to thread. Creates ``Queue`` @@ -72,49 +63,65 @@ class DelayQueue(threading.Thread): route exceptions from processor thread to main thread; is called on `Exception` subclass exceptions. If not provided, exceptions are routed through dummy handler, which re-raises them. - autostart (:obj:`bool`, optional): If True, processor is started immediately after object's - creation; if ``False``, should be started manually by `start` method. Defaults to True. + autostart (:obj:`bool`, optional): If :obj:`True`, processor is started immediately after + object's creation; if :obj:`False`, should be started manually by `start` method. + Defaults to :obj:`True`. name (:obj:`str`, optional): Thread's name. Defaults to ``'DelayQueue-N'``, where N is sequential number of object created. + Attributes: + burst_limit (:obj:`int`): Number of maximum callbacks to process per time-window. + time_limit (:obj:`int`): Defines width of time-window used when each processing limit is + calculated. + exc_route (:obj:`callable`): A callable, accepting 1 positional argument; used to route + exceptions from processor thread to main thread; + name (:obj:`str`): Thread's name. + """ _instcnt = 0 # instance counter - def __init__(self, - queue=None, - burst_limit=30, - time_limit_ms=1000, - exc_route=None, - autostart=True, - name=None): + def __init__( + self, + queue: q.Queue = None, + burst_limit: int = 30, + time_limit_ms: int = 1000, + exc_route: Callable[[Exception], None] = None, + autostart: bool = True, + name: str = None, + ): + warnings.warn( + 'DelayQueue in its current form is deprecated and will be reinvented in a future ' + 'release. See https://git.io/JtDbF for a list of known bugs.', + category=TelegramDeprecationWarning, + ) + self._queue = queue if queue is not None else q.Queue() self.burst_limit = burst_limit self.time_limit = time_limit_ms / 1000 - self.exc_route = (exc_route if exc_route is not None else self._default_exception_handler) + self.exc_route = exc_route if exc_route is not None else self._default_exception_handler self.__exit_req = False # flag to gently exit thread self.__class__._instcnt += 1 if name is None: - name = '%s-%s' % (self.__class__.__name__, self.__class__._instcnt) - super(DelayQueue, self).__init__(name=name) + name = f'{self.__class__.__name__}-{self.__class__._instcnt}' + super().__init__(name=name) self.daemon = False if autostart: # immediately start processing - super(DelayQueue, self).start() + super().start() - def run(self): + def run(self) -> None: """ Do not use the method except for unthreaded testing purposes, the method normally is automatically called by autostart argument. """ - - times = [] # used to store each callable processing time + times: List[float] = [] # used to store each callable processing time while True: item = self._queue.get() if self.__exit_req: return # shutdown thread # delay routine - now = curtime() + now = time.perf_counter() t_delta = now - self.time_limit # calculate early to improve perf. if times and t_delta > times[-1]: # if last call was before the limit time-window @@ -133,32 +140,31 @@ def run(self): except Exception as exc: # re-route any exceptions self.exc_route(exc) # to prevent thread exit - def stop(self, timeout=None): + def stop(self, timeout: float = None) -> None: """Used to gently stop processor and shutdown its thread. Args: timeout (:obj:`float`): Indicates maximum time to wait for processor to stop and its thread to exit. If timeout exceeds and processor has not stopped, method silently returns. :attr:`is_alive` could be used afterwards to check the actual status. - ``timeout`` set to None, blocks until processor is shut down. Defaults to None. + ``timeout`` set to :obj:`None`, blocks until processor is shut down. + Defaults to :obj:`None`. """ - self.__exit_req = True # gently request self._queue.put(None) # put something to unfreeze if frozen - super(DelayQueue, self).join(timeout=timeout) + super().join(timeout=timeout) @staticmethod - def _default_exception_handler(exc): + def _default_exception_handler(exc: Exception) -> NoReturn: """ Dummy exception handler which re-raises exception in thread. Could be possibly overwritten by subclasses. """ - raise exc - def __call__(self, func, *args, **kwargs): + def __call__(self, func: Callable, *args: object, **kwargs: object) -> None: """Used to process callbacks in throughput-limiting thread through queue. Args: @@ -168,25 +174,28 @@ def __call__(self, func, *args, **kwargs): **kwargs (:obj:`dict`): Arbitrary keyword-arguments to `func`. """ - if not self.is_alive() or self.__exit_req: raise DelayQueueError('Could not process callback in stopped thread') self._queue.put((func, args, kwargs)) -# The most straightforward way to implement this is to use 2 sequenital delay +# The most straightforward way to implement this is to use 2 sequential delay # queues, like on classic delay chain schematics in electronics. # So, message path is: # msg --> group delay if group msg, else no delay --> normal msg delay --> out # This way OS threading scheduler cares of timings accuracy. # (see time.time, time.clock, time.perf_counter, time.sleep @ docs.python.org) -class MessageQueue(object): +class MessageQueue: """ Implements callback processing with proper delays to avoid hitting Telegram's message limits. Contains two ``DelayQueue``, for group and for all messages, interconnected in delay chain. Callables are processed through *group* ``DelayQueue``, then through *all* ``DelayQueue`` for group-type messages. For non-group messages, only the *all* ``DelayQueue`` is used. + .. deprecated:: 13.3 + :class:`telegram.ext.MessageQueue` in its current form is deprecated and will be reinvented + in a future release. See `this thread `_ for a list of known bugs. + Args: all_burst_limit (:obj:`int`, optional): Number of maximum *all-type* callbacks to process per time-window defined by :attr:`all_time_limit_ms`. Defaults to 30. @@ -200,56 +209,67 @@ class MessageQueue(object): to route exceptions from processor threads to main thread; is called on ``Exception`` subclass exceptions. If not provided, exceptions are routed through dummy handler, which re-raises them. - autostart (:obj:`bool`, optional): If True, processors are started immediately after - object's creation; if ``False``, should be started manually by :attr:`start` method. - Defaults to ``True``. + autostart (:obj:`bool`, optional): If :obj:`True`, processors are started immediately after + object's creation; if :obj:`False`, should be started manually by :attr:`start` method. + Defaults to :obj:`True`. """ - def __init__(self, - all_burst_limit=30, - all_time_limit_ms=1000, - group_burst_limit=20, - group_time_limit_ms=60000, - exc_route=None, - autostart=True): - # create accoring delay queues, use composition + def __init__( + self, + all_burst_limit: int = 30, + all_time_limit_ms: int = 1000, + group_burst_limit: int = 20, + group_time_limit_ms: int = 60000, + exc_route: Callable[[Exception], None] = None, + autostart: bool = True, + ): + warnings.warn( + 'MessageQueue in its current form is deprecated and will be reinvented in a future ' + 'release. See https://git.io/JtDbF for a list of known bugs.', + category=TelegramDeprecationWarning, + ) + + # create according delay queues, use composition self._all_delayq = DelayQueue( burst_limit=all_burst_limit, time_limit_ms=all_time_limit_ms, exc_route=exc_route, - autostart=autostart) + autostart=autostart, + ) self._group_delayq = DelayQueue( burst_limit=group_burst_limit, time_limit_ms=group_time_limit_ms, exc_route=exc_route, - autostart=autostart) + autostart=autostart, + ) - def start(self): + def start(self) -> None: """Method is used to manually start the ``MessageQueue`` processing.""" self._all_delayq.start() self._group_delayq.start() - def stop(self, timeout=None): + def stop(self, timeout: float = None) -> None: + """Stops the ``MessageQueue``.""" self._group_delayq.stop(timeout=timeout) self._all_delayq.stop(timeout=timeout) - stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docsting if any + stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docstring if any - def __call__(self, promise, is_group_msg=False): + def __call__(self, promise: Callable, is_group_msg: bool = False) -> Callable: """ - Processes callables in troughput-limiting queues to avoid hitting limits (specified with + Processes callables in throughput-limiting queues to avoid hitting limits (specified with :attr:`burst_limit` and :attr:`time_limit`. Args: promise (:obj:`callable`): Mainly the ``telegram.utils.promise.Promise`` (see Notes for other callables), that is processed in delay queues. is_group_msg (:obj:`bool`, optional): Defines whether ``promise`` would be processed in - group*+*all* ``DelayQueue``s (if set to ``True``), or only through *all* - ``DelayQueue`` (if set to ``False``), resulting in needed delays to avoid - hitting specified limits. Defaults to ``True``. + group*+*all* ``DelayQueue``s (if set to :obj:`True`), or only through *all* + ``DelayQueue`` (if set to :obj:`False`), resulting in needed delays to avoid + hitting specified limits. Defaults to :obj:`False`. - Notes: + Note: Method is designed to accept ``telegram.utils.promise.Promise`` as ``promise`` argument, but other callables could be used too. For example, lambdas or simple functions could be used to wrap original func to be called with needed args. In that @@ -260,7 +280,6 @@ def __call__(self, promise, is_group_msg=False): :obj:`callable`: Used as ``promise`` argument. """ - if not is_group_msg: # ignore middle group delay self._all_delayq(promise) else: # use middle group delay @@ -268,7 +287,7 @@ def __call__(self, promise, is_group_msg=False): return promise -def queuedmessage(method): +def queuedmessage(method: Callable) -> Callable: """A decorator to be used with :attr:`telegram.Bot` send* methods. Note: @@ -287,12 +306,12 @@ def queuedmessage(method): Wrapped method starts accepting the next kwargs: Args: - queued (:obj:`bool`, optional): If set to ``True``, the ``MessageQueue`` is used to process - output messages. Defaults to `self._is_queued_out`. - isgroup (:obj:`bool`, optional): If set to ``True``, the message is meant to be group-type - (as there's no obvious way to determine its type in other way at the moment). + queued (:obj:`bool`, optional): If set to :obj:`True`, the ``MessageQueue`` is used to + process output messages. Defaults to `self._is_queued_out`. + isgroup (:obj:`bool`, optional): If set to :obj:`True`, the message is meant to be + group-type(as there's no obvious way to determine its type in other way at the moment). Group-type messages could have additional processing delay according to limits set - in `self._out_queue`. Defaults to ``False``. + in `self._out_queue`. Defaults to :obj:`False`. Returns: ``telegram.utils.promise.Promise``: In case call is queued or original method's return @@ -301,12 +320,15 @@ def queuedmessage(method): """ @functools.wraps(method) - def wrapped(self, *args, **kwargs): - queued = kwargs.pop('queued', self._is_messages_queued_default) + def wrapped(self: 'Bot', *args: object, **kwargs: object) -> object: + # pylint: disable=W0212 + queued = kwargs.pop( + 'queued', self._is_messages_queued_default # type: ignore[attr-defined] + ) isgroup = kwargs.pop('isgroup', False) if queued: - prom = promise.Promise(method, (self, ) + args, kwargs) - return self._msg_queue(prom, isgroup) + prom = Promise(method, (self,) + args, kwargs) + return self._msg_queue(prom, isgroup) # type: ignore[attr-defined] return method(self, *args, **kwargs) return wrapped diff --git a/telegramer/include/telegram/ext/picklepersistence.py b/telegramer/include/telegram/ext/picklepersistence.py new file mode 100644 index 0000000..c3e4ba8 --- /dev/null +++ b/telegramer/include/telegram/ext/picklepersistence.py @@ -0,0 +1,463 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the PicklePersistence class.""" +import pickle +from collections import defaultdict +from typing import ( + Any, + Dict, + Optional, + Tuple, + overload, + cast, + DefaultDict, +) + +from telegram.ext import BasePersistence +from .utils.types import UD, CD, BD, ConversationDict, CDCData +from .contexttypes import ContextTypes + + +class PicklePersistence(BasePersistence[UD, CD, BD]): + """Using python's builtin pickle for making your bot persistent. + + Warning: + :class:`PicklePersistence` will try to replace :class:`telegram.Bot` instances by + :attr:`REPLACED_BOT` and insert the bot set with + :meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure + that changes to the bot apply to the saved objects, too. If you change the bots token, this + may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see + :meth:`telegram.ext.BasePersistence.replace_bot` and + :meth:`telegram.ext.BasePersistence.insert_bot`. + + Args: + filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` + is :obj:`False` this will be used as a prefix. + store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this + persistence class. Default is :obj:`True`. + store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this + persistence class. Default is :obj:`True`. + store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this + persistence class. Default is :obj:`True`. + store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this + persistence class. Default is :obj:`False`. + + .. versionadded:: 13.6 + single_file (:obj:`bool`, optional): When :obj:`False` will store 5 separate files of + `filename_user_data`, `filename_bot_data`, `filename_chat_data`, + `filename_callback_data` and `filename_conversations`. Default is :obj:`True`. + on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when + :meth:`flush` is called and keep data in memory until that happens. When + :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. + Default is :obj:`False`. + context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance + of :class:`telegram.ext.ContextTypes` to customize the types used in the + ``context`` interface. If not passed, the defaults documented in + :class:`telegram.ext.ContextTypes` will be used. + + .. versionadded:: 13.6 + + Attributes: + filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` + is :obj:`False` this will be used as a prefix. + store_user_data (:obj:`bool`): Optional. Whether user_data should be saved by this + persistence class. + store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this + persistence class. + store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this + persistence class. + store_callback_data (:obj:`bool`): Optional. Whether callback_data be saved by this + persistence class. + + .. versionadded:: 13.6 + single_file (:obj:`bool`): Optional. When :obj:`False` will store 5 separate files of + `filename_user_data`, `filename_bot_data`, `filename_chat_data`, + `filename_callback_data` and `filename_conversations`. Default is :obj:`True`. + on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when + :meth:`flush` is called and keep data in memory until that happens. When + :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. + Default is :obj:`False`. + context_types (:class:`telegram.ext.ContextTypes`): Container for the types used + in the ``context`` interface. + + .. versionadded:: 13.6 + """ + + __slots__ = ( + 'filename', + 'single_file', + 'on_flush', + 'user_data', + 'chat_data', + 'bot_data', + 'callback_data', + 'conversations', + 'context_types', + ) + + @overload + def __init__( + self: 'PicklePersistence[Dict, Dict, Dict]', + filename: str, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + single_file: bool = True, + on_flush: bool = False, + store_callback_data: bool = False, + ): + ... + + @overload + def __init__( + self: 'PicklePersistence[UD, CD, BD]', + filename: str, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + single_file: bool = True, + on_flush: bool = False, + store_callback_data: bool = False, + context_types: ContextTypes[Any, UD, CD, BD] = None, + ): + ... + + def __init__( + self, + filename: str, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + single_file: bool = True, + on_flush: bool = False, + store_callback_data: bool = False, + context_types: ContextTypes[Any, UD, CD, BD] = None, + ): + super().__init__( + store_user_data=store_user_data, + store_chat_data=store_chat_data, + store_bot_data=store_bot_data, + store_callback_data=store_callback_data, + ) + self.filename = filename + self.single_file = single_file + self.on_flush = on_flush + self.user_data: Optional[DefaultDict[int, UD]] = None + self.chat_data: Optional[DefaultDict[int, CD]] = None + self.bot_data: Optional[BD] = None + self.callback_data: Optional[CDCData] = None + self.conversations: Optional[Dict[str, Dict[Tuple, object]]] = None + self.context_types = cast(ContextTypes[Any, UD, CD, BD], context_types or ContextTypes()) + + def _load_singlefile(self) -> None: + try: + filename = self.filename + with open(self.filename, "rb") as file: + data = pickle.load(file) + self.user_data = defaultdict(self.context_types.user_data, data['user_data']) + self.chat_data = defaultdict(self.context_types.chat_data, data['chat_data']) + # For backwards compatibility with files not containing bot data + self.bot_data = data.get('bot_data', self.context_types.bot_data()) + self.callback_data = data.get('callback_data', {}) + self.conversations = data['conversations'] + except OSError: + self.conversations = {} + self.user_data = defaultdict(self.context_types.user_data) + self.chat_data = defaultdict(self.context_types.chat_data) + self.bot_data = self.context_types.bot_data() + self.callback_data = None + except pickle.UnpicklingError as exc: + raise TypeError(f"File {filename} does not contain valid pickle data") from exc + except Exception as exc: + raise TypeError(f"Something went wrong unpickling {filename}") from exc + + @staticmethod + def _load_file(filename: str) -> Any: + try: + with open(filename, "rb") as file: + return pickle.load(file) + except OSError: + return None + except pickle.UnpicklingError as exc: + raise TypeError(f"File {filename} does not contain valid pickle data") from exc + except Exception as exc: + raise TypeError(f"Something went wrong unpickling {filename}") from exc + + def _dump_singlefile(self) -> None: + with open(self.filename, "wb") as file: + data = { + 'conversations': self.conversations, + 'user_data': self.user_data, + 'chat_data': self.chat_data, + 'bot_data': self.bot_data, + 'callback_data': self.callback_data, + } + pickle.dump(data, file) + + @staticmethod + def _dump_file(filename: str, data: object) -> None: + with open(filename, "wb") as file: + pickle.dump(data, file) + + def get_user_data(self) -> DefaultDict[int, UD]: + """Returns the user_data from the pickle file if it exists or an empty :obj:`defaultdict`. + + Returns: + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data. + """ + if self.user_data: + pass + elif not self.single_file: + filename = f"{self.filename}_user_data" + data = self._load_file(filename) + if not data: + data = defaultdict(self.context_types.user_data) + else: + data = defaultdict(self.context_types.user_data, data) + self.user_data = data + else: + self._load_singlefile() + return self.user_data # type: ignore[return-value] + + def get_chat_data(self) -> DefaultDict[int, CD]: + """Returns the chat_data from the pickle file if it exists or an empty :obj:`defaultdict`. + + Returns: + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data. + """ + if self.chat_data: + pass + elif not self.single_file: + filename = f"{self.filename}_chat_data" + data = self._load_file(filename) + if not data: + data = defaultdict(self.context_types.chat_data) + else: + data = defaultdict(self.context_types.chat_data, data) + self.chat_data = data + else: + self._load_singlefile() + return self.chat_data # type: ignore[return-value] + + def get_bot_data(self) -> BD: + """Returns the bot_data from the pickle file if it exists or an empty object of type + :class:`telegram.ext.utils.types.BD`. + + Returns: + :class:`telegram.ext.utils.types.BD`: The restored bot data. + """ + if self.bot_data: + pass + elif not self.single_file: + filename = f"{self.filename}_bot_data" + data = self._load_file(filename) + if not data: + data = self.context_types.bot_data() + self.bot_data = data + else: + self._load_singlefile() + return self.bot_data # type: ignore[return-value] + + def get_callback_data(self) -> Optional[CDCData]: + """Returns the callback data from the pickle file if it exists or :obj:`None`. + + .. versionadded:: 13.6 + + Returns: + Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or + :obj:`None`, if no data was stored. + """ + if self.callback_data: + pass + elif not self.single_file: + filename = f"{self.filename}_callback_data" + data = self._load_file(filename) + if not data: + data = None + self.callback_data = data + else: + self._load_singlefile() + if self.callback_data is None: + return None + return self.callback_data[0], self.callback_data[1].copy() + + def get_conversations(self, name: str) -> ConversationDict: + """Returns the conversations from the pickle file if it exists or an empty dict. + + Args: + name (:obj:`str`): The handlers name. + + Returns: + :obj:`dict`: The restored conversations for the handler. + """ + if self.conversations: + pass + elif not self.single_file: + filename = f"{self.filename}_conversations" + data = self._load_file(filename) + if not data: + data = {name: {}} + self.conversations = data + else: + self._load_singlefile() + return self.conversations.get(name, {}).copy() # type: ignore[union-attr] + + def update_conversation( + self, name: str, key: Tuple[int, ...], new_state: Optional[object] + ) -> None: + """Will update the conversations for the given handler and depending on :attr:`on_flush` + save the pickle file. + + Args: + name (:obj:`str`): The handler's name. + key (:obj:`tuple`): The key the state is changed for. + new_state (:obj:`tuple` | :obj:`any`): The new state for the given key. + """ + if not self.conversations: + self.conversations = {} + if self.conversations.setdefault(name, {}).get(key) == new_state: + return + self.conversations[name][key] = new_state + if not self.on_flush: + if not self.single_file: + filename = f"{self.filename}_conversations" + self._dump_file(filename, self.conversations) + else: + self._dump_singlefile() + + def update_user_data(self, user_id: int, data: UD) -> None: + """Will update the user_data and depending on :attr:`on_flush` save the pickle file. + + Args: + user_id (:obj:`int`): The user the data might have been changed for. + data (:class:`telegram.ext.utils.types.UD`): The + :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``. + """ + if self.user_data is None: + self.user_data = defaultdict(self.context_types.user_data) + if self.user_data.get(user_id) == data: + return + self.user_data[user_id] = data + if not self.on_flush: + if not self.single_file: + filename = f"{self.filename}_user_data" + self._dump_file(filename, self.user_data) + else: + self._dump_singlefile() + + def update_chat_data(self, chat_id: int, data: CD) -> None: + """Will update the chat_data and depending on :attr:`on_flush` save the pickle file. + + Args: + chat_id (:obj:`int`): The chat the data might have been changed for. + data (:class:`telegram.ext.utils.types.CD`): The + :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``. + """ + if self.chat_data is None: + self.chat_data = defaultdict(self.context_types.chat_data) + if self.chat_data.get(chat_id) == data: + return + self.chat_data[chat_id] = data + if not self.on_flush: + if not self.single_file: + filename = f"{self.filename}_chat_data" + self._dump_file(filename, self.chat_data) + else: + self._dump_singlefile() + + def update_bot_data(self, data: BD) -> None: + """Will update the bot_data and depending on :attr:`on_flush` save the pickle file. + + Args: + data (:class:`telegram.ext.utils.types.BD`): The + :attr:`telegram.ext.Dispatcher.bot_data`. + """ + if self.bot_data == data: + return + self.bot_data = data + if not self.on_flush: + if not self.single_file: + filename = f"{self.filename}_bot_data" + self._dump_file(filename, self.bot_data) + else: + self._dump_singlefile() + + def update_callback_data(self, data: CDCData) -> None: + """Will update the callback_data (if changed) and depending on :attr:`on_flush` save the + pickle file. + + .. versionadded:: 13.6 + + Args: + data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore + :class:`telegram.ext.CallbackDataCache`. + """ + if self.callback_data == data: + return + self.callback_data = (data[0], data[1].copy()) + if not self.on_flush: + if not self.single_file: + filename = f"{self.filename}_callback_data" + self._dump_file(filename, self.callback_data) + else: + self._dump_singlefile() + + def refresh_user_data(self, user_id: int, user_data: UD) -> None: + """Does nothing. + + .. versionadded:: 13.6 + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` + """ + + def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: + """Does nothing. + + .. versionadded:: 13.6 + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` + """ + + def refresh_bot_data(self, bot_data: BD) -> None: + """Does nothing. + + .. versionadded:: 13.6 + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` + """ + + def flush(self) -> None: + """Will save all data in memory to pickle file(s).""" + if self.single_file: + if ( + self.user_data + or self.chat_data + or self.bot_data + or self.callback_data + or self.conversations + ): + self._dump_singlefile() + else: + if self.user_data: + self._dump_file(f"{self.filename}_user_data", self.user_data) + if self.chat_data: + self._dump_file(f"{self.filename}_chat_data", self.chat_data) + if self.bot_data: + self._dump_file(f"{self.filename}_bot_data", self.bot_data) + if self.callback_data: + self._dump_file(f"{self.filename}_callback_data", self.callback_data) + if self.conversations: + self._dump_file(f"{self.filename}_conversations", self.conversations) diff --git a/telegramer/include/telegram/ext/pollanswerhandler.py b/telegramer/include/telegram/ext/pollanswerhandler.py new file mode 100644 index 0000000..53172b2 --- /dev/null +++ b/telegramer/include/telegram/ext/pollanswerhandler.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the PollAnswerHandler class.""" + + +from telegram import Update + +from .handler import Handler +from .utils.types import CCT + + +class PollAnswerHandler(Handler[Update, CCT]): + """Handler class to handle Telegram updates that contain a poll answer. + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + + Args: + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + + """ + + __slots__ = () + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + return isinstance(update, Update) and bool(update.poll_answer) diff --git a/telegramer/include/telegram/ext/pollhandler.py b/telegramer/include/telegram/ext/pollhandler.py new file mode 100644 index 0000000..0e2dee6 --- /dev/null +++ b/telegramer/include/telegram/ext/pollhandler.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the PollHandler classes.""" + + +from telegram import Update + +from .handler import Handler +from .utils.types import CCT + + +class PollHandler(Handler[Update, CCT]): + """Handler class to handle Telegram updates that contain a poll. + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + + Args: + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + + """ + + __slots__ = () + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + return isinstance(update, Update) and bool(update.poll) diff --git a/telegramer/include/telegram/ext/precheckoutqueryhandler.py b/telegramer/include/telegram/ext/precheckoutqueryhandler.py index e3bbe7c..bbef18e 100644 --- a/telegramer/include/telegram/ext/precheckoutqueryhandler.py +++ b/telegramer/include/telegram/ext/precheckoutqueryhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,81 +18,81 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the PreCheckoutQueryHandler class.""" + from telegram import Update + from .handler import Handler +from .utils.types import CCT -class PreCheckoutQueryHandler(Handler): +class PreCheckoutQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram PreCheckout callback queries. - Attributes: - callback (:obj:`callable`): The callback function for this handler. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. - Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + Args: - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + DEPRECATED: Please switch to context based callbacks. instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ - def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False): - super(PreCheckoutQueryHandler, self).__init__( - callback, - pass_update_queue=pass_update_queue, - pass_job_queue=pass_job_queue, - pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) - - def check_update(self, update): + __slots__ = () + + def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ - return isinstance(update, Update) and update.pre_checkout_query - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. - - """ - optional_args = self.collect_optional_args(dispatcher, update) - return self.callback(dispatcher.bot, update, **optional_args) + return isinstance(update, Update) and bool(update.pre_checkout_query) diff --git a/telegramer/include/telegram/ext/regexhandler.py b/telegramer/include/telegram/ext/regexhandler.py index 71c5821..8211d83 100644 --- a/telegramer/include/telegram/ext/regexhandler.py +++ b/telegramer/include/telegram/ext/regexhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,164 +19,148 @@ # TODO: Remove allow_edited """This module contains the RegexHandler class.""" -import re import warnings +from typing import TYPE_CHECKING, Callable, Dict, Optional, Pattern, TypeVar, Union, Any -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass +from telegram import Update +from telegram.ext import Filters, MessageHandler +from telegram.utils.deprecate import TelegramDeprecationWarning +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from telegram.ext.utils.types import CCT -try: - string_types -except NameError: - string_types = str +if TYPE_CHECKING: + from telegram.ext import Dispatcher -from telegram import Update -from .handler import Handler +RT = TypeVar('RT') -class RegexHandler(Handler): +class RegexHandler(MessageHandler): """Handler class to handle Telegram updates based on a regex. It uses a regular expression to check text messages. Read the documentation of the ``re`` module for more information. The ``re.match`` function is used to determine if an update should be handled by this handler. - Attributes: - pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. - callback (:obj:`callable`): The callback function for this handler. - pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the - callback function. - pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to - the callback function. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. - Note: - :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you - can use to keep any data in will be sent to the :attr:`callback` function. Related to - either the user or the chat that the update was sent in. For each update from the same user - or in the same chat, it will be the same ``dict``. + This handler is being deprecated. For the same use case use: + ``MessageHandler(Filters.regex(r'pattern'), callback)`` + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + Args: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. - Default is ``False`` + Default is :obj:`False` pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. - Default is ``False`` - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + Default is :obj:`False` + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. message_updates (:obj:`bool`, optional): Should "normal" message updates be handled? - Default is ``True``. + Default is :obj:`True`. channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled? - Default is ``True``. + Default is :obj:`True`. edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default - is ``False``. - allow_edited (:obj:`bool`, optional): If the handler should also accept edited messages. - Default is ``False`` - Deprecated. use edited_updates instead. + is :obj:`False`. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. Raises: ValueError + Attributes: + pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. + callback (:obj:`callable`): The callback function for this handler. + pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the + callback function. + pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to + the callback function. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + """ - def __init__(self, - pattern, - callback, - pass_groups=False, - pass_groupdict=False, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False, - allow_edited=False, - message_updates=True, - channel_post_updates=False, - edited_updates=False - ): - if not message_updates and not channel_post_updates and not edited_updates: - raise ValueError( - 'message_updates, channel_post_updates and edited_updates are all False') - if allow_edited: - warnings.warn('allow_edited is getting deprecated, please use edited_updates instead') - edited_updates = allow_edited - - super(RegexHandler, self).__init__( + __slots__ = ('pass_groups', 'pass_groupdict') + + def __init__( + self, + pattern: Union[str, Pattern], + callback: Callable[[Update, CCT], RT], + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + allow_edited: bool = False, # pylint: disable=W0613 + message_updates: bool = True, + channel_post_updates: bool = False, + edited_updates: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + warnings.warn( + 'RegexHandler is deprecated. See https://git.io/fxJuV for more info', + TelegramDeprecationWarning, + stacklevel=2, + ) + super().__init__( + Filters.regex(pattern), callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) - - if isinstance(pattern, string_types): - pattern = re.compile(pattern) - - self.pattern = pattern + pass_chat_data=pass_chat_data, + message_updates=message_updates, + channel_post_updates=channel_post_updates, + edited_updates=edited_updates, + run_async=run_async, + ) self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - self.allow_edited = allow_edited - self.message_updates = message_updates - self.channel_post_updates = channel_post_updates - self.edited_updates = edited_updates - - def check_update(self, update): - """Determines whether an update should be passed to this handlers :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - - Returns: - :obj:`bool` + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: Update = None, + check_result: Optional[Union[bool, Dict[str, Any]]] = None, + ) -> Dict[str, object]: + """Pass the results of ``re.match(pattern, text).{groups(), groupdict()}`` to the + callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if + needed. """ - if not isinstance(update, Update) and not update.effective_message: - return False - if any([self.message_updates and update.message, - self.edited_updates and (update.edited_message or update.edited_channel_post), - self.channel_post_updates and update.channel_post]) and \ - update.effective_message.text: - match = re.match(self.pattern, update.effective_message.text) - return bool(match) - return False - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. - - """ - - optional_args = self.collect_optional_args(dispatcher, update) - match = re.match(self.pattern, update.effective_message.text) - - if self.pass_groups: - optional_args['groups'] = match.groups() - if self.pass_groupdict: - optional_args['groupdict'] = match.groupdict() - - return self.callback(dispatcher.bot, update, **optional_args) + optional_args = super().collect_optional_args(dispatcher, update, check_result) + if isinstance(check_result, dict): + if self.pass_groups: + optional_args['groups'] = check_result['matches'][0].groups() + if self.pass_groupdict: + optional_args['groupdict'] = check_result['matches'][0].groupdict() + return optional_args diff --git a/telegramer/include/telegram/ext/shippingqueryhandler.py b/telegramer/include/telegram/ext/shippingqueryhandler.py index 663a3f7..d8b0218 100644 --- a/telegramer/include/telegram/ext/shippingqueryhandler.py +++ b/telegramer/include/telegram/ext/shippingqueryhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,81 +18,80 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the ShippingQueryHandler class.""" + from telegram import Update from .handler import Handler +from .utils.types import CCT -class ShippingQueryHandler(Handler): +class ShippingQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram shipping callback queries. - Attributes: - callback (:obj:`callable`): The callback function for this handler. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to - the callback function. - pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to - the callback function. - Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + Args: - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. - pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``user_data`` will be passed to the callback function. Default is ``False``. - pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called - ``chat_data`` will be passed to the callback function. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ - def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False): - super(ShippingQueryHandler, self).__init__( - callback, - pass_update_queue=pass_update_queue, - pass_job_queue=pass_job_queue, - pass_user_data=pass_user_data, - pass_chat_data=pass_chat_data) - - def check_update(self, update): + __slots__ = () + + def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ - return isinstance(update, Update) and update.shipping_query - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. - - """ - optional_args = self.collect_optional_args(dispatcher, update) - return self.callback(dispatcher.bot, update, **optional_args) + return isinstance(update, Update) and bool(update.shipping_query) diff --git a/telegramer/include/telegram/ext/stringcommandhandler.py b/telegramer/include/telegram/ext/stringcommandhandler.py index 00e3dd1..e3945ae 100644 --- a/telegramer/include/telegram/ext/stringcommandhandler.py +++ b/telegramer/include/telegram/ext/stringcommandhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,90 +18,132 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the StringCommandHandler class.""" -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, TypeVar, Union + +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler +from .utils.types import CCT + +if TYPE_CHECKING: + from telegram.ext import Dispatcher +RT = TypeVar('RT') -class StringCommandHandler(Handler): + +class StringCommandHandler(Handler[str, CCT]): """Handler class to handle string commands. Commands are string updates that start with ``/``. + The handler will add a ``list`` to the + :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings, + which is the text following the command split on single whitespace characters. Note: This handler is not used to handle Telegram :attr:`telegram.Update`, but strings manually put in the queue. For example to send messages with the bot using command line or API. - Attributes: - command (:obj:`str`): The command this handler should listen for. - callback (:obj:`callable`): The callback function for this handler. - pass_args (:obj:`bool`): Optional. Determines whether the handler should be passed - ``args``. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. - + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: command (:obj:`str`): The command this handler should listen for. - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that a command should be - processed by this handler. + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the arguments passed to the command as a keyword argument called ``args``. It will contain a list of strings, which is the text following the command split on single or - consecutive whitespace characters. Default is ``False`` - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + consecutive whitespace characters. Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + command (:obj:`str`): The command this handler should listen for. + callback (:obj:`callable`): The callback function for this handler. + pass_args (:obj:`bool`): Determines whether the handler should be passed + ``args``. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ - def __init__(self, - command, - callback, - pass_args=False, - pass_update_queue=False, - pass_job_queue=False): - super(StringCommandHandler, self).__init__( - callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue) + __slots__ = ('command', 'pass_args') + + def __init__( + self, + command: str, + callback: Callable[[str, CCT], RT], + pass_args: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + super().__init__( + callback, + pass_update_queue=pass_update_queue, + pass_job_queue=pass_job_queue, + run_async=run_async, + ) self.command = command self.pass_args = pass_args - def check_update(self, update): + def check_update(self, update: object) -> Optional[List[str]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:obj:`str`): An incomming command. + update (:obj:`object`): The incoming update. Returns: :obj:`bool` """ - - return (isinstance(update, string_types) and update.startswith('/') - and update[1:].split(' ')[0] == self.command) - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:obj:`str`): An incomming command. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the command. - + if isinstance(update, str) and update.startswith('/'): + args = update[1:].split(' ') + if args[0] == self.command: + return args[1:] + return None + + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: str = None, + check_result: Optional[List[str]] = None, + ) -> Dict[str, object]: + """Provide text after the command to the callback the ``args`` argument as list, split on + single whitespaces. """ - - optional_args = self.collect_optional_args(dispatcher) - + optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pass_args: - optional_args['args'] = update.split()[1:] - - return self.callback(dispatcher.bot, update, **optional_args) + optional_args['args'] = check_result + return optional_args + + def collect_additional_context( + self, + context: CCT, + update: str, + dispatcher: 'Dispatcher', + check_result: Optional[List[str]], + ) -> None: + """Add text after the command to :attr:`CallbackContext.args` as list, split on single + whitespaces. + """ + context.args = check_result diff --git a/telegramer/include/telegram/ext/stringregexhandler.py b/telegramer/include/telegram/ext/stringregexhandler.py index 0131bdc..9be6b36 100644 --- a/telegramer/include/telegram/ext/stringregexhandler.py +++ b/telegramer/include/telegram/ext/stringregexhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,22 +19,20 @@ """This module contains the StringRegexHandler class.""" import re +from typing import TYPE_CHECKING, Callable, Dict, Match, Optional, Pattern, TypeVar, Union -# REMREM from future.utils import string_types -try: - from future.utils import string_types -except Exception as e: - pass - -try: - string_types -except NameError: - string_types = str +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler +from .utils.types import CCT + +if TYPE_CHECKING: + from telegram.ext import Dispatcher +RT = TypeVar('RT') -class StringRegexHandler(Handler): + +class StringRegexHandler(Handler[str, CCT]): """Handler class to handle string updates based on a regex which checks the update content. Read the documentation of the ``re`` module for more information. The ``re.match`` function is @@ -44,83 +42,125 @@ class StringRegexHandler(Handler): This handler is not used to handle Telegram :attr:`telegram.Update`, but strings manually put in the queue. For example to send messages with the bot using command line or API. - Attributes: - pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. - callback (:obj:`callable`): The callback function for this handler. - pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the - callback function. - pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to - the callback function. - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. - Default is ``False`` + Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. - Default is ``False`` - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + Default is :obj:`False` + DEPRECATED: Please switch to context based callbacks. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. + callback (:obj:`callable`): The callback function for this handler. + pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the + callback function. + pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to + the callback function. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ - def __init__(self, - pattern, - callback, - pass_groups=False, - pass_groupdict=False, - pass_update_queue=False, - pass_job_queue=False): - super(StringRegexHandler, self).__init__( - callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue) - - if isinstance(pattern, string_types): + __slots__ = ('pass_groups', 'pass_groupdict', 'pattern') + + def __init__( + self, + pattern: Union[str, Pattern], + callback: Callable[[str, CCT], RT], + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + super().__init__( + callback, + pass_update_queue=pass_update_queue, + pass_job_queue=pass_job_queue, + run_async=run_async, + ) + + if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern = pattern self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - def check_update(self, update): + def check_update(self, update: object) -> Optional[Match]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:obj:`str`): An incomming command. + update (:obj:`object`): The incoming update. Returns: :obj:`bool` """ - return isinstance(update, string_types) and bool(re.match(self.pattern, update)) - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:obj:`str`): An incomming command. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the command. - + if isinstance(update, str): + match = re.match(self.pattern, update) + if match: + return match + return None + + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: str = None, + check_result: Optional[Match] = None, + ) -> Dict[str, object]: + """Pass the results of ``re.match(pattern, update).{groups(), groupdict()}`` to the + callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if + needed. """ - optional_args = self.collect_optional_args(dispatcher) - match = re.match(self.pattern, update) - - if self.pass_groups: - optional_args['groups'] = match.groups() - if self.pass_groupdict: - optional_args['groupdict'] = match.groupdict() - - return self.callback(dispatcher.bot, update, **optional_args) + optional_args = super().collect_optional_args(dispatcher, update, check_result) + if self.pattern: + if self.pass_groups and check_result: + optional_args['groups'] = check_result.groups() + if self.pass_groupdict and check_result: + optional_args['groupdict'] = check_result.groupdict() + return optional_args + + def collect_additional_context( + self, + context: CCT, + update: str, + dispatcher: 'Dispatcher', + check_result: Optional[Match], + ) -> None: + """Add the result of ``re.match(pattern, update)`` to :attr:`CallbackContext.matches` as + list with one element. + """ + if self.pattern and check_result: + context.matches = [check_result] diff --git a/telegramer/include/telegram/ext/typehandler.py b/telegramer/include/telegram/ext/typehandler.py index bb388e4..14029ab 100644 --- a/telegramer/include/telegram/ext/typehandler.py +++ b/telegramer/include/telegram/ext/typehandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,72 +18,91 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the TypeHandler class.""" +from typing import Callable, Type, TypeVar, Union +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE + from .handler import Handler +from .utils.types import CCT + +RT = TypeVar('RT') +UT = TypeVar('UT') -class TypeHandler(Handler): +class TypeHandler(Handler[UT, CCT]): """Handler class to handle updates of custom types. - Attributes: - type (:obj:`type`): The ``type`` of updates this handler should process. - callback (:obj:`callable`): The callback function for this handler. - strict (:obj:`bool`): Optional. Use ``type`` instead of ``isinstance``. - Default is ``False`` - pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be - passed to the callback function. - pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to - the callback function. + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: type (:obj:`type`): The ``type`` of updates this handler should process, as determined by ``isinstance`` - callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments. - It will be called when the :attr:`check_update` has determined that an update should be - processed by this handler. + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. strict (:obj:`bool`, optional): Use ``type`` instead of ``isinstance``. - Default is ``False`` - pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + Default is :obj:`False` + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` - that contains new updates which can be used to insert updates. Default is ``False``. - pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` - which can be used to schedule new jobs. Default is ``False``. + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. - """ + Attributes: + type (:obj:`type`): The ``type`` of updates this handler should process. + callback (:obj:`callable`): The callback function for this handler. + strict (:obj:`bool`): Use ``type`` instead of ``isinstance``. Default is :obj:`False`. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. - def __init__(self, type, callback, strict=False, pass_update_queue=False, - pass_job_queue=False): - super(TypeHandler, self).__init__( - callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue) - self.type = type - self.strict = strict + """ - def check_update(self, update): + __slots__ = ('type', 'strict') + + def __init__( + self, + type: Type[UT], # pylint: disable=W0622 + callback: Callable[[UT, CCT], RT], + strict: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + super().__init__( + callback, + pass_update_queue=pass_update_queue, + pass_job_queue=pass_job_queue, + run_async=run_async, + ) + self.type = type # pylint: disable=E0237 + self.strict = strict # pylint: disable=E0237 + + def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: - update (:class:`telegram.Update`): Incoming telegram update. + update (:obj:`object`): Incoming update. Returns: :obj:`bool` """ - if not self.strict: return isinstance(update, self.type) - else: - return type(update) is self.type - - def handle_update(self, update, dispatcher): - """Send the update to the :attr:`callback`. - - Args: - update (:class:`telegram.Update`): Incoming telegram update. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. - - """ - optional_args = self.collect_optional_args(dispatcher) - - return self.callback(dispatcher.bot, update, **optional_args) + return type(update) is self.type # pylint: disable=C0123 diff --git a/telegramer/include/telegram/ext/updater.py b/telegramer/include/telegram/ext/updater.py index 7aa44cf..b2c0512 100644 --- a/telegramer/include/telegram/ext/updater.py +++ b/telegramer/include/telegram/ext/updater.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,26 +19,40 @@ """This module contains the class Updater, which tries to make creating Telegram bots intuitive.""" import logging -import os import ssl -from threading import Thread, Lock, current_thread, Event +import warnings +from queue import Queue +from signal import SIGABRT, SIGINT, SIGTERM, signal +from threading import Event, Lock, Thread, current_thread from time import sleep -import subprocess -from signal import signal, SIGINT, SIGTERM, SIGABRT -# REMREM from queue import Queue -from Queue import Queue +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Union, + no_type_check, + Generic, + overload, +) from telegram import Bot, TelegramError -from telegram.ext import Dispatcher, JobQueue -from telegram.error import Unauthorized, InvalidToken, RetryAfter, TimedOut -from telegram.utils.helpers import get_signal_name +from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized +from telegram.ext import Dispatcher, JobQueue, ContextTypes, ExtBot +from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated +from telegram.utils.helpers import get_signal_name, DEFAULT_FALSE, DefaultValue from telegram.utils.request import Request -from telegram.utils.webhookhandler import (WebhookServer, WebhookHandler) +from telegram.ext.utils.types import CCT, UD, CD, BD +from telegram.ext.utils.webhookhandler import WebhookAppClass, WebhookServer -logging.getLogger(__name__).addHandler(logging.NullHandler()) +if TYPE_CHECKING: + from telegram.ext import BasePersistence, Defaults, CallbackContext -class Updater(object): +class Updater(Generic[CCT, UD, CD, BD]): """ This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to @@ -49,150 +63,354 @@ class Updater(object): production, use a webhook to receive updates. This is achieved using the WebhookServer and WebhookHandler classes. + Note: + * You must supply either a :attr:`bot` or a :attr:`token` argument. + * If you supply a :attr:`bot`, you will need to pass :attr:`arbitrary_callback_data`, + and :attr:`defaults` to the bot instead of the :class:`telegram.ext.Updater`. In this + case, you'll have to use the class :class:`telegram.ext.ExtBot`. - Attributes: - bot (:class:`telegram.Bot`): The bot used with this Updater. - user_sig_handler (:obj:`signal`): signals the updater will respond to. - update_queue (:obj:`Queue`): Queue for the updates. - job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater. - dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and - dispatches them to the handlers. - running (:obj:`bool`): Indicates if the updater is running. + .. versionchanged:: 13.6 Args: token (:obj:`str`, optional): The bot's token given by the @BotFather. base_url (:obj:`str`, optional): Base_url for the bot. + base_file_url (:obj:`str`, optional): Base_file_url for the bot. workers (:obj:`int`, optional): Amount of threads in the thread pool for functions - decorated with ``@run_async``. - bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance. If a pre-initialized - bot is used, it is the user's responsibility to create it using a `Request` - instance with a large enough connection pool. + decorated with ``@run_async`` (ignored if `dispatcher` argument is used). + bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance (ignored if + `dispatcher` argument is used). If a pre-initialized bot is used, it is the user's + responsibility to create it using a `Request` instance with a large enough connection + pool. + dispatcher (:class:`telegram.ext.Dispatcher`, optional): A pre-initialized dispatcher + instance. If a pre-initialized dispatcher is used, it is the user's responsibility to + create it with proper arguments. private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. private_key_password (:obj:`bytes`, optional): Password for above private key. user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional arguments. This will be called when a signal is received, defaults are (SIGINT, - SIGTERM, SIGABRT) setable with :attr:`idle`. + SIGTERM, SIGABRT) settable with :attr:`idle`. request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a - `telegram.utils.request.Request` object (ignored if `bot` argument is used). The - request_kwargs are very useful for the advanced users who would like to control the - default timeouts and/or control the proxy used for http communication. - - Note: - You must supply either a :attr:`bot` or a :attr:`token` argument. + `telegram.utils.request.Request` object (ignored if `bot` or `dispatcher` argument is + used). The request_kwargs are very useful for the advanced users who would like to + control the default timeouts and/or control the proxy used for http communication. + use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback + API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. + **New users**: set this to :obj:`True`. + persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to + store data that should be persistent over restarts (ignored if `dispatcher` argument is + used). + defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to + be used if not set explicitly in the bot methods. + arbitrary_callback_data (:obj:`bool` | :obj:`int` | :obj:`None`, optional): Whether to + allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`. + Pass an integer to specify the maximum number of cached objects. For more details, + please see our wiki. Defaults to :obj:`False`. + + .. versionadded:: 13.6 + context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance + of :class:`telegram.ext.ContextTypes` to customize the types used in the + ``context`` interface. If not passed, the defaults documented in + :class:`telegram.ext.ContextTypes` will be used. + + .. versionadded:: 13.6 Raises: ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them. + + Attributes: + bot (:class:`telegram.Bot`): The bot used with this Updater. + user_sig_handler (:obj:`function`): Optional. Function to be called when a signal is + received. + update_queue (:obj:`Queue`): Queue for the updates. + job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater. + dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and + dispatches them to the handlers. + running (:obj:`bool`): Indicates if the updater is running. + persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to + store data that should be persistent over restarts. + use_context (:obj:`bool`): Optional. :obj:`True` if using context based callbacks. + """ - _request = None - - def __init__(self, - token=None, - base_url=None, - workers=4, - bot=None, - private_key=None, - private_key_password=None, - user_sig_handler=None, - request_kwargs=None): - - if (token is None) and (bot is None): - raise ValueError('`token` or `bot` must be passed') - if (token is not None) and (bot is not None): - raise ValueError('`token` and `bot` are mutually exclusive') - if (private_key is not None) and (bot is not None): - raise ValueError('`bot` and `private_key` are mutually exclusive') + __slots__ = ( + 'persistence', + 'dispatcher', + 'user_sig_handler', + 'bot', + 'logger', + 'update_queue', + 'job_queue', + '__exception_event', + 'last_update_id', + 'running', + '_request', + 'is_idle', + 'httpd', + '__lock', + '__threads', + '__dict__', + ) + + @overload + def __init__( + self: 'Updater[CallbackContext, dict, dict, dict]', + token: str = None, + base_url: str = None, + workers: int = 4, + bot: Bot = None, + private_key: bytes = None, + private_key_password: bytes = None, + user_sig_handler: Callable = None, + request_kwargs: Dict[str, Any] = None, + persistence: 'BasePersistence' = None, # pylint: disable=E0601 + defaults: 'Defaults' = None, + use_context: bool = True, + base_file_url: str = None, + arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE, + ): + ... + + @overload + def __init__( + self: 'Updater[CCT, UD, CD, BD]', + token: str = None, + base_url: str = None, + workers: int = 4, + bot: Bot = None, + private_key: bytes = None, + private_key_password: bytes = None, + user_sig_handler: Callable = None, + request_kwargs: Dict[str, Any] = None, + persistence: 'BasePersistence' = None, + defaults: 'Defaults' = None, + use_context: bool = True, + base_file_url: str = None, + arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE, + context_types: ContextTypes[CCT, UD, CD, BD] = None, + ): + ... + + @overload + def __init__( + self: 'Updater[CCT, UD, CD, BD]', + user_sig_handler: Callable = None, + dispatcher: Dispatcher[CCT, UD, CD, BD] = None, + ): + ... + + def __init__( # type: ignore[no-untyped-def,misc] + self, + token: str = None, + base_url: str = None, + workers: int = 4, + bot: Bot = None, + private_key: bytes = None, + private_key_password: bytes = None, + user_sig_handler: Callable = None, + request_kwargs: Dict[str, Any] = None, + persistence: 'BasePersistence' = None, + defaults: 'Defaults' = None, + use_context: bool = True, + dispatcher=None, + base_file_url: str = None, + arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE, + context_types: ContextTypes[CCT, UD, CD, BD] = None, + ): + + if defaults and bot: + warnings.warn( + 'Passing defaults to an Updater has no effect when a Bot is passed ' + 'as well. Pass them to the Bot instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + if arbitrary_callback_data is not DEFAULT_FALSE and bot: + warnings.warn( + 'Passing arbitrary_callback_data to an Updater has no ' + 'effect when a Bot is passed as well. Pass them to the Bot instead.', + stacklevel=2, + ) + + if dispatcher is None: + if (token is None) and (bot is None): + raise ValueError('`token` or `bot` must be passed') + if (token is not None) and (bot is not None): + raise ValueError('`token` and `bot` are mutually exclusive') + if (private_key is not None) and (bot is not None): + raise ValueError('`bot` and `private_key` are mutually exclusive') + else: + if bot is not None: + raise ValueError('`dispatcher` and `bot` are mutually exclusive') + if persistence is not None: + raise ValueError('`dispatcher` and `persistence` are mutually exclusive') + if use_context != dispatcher.use_context: + raise ValueError('`dispatcher` and `use_context` are mutually exclusive') + if context_types is not None: + raise ValueError('`dispatcher` and `context_types` are mutually exclusive') + if workers is not None: + raise ValueError('`dispatcher` and `workers` are mutually exclusive') self.logger = logging.getLogger(__name__) + self._request = None + + if dispatcher is None: + con_pool_size = workers + 4 + + if bot is not None: + self.bot = bot + if bot.request.con_pool_size < con_pool_size: + self.logger.warning( + 'Connection pool of Request object is smaller than optimal value (%s)', + con_pool_size, + ) + else: + # we need a connection pool the size of: + # * for each of the workers + # * 1 for Dispatcher + # * 1 for polling Updater (even if webhook is used, we can spare a connection) + # * 1 for JobQueue + # * 1 for main thread + if request_kwargs is None: + request_kwargs = {} + if 'con_pool_size' not in request_kwargs: + request_kwargs['con_pool_size'] = con_pool_size + self._request = Request(**request_kwargs) + self.bot = ExtBot( + token, # type: ignore[arg-type] + base_url, + base_file_url=base_file_url, + request=self._request, + private_key=private_key, + private_key_password=private_key_password, + defaults=defaults, + arbitrary_callback_data=( + False # type: ignore[arg-type] + if arbitrary_callback_data is DEFAULT_FALSE + else arbitrary_callback_data + ), + ) + self.update_queue: Queue = Queue() + self.job_queue = JobQueue() + self.__exception_event = Event() + self.persistence = persistence + self.dispatcher = Dispatcher( + self.bot, + self.update_queue, + job_queue=self.job_queue, + workers=workers, + exception_event=self.__exception_event, + persistence=persistence, + use_context=use_context, + context_types=context_types, + ) + self.job_queue.set_dispatcher(self.dispatcher) + else: + con_pool_size = dispatcher.workers + 4 - con_pool_size = workers + 4 - - if bot is not None: - self.bot = bot - if bot.request.con_pool_size < con_pool_size: + self.bot = dispatcher.bot + if self.bot.request.con_pool_size < con_pool_size: self.logger.warning( 'Connection pool of Request object is smaller than optimal value (%s)', - con_pool_size) - else: - # we need a connection pool the size of: - # * for each of the workers - # * 1 for Dispatcher - # * 1 for polling Updater (even if webhook is used, we can spare a connection) - # * 1 for JobQueue - # * 1 for main thread - if request_kwargs is None: - request_kwargs = {} - if 'con_pool_size' not in request_kwargs: - request_kwargs['con_pool_size'] = con_pool_size - self._request = Request(**request_kwargs) - self.bot = Bot(token, base_url, request=self._request, private_key=private_key, - private_key_password=private_key_password) + con_pool_size, + ) + self.update_queue = dispatcher.update_queue + self.__exception_event = dispatcher.exception_event + self.persistence = dispatcher.persistence + self.job_queue = dispatcher.job_queue + self.dispatcher = dispatcher + self.user_sig_handler = user_sig_handler - self.update_queue = Queue() - self.job_queue = JobQueue(self.bot) - self.__exception_event = Event() - self.dispatcher = Dispatcher( - self.bot, - self.update_queue, - job_queue=self.job_queue, - workers=workers, - exception_event=self.__exception_event) self.last_update_id = 0 self.running = False self.is_idle = False self.httpd = None self.__lock = Lock() - self.__threads = [] - - def _init_thread(self, target, name, *args, **kwargs): - thr = Thread(target=self._thread_wrapper, name=name, args=(target,) + args, kwargs=kwargs) + self.__threads: List[Thread] = [] + + def __setattr__(self, key: str, value: object) -> None: + if key.startswith('__'): + key = f"_{self.__class__.__name__}{key}" + if issubclass(self.__class__, Updater) and self.__class__ is not Updater: + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + + def _init_thread(self, target: Callable, name: str, *args: object, **kwargs: object) -> None: + thr = Thread( + target=self._thread_wrapper, + name=f"Bot:{self.bot.id}:{name}", + args=(target,) + args, + kwargs=kwargs, + ) thr.start() self.__threads.append(thr) - def _thread_wrapper(self, target, *args, **kwargs): + def _thread_wrapper(self, target: Callable, *args: object, **kwargs: object) -> None: thr_name = current_thread().name - self.logger.debug('{0} - started'.format(thr_name)) + self.logger.debug('%s - started', thr_name) try: target(*args, **kwargs) except Exception: self.__exception_event.set() self.logger.exception('unhandled exception in %s', thr_name) raise - self.logger.debug('{0} - ended'.format(thr_name)) - - def start_polling(self, - poll_interval=0.0, - timeout=10, - clean=False, - bootstrap_retries=-1, - read_latency=2., - allowed_updates=None): + self.logger.debug('%s - ended', thr_name) + + def start_polling( + self, + poll_interval: float = 0.0, + timeout: float = 10, + clean: bool = None, + bootstrap_retries: int = -1, + read_latency: float = 2.0, + allowed_updates: List[str] = None, + drop_pending_updates: bool = None, + ) -> Optional[Queue]: """Starts polling updates from Telegram. Args: poll_interval (:obj:`float`, optional): Time to wait between polling updates from - Telegram in seconds. Default is 0.0. - timeout (:obj:`float`, optional): Passed to :attr:`telegram.Bot.get_updates`. - clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers - before actually starting to poll. Default is False. + Telegram in seconds. Default is ``0.0``. + timeout (:obj:`float`, optional): Passed to :meth:`telegram.Bot.get_updates`. + drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on + Telegram servers before actually starting to poll. Default is :obj:`False`. + + .. versionadded :: 13.4 + clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``. + + .. deprecated:: 13.4 + Use ``drop_pending_updates`` instead. bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the - `Updater` will retry on failures on the Telegram server. + :class:`telegram.ext.Updater` will retry on failures on the Telegram server. * < 0 - retry indefinitely (default) * 0 - no retries * > 0 - retry up to X times allowed_updates (List[:obj:`str`], optional): Passed to - :attr:`telegram.Bot.get_updates`. + :meth:`telegram.Bot.get_updates`. read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving - the reply from server. Will be added to the `timeout` value and used as the read - timeout from server (Default: 2). + the reply from server. Will be added to the ``timeout`` value and used as the read + timeout from server (Default: ``2``). Returns: :obj:`Queue`: The update queue that can be filled from the main thread. """ + if (clean is not None) and (drop_pending_updates is not None): + raise TypeError('`clean` and `drop_pending_updates` are mutually exclusive.') + + if clean is not None: + warnings.warn( + 'The argument `clean` of `start_polling` is deprecated. Please use ' + '`drop_pending_updates` instead.', + category=TelegramDeprecationWarning, + stacklevel=2, + ) + + drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean + with self.__lock: if not self.running: self.running = True @@ -200,31 +418,54 @@ def start_polling(self, # Create & start threads self.job_queue.start() dispatcher_ready = Event() + polling_ready = Event() self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready) - self._init_thread(self._start_polling, "updater", poll_interval, timeout, - read_latency, bootstrap_retries, clean, allowed_updates) - + self._init_thread( + self._start_polling, + "updater", + poll_interval, + timeout, + read_latency, + bootstrap_retries, + drop_pending_updates, + allowed_updates, + ready=polling_ready, + ) + + self.logger.debug('Waiting for Dispatcher and polling to start') dispatcher_ready.wait() + polling_ready.wait() # Return the update queue so the main thread can insert updates return self.update_queue - - def start_webhook(self, - listen='127.0.0.1', - port=80, - url_path='', - cert=None, - key=None, - clean=False, - bootstrap_retries=0, - webhook_url=None, - allowed_updates=None): + return None + + def start_webhook( + self, + listen: str = '127.0.0.1', + port: int = 80, + url_path: str = '', + cert: str = None, + key: str = None, + clean: bool = None, + bootstrap_retries: int = 0, + webhook_url: str = None, + allowed_updates: List[str] = None, + force_event_loop: bool = None, + drop_pending_updates: bool = None, + ip_address: str = None, + max_connections: int = 40, + ) -> Optional[Queue]: """ - Starts a small http server to listen for updates via webhook. If cert - and key are not provided, the webhook will be started directly on + Starts a small http server to listen for updates via webhook. If :attr:`cert` + and :attr:`key` are not provided, the webhook will be started directly on http://listen:port/url_path, so SSL can be handled by another application. Else, the webhook will be started on - https://listen:port/url_path + https://listen:port/url_path. Also calls :meth:`telegram.Bot.set_webhook` as required. + + .. versionchanged:: 13.4 + :meth:`start_webhook` now *always* calls :meth:`telegram.Bot.set_webhook`, so pass + ``webhook_url`` instead of calling ``updater.bot.set_webhook(webhook_url)`` manually. Args: listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``. @@ -232,53 +473,133 @@ def start_webhook(self, url_path (:obj:`str`, optional): Path inside url. cert (:obj:`str`, optional): Path to the SSL certificate file. key (:obj:`str`, optional): Path to the SSL key file. - clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers - before actually starting the webhook. Default is ``False``. + drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on + Telegram servers before actually starting to poll. Default is :obj:`False`. + + .. versionadded :: 13.4 + clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``. + + .. deprecated:: 13.4 + Use ``drop_pending_updates`` instead. bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the - `Updater` will retry on failures on the Telegram server. + :class:`telegram.ext.Updater` will retry on failures on the Telegram server. * < 0 - retry indefinitely (default) * 0 - no retries * > 0 - retry up to X times webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind - NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`. + NAT, reverse proxy, etc. Default is derived from ``listen``, ``port`` & + ``url_path``. + ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. + + .. versionadded :: 13.4 allowed_updates (List[:obj:`str`], optional): Passed to - :attr:`telegram.Bot.set_webhook`. + :meth:`telegram.Bot.set_webhook`. + force_event_loop (:obj:`bool`, optional): Legacy parameter formerly used for a + workaround on Windows + Python 3.8+. No longer has any effect. + + .. deprecated:: 13.6 + Since version 13.6, ``tornade>=6.1`` is required, which resolves the former + issue. + + max_connections (:obj:`int`, optional): Passed to + :meth:`telegram.Bot.set_webhook`. + + .. versionadded:: 13.6 Returns: :obj:`Queue`: The update queue that can be filled from the main thread. """ + if (clean is not None) and (drop_pending_updates is not None): + raise TypeError('`clean` and `drop_pending_updates` are mutually exclusive.') + + if clean is not None: + warnings.warn( + 'The argument `clean` of `start_webhook` is deprecated. Please use ' + '`drop_pending_updates` instead.', + category=TelegramDeprecationWarning, + stacklevel=2, + ) + + if force_event_loop is not None: + warnings.warn( + 'The argument `force_event_loop` of `start_webhook` is deprecated and no longer ' + 'has any effect.', + category=TelegramDeprecationWarning, + stacklevel=2, + ) + + drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean + with self.__lock: if not self.running: self.running = True # Create & start threads + webhook_ready = Event() + dispatcher_ready = Event() self.job_queue.start() - self._init_thread(self.dispatcher.start, "dispatcher"), - self._init_thread(self._start_webhook, "updater", listen, port, url_path, cert, - key, bootstrap_retries, clean, webhook_url, allowed_updates) + self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready) + self._init_thread( + self._start_webhook, + "updater", + listen, + port, + url_path, + cert, + key, + bootstrap_retries, + drop_pending_updates, + webhook_url, + allowed_updates, + ready=webhook_ready, + ip_address=ip_address, + max_connections=max_connections, + ) + + self.logger.debug('Waiting for Dispatcher and Webhook to start') + webhook_ready.wait() + dispatcher_ready.wait() # Return the update queue so the main thread can insert updates return self.update_queue - - def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean, - allowed_updates): # pragma: no cover + return None + + @no_type_check + def _start_polling( + self, + poll_interval, + timeout, + read_latency, + bootstrap_retries, + drop_pending_updates, + allowed_updates, + ready=None, + ): # pragma: no cover # Thread target of thread 'updater'. Runs in background, pulls # updates from Telegram and inserts them in the update queue of the # Dispatcher. self.logger.debug('Updater thread started (polling)') - self._bootstrap(bootstrap_retries, clean=clean, webhook_url='', allowed_updates=None) + self._bootstrap( + bootstrap_retries, + drop_pending_updates=drop_pending_updates, + webhook_url='', + allowed_updates=None, + ) self.logger.debug('Bootstrap done') def polling_action_cb(): updates = self.bot.get_updates( - self.last_update_id, timeout=timeout, read_latency=read_latency, - allowed_updates=allowed_updates) + self.last_update_id, + timeout=timeout, + read_latency=read_latency, + allowed_updates=allowed_updates, + ) if updates: if not self.running: @@ -295,14 +616,19 @@ def polling_onerr_cb(exc): # broadcast it self.update_queue.put(exc) - self._network_loop_retry(polling_action_cb, polling_onerr_cb, 'getting Updates', - poll_interval) + if ready is not None: + ready.set() + + self._network_loop_retry( + polling_action_cb, polling_onerr_cb, 'getting Updates', poll_interval + ) + @no_type_check def _network_loop_retry(self, action_cb, onerr_cb, description, interval): """Perform a loop calling `action_cb`, retrying after network errors. - Stop condition for loop: `self.running` evaluates False or return value of `action_cb` - evaluates False. + Stop condition for loop: `self.running` evaluates :obj:`False` or return value of + `action_cb` evaluates :obj:`False`. Args: action_cb (:obj:`callable`): Network oriented callback function to call. @@ -319,9 +645,9 @@ def _network_loop_retry(self, action_cb, onerr_cb, description, interval): try: if not action_cb(): break - except RetryAfter as e: - self.logger.info('%s', e) - cur_interval = 0.5 + e.retry_after + except RetryAfter as exc: + self.logger.info('%s', exc) + cur_interval = 0.5 + exc.retry_after except TimedOut as toe: self.logger.debug('Timed out %s: %s', description, toe) # If failure is due to timeout, we should retry asap. @@ -329,9 +655,9 @@ def _network_loop_retry(self, action_cb, onerr_cb, description, interval): except InvalidToken as pex: self.logger.error('Invalid token; aborting') raise pex - except TelegramError as te: - self.logger.error('Error while %s: %s', description, te) - onerr_cb(te) + except TelegramError as telegram_exc: + self.logger.error('Error while %s: %s', description, telegram_exc) + onerr_cb(telegram_exc) cur_interval = self._increase_poll_interval(cur_interval) else: cur_interval = interval @@ -340,123 +666,152 @@ def _network_loop_retry(self, action_cb, onerr_cb, description, interval): sleep(cur_interval) @staticmethod - def _increase_poll_interval(current_interval): + def _increase_poll_interval(current_interval: float) -> float: # increase waiting times on subsequent errors up to 30secs if current_interval == 0: current_interval = 1 elif current_interval < 30: - current_interval += current_interval / 2 - elif current_interval > 30: - current_interval = 30 + current_interval *= 1.5 + else: + current_interval = min(30.0, current_interval) return current_interval - def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean, - webhook_url, allowed_updates): + @no_type_check + def _start_webhook( + self, + listen, + port, + url_path, + cert, + key, + bootstrap_retries, + drop_pending_updates, + webhook_url, + allowed_updates, + ready=None, + ip_address=None, + max_connections: int = 40, + ): self.logger.debug('Updater thread started (webhook)') + + # Note that we only use the SSL certificate for the WebhookServer, if the key is also + # present. This is because the WebhookServer may not actually be in charge of performing + # the SSL handshake, e.g. in case a reverse proxy is used use_ssl = cert is not None and key is not None + if not url_path.startswith('/'): - url_path = '/{0}'.format(url_path) + url_path = f'/{url_path}' - # Create and start server - self.httpd = WebhookServer((listen, port), WebhookHandler, self.update_queue, url_path, - self.bot) + # Create Tornado app instance + app = WebhookAppClass(url_path, self.bot, self.update_queue) + # Form SSL Context + # An SSLError is raised if the private key does not match with the certificate if use_ssl: - self._check_ssl_cert(cert, key) - - # DO NOT CHANGE: Only set webhook if SSL is handled by library - if not webhook_url: - webhook_url = self._gen_webhook_url(listen, port, url_path) - - self._bootstrap( - max_retries=bootstrap_retries, - clean=clean, - webhook_url=webhook_url, - cert=open(cert, 'rb'), - allowed_updates=allowed_updates) - elif clean: - self.logger.warning("cleaning updates is not supported if " - "SSL-termination happens elsewhere; skipping") - - self.httpd.serve_forever(poll_interval=1) - - def _check_ssl_cert(self, cert, key): - # Check SSL-Certificate with openssl, if possible - try: - exit_code = subprocess.call( - ["openssl", "x509", "-text", "-noout", "-in", cert], - stdout=open(os.devnull, 'wb'), - stderr=subprocess.STDOUT) - except OSError: - exit_code = 0 - if exit_code == 0: try: - self.httpd.socket = ssl.wrap_socket( - self.httpd.socket, certfile=cert, keyfile=key, server_side=True) - except ssl.SSLError as error: - self.logger.exception('Failed to init SSL socket') - raise TelegramError(str(error)) + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_ctx.load_cert_chain(cert, key) + except ssl.SSLError as exc: + raise TelegramError('Invalid SSL Certificate') from exc else: - raise TelegramError('SSL Certificate invalid') + ssl_ctx = None - @staticmethod - def _gen_webhook_url(listen, port, url_path): - return 'https://{listen}:{port}{path}'.format(listen=listen, port=port, path=url_path) + # Create and start server + self.httpd = WebhookServer(listen, port, app, ssl_ctx) + + if not webhook_url: + webhook_url = self._gen_webhook_url(listen, port, url_path) + + # We pass along the cert to the webhook if present. + cert_file = open(cert, 'rb') if cert is not None else None + self._bootstrap( + max_retries=bootstrap_retries, + drop_pending_updates=drop_pending_updates, + webhook_url=webhook_url, + allowed_updates=allowed_updates, + cert=cert_file, + ip_address=ip_address, + max_connections=max_connections, + ) + if cert_file is not None: + cert_file.close() + + self.httpd.serve_forever(ready=ready) - def _bootstrap(self, max_retries, clean, webhook_url, allowed_updates, cert=None, - bootstrap_interval=5): + @staticmethod + def _gen_webhook_url(listen: str, port: int, url_path: str) -> str: + return f'https://{listen}:{port}{url_path}' + + @no_type_check + def _bootstrap( + self, + max_retries, + drop_pending_updates, + webhook_url, + allowed_updates, + cert=None, + bootstrap_interval=5, + ip_address=None, + max_connections: int = 40, + ): retries = [0] def bootstrap_del_webhook(): - self.bot.delete_webhook() - return False - - def bootstrap_clean_updates(): - self.logger.debug('Cleaning updates from Telegram server') - updates = self.bot.get_updates() - while updates: - updates = self.bot.get_updates(updates[-1].update_id + 1) + self.logger.debug('Deleting webhook') + if drop_pending_updates: + self.logger.debug('Dropping pending updates from Telegram server') + self.bot.delete_webhook(drop_pending_updates=drop_pending_updates) return False def bootstrap_set_webhook(): + self.logger.debug('Setting webhook') + if drop_pending_updates: + self.logger.debug('Dropping pending updates from Telegram server') self.bot.set_webhook( - url=webhook_url, certificate=cert, allowed_updates=allowed_updates) + url=webhook_url, + certificate=cert, + allowed_updates=allowed_updates, + ip_address=ip_address, + drop_pending_updates=drop_pending_updates, + max_connections=max_connections, + ) return False def bootstrap_onerr_cb(exc): if not isinstance(exc, Unauthorized) and (max_retries < 0 or retries[0] < max_retries): retries[0] += 1 - self.logger.warning('Failed bootstrap phase; try=%s max_retries=%s', - retries[0], max_retries) + self.logger.warning( + 'Failed bootstrap phase; try=%s max_retries=%s', retries[0], max_retries + ) else: self.logger.error('Failed bootstrap phase after %s retries (%s)', retries[0], exc) raise exc - # Cleaning pending messages is done by polling for them - so we need to delete webhook if - # one is configured. - # We also take this chance to delete pre-configured webhook if this is a polling Updater. - # NOTE: We don't know ahead if a webhook is configured, so we just delete. - if clean or not webhook_url: - self._network_loop_retry(bootstrap_del_webhook, bootstrap_onerr_cb, - 'bootstrap del webhook', bootstrap_interval) - retries[0] = 0 - - # Clean pending messages, if requested. - if clean: - self._network_loop_retry(bootstrap_clean_updates, bootstrap_onerr_cb, - 'bootstrap clean updates', bootstrap_interval) + # Dropping pending updates from TG can be efficiently done with the drop_pending_updates + # parameter of delete/start_webhook, even in the case of polling. Also we want to make + # sure that no webhook is configured in case of polling, so we just always call + # delete_webhook for polling + if drop_pending_updates or not webhook_url: + self._network_loop_retry( + bootstrap_del_webhook, + bootstrap_onerr_cb, + 'bootstrap del webhook', + bootstrap_interval, + ) retries[0] = 0 - sleep(1) # Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set, # so we set it anyhow. if webhook_url: - self._network_loop_retry(bootstrap_set_webhook, bootstrap_onerr_cb, - 'bootstrap set webhook', bootstrap_interval) - - def stop(self): + self._network_loop_retry( + bootstrap_set_webhook, + bootstrap_onerr_cb, + 'bootstrap set webhook', + bootstrap_interval, + ) + + def stop(self) -> None: """Stops the polling/webhook thread, the dispatcher and the job queue.""" - self.job_queue.stop() with self.__lock: if self.running or self.dispatcher.has_running_threads: @@ -472,49 +827,62 @@ def stop(self): if self._request: self._request.stop() - def _stop_httpd(self): + @no_type_check + def _stop_httpd(self) -> None: if self.httpd: - self.logger.debug('Waiting for current webhook connection to be ' - 'closed... Send a Telegram message to the bot to exit ' - 'immediately.') + self.logger.debug( + 'Waiting for current webhook connection to be ' + 'closed... Send a Telegram message to the bot to exit ' + 'immediately.' + ) self.httpd.shutdown() self.httpd = None - def _stop_dispatcher(self): + @no_type_check + def _stop_dispatcher(self) -> None: self.logger.debug('Requesting Dispatcher to stop...') self.dispatcher.stop() - def _join_threads(self): + @no_type_check + def _join_threads(self) -> None: for thr in self.__threads: - self.logger.debug('Waiting for {0} thread to end'.format(thr.name)) + self.logger.debug('Waiting for %s thread to end', thr.name) thr.join() - self.logger.debug('{0} thread has ended'.format(thr.name)) + self.logger.debug('%s thread has ended', thr.name) self.__threads = [] - def signal_handler(self, signum, frame): + @no_type_check + def _signal_handler(self, signum, frame) -> None: self.is_idle = False if self.running: - self.logger.info('Received signal {} ({}), stopping...'.format( - signum, get_signal_name(signum))) + self.logger.info( + 'Received signal %s (%s), stopping...', signum, get_signal_name(signum) + ) + if self.persistence: + # Update user_data, chat_data and bot_data before flushing + self.dispatcher.update_persistence() + self.persistence.flush() self.stop() if self.user_sig_handler: self.user_sig_handler(signum, frame) else: self.logger.warning('Exiting immediately!') + # pylint: disable=C0415,W0212 import os + os._exit(1) - def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)): + def idle(self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)) -> None: """Blocks until one of the signals are received and stops the updater. Args: - stop_signals (:obj:`iterable`): Iterable containing signals from the signal module that - should be subscribed to. Updater.stop() will be called on receiving one of those - signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``). + stop_signals (:obj:`list` | :obj:`tuple`): List containing signals from the signal + module that should be subscribed to. :meth:`Updater.stop()` will be called on + receiving one of those signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``). """ for sig in stop_signals: - signal(sig, self.signal_handler) + signal(sig, self._signal_handler) self.is_idle = True diff --git a/telegramer/include/telegram/ext/utils/__init__.py b/telegramer/include/telegram/ext/utils/__init__.py new file mode 100644 index 0000000..b624e1e --- /dev/null +++ b/telegramer/include/telegram/ext/utils/__init__.py @@ -0,0 +1,17 @@ +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. diff --git a/telegramer/include/telegram/ext/utils/promise.py b/telegramer/include/telegram/ext/utils/promise.py new file mode 100644 index 0000000..86b3981 --- /dev/null +++ b/telegramer/include/telegram/ext/utils/promise.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the Promise class.""" + +import logging +from threading import Event +from typing import Callable, List, Optional, Tuple, TypeVar, Union + +from telegram.utils.deprecate import set_new_attribute_deprecated +from telegram.utils.types import JSONDict + +RT = TypeVar('RT') + + +logger = logging.getLogger(__name__) + + +class Promise: + """A simple Promise implementation for use with the run_async decorator, DelayQueue etc. + + Args: + pooled_function (:obj:`callable`): The callable that will be called concurrently. + args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`. + kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`. + update (:class:`telegram.Update` | :obj:`object`, optional): The update this promise is + associated with. + error_handling (:obj:`bool`, optional): Whether exceptions raised by :attr:`func` + may be handled by error handlers. Defaults to :obj:`True`. + + Attributes: + pooled_function (:obj:`callable`): The callable that will be called concurrently. + args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`. + kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`. + done (:obj:`threading.Event`): Is set when the result is available. + update (:class:`telegram.Update` | :obj:`object`): Optional. The update this promise is + associated with. + error_handling (:obj:`bool`): Optional. Whether exceptions raised by :attr:`func` + may be handled by error handlers. Defaults to :obj:`True`. + + """ + + __slots__ = ( + 'pooled_function', + 'args', + 'kwargs', + 'update', + 'error_handling', + 'done', + '_done_callback', + '_result', + '_exception', + '__dict__', + ) + + # TODO: Remove error_handling parameter once we drop the @run_async decorator + def __init__( + self, + pooled_function: Callable[..., RT], + args: Union[List, Tuple], + kwargs: JSONDict, + update: object = None, + error_handling: bool = True, + ): + self.pooled_function = pooled_function + self.args = args + self.kwargs = kwargs + self.update = update + self.error_handling = error_handling + self.done = Event() + self._done_callback: Optional[Callable] = None + self._result: Optional[RT] = None + self._exception: Optional[Exception] = None + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + + def run(self) -> None: + """Calls the :attr:`pooled_function` callable.""" + try: + self._result = self.pooled_function(*self.args, **self.kwargs) + + except Exception as exc: + self._exception = exc + + finally: + self.done.set() + if self._exception is None and self._done_callback: + try: + self._done_callback(self.result()) + except Exception as exc: + logger.warning( + "`done_callback` of a Promise raised the following exception." + " The exception won't be handled by error handlers." + ) + logger.warning("Full traceback:", exc_info=exc) + + def __call__(self) -> None: + self.run() + + def result(self, timeout: float = None) -> Optional[RT]: + """Return the result of the ``Promise``. + + Args: + timeout (:obj:`float`, optional): Maximum time in seconds to wait for the result to be + calculated. ``None`` means indefinite. Default is ``None``. + + Returns: + Returns the return value of :attr:`pooled_function` or ``None`` if the ``timeout`` + expires. + + Raises: + object exception raised by :attr:`pooled_function`. + """ + self.done.wait(timeout=timeout) + if self._exception is not None: + raise self._exception # pylint: disable=raising-bad-type + return self._result + + def add_done_callback(self, callback: Callable) -> None: + """ + Callback to be run when :class:`telegram.ext.utils.promise.Promise` becomes done. + + Note: + Callback won't be called if :attr:`pooled_function` + raises an exception. + + Args: + callback (:obj:`callable`): The callable that will be called when promise is done. + callback will be called by passing ``Promise.result()`` as only positional argument. + + """ + if self.done.wait(0): + callback(self.result()) + else: + self._done_callback = callback + + @property + def exception(self) -> Optional[Exception]: + """The exception raised by :attr:`pooled_function` or ``None`` if no exception has been + raised (yet). + """ + return self._exception diff --git a/telegramer/include/telegram/ext/utils/types.py b/telegramer/include/telegram/ext/utils/types.py new file mode 100644 index 0000000..a63e528 --- /dev/null +++ b/telegramer/include/telegram/ext/utils/types.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains custom typing aliases. + +.. versionadded:: 13.6 +""" +from typing import TypeVar, TYPE_CHECKING, Tuple, List, Dict, Any, Optional + +if TYPE_CHECKING: + from telegram.ext import CallbackContext # noqa: F401 + + +ConversationDict = Dict[Tuple[int, ...], Optional[object]] +"""Dicts as maintained by the :class:`telegram.ext.ConversationHandler`. + + .. versionadded:: 13.6 +""" + +CDCData = Tuple[List[Tuple[str, float, Dict[str, Any]]], Dict[str, str]] +"""Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :obj:`any`]]], \ + Dict[:obj:`str`, :obj:`str`]]: Data returned by + :attr:`telegram.ext.CallbackDataCache.persistence_data`. + + .. versionadded:: 13.6 +""" + +CCT = TypeVar('CCT', bound='CallbackContext') +"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass. + +.. versionadded:: 13.6 +""" +UD = TypeVar('UD') +"""Type of the user data for a single user. + +.. versionadded:: 13.6 +""" +CD = TypeVar('CD') +"""Type of the chat data for a single user. + +.. versionadded:: 13.6 +""" +BD = TypeVar('BD') +"""Type of the bot data. + +.. versionadded:: 13.6 +""" diff --git a/telegramer/include/telegram/ext/utils/webhookhandler.py b/telegramer/include/telegram/ext/utils/webhookhandler.py new file mode 100644 index 0000000..64e639b --- /dev/null +++ b/telegramer/include/telegram/ext/utils/webhookhandler.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0114 + +import logging +from queue import Queue +from ssl import SSLContext +from threading import Event, Lock +from typing import TYPE_CHECKING, Any, Optional + +import tornado.web +from tornado import httputil +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop + +from telegram import Update +from telegram.ext import ExtBot +from telegram.utils.deprecate import set_new_attribute_deprecated +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + +try: + import ujson as json +except ImportError: + import json # type: ignore[no-redef] + + +class WebhookServer: + __slots__ = ( + 'http_server', + 'listen', + 'port', + 'loop', + 'logger', + 'is_running', + 'server_lock', + 'shutdown_lock', + '__dict__', + ) + + def __init__( + self, listen: str, port: int, webhook_app: 'WebhookAppClass', ssl_ctx: SSLContext + ): + self.http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx) + self.listen = listen + self.port = port + self.loop: Optional[IOLoop] = None + self.logger = logging.getLogger(__name__) + self.is_running = False + self.server_lock = Lock() + self.shutdown_lock = Lock() + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + + def serve_forever(self, ready: Event = None) -> None: + with self.server_lock: + IOLoop().make_current() + self.is_running = True + self.logger.debug('Webhook Server started.') + self.loop = IOLoop.current() + self.http_server.listen(self.port, address=self.listen) + + if ready is not None: + ready.set() + + self.loop.start() + self.logger.debug('Webhook Server stopped.') + self.is_running = False + + def shutdown(self) -> None: + with self.shutdown_lock: + if not self.is_running: + self.logger.warning('Webhook Server already stopped.') + return + self.loop.add_callback(self.loop.stop) # type: ignore + + def handle_error(self, request: object, client_address: str) -> None: # pylint: disable=W0613 + """Handle an error gracefully.""" + self.logger.debug( + 'Exception happened during processing of request from %s', + client_address, + exc_info=True, + ) + + +class WebhookAppClass(tornado.web.Application): + def __init__(self, webhook_path: str, bot: 'Bot', update_queue: Queue): + self.shared_objects = {"bot": bot, "update_queue": update_queue} + handlers = [(rf"{webhook_path}/?", WebhookHandler, self.shared_objects)] # noqa + tornado.web.Application.__init__(self, handlers) # type: ignore + + def log_request(self, handler: tornado.web.RequestHandler) -> None: # skipcq: PTC-W0049 + pass + + +# WebhookHandler, process webhook calls +# pylint: disable=W0223 +class WebhookHandler(tornado.web.RequestHandler): + SUPPORTED_METHODS = ["POST"] # type: ignore + + def __init__( + self, + application: tornado.web.Application, + request: httputil.HTTPServerRequest, + **kwargs: JSONDict, + ): + super().__init__(application, request, **kwargs) + self.logger = logging.getLogger(__name__) + + def initialize(self, bot: 'Bot', update_queue: Queue) -> None: + # pylint: disable=W0201 + self.bot = bot + self.update_queue = update_queue + + def set_default_headers(self) -> None: + self.set_header("Content-Type", 'application/json; charset="utf-8"') + + def post(self) -> None: + self.logger.debug('Webhook triggered') + self._validate_post() + json_string = self.request.body.decode() + data = json.loads(json_string) + self.set_status(200) + self.logger.debug('Webhook received data: %s', json_string) + update = Update.de_json(data, self.bot) + if update: + self.logger.debug('Received Update with ID %d on Webhook', update.update_id) + # handle arbitrary callback data, if necessary + if isinstance(self.bot, ExtBot): + self.bot.insert_callback_data(update) + self.update_queue.put(update) + + def _validate_post(self) -> None: + ct_header = self.request.headers.get("Content-Type", None) + if ct_header != 'application/json': + raise tornado.web.HTTPError(403) + + def write_error(self, status_code: int, **kwargs: Any) -> None: + """Log an arbitrary message. + + This is used by all other logging functions. + + It overrides ``BaseHTTPRequestHandler.log_message``, which logs to ``sys.stderr``. + + The first argument, FORMAT, is a format string for the message to be logged. If the format + string contains any % escapes requiring parameters, they should be specified as subsequent + arguments (it's just like printf!). + + The client ip is prefixed to every message. + + """ + super().write_error(status_code, **kwargs) + self.logger.debug( + "%s - - %s", + self.request.remote_ip, + "Exception in WebhookHandler", + exc_info=kwargs['exc_info'], + ) diff --git a/telegramer/include/telegram/files/animation.py b/telegramer/include/telegram/files/animation.py index 4ff6594..a9f2ab5 100644 --- a/telegramer/include/telegram/files/animation.py +++ b/telegramer/include/telegram/files/animation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,26 +17,28 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Animation.""" -from telegram import PhotoSize -from telegram import TelegramObject +from typing import TYPE_CHECKING, Any, Optional + +from telegram import PhotoSize, TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class Animation(TelegramObject): - """This object represents an animation file to be displayed in the message containing a game. + """This object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound). - Attributes: - file_id (:obj:`str`): Unique file identifier. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined - by sender. - file_name (:obj:`str`): Optional. Original animation filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. - file_size (:obj:`int`): Optional. File size. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique file identifier. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. width (:obj:`int`): Video width as defined by sender. height (:obj:`int`): Video height as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. @@ -44,38 +46,92 @@ class Animation(TelegramObject): file_name (:obj:`str`, optional): Original animation filename as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. file_size (:obj:`int`, optional): File size. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + file_id (:obj:`str`): File identifier. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + width (:obj:`int`): Video width as defined by sender. + height (:obj:`int`): Video height as defined by sender. + duration (:obj:`int`): Duration of the video in seconds as defined by sender. + thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined by sender. + file_name (:obj:`str`): Optional. Original animation filename as defined by sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + file_size (:obj:`int`): Optional. File size. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ - def __init__(self, - file_id, - width, - height, - duration, - thumb=None, - file_name=None, - mime_type=None, - file_size=None, - **kwargs): + __slots__ = ( + 'bot', + 'width', + 'file_id', + 'file_size', + 'file_name', + 'thumb', + 'duration', + 'mime_type', + 'height', + 'file_unique_id', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + width: int, + height: int, + duration: int, + thumb: PhotoSize = None, + file_name: str = None, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) self.duration = duration + # Optionals self.thumb = thumb self.file_name = file_name self.mime_type = mime_type self.file_size = file_size + self.bot = bot - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Animation']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(Animation, cls).de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) - return cls(**data) + return cls(bot=bot, **data) + + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': + """Convenience wrapper over :attr:`telegram.Bot.get_file` + + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. + + Returns: + :class:`telegram.File` + + Raises: + :class:`telegram.error.TelegramError` + + """ + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/files/audio.py b/telegramer/include/telegram/files/audio.py index b743669..af6683c 100644 --- a/telegramer/include/telegram/files/audio.py +++ b/telegramer/include/telegram/files/audio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,64 +18,105 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Audio.""" -from telegram import TelegramObject, PhotoSize +from typing import TYPE_CHECKING, Any, Optional + +from telegram import PhotoSize, TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class Audio(TelegramObject): """This object represents an audio file to be treated as music by the Telegram clients. - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - duration (:obj:`int`): Duration of the audio in seconds. - performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. - file_size (:obj:`int`): Optional. File size. - thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to - which the music file belongs - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique identifier for this file. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be + the same over time and for different bots. Can't be used to download or reuse the file. duration (:obj:`int`): Duration of the audio in seconds as defined by sender. performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio tags. title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. + file_name (:obj:`str`, optional): Original filename as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. file_size (:obj:`int`, optional): File size. thumb (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to - which the music file belongs + which the music file belongs. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + duration (:obj:`int`): Duration of the audio in seconds. + performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio + tags. + title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. + file_name (:obj:`str`): Optional. Original filename as defined by sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + file_size (:obj:`int`): Optional. File size. + thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to + which the music file belongs. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - def __init__(self, - file_id, - duration, - performer=None, - title=None, - mime_type=None, - file_size=None, - thumb=None, - bot=None, - **kwargs): + __slots__ = ( + 'file_id', + 'bot', + 'file_size', + 'file_name', + 'thumb', + 'title', + 'duration', + 'performer', + 'mime_type', + 'file_unique_id', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + duration: int, + performer: str = None, + title: str = None, + mime_type: str = None, + file_size: int = None, + thumb: PhotoSize = None, + bot: 'Bot' = None, + file_name: str = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) self.duration = int(duration) # Optionals self.performer = performer self.title = title + self.file_name = file_name self.mime_type = mime_type self.file_size = file_size self.thumb = thumb self.bot = bot - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Audio']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None @@ -83,20 +124,18 @@ def de_json(cls, data, bot): return cls(bot=bot, **data) - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/files/chatphoto.py b/telegramer/include/telegram/files/chatphoto.py index a3f5171..2f55bf9 100644 --- a/telegramer/include/telegram/files/chatphoto.py +++ b/telegramer/include/telegram/files/chatphoto.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,36 +17,116 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatPhoto.""" - -# TODO: add direct download shortcuts. +from typing import TYPE_CHECKING, Any from telegram import TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class ChatPhoto(TelegramObject): """This object represents a chat photo. - Attributes: - small_file_id (:obj:`str`): Unique file identifier of small (160x160) chat photo. - big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`small_file_unique_id` and :attr:`big_file_unique_id` are + equal. Args: small_file_id (:obj:`str`): Unique file identifier of small (160x160) chat photo. This - file_id can be used only for photo download. + file_id can be used only for photo download and only for as long + as the photo is not changed. + small_file_unique_id (:obj:`str`): Unique file identifier of small (160x160) chat photo, + which is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo. This file_id - can be used only for photo download. + can be used only for photo download and only for as long as the photo is not changed. + big_file_unique_id (:obj:`str`): Unique file identifier of big (640x640) chat photo, + which is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + small_file_id (:obj:`str`): File identifier of small (160x160) chat photo. + This file_id can be used only for photo download and only for as long + as the photo is not changed. + small_file_unique_id (:obj:`str`): Unique file identifier of small (160x160) chat photo, + which is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + big_file_id (:obj:`str`): File identifier of big (640x640) chat photo. + This file_id can be used only for photo download and only for as long as + the photo is not changed. + big_file_unique_id (:obj:`str`): Unique file identifier of big (640x640) chat photo, + which is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + """ - def __init__(self, small_file_id, big_file_id, bot=None, **kwargs): + __slots__ = ( + 'big_file_unique_id', + 'bot', + 'small_file_id', + 'small_file_unique_id', + 'big_file_id', + '_id_attrs', + ) + + def __init__( + self, + small_file_id: str, + small_file_unique_id: str, + big_file_id: str, + big_file_unique_id: str, + bot: 'Bot' = None, + **_kwargs: Any, + ): self.small_file_id = small_file_id + self.small_file_unique_id = small_file_unique_id self.big_file_id = big_file_id + self.big_file_unique_id = big_file_unique_id + + self.bot = bot + + self._id_attrs = ( + self.small_file_unique_id, + self.big_file_unique_id, + ) + + def get_small_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': + """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the + small (160x160) chat photo + + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. + + Returns: + :class:`telegram.File` + + Raises: + :class:`telegram.error.TelegramError` + + """ + return self.bot.get_file( + file_id=self.small_file_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def get_big_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': + """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the + big (640x640) chat photo + + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. + + Returns: + :class:`telegram.File` - @classmethod - def de_json(cls, data, bot): - if not data: - return None + Raises: + :class:`telegram.error.TelegramError` - return cls(bot=bot, **data) + """ + return self.bot.get_file(file_id=self.big_file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/files/contact.py b/telegramer/include/telegram/files/contact.py index e3cd88c..cee769f 100644 --- a/telegramer/include/telegram/files/contact.py +++ b/telegramer/include/telegram/files/contact.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,18 +18,16 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Contact.""" +from typing import Any + from telegram import TelegramObject class Contact(TelegramObject): """This object represents a phone contact. - Attributes: - phone_number (:obj:`str`): Contact's phone number. - first_name (:obj:`str`): Contact's first name. - last_name (:obj:`str`): Optional. Contact's last name. - user_id (:obj:`int`): Optional. Contact's user identifier in Telegram. - vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`phone_number` is equal. Args: phone_number (:obj:`str`): Contact's phone number. @@ -39,10 +37,26 @@ class Contact(TelegramObject): vcard (:obj:`str`, optional): Additional data about the contact in the form of a vCard. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + phone_number (:obj:`str`): Contact's phone number. + first_name (:obj:`str`): Contact's first name. + last_name (:obj:`str`): Optional. Contact's last name. + user_id (:obj:`int`): Optional. Contact's user identifier in Telegram. + vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard. + """ - def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard=None, - **kwargs): + __slots__ = ('vcard', 'user_id', 'first_name', 'last_name', 'phone_number', '_id_attrs') + + def __init__( + self, + phone_number: str, + first_name: str, + last_name: str = None, + user_id: int = None, + vcard: str = None, + **_kwargs: Any, + ): # Required self.phone_number = str(phone_number) self.first_name = first_name @@ -52,10 +66,3 @@ def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard self.vcard = vcard self._id_attrs = (self.phone_number,) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegramer/include/telegram/files/document.py b/telegramer/include/telegram/files/document.py index f23940c..a0ffcef 100644 --- a/telegramer/include/telegram/files/document.py +++ b/telegramer/include/telegram/files/document.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,22 +18,28 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Document.""" +from typing import TYPE_CHECKING, Any, Optional + from telegram import PhotoSize, TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class Document(TelegramObject): - """This object represents a general file (as opposed to photos, voice messages and audio files). + """This object represents a general file + (as opposed to photos, voice messages and audio files). - Attributes: - file_id (:obj:`str`): Unique file identifier. - thumb (:class:`telegram.PhotoSize`): Optional. Document thumbnail. - file_name (:obj:`str`): Original filename. - mime_type (:obj:`str`): Optional. MIME type of the file. - file_size (:obj:`int`): Optional. File size. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique file identifier + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be + the same over time and for different bots. Can't be used to download or reuse the file. thumb (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by sender. file_name (:obj:`str`, optional): Original filename as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. @@ -41,19 +47,46 @@ class Document(TelegramObject): bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + file_id (:obj:`str`): File identifier. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + thumb (:class:`telegram.PhotoSize`): Optional. Document thumbnail. + file_name (:obj:`str`): Original filename. + mime_type (:obj:`str`): Optional. MIME type of the file. + file_size (:obj:`int`): Optional. File size. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ + + __slots__ = ( + 'bot', + 'file_id', + 'file_size', + 'file_name', + 'thumb', + 'mime_type', + 'file_unique_id', + '_id_attrs', + ) + _id_keys = ('file_id',) - def __init__(self, - file_id, - thumb=None, - file_name=None, - mime_type=None, - file_size=None, - bot=None, - **kwargs): + def __init__( + self, + file_id: str, + file_unique_id: str, + thumb: PhotoSize = None, + file_name: str = None, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) # Optionals self.thumb = thumb self.file_name = file_name @@ -61,33 +94,32 @@ def __init__(self, self.file_size = file_size self.bot = bot - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Document']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(Document, cls).de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/files/file.py b/telegramer/include/telegram/files/file.py index 7e35235..ca77f9d 100644 --- a/telegramer/include/telegram/files/file.py +++ b/telegramer/include/telegram/files/file.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,72 +17,101 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram File.""" +import os +import shutil +import urllib.parse as urllib_parse from base64 import b64decode from os.path import basename - -# REMREM from future.backports.urllib import parse as urllib_parse -from urlparse import urlparse as urllib_parse +from typing import IO, TYPE_CHECKING, Any, Optional, Union from telegram import TelegramObject from telegram.passport.credentials import decrypt +from telegram.utils.helpers import is_local_file + +if TYPE_CHECKING: + from telegram import Bot, FileCredentials class File(TelegramObject): """ This object represents a file ready to be downloaded. The file can be downloaded with :attr:`download`. It is guaranteed that the link will be valid for at least 1 hour. When the - link expires, a new one can be requested by calling getFile. + link expires, a new one can be requested by calling :meth:`telegram.Bot.get_file`. - Note: - Maximum file size to download is 20 MB + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - file_size (:obj:`str`): Optional. File size. - file_path (:obj:`str`): Optional. File path. Use :attr:`download` to get the file. + Note: + * Maximum file size to download is 20 MB. + * If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`, + then it will automatically be decrypted as it downloads when you call :attr:`download()`. Args: - file_id (:obj:`str`): Unique identifier for this file. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. file_size (:obj:`int`, optional): Optional. File size, if known. file_path (:obj:`str`, optional): File path. Use :attr:`download` to get the file. bot (:obj:`telegram.Bot`, optional): Bot to use with shortcut method. **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Note: - If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`, - then it will automatically be decrypted as it downloads when you call :attr:`download()`. + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + file_size (:obj:`str`): Optional. File size. + file_path (:obj:`str`): Optional. File path. Use :attr:`download` to get the file. """ - def __init__(self, file_id, bot=None, file_size=None, file_path=None, **kwargs): + __slots__ = ( + 'bot', + 'file_id', + 'file_size', + 'file_unique_id', + 'file_path', + '_credentials', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + bot: 'Bot' = None, + file_size: int = None, + file_path: str = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) - + self.file_unique_id = str(file_unique_id) # Optionals self.file_size = file_size self.file_path = file_path - self.bot = bot - self._credentials = None + self._credentials: Optional['FileCredentials'] = None - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - - def download(self, custom_path=None, out=None, timeout=None): + def download( + self, custom_path: str = None, out: IO = None, timeout: int = None + ) -> Union[str, IO]: """ Download this file. By default, the file is saved in the current working directory with its - original filename as reported by Telegram. If a :attr:`custom_path` is supplied, it will be - saved to that path instead. If :attr:`out` is defined, the file contents will be saved to - that object using the ``out.write`` method. + original filename as reported by Telegram. If the file has no filename, it the file ID will + be used as filename. If a :attr:`custom_path` is supplied, it will be saved to that path + instead. If :attr:`out` is defined, the file contents will be saved to that object using + the ``out.write`` method. Note: - :attr:`custom_path` and :attr:`out` are mutually exclusive. + * :attr:`custom_path` and :attr:`out` are mutually exclusive. + * If neither :attr:`custom_path` nor :attr:`out` is provided and :attr:`file_path` is + the path of a local file (which is the case when a Bot API Server is running in + local mode), this method will just return the path. Args: custom_path (:obj:`str`, optional): Custom path. @@ -94,7 +123,7 @@ def download(self, custom_path=None, out=None, timeout=None): Returns: :obj:`str` | :obj:`io.BufferedWriter`: The same object as :attr:`out` if specified. - Otherwise, returns the filename downloaded to. + Otherwise, returns the filename downloaded to or the file path of the local file. Raises: ValueError: If both :attr:`custom_path` and :attr:`out` are passed. @@ -103,39 +132,59 @@ def download(self, custom_path=None, out=None, timeout=None): if custom_path is not None and out is not None: raise ValueError('custom_path and out are mutually exclusive') - # Convert any UTF-8 char into a url encoded ASCII string. - url = self._get_encoded_url() + local_file = is_local_file(self.file_path) + + if local_file: + url = self.file_path + else: + # Convert any UTF-8 char into a url encoded ASCII string. + url = self._get_encoded_url() if out: - buf = self.bot.request.retrieve(url) - if self._credentials: - buf = decrypt(b64decode(self._credentials.secret), - b64decode(self._credentials.hash), - buf) + if local_file: + with open(url, 'rb') as file: + buf = file.read() + else: + buf = self.bot.request.retrieve(url) + if self._credentials: + buf = decrypt( + b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf + ) out.write(buf) return out + + if custom_path and local_file: + shutil.copyfile(self.file_path, custom_path) + return custom_path + + if custom_path: + filename = custom_path + elif local_file: + return self.file_path + elif self.file_path: + filename = basename(self.file_path) else: - if custom_path: - filename = custom_path - else: - filename = basename(self.file_path) - - buf = self.bot.request.retrieve(url, timeout=timeout) - if self._credentials: - buf = decrypt(b64decode(self._credentials.secret), - b64decode(self._credentials.hash), - buf) - with open(filename, 'wb') as fobj: - fobj.write(buf) - return filename - - def _get_encoded_url(self): + filename = os.path.join(os.getcwd(), self.file_id) + + buf = self.bot.request.retrieve(url, timeout=timeout) + if self._credentials: + buf = decrypt( + b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf + ) + with open(filename, 'wb') as fobj: + fobj.write(buf) + return filename + + def _get_encoded_url(self) -> str: """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" sres = urllib_parse.urlsplit(self.file_path) - return urllib_parse.urlunsplit(urllib_parse.SplitResult( - sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment)) + return urllib_parse.urlunsplit( + urllib_parse.SplitResult( + sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment + ) + ) - def download_as_bytearray(self, buf=None): + def download_as_bytearray(self, buf: bytearray = None) -> bytes: """Download this file and return it as a bytearray. Args: @@ -148,9 +197,17 @@ def download_as_bytearray(self, buf=None): """ if buf is None: buf = bytearray() - - buf.extend(self.bot.request.retrieve(self._get_encoded_url())) + if is_local_file(self.file_path): + with open(self.file_path, "rb") as file: + buf.extend(file.read()) + else: + buf.extend(self.bot.request.retrieve(self._get_encoded_url())) return buf - def set_credentials(self, credentials): + def set_credentials(self, credentials: 'FileCredentials') -> None: + """Sets the passport credentials for the file. + + Args: + credentials (:class:`telegram.FileCredentials`): The credentials. + """ self._credentials = credentials diff --git a/telegramer/include/telegram/files/inputfile.py b/telegramer/include/telegram/files/inputfile.py index a328ad8..2c3196f 100644 --- a/telegramer/include/telegram/files/inputfile.py +++ b/telegramer/include/telegram/files/inputfile.py @@ -2,7 +2,7 @@ # pylint: disable=W0622,E0611 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -20,26 +20,24 @@ """This module contains an object that represents a Telegram InputFile.""" import imghdr +import logging import mimetypes import os -import sys +from typing import IO, Optional, Tuple, Union from uuid import uuid4 -from telegram import TelegramError +from telegram.utils.deprecate import set_new_attribute_deprecated DEFAULT_MIME_TYPE = 'application/octet-stream' +logger = logging.getLogger(__name__) -class InputFile(object): +class InputFile: """This object represents a Telegram InputFile. - Attributes: - input_file_content (:obj:`bytes`): The binaray content of the file to send. - filename (:obj:`str`): Optional, Filename for the file to be sent. - attach (:obj:`str`): Optional, attach id for sending multiple files. - Args: - obj (:obj:`File handler`): An open file descriptor. + obj (:obj:`File handler` | :obj:`bytes`): An open file descriptor or the files content as + bytes. filename (:obj:`str`, optional): Filename for this InputFile. attach (:obj:`bool`, optional): Whether this should be send as one file or is part of a collection of files. @@ -47,62 +45,75 @@ class InputFile(object): Raises: TelegramError + Attributes: + input_file_content (:obj:`bytes`): The binary content of the file to send. + filename (:obj:`str`): Optional. Filename for the file to be sent. + attach (:obj:`str`): Optional. Attach id for sending multiple files. + """ - def __init__(self, obj, filename=None, attach=None): + __slots__ = ('filename', 'attach', 'input_file_content', 'mimetype', '__dict__') + + def __init__(self, obj: Union[IO, bytes], filename: str = None, attach: bool = None): self.filename = None - self.input_file_content = obj.read() + if isinstance(obj, bytes): + self.input_file_content = obj + else: + self.input_file_content = obj.read() self.attach = 'attached' + uuid4().hex if attach else None if filename: self.filename = filename - elif (hasattr(obj, 'name') and - not isinstance(obj.name, int) and # py3 - obj.name != ''): # py2 - # on py2.7, pylint fails to understand this properly - # pylint: disable=E1101 - self.filename = os.path.basename(obj.name) - - try: - self.mimetype = self.is_image(self.input_file_content) - except TelegramError: - if self.filename: - self.mimetype = mimetypes.guess_type( - self.filename)[0] or DEFAULT_MIME_TYPE - else: - self.mimetype = DEFAULT_MIME_TYPE - if not self.filename or '.' not in self.filename: + elif hasattr(obj, 'name') and not isinstance(obj.name, int): # type: ignore[union-attr] + self.filename = os.path.basename(obj.name) # type: ignore[union-attr] + + image_mime_type = self.is_image(self.input_file_content) + if image_mime_type: + self.mimetype = image_mime_type + elif self.filename: + self.mimetype = mimetypes.guess_type(self.filename)[0] or DEFAULT_MIME_TYPE + else: + self.mimetype = DEFAULT_MIME_TYPE + + if not self.filename: self.filename = self.mimetype.replace('/', '.') - if sys.version_info < (3,): - if isinstance(self.filename, unicode): # flake8: noqa pylint: disable=E0602 - self.filename = self.filename.encode('utf-8', 'replace') + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) @property - def field_tuple(self): + def field_tuple(self) -> Tuple[str, bytes, str]: # skipcq: PY-D0003 return self.filename, self.input_file_content, self.mimetype @staticmethod - def is_image(stream): + def is_image(stream: bytes) -> Optional[str]: """Check if the content file is an image by analyzing its headers. Args: - stream (:obj:`str`): A str representing the content of a file. + stream (:obj:`bytes`): A byte stream representing the content of a file. Returns: - :obj:`str`: The str mime-type of an image. + :obj:`str` | :obj:`None`: The mime-type of an image, if the input is an image, or + :obj:`None` else. """ - image = imghdr.what(None, stream) - if image: - return 'image/%s' % image - - raise TelegramError('Could not parse file content') + try: + image = imghdr.what(None, stream) + if image: + return f'image/{image}' + return None + except Exception: + logger.debug( + "Could not parse file content. Assuming that file is not an image.", exc_info=True + ) + return None @staticmethod - def is_file(obj): + def is_file(obj: object) -> bool: # skipcq: PY-D0003 return hasattr(obj, 'read') - def to_dict(self): + def to_dict(self) -> Optional[str]: + """See :meth:`telegram.TelegramObject.to_dict`.""" if self.attach: return 'attach://' + self.attach + return None diff --git a/telegramer/include/telegram/files/inputmedia.py b/telegramer/include/telegram/files/inputmedia.py index b639872..c4899fb 100644 --- a/telegramer/include/telegram/files/inputmedia.py +++ b/telegramer/include/telegram/files/inputmedia.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,20 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """Base class for Telegram InputMedia Objects.""" -from telegram import TelegramObject, InputFile, PhotoSize, Animation, Video, Audio, Document +from typing import Union, List, Tuple + +from telegram import ( + Animation, + Audio, + Document, + InputFile, + PhotoSize, + TelegramObject, + Video, + MessageEntity, +) +from telegram.utils.helpers import DEFAULT_NONE, parse_file_input +from telegram.utils.types import FileInput, JSONDict, ODVInput class InputMedia(TelegramObject): @@ -29,75 +42,119 @@ class InputMedia(TelegramObject): :class:`telegram.InputMediaVideo` for detailed use. """ - pass + + __slots__ = () + caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...], None] = None + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + if self.caption_entities: + data['caption_entities'] = [ + ce.to_dict() for ce in self.caption_entities # pylint: disable=E1133 + ] + + return data class InputMediaAnimation(InputMedia): """Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. - Attributes: - type (:obj:`str`): ``animation``. - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Animation` object to send. - thumb (`filelike object`): Optional. Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. - caption (:obj:`str`): Optional. Caption of the animation to be sent, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - width (:obj:`int`): Optional. Animation width. - height (:obj:`int`): Optional. Animation height. - duration (:obj:`int`): Optional. Animation duration. - + Note: + When using a :class:`telegram.Animation` for the :attr:`media` attribute. It will take the + width, height and duration from that video, unless otherwise specified with the optional + arguments. Args: - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Animation` object to send. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. - caption (:obj:`str`, optional): Caption of the animation to be sent, 0-200 characters. + media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Animation`): File to send. Pass a + file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP + URL for Telegram to get a file from the Internet. Lastly you can pass an existing + :class:`telegram.Animation` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the animation, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of + the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + caption (:obj:`str`, optional): Caption of the animation to be sent, 0-1024 characters + after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of parse_mode. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. duration (:obj:`int`, optional): Animation duration. - Note: - When using a :class:`telegram.Animation` for the :attr:`media` attribute. It will take the - width, height and duration from that video, unless otherwise specified with the optional - arguments. + Attributes: + type (:obj:`str`): ``animation``. + media (:obj:`str` | :class:`telegram.InputFile`): Animation to send. + caption (:obj:`str`): Optional. Caption of the document to be sent. + parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption. + thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. + width (:obj:`int`): Optional. Animation width. + height (:obj:`int`): Optional. Animation height. + duration (:obj:`int`): Optional. Animation duration. + """ - def __init__(self, media, thumb=None, caption=None, parse_mode=None, width=None, height=None, - duration=None): + __slots__ = ( + 'caption_entities', + 'width', + 'media', + 'thumb', + 'caption', + 'duration', + 'parse_mode', + 'height', + 'type', + ) + + def __init__( + self, + media: Union[FileInput, Animation], + thumb: FileInput = None, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + width: int = None, + height: int = None, + duration: int = None, + caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, + ): self.type = 'animation' if isinstance(media, Animation): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id self.width = media.width self.height = media.height self.duration = media.duration - elif InputFile.is_file(media): - self.media = InputFile(media, attach=True) else: - self.media = media + self.media = parse_file_input(media, attach=True, filename=filename) if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption - if parse_mode: - self.parse_mode = parse_mode + self.parse_mode = parse_mode + self.caption_entities = caption_entities if width: self.width = width if height: @@ -109,111 +166,163 @@ def __init__(self, media, thumb=None, caption=None, parse_mode=None, width=None, class InputMediaPhoto(InputMedia): """Represents a photo to be sent. - Attributes: - type (:obj:`str`): ``photo``. - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the - Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the - Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. - caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - Args: - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the - Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the - Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. - caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-200 characters. + media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.PhotoSize`): File to send. Pass a + file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP + URL for Telegram to get a file from the Internet. Lastly you can pass an existing + :class:`telegram.PhotoSize` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the photo, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-1024 characters after + entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of parse_mode. + + Attributes: + type (:obj:`str`): ``photo``. + media (:obj:`str` | :class:`telegram.InputFile`): Photo to send. + caption (:obj:`str`): Optional. Caption of the document to be sent. + parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption. + """ - def __init__(self, media, caption=None, parse_mode=None): - self.type = 'photo' + __slots__ = ('caption_entities', 'media', 'caption', 'parse_mode', 'type') - if isinstance(media, PhotoSize): - self.media = media.file_id - elif InputFile.is_file(media): - self.media = InputFile(media, attach=True) - else: - self.media = media + def __init__( + self, + media: Union[FileInput, PhotoSize], + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, + ): + self.type = 'photo' + self.media = parse_file_input(media, PhotoSize, attach=True, filename=filename) if caption: self.caption = caption - if parse_mode: - self.parse_mode = parse_mode + self.parse_mode = parse_mode + self.caption_entities = caption_entities class InputMediaVideo(InputMedia): """Represents a video to be sent. - Attributes: - type (:obj:`str`): ``video``. - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Video` object to send. - caption (:obj:`str`): Optional. Caption of the video to be sent, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - width (:obj:`int`): Optional. Video width. - height (:obj:`int`): Optional. Video height. - duration (:obj:`int`): Optional. Video duration. - supports_streaming (:obj:`bool`): Optional. Pass True, if the uploaded video is suitable - for streaming. - thumb (`filelike object`): Optional. Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + Note: + * When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the + width, height and duration from that video, unless otherwise specified with the optional + arguments. + * ``thumb`` will be ignored for small video files, for which Telegram can easily + generate thumb nails. However, this behaviour is undocumented and might be changed + by Telegram. Args: - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Video` object to send. - caption (:obj:`str`, optional): Caption of the video to be sent, 0-200 characters. + media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Video`): File to send. Pass a + file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP + URL for Telegram to get a file from the Internet. Lastly you can pass an existing + :class:`telegram.Video` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the video, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after + entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of parse_mode. width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. duration (:obj:`int`, optional): Video duration. - supports_streaming (:obj:`bool`, optional): Pass True, if the uploaded video is suitable - for streaming. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is + suitable for streaming. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of + the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + Attributes: + type (:obj:`str`): ``video``. + media (:obj:`str` | :class:`telegram.InputFile`): Video file to send. + caption (:obj:`str`): Optional. Caption of the document to be sent. + parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption. + width (:obj:`int`): Optional. Video width. + height (:obj:`int`): Optional. Video height. + duration (:obj:`int`): Optional. Video duration. + supports_streaming (:obj:`bool`): Optional. Pass :obj:`True`, if the uploaded video is + suitable for streaming. + thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. - Note: - When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the - width, height and duration from that video, unless otherwise specified with the optional - arguments. """ - def __init__(self, media, caption=None, width=None, height=None, duration=None, - supports_streaming=None, parse_mode=None, thumb=None): + __slots__ = ( + 'caption_entities', + 'width', + 'media', + 'thumb', + 'supports_streaming', + 'caption', + 'duration', + 'parse_mode', + 'height', + 'type', + ) + + def __init__( + self, + media: Union[FileInput, Video], + caption: str = None, + width: int = None, + height: int = None, + duration: int = None, + supports_streaming: bool = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, + ): self.type = 'video' if isinstance(media, Video): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id self.width = media.width self.height = media.height self.duration = media.duration - elif InputFile.is_file(media): - self.media = InputFile(media, attach=True) else: - self.media = media + self.media = parse_file_input(media, attach=True, filename=filename) if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption - if parse_mode: - self.parse_mode = parse_mode + self.parse_mode = parse_mode + self.caption_entities = caption_entities if width: self.width = width if height: @@ -227,70 +336,103 @@ def __init__(self, media, caption=None, width=None, height=None, duration=None, class InputMediaAudio(InputMedia): """Represents an audio file to be treated as music to be sent. - Attributes: - type (:obj:`str`): ``audio``. - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Audio` object to send. - caption (:obj:`str`): Optional. Caption of the audio to be sent, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - duration (:obj:`int`): Duration of the audio in seconds. - performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. - thumb (`filelike object`): Optional. Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + Note: + When using a :class:`telegram.Audio` for the :attr:`media` attribute. It will take the + duration, performer and title from that video, unless otherwise specified with the + optional arguments. Args: - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Document` object to send. - caption (:obj:`str`, optional): Caption of the audio to be sent, 0-200 characters. + media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Audio`): + File to send. Pass a + file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP + URL for Telegram to get a file from the Internet. Lastly you can pass an existing + :class:`telegram.Audio` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the audio, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + caption (:obj:`str`, optional): Caption of the audio to be sent, 0-1024 characters after + entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of parse_mode. duration (:obj:`int`): Duration of the audio in seconds as defined by sender. performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio tags. title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of + the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + Attributes: + type (:obj:`str`): ``audio``. + media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send. + caption (:obj:`str`): Optional. Caption of the document to be sent. + parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption. + duration (:obj:`int`): Duration of the audio in seconds. + performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio + tags. + title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. + thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. - Note: - When using a :class:`telegram.Audio` for the :attr:`media` attribute. It will take the - duration, performer and title from that video, unless otherwise specified with the - optional arguments. """ - def __init__(self, media, thumb=None, caption=None, parse_mode=None, - duration=None, performer=None, title=None): + __slots__ = ( + 'caption_entities', + 'media', + 'thumb', + 'caption', + 'title', + 'duration', + 'type', + 'parse_mode', + 'performer', + ) + + def __init__( + self, + media: Union[FileInput, Audio], + thumb: FileInput = None, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + duration: int = None, + performer: str = None, + title: str = None, + caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, + ): self.type = 'audio' if isinstance(media, Audio): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id self.duration = media.duration self.performer = media.performer self.title = media.title - elif InputFile.is_file(media): - self.media = InputFile(media, attach=True) else: - self.media = media + self.media = parse_file_input(media, attach=True, filename=filename) if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption - if parse_mode: - self.parse_mode = parse_mode + self.parse_mode = parse_mode + self.caption_entities = caption_entities if duration: self.duration = duration if performer: @@ -302,50 +444,82 @@ def __init__(self, media, thumb=None, caption=None, parse_mode=None, class InputMediaDocument(InputMedia): """Represents a general file to be sent. - Attributes: - type (:obj:`str`): ``document``. - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Document` object to send. - caption (:obj:`str`): Optional. Caption of the document to be sent, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - thumb (`filelike object`): Optional. Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. - Args: - media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. - Lastly you can pass an existing :class:`telegram.Document` object to send. - caption (:obj:`str`, optional): Caption of the document to be sent, 0-200 characters. + media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Document`): File to send. Pass a + file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP + URL for Telegram to get a file from the Internet. Lastly you can pass an existing + :class:`telegram.Document` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + filename (:obj:`str`, optional): Custom file name for the document, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters after + entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - thumb (`filelike object`, optional): Thumbnail of the - file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail's width and height should not exceed 90. Ignored if the file is not - is passed as a string or file_id. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of parse_mode. + thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of + the file sent; can be ignored if + thumbnail generation for the file is supported server-side. The thumbnail should be + in JPEG format and less than 200 kB in size. A thumbnail's width and height should + not exceed 320. Ignored if the file is not uploaded using multipart/form-data. + Thumbnails can't be reused and can be only uploaded as a new file. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side + content type detection for files uploaded using multipart/form-data. Always true, if + the document is sent as part of an album. + + Attributes: + type (:obj:`str`): ``document``. + media (:obj:`str` | :class:`telegram.InputFile`): File to send. + caption (:obj:`str`): Optional. Caption of the document to be sent. + parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption. + thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. + disable_content_type_detection (:obj:`bool`): Optional. Disables automatic server-side + content type detection for files uploaded using multipart/form-data. Always true, if + the document is sent as part of an album. + """ - def __init__(self, media, thumb=None, caption=None, parse_mode=None): + __slots__ = ( + 'caption_entities', + 'media', + 'thumb', + 'caption', + 'parse_mode', + 'type', + 'disable_content_type_detection', + ) + + def __init__( + self, + media: Union[FileInput, Document], + thumb: FileInput = None, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_content_type_detection: bool = None, + caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, + ): self.type = 'document' - - if isinstance(media, Document): - self.media = media.file_id - elif InputFile.is_file(media): - self.media = InputFile(media, attach=True) - else: - self.media = media + self.media = parse_file_input(media, Document, attach=True, filename=filename) if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption - if parse_mode: - self.parse_mode = parse_mode + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.disable_content_type_detection = disable_content_type_detection diff --git a/telegramer/include/telegram/files/location.py b/telegramer/include/telegram/files/location.py index f80c8e7..a5f5065 100644 --- a/telegramer/include/telegram/files/location.py +++ b/telegramer/include/telegram/files/location.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,33 +18,74 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Location.""" +from typing import Any + from telegram import TelegramObject class Location(TelegramObject): """This object represents a point on the map. - Attributes: - longitude (:obj:`float`): Longitude as defined by sender. - latitude (:obj:`float`): Latitude as defined by sender. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`longitute` and :attr:`latitude` are equal. Args: longitude (:obj:`float`): Longitude as defined by sender. latitude (:obj:`float`): Latitude as defined by sender. + horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, + measured in meters; 0-1500. + live_period (:obj:`int`, optional): Time relative to the message sending date, during which + the location can be updated, in seconds. For active live locations only. + heading (:obj:`int`, optional): The direction in which user is moving, in degrees; 1-360. + For active live locations only. + proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts about + approaching another chat member, in meters. For sent live locations only. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + longitude (:obj:`float`): Longitude as defined by sender. + latitude (:obj:`float`): Latitude as defined by sender. + horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, + measured in meters. + live_period (:obj:`int`): Optional. Time relative to the message sending date, during which + the location can be updated, in seconds. For active live locations only. + heading (:obj:`int`): Optional. The direction in which user is moving, in degrees. + For active live locations only. + proximity_alert_radius (:obj:`int`): Optional. Maximum distance for proximity alerts about + approaching another chat member, in meters. For sent live locations only. + """ - def __init__(self, longitude, latitude, **kwargs): + __slots__ = ( + 'longitude', + 'horizontal_accuracy', + 'proximity_alert_radius', + 'live_period', + 'latitude', + 'heading', + '_id_attrs', + ) + + def __init__( + self, + longitude: float, + latitude: float, + horizontal_accuracy: float = None, + live_period: int = None, + heading: int = None, + proximity_alert_radius: int = None, + **_kwargs: Any, + ): # Required self.longitude = float(longitude) self.latitude = float(latitude) - self._id_attrs = (self.longitude, self.latitude) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None + # Optionals + self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None + self.live_period = int(live_period) if live_period else None + self.heading = int(heading) if heading else None + self.proximity_alert_radius = ( + int(proximity_alert_radius) if proximity_alert_radius else None + ) - return cls(**data) + self._id_attrs = (self.longitude, self.latitude) diff --git a/telegramer/include/telegram/files/photosize.py b/telegramer/include/telegram/files/photosize.py index 60440a0..9a7988b 100644 --- a/telegramer/include/telegram/files/photosize.py +++ b/telegramer/include/telegram/files/photosize.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,72 +18,81 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram PhotoSize.""" +from typing import TYPE_CHECKING, Any + from telegram import TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class PhotoSize(TelegramObject): """This object represents one size of a photo or a file/sticker thumbnail. - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - width (:obj:`int`): Photo width. - height (:obj:`int`): Photo height. - file_size (:obj:`int`): Optional. File size. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique identifier for this file. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. width (:obj:`int`): Photo width. height (:obj:`int`): Photo height. file_size (:obj:`int`, optional): File size. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + width (:obj:`int`): Photo width. + height (:obj:`int`): Photo height. + file_size (:obj:`int`): Optional. File size. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - def __init__(self, file_id, width, height, file_size=None, bot=None, **kwargs): + __slots__ = ('bot', 'width', 'file_id', 'file_size', 'height', 'file_unique_id', '_id_attrs') + + def __init__( + self, + file_id: str, + file_unique_id: str, + width: int, + height: int, + file_size: int = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) # Optionals self.file_size = file_size self.bot = bot - self._id_attrs = (self.file_id,) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - - @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - photos = list() - for photo in data: - photos.append(cls.de_json(photo, bot)) - - return photos + self._id_attrs = (self.file_unique_id,) - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/files/sticker.py b/telegramer/include/telegram/files/sticker.py index e42509d..e3f22a9 100644 --- a/telegramer/include/telegram/files/sticker.py +++ b/telegramer/include/telegram/files/sticker.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,30 +18,40 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects that represents stickers.""" -from telegram import PhotoSize, TelegramObject +from typing import TYPE_CHECKING, Any, List, Optional, ClassVar + +from telegram import PhotoSize, TelegramObject, constants +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class Sticker(TelegramObject): """This object represents a sticker. - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - width (:obj:`int`): Sticker width. - height (:obj:`int`): Sticker height. - thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg - format. - emoji (:obj:`str`): Optional. Emoji associated with the sticker. - set_name (:obj:`str`): Optional. Name of the sticker set to which the sticker belongs. - mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position - where the mask should be placed. - file_size (:obj:`int`): Optional. File size. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. + + Note: + As of v13.11 ``is_video`` is a required argument and therefore the order of the + arguments had to be changed. Use keyword arguments to make sure that the arguments are + passed correctly. Args: - file_id (:obj:`str`): Unique identifier for this file. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. width (:obj:`int`): Sticker width. height (:obj:`int`): Sticker height. - thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .webp or .jpg + is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated. + is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker. + + .. versionadded:: 13.11 + thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .WEBP or .JPG format. emoji (:obj:`str`, optional): Emoji associated with the sticker set_name (:obj:`str`, optional): Name of the sticker set to which the sticker @@ -49,26 +59,70 @@ class Sticker(TelegramObject): mask_position (:class:`telegram.MaskPosition`, optional): For mask stickers, the position where the mask should be placed. file_size (:obj:`int`, optional): File size. - **kwargs (obj:`dict`): Arbitrary keyword arguments.7 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + **kwargs (obj:`dict`): Arbitrary keyword arguments. + + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + width (:obj:`int`): Sticker width. + height (:obj:`int`): Sticker height. + is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated. + is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker. + + .. versionadded:: 13.11 + thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg + format. + emoji (:obj:`str`): Optional. Emoji associated with the sticker. + set_name (:obj:`str`): Optional. Name of the sticker set to which the sticker belongs. + mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position + where the mask should be placed. + file_size (:obj:`int`): Optional. File size. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ - def __init__(self, - file_id, - width, - height, - thumb=None, - emoji=None, - file_size=None, - set_name=None, - mask_position=None, - bot=None, - **kwargs): + __slots__ = ( + 'bot', + 'width', + 'file_id', + 'is_animated', + 'is_video', + 'file_size', + 'thumb', + 'set_name', + 'mask_position', + 'height', + 'file_unique_id', + 'emoji', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + width: int, + height: int, + is_animated: bool, + is_video: bool, + thumb: PhotoSize = None, + emoji: str = None, + file_size: int = None, + set_name: str = None, + mask_position: 'MaskPosition' = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) + self.is_animated = is_animated + self.is_video = is_video # Optionals self.thumb = thumb self.emoji = emoji @@ -77,84 +131,122 @@ def __init__(self, self.mask_position = mask_position self.bot = bot - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Sticker']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(Sticker, cls).de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['mask_position'] = MaskPosition.de_json(data.get('mask_position'), bot) return cls(bot=bot, **data) - @classmethod - def de_list(cls, data, bot): - if not data: - return list() - - return [cls.de_json(d, bot) for d in data] - - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) class StickerSet(TelegramObject): """This object represents a sticker set. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` is equal. + + Note: + As of v13.11 ``is_video`` is a required argument and therefore the order of the + arguments had to be changed. Use keyword arguments to make sure that the arguments are + passed correctly. + + Args: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. - contains_masks (:obj:`bool`): True, if the sticker set contains masks. + is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. + is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. + + .. versionadded:: 13.11 + contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``, + ``.TGS``, or ``.WEBM`` format. - Args: + Attributes: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. - contains_masks (:obj:`bool`): True, if the sticker set contains masks. + is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. + is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. + + .. versionadded:: 13.11 + contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``, + ``.TGS`` or ``.WEBM`` format. """ - def __init__(self, name, title, contains_masks, stickers, bot=None, **kwargs): + __slots__ = ( + 'is_animated', + 'is_video', + 'contains_masks', + 'thumb', + 'title', + 'stickers', + 'name', + '_id_attrs', + ) + + def __init__( + self, + name: str, + title: str, + is_animated: bool, + contains_masks: bool, + stickers: List[Sticker], + is_video: bool, + thumb: PhotoSize = None, + **_kwargs: Any, + ): self.name = name self.title = title + self.is_animated = is_animated + self.is_video = is_video self.contains_masks = contains_masks self.stickers = stickers + # Optionals + self.thumb = thumb self._id_attrs = (self.name,) - @staticmethod - def de_json(data, bot): + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['StickerSet']: + """See :meth:`telegram.TelegramObject.de_json`.""" if not data: return None - data = super(StickerSet, StickerSet).de_json(data, bot) - + data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['stickers'] = Sticker.de_list(data.get('stickers'), bot) - return StickerSet(bot=bot, **data) + return cls(bot=bot, **data) - def to_dict(self): - data = super(StickerSet, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['stickers'] = [s.to_dict() for s in data.get('stickers')] @@ -164,20 +256,26 @@ def to_dict(self): class MaskPosition(TelegramObject): """This object describes the position on faces where a mask should be placed by default. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`point`, :attr:`x_shift`, :attr:`y_shift` and, :attr:`scale` + are equal. + Attributes: point (:obj:`str`): The part of the face relative to which the mask should be placed. + One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'chin'``. x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face size, from left to right. y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face size, from top to bottom. scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size. - Notes: + Note: :attr:`type` should be one of the following: `forehead`, `eyes`, `mouth` or `chin`. You can - use the classconstants for those. + use the class constants for those. Args: point (:obj:`str`): The part of the face relative to which the mask should be placed. + One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'chin'``. x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face size, from left to right. For example, choosing -1.0 will place mask just to the left of the default mask position. @@ -187,23 +285,31 @@ class MaskPosition(TelegramObject): scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size. """ - FOREHEAD = 'forehead' - """:obj:`str`: 'forehead'""" - EYES = 'eyes' - """:obj:`str`: 'eyes'""" - MOUTH = 'mouth' - """:obj:`str`: 'mouth'""" - CHIN = 'chin' - """:obj:`str`: 'chin'""" - - def __init__(self, point, x_shift, y_shift, scale, **kwargs): + + __slots__ = ('point', 'scale', 'x_shift', 'y_shift', '_id_attrs') + + FOREHEAD: ClassVar[str] = constants.STICKER_FOREHEAD + """:const:`telegram.constants.STICKER_FOREHEAD`""" + EYES: ClassVar[str] = constants.STICKER_EYES + """:const:`telegram.constants.STICKER_EYES`""" + MOUTH: ClassVar[str] = constants.STICKER_MOUTH + """:const:`telegram.constants.STICKER_MOUTH`""" + CHIN: ClassVar[str] = constants.STICKER_CHIN + """:const:`telegram.constants.STICKER_CHIN`""" + + def __init__(self, point: str, x_shift: float, y_shift: float, scale: float, **_kwargs: Any): self.point = point self.x_shift = x_shift self.y_shift = y_shift self.scale = scale + self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale) + @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MaskPosition']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if data is None: return None diff --git a/telegramer/include/telegram/files/venue.py b/telegramer/include/telegram/files/venue.py index 27779ea..aad46db 100644 --- a/telegramer/include/telegram/files/venue.py +++ b/telegramer/include/telegram/files/venue.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,19 +18,24 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Venue.""" -from telegram import TelegramObject, Location +from typing import TYPE_CHECKING, Any, Optional + +from telegram import Location, TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class Venue(TelegramObject): """This object represents a venue. - Attributes: - location (:class:`telegram.Location`): Venue location. - title (:obj:`str`): Name of the venue. - address (:obj:`str`): Address of the venue. - foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue. - foursquare_type (:obj:`str`): Optional. Foursquare type of the venue. (For example, - "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`location` and :attr:`title` are equal. + + Note: + Foursquare details and Google Pace details are mutually exclusive. However, this + behaviour is undocumented and might be changed by Telegram. Args: location (:class:`telegram.Location`): Venue location. @@ -39,12 +44,44 @@ class Venue(TelegramObject): foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue. foursquare_type (:obj:`str`, optional): Foursquare type of the venue. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) + google_place_id (:obj:`str`, optional): Google Places identifier of the venue. + google_place_type (:obj:`str`, optional): Google Places type of the venue. (See + `supported types `_.) **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + location (:class:`telegram.Location`): Venue location. + title (:obj:`str`): Name of the venue. + address (:obj:`str`): Address of the venue. + foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue. + foursquare_type (:obj:`str`): Optional. Foursquare type of the venue. + google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. + google_place_type (:obj:`str`): Optional. Google Places type of the venue. + """ - def __init__(self, location, title, address, foursquare_id=None, foursquare_type=None, - **kwargs): + __slots__ = ( + 'google_place_type', + 'location', + 'title', + 'address', + 'foursquare_type', + 'foursquare_id', + 'google_place_id', + '_id_attrs', + ) + + def __init__( + self, + location: Location, + title: str, + address: str, + foursquare_id: str = None, + foursquare_type: str = None, + google_place_id: str = None, + google_place_type: str = None, + **_kwargs: Any, + ): # Required self.location = location self.title = title @@ -52,12 +89,15 @@ def __init__(self, location, title, address, foursquare_id=None, foursquare_type # Optionals self.foursquare_id = foursquare_id self.foursquare_type = foursquare_type + self.google_place_id = google_place_id + self.google_place_type = google_place_type self._id_attrs = (self.location, self.title) @classmethod - def de_json(cls, data, bot): - data = super(Venue, cls).de_json(data, bot) + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Venue']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) if not data: return None diff --git a/telegramer/include/telegram/files/video.py b/telegramer/include/telegram/files/video.py index 8acc4bb..92e7c5e 100644 --- a/telegramer/include/telegram/files/video.py +++ b/telegramer/include/telegram/files/video.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,83 +18,121 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Video.""" +from typing import TYPE_CHECKING, Any, Optional + from telegram import PhotoSize, TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class Video(TelegramObject): """This object represents a video file. - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail. - mime_type (:obj:`str`): Optional. Mime type of a file as defined by sender. - file_size (:obj:`int`): Optional. File size. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique identifier for this file. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. width (:obj:`int`): Video width as defined by sender. height (:obj:`int`): Video height as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail. + file_name (:obj:`str`, optional): Original filename as defined by sender. mime_type (:obj:`str`, optional): Mime type of a file as defined by sender. file_size (:obj:`int`, optional): File size. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + width (:obj:`int`): Video width as defined by sender. + height (:obj:`int`): Video height as defined by sender. + duration (:obj:`int`): Duration of the video in seconds as defined by sender. + thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail. + file_name (:obj:`str`): Optional. Original filename as defined by sender. + mime_type (:obj:`str`): Optional. Mime type of a file as defined by sender. + file_size (:obj:`int`): Optional. File size. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - def __init__(self, - file_id, - width, - height, - duration, - thumb=None, - mime_type=None, - file_size=None, - bot=None, - **kwargs): + __slots__ = ( + 'bot', + 'width', + 'file_id', + 'file_size', + 'file_name', + 'thumb', + 'duration', + 'mime_type', + 'height', + 'file_unique_id', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + width: int, + height: int, + duration: int, + thumb: PhotoSize = None, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + file_name: str = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) self.duration = int(duration) # Optionals self.thumb = thumb + self.file_name = file_name self.mime_type = mime_type self.file_size = file_size self.bot = bot - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Video']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(Video, cls).de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/files/videonote.py b/telegramer/include/telegram/files/videonote.py index 5201a58..17d6207 100644 --- a/telegramer/include/telegram/files/videonote.py +++ b/telegramer/include/telegram/files/videonote.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,34 +18,74 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram VideoNote.""" +from typing import TYPE_CHECKING, Optional, Any + from telegram import PhotoSize, TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class VideoNote(TelegramObject): """This object represents a video message (available in Telegram apps as of v.4.0). - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - length (:obj:`int`): Video width and height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail. - file_size (:obj:`int`): Optional. File size. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique identifier for this file. - length (:obj:`int`): Video width and height as defined by sender. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + length (:obj:`int`): Video width and height (diameter of the video message) as defined + by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail. file_size (:obj:`int`, optional): File size. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + length (:obj:`int`): Video width and height as defined by sender. + duration (:obj:`int`): Duration of the video in seconds as defined by sender. + thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail. + file_size (:obj:`int`): Optional. File size. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - def __init__(self, file_id, length, duration, thumb=None, file_size=None, bot=None, **kwargs): + __slots__ = ( + 'bot', + 'length', + 'file_id', + 'file_size', + 'thumb', + 'duration', + 'file_unique_id', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + length: int, + duration: int, + thumb: PhotoSize = None, + file_size: int = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) self.length = int(length) self.duration = int(duration) # Optionals @@ -53,33 +93,32 @@ def __init__(self, file_id, length, duration, thumb=None, file_size=None, bot=No self.file_size = file_size self.bot = bot - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['VideoNote']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(VideoNote, cls).de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/files/voice.py b/telegramer/include/telegram/files/voice.py index 41d5bc7..c878282 100644 --- a/telegramer/include/telegram/files/voice.py +++ b/telegramer/include/telegram/files/voice.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,63 +18,89 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Voice.""" +from typing import TYPE_CHECKING, Any + from telegram import TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File class Voice(TelegramObject): """This object represents a voice note. - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. - file_size (:obj:`int`): Optional. File size. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique identifier for this file. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. duration (:obj:`int`, optional): Duration of the audio in seconds as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. file_size (:obj:`int`, optional): File size. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + duration (:obj:`int`): Duration of the audio in seconds as defined by sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + file_size (:obj:`int`): Optional. File size. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - def __init__(self, file_id, duration, mime_type=None, file_size=None, bot=None, **kwargs): + __slots__ = ( + 'bot', + 'file_id', + 'file_size', + 'duration', + 'mime_type', + 'file_unique_id', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + duration: int, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.file_id = str(file_id) + self.file_unique_id = str(file_unique_id) self.duration = int(duration) # Optionals self.mime_type = mime_type self.file_size = file_size self.bot = bot - self._id_attrs = (self.file_id,) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - data = super(Voice, cls).de_json(data, bot) - - return cls(bot=bot, **data) + self._id_attrs = (self.file_unique_id,) - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegramer/include/telegram/forcereply.py b/telegramer/include/telegram/forcereply.py index fa89f31..792b211 100644 --- a/telegramer/include/telegram/forcereply.py +++ b/telegramer/include/telegram/forcereply.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,6 +18,8 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ForceReply.""" +from typing import Any + from telegram import ReplyMarkup @@ -28,24 +30,49 @@ class ForceReply(ReplyMarkup): extremely useful if you want to create user-friendly step-by-step interfaces without having to sacrifice privacy mode. - Attributes: - force_reply (:obj:`True`): Shows reply interface to the user. - selective (:obj:`bool`): Optional. Force reply from specific users only. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`selective` is equal. Args: selective (:obj:`bool`, optional): Use this parameter if you want to force reply from specific users only. Targets: - 1) users that are @mentioned in the text of the Message object - 2) if the bot's message is a reply (has reply_to_message_id), sender of the + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the original message. + input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input + field when the reply is active; 1-64 characters. + + .. versionadded:: 13.7 + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + force_reply (:obj:`True`): Shows reply interface to the user, as if they manually selected + the bots message and tapped 'Reply'. + selective (:obj:`bool`): Optional. Force reply from specific users only. + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + """ - def __init__(self, force_reply=True, selective=False, **kwargs): + __slots__ = ('selective', 'force_reply', 'input_field_placeholder', '_id_attrs') + + def __init__( + self, + force_reply: bool = True, + selective: bool = False, + input_field_placeholder: str = None, + **_kwargs: Any, + ): # Required self.force_reply = bool(force_reply) # Optionals self.selective = bool(selective) + self.input_field_placeholder = input_field_placeholder + + self._id_attrs = (self.selective,) diff --git a/telegramer/include/telegram/games/animation.py b/telegramer/include/telegram/games/animation.py deleted file mode 100644 index f8f02c6..0000000 --- a/telegramer/include/telegram/games/animation.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python -# -# A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2017 -# Leandro Toledo de Souza -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program 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 Public License for more details. -# -# You should have received a copy of the GNU Lesser Public License -# along with this program. If not, see [http://www.gnu.org/licenses/]. -"""This module contains an object that represents a Telegram Animation.""" -from telegram import PhotoSize -from telegram import TelegramObject - - -class Animation(TelegramObject): - """This object represents an animation file to be displayed in the message containing a game. - - Attributes: - file_id (:obj:`str`): Unique file identifier. - thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined - by sender. - file_name (:obj:`str`): Optional. Original animation filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. - file_size (:obj:`int`): Optional. File size. - - Args: - file_id (:obj:`str`): Unique file identifier. - thumb (:class:`telegram.PhotoSize`, optional): Animation thumbnail as defined by sender. - file_name (:obj:`str`, optional): Original animation filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. - file_size (:obj:`int`, optional): File size. - - """ - - def __init__(self, - file_id, - thumb=None, - file_name=None, - mime_type=None, - file_size=None, - **kwargs): - self.file_id = file_id - self.thumb = thumb - self.file_name = file_name - self.mime_type = mime_type - self.file_size = file_size - - self._id_attrs = (self.file_id,) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - data = super(Animation, cls).de_json(data, bot) - - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) - - return cls(**data) diff --git a/telegramer/include/telegram/games/callbackgame.py b/telegramer/include/telegram/games/callbackgame.py index 0b88d72..6803a44 100644 --- a/telegramer/include/telegram/games/callbackgame.py +++ b/telegramer/include/telegram/games/callbackgame.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -23,3 +23,5 @@ class CallbackGame(TelegramObject): """A placeholder, currently holds no information. Use BotFather to set up your game.""" + + __slots__ = () diff --git a/telegramer/include/telegram/games/game.py b/telegramer/include/telegram/games/game.py index 089ac8d..86eb2ab 100644 --- a/telegramer/include/telegram/games/game.py +++ b/telegramer/include/telegram/games/game.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,74 +19,102 @@ """This module contains an object that represents a Telegram Game.""" import sys +from typing import TYPE_CHECKING, Any, Dict, List, Optional -from telegram import MessageEntity, TelegramObject, Animation, PhotoSize +from telegram import Animation, MessageEntity, PhotoSize, TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class Game(TelegramObject): """ - This object represents a game. Use BotFather to create and edit games, their short names will - act as unique identifiers. + This object represents a game. Use `BotFather `_ to create and edit + games, their short names will act as unique identifiers. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`title`, :attr:`description` and :attr:`photo` are equal. + + Args: title (:obj:`str`): Title of the game. description (:obj:`str`): Description of the game. photo (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message in chats. - text (:obj:`str`): Optional. Brief description of the game or high scores included in the + text (:obj:`str`, optional): Brief description of the game or high scores included in the game message. Can be automatically edited to include current high scores for the game - when the bot calls set_game_score, or manually edited using edit_message_text. - text_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities that + when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited + using :meth:`telegram.Bot.edit_message_text`. + 0-4096 characters. Also found as ``telegram.constants.MAX_MESSAGE_LENGTH``. + text_entities (List[:class:`telegram.MessageEntity`], optional): Special entities that appear in text, such as usernames, URLs, bot commands, etc. - animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the - game message in chats. Upload via BotFather. + animation (:class:`telegram.Animation`, optional): Animation that will be displayed in the + game message in chats. Upload via `BotFather `_. - Args: + Attributes: title (:obj:`str`): Title of the game. description (:obj:`str`): Description of the game. photo (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message in chats. - text (:obj:`str`, optional): Brief description of the game or high scores included in the + text (:obj:`str`): Optional. Brief description of the game or high scores included in the game message. Can be automatically edited to include current high scores for the game - when the bot calls set_game_score, or manually edited using edit_message_text. - 0-4096 characters. Also found as ``telegram.constants.MAX_MESSAGE_LENGTH``. - text_entities (List[:class:`telegram.MessageEntity`], optional): Special entities that + when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited + using :meth:`telegram.Bot.edit_message_text`. + text_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities that appear in text, such as usernames, URLs, bot commands, etc. - animation (:class:`telegram.Animation`, optional): Animation that will be displayed in the - game message in chats. Upload via BotFather. + animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the + game message in chats. Upload via `BotFather `_. """ - def __init__(self, - title, - description, - photo, - text=None, - text_entities=None, - animation=None, - **kwargs): + __slots__ = ( + 'title', + 'photo', + 'description', + 'text_entities', + 'text', + 'animation', + '_id_attrs', + ) + + def __init__( + self, + title: str, + description: str, + photo: List[PhotoSize], + text: str = None, + text_entities: List[MessageEntity] = None, + animation: Animation = None, + **_kwargs: Any, + ): + # Required self.title = title self.description = description self.photo = photo + # Optionals self.text = text - self.text_entities = text_entities or list() + self.text_entities = text_entities or [] self.animation = animation + self._id_attrs = (self.title, self.description, self.photo) + @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Game']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(Game, cls).de_json(data, bot) - data['photo'] = PhotoSize.de_list(data.get('photo'), bot) data['text_entities'] = MessageEntity.de_list(data.get('text_entities'), bot) data['animation'] = Animation.de_json(data.get('animation'), bot) return cls(**data) - def to_dict(self): - data = super(Game, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['photo'] = [p.to_dict() for p in self.photo] if self.text_entities: @@ -94,7 +122,7 @@ def to_dict(self): return data - def parse_text_entity(self, entity): + def parse_text_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. Note: @@ -109,17 +137,22 @@ def parse_text_entity(self, entity): Returns: :obj:`str`: The text of the given entity. + Raises: + RuntimeError: If this game has no text. + """ + if not self.text: + raise RuntimeError("This Game has no 'text'.") + # Is it a narrow build, if so we don't need to convert - if sys.maxunicode == 0xffff: - return self.text[entity.offset:entity.offset + entity.length] - else: - entity_text = self.text.encode('utf-16-le') - entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2] + if sys.maxunicode == 0xFFFF: + return self.text[entity.offset : entity.offset + entity.length] + entity_text = self.text.encode('utf-16-le') + entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] return entity_text.decode('utf-16-le') - def parse_text_entities(self, types=None): + def parse_text_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message filtered by their ``type`` attribute as the key, and @@ -145,5 +178,9 @@ def parse_text_entities(self, types=None): return { entity: self.parse_text_entity(entity) - for entity in self.text_entities if entity.type in types + for entity in (self.text_entities or []) + if entity.type in types } + + def __hash__(self) -> int: + return hash((self.title, self.description, tuple(p for p in self.photo))) diff --git a/telegramer/include/telegram/games/gamehighscore.py b/telegramer/include/telegram/games/gamehighscore.py index aea2939..5967dda 100644 --- a/telegramer/include/telegram/games/gamehighscore.py +++ b/telegramer/include/telegram/games/gamehighscore.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,36 +18,50 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram GameHighScore.""" +from typing import TYPE_CHECKING, Optional + from telegram import TelegramObject, User +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class GameHighScore(TelegramObject): """This object represents one row of the high scores table for a game. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`position`, :attr:`user` and :attr:`score` are equal. + + Args: position (:obj:`int`): Position in high score table for the game. user (:class:`telegram.User`): User. score (:obj:`int`): Score. - Args: + Attributes: position (:obj:`int`): Position in high score table for the game. user (:class:`telegram.User`): User. score (:obj:`int`): Score. """ - def __init__(self, position, user, score): + __slots__ = ('position', 'user', 'score', '_id_attrs') + + def __init__(self, position: int, user: User, score: int): self.position = position self.user = user self.score = score + self._id_attrs = (self.position, self.user, self.score) + @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['GameHighScore']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(GameHighScore, cls).de_json(data, bot) - data['user'] = User.de_json(data.get('user'), bot) return cls(**data) diff --git a/telegramer/include/telegram/inline/inlinekeyboardbutton.py b/telegramer/include/telegram/inline/inlinekeyboardbutton.py index d949cf9..49b6e07 100644 --- a/telegramer/include/telegram/inline/inlinekeyboardbutton.py +++ b/telegramer/include/telegram/inline/inlinekeyboardbutton.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,35 +18,57 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram InlineKeyboardButton.""" +from typing import TYPE_CHECKING, Any + from telegram import TelegramObject +if TYPE_CHECKING: + from telegram import CallbackGame, LoginUrl + class InlineKeyboardButton(TelegramObject): """This object represents one button of an inline keyboard. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text`, :attr:`url`, :attr:`login_url`, :attr:`callback_data`, + :attr:`switch_inline_query`, :attr:`switch_inline_query_current_chat`, :attr:`callback_game` + and :attr:`pay` are equal. + Note: - You must use exactly one of the optional fields. Mind that :attr:`callback_game` is not - working as expected. Putting a game short name in it might, but is not guaranteed to work. + * You must use exactly one of the optional fields. Mind that :attr:`callback_game` is not + working as expected. Putting a game short name in it might, but is not guaranteed to + work. + * If your bot allows for arbitrary callback data, in keyboards returned in a response + from telegram, :attr:`callback_data` maybe be an instance of + :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data + associated with the button was already deleted. - Attributes: - text (:obj:`str`): Label text on the button. - url (:obj:`str`): Optional. HTTP url to be opened when button is pressed. - callback_data (:obj:`str`): Optional. Data to be sent in a callback query to the bot when - button is pressed, 1-64 bytes. - switch_inline_query (:obj:`str`): Optional. Will prompt the user to select one of their - chats, open that chat and insert the bot's username and the specified inline query in - the input field. - switch_inline_query_current_chat (:obj:`str`): Optional. Will insert the bot's username and - the specified inline query in the current chat's input field. - callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will - be launched when the user presses the button. - pay (:obj:`bool`): Optional. Specify True, to send a Pay button. + .. versionadded:: 13.6 + + * Since Bot API 5.5, it's now allowed to mention users by their ID in inline keyboards. + This will only work in Telegram versions released after December 7, 2021. + Older clients will display *unsupported message*. + + Warning: + If your bot allows your arbitrary callback data, buttons whose callback data is a + non-hashable object will become unhashable. Trying to evaluate ``hash(button)`` will + result in a :class:`TypeError`. + + .. versionchanged:: 13.6 Args: text (:obj:`str`): Label text on the button. - url (:obj:`str`): HTTP url to be opened when button is pressed. - callback_data (:obj:`str`, optional): Data to be sent in a callback query to the bot when - button is pressed, 1-64 bytes. + url (:obj:`str`, optional): HTTP or tg:// url to be opened when the button is pressed. + Links ``tg://user?id=`` can be used to mention a user by + their ID without using a username, if this is allowed by their privacy settings. + + .. versionchanged:: 13.9 + You can now mention a user using ``tg://user?id=``. + login_url (:class:`telegram.LoginUrl`, optional): An HTTP URL used to automatically + authorize the user. Can be used as a replacement for the Telegram Login Widget. + callback_data (:obj:`str` | :obj:`Any`, optional): Data to be sent in a callback query to + the bot when button is pressed, UTF-8 1-64 bytes. If the bot instance allows arbitrary + callback data, anything can be passed. switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the user to select one of their chats, open that chat and insert the bot's username and the specified inline query in the input field. Can be empty, in which case just the bot's @@ -62,28 +84,94 @@ class InlineKeyboardButton(TelegramObject): callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will be launched when the user presses the button. This type of button must always be the ``first`` button in the first row. - pay (:obj:`bool`, optional): Specify True, to send a Pay button. This type of button must - always be the ``first`` button in the first row. + pay (:obj:`bool`, optional): Specify :obj:`True`, to send a Pay button. This type of button + must always be the `first` button in the first row and can only be used in invoice + messages. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + text (:obj:`str`): Label text on the button. + url (:obj:`str`): Optional. HTTP or tg:// url to be opened when the button is pressed. + Links ``tg://user?id=`` can be used to mention a user by + their ID without using a username, if this is allowed by their privacy settings. + + .. versionchanged:: 13.9 + You can now mention a user using ``tg://user?id=``. + login_url (:class:`telegram.LoginUrl`): Optional. An HTTP URL used to automatically + authorize the user. Can be used as a replacement for the Telegram Login Widget. + callback_data (:obj:`str` | :obj:`object`): Optional. Data to be sent in a callback query + to the bot when button is pressed, UTF-8 1-64 bytes. + switch_inline_query (:obj:`str`): Optional. Will prompt the user to select one of their + chats, open that chat and insert the bot's username and the specified inline query in + the input field. Can be empty, in which case just the bot’s username will be inserted. + switch_inline_query_current_chat (:obj:`str`): Optional. Will insert the bot's username and + the specified inline query in the current chat's input field. Can be empty, in which + case just the bot’s username will be inserted. + callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will + be launched when the user presses the button. + pay (:obj:`bool`): Optional. Specify :obj:`True`, to send a Pay button. + """ - def __init__(self, - text, - url=None, - callback_data=None, - switch_inline_query=None, - switch_inline_query_current_chat=None, - callback_game=None, - pay=None, - **kwargs): + __slots__ = ( + 'callback_game', + 'url', + 'switch_inline_query_current_chat', + 'callback_data', + 'pay', + 'switch_inline_query', + 'text', + '_id_attrs', + 'login_url', + ) + + def __init__( + self, + text: str, + url: str = None, + callback_data: object = None, + switch_inline_query: str = None, + switch_inline_query_current_chat: str = None, + callback_game: 'CallbackGame' = None, + pay: bool = None, + login_url: 'LoginUrl' = None, + **_kwargs: Any, + ): # Required self.text = text # Optionals self.url = url + self.login_url = login_url self.callback_data = callback_data self.switch_inline_query = switch_inline_query self.switch_inline_query_current_chat = switch_inline_query_current_chat self.callback_game = callback_game self.pay = pay + self._id_attrs = () + self._set_id_attrs() + + def _set_id_attrs(self) -> None: + self._id_attrs = ( + self.text, + self.url, + self.login_url, + self.callback_data, + self.switch_inline_query, + self.switch_inline_query_current_chat, + self.callback_game, + self.pay, + ) + + def update_callback_data(self, callback_data: object) -> None: + """ + Sets :attr:`callback_data` to the passed object. Intended to be used by + :class:`telegram.ext.CallbackDataCache`. + + .. versionadded:: 13.6 + + Args: + callback_data (:obj:`obj`): The new callback data. + """ + self.callback_data = callback_data + self._set_id_attrs() diff --git a/telegramer/include/telegram/inline/inlinekeyboardmarkup.py b/telegramer/include/telegram/inline/inlinekeyboardmarkup.py index 26f77af..61724f0 100644 --- a/telegramer/include/telegram/inline/inlinekeyboardmarkup.py +++ b/telegramer/include/telegram/inline/inlinekeyboardmarkup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,33 +18,121 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram InlineKeyboardMarkup.""" -from telegram import ReplyMarkup +from typing import TYPE_CHECKING, Any, List, Optional + +from telegram import InlineKeyboardButton, ReplyMarkup +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class InlineKeyboardMarkup(ReplyMarkup): """ This object represents an inline keyboard that appears right next to the message it belongs to. - Attributes: - inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): Array of button rows, - each represented by an Array of InlineKeyboardButton objects. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their the size of :attr:`inline_keyboard` and all the buttons are equal. Args: - inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): Array of button rows, - each represented by an Array of InlineKeyboardButton objects. + inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows, + each represented by a list of InlineKeyboardButton objects. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows, + each represented by a list of InlineKeyboardButton objects. + """ - def __init__(self, inline_keyboard, **kwargs): + __slots__ = ('inline_keyboard', '_id_attrs') + + def __init__(self, inline_keyboard: List[List[InlineKeyboardButton]], **_kwargs: Any): # Required self.inline_keyboard = inline_keyboard - def to_dict(self): - data = super(InlineKeyboardMarkup, self).to_dict() + self._id_attrs = (self.inline_keyboard,) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['inline_keyboard'] = [] for inline_keyboard in self.inline_keyboard: data['inline_keyboard'].append([x.to_dict() for x in inline_keyboard]) return data + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['InlineKeyboardMarkup']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + keyboard = [] + for row in data['inline_keyboard']: + tmp = [] + for col in row: + btn = InlineKeyboardButton.de_json(col, bot) + if btn: + tmp.append(btn) + keyboard.append(tmp) + + return cls(keyboard) + + @classmethod + def from_button(cls, button: InlineKeyboardButton, **kwargs: object) -> 'InlineKeyboardMarkup': + """Shortcut for:: + + InlineKeyboardMarkup([[button]], **kwargs) + + Return an InlineKeyboardMarkup from a single InlineKeyboardButton + + Args: + button (:class:`telegram.InlineKeyboardButton`): The button to use in the markup + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + return cls([[button]], **kwargs) + + @classmethod + def from_row( + cls, button_row: List[InlineKeyboardButton], **kwargs: object + ) -> 'InlineKeyboardMarkup': + """Shortcut for:: + + InlineKeyboardMarkup([button_row], **kwargs) + + Return an InlineKeyboardMarkup from a single row of InlineKeyboardButtons + + Args: + button_row (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the + markup + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + return cls([button_row], **kwargs) + + @classmethod + def from_column( + cls, button_column: List[InlineKeyboardButton], **kwargs: object + ) -> 'InlineKeyboardMarkup': + """Shortcut for:: + + InlineKeyboardMarkup([[button] for button in button_column], **kwargs) + + Return an InlineKeyboardMarkup from a single column of InlineKeyboardButtons + + Args: + button_column (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the + markup + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + button_grid = [[button] for button in button_column] + return cls(button_grid, **kwargs) + + def __hash__(self) -> int: + return hash(tuple(tuple(button for button in row) for row in self.inline_keyboard)) diff --git a/telegramer/include/telegram/inline/inlinequery.py b/telegramer/include/telegram/inline/inlinequery.py index 79b7e12..1230151 100644 --- a/telegramer/include/telegram/inline/inlinequery.py +++ b/telegramer/include/telegram/inline/inlinequery.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -# pylint: disable=R0902,R0912,R0913 +# pylint: disable=R0902,R0913 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -19,7 +19,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram InlineQuery.""" -from telegram import TelegramObject, User, Location +from typing import TYPE_CHECKING, Any, Optional, Union, Callable, ClassVar, Sequence + +from telegram import Location, TelegramObject, User, constants +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, InlineQueryResult class InlineQuery(TelegramObject): @@ -27,45 +34,73 @@ class InlineQuery(TelegramObject): This object represents an incoming inline query. When the user sends an empty query, your bot could return some default or trending results. - Note: - * In Python `from` is a reserved word, use `from_user` instead. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. - Attributes: - id (:obj:`str`): Unique identifier for this query. - from_user (:class:`telegram.User`): Sender. - location (:class:`telegram.Location`): Optional. Sender location, only for bots that - request user location. - query (:obj:`str`): Text of the query (up to 512 characters). - offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot. + Note: + In Python ``from`` is a reserved word, use ``from_user`` instead. Args: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. + query (:obj:`str`): Text of the query (up to 256 characters). + offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot. + chat_type (:obj:`str`, optional): Type of the chat, from which the inline query was sent. + Can be either :attr:`telegram.Chat.SENDER` for a private chat with the inline query + sender, :attr:`telegram.Chat.PRIVATE`, :attr:`telegram.Chat.GROUP`, + :attr:`telegram.Chat.SUPERGROUP` or :attr:`telegram.Chat.CHANNEL`. The chat type should + be always known for requests sent from official clients and most third-party clients, + unless the request was sent from a secret chat. + + .. versionadded:: 13.5 location (:class:`telegram.Location`, optional): Sender location, only for bots that request user location. - query (:obj:`str`): Text of the query (up to 512 characters). - offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + id (:obj:`str`): Unique identifier for this query. + from_user (:class:`telegram.User`): Sender. + query (:obj:`str`): Text of the query (up to 256 characters). + offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot. + location (:class:`telegram.Location`): Optional. Sender location, only for bots that + request user location. + chat_type (:obj:`str`, optional): Type of the chat, from which the inline query was sent. + + .. versionadded:: 13.5 + """ - def __init__(self, id, from_user, query, offset, location=None, bot=None, **kwargs): + __slots__ = ('bot', 'location', 'chat_type', 'id', 'offset', 'from_user', 'query', '_id_attrs') + + def __init__( + self, + id: str, # pylint: disable=W0622 + from_user: User, + query: str, + offset: str, + location: Location = None, + bot: 'Bot' = None, + chat_type: str = None, + **_kwargs: Any, + ): # Required - self.id = id + self.id = id # pylint: disable=C0103 self.from_user = from_user self.query = query self.offset = offset # Optional self.location = location + self.chat_type = chat_type self.bot = bot self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): - data = super(InlineQuery, cls).de_json(data, bot) + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['InlineQuery']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) if not data: return None @@ -75,29 +110,59 @@ def de_json(cls, data, bot): return cls(bot=bot, **data) - def answer(self, *args, **kwargs): + def answer( + self, + results: Union[ + Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] + ], + cache_time: int = 300, + is_personal: bool = None, + next_offset: str = None, + switch_pm_text: str = None, + switch_pm_parameter: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + current_offset: str = None, + api_kwargs: JSONDict = None, + auto_pagination: bool = False, + ) -> bool: """Shortcut for:: - bot.answer_inline_query(update.inline_query.id, *args, **kwargs) + bot.answer_inline_query(update.inline_query.id, + *args, + current_offset=self.offset if auto_pagination else None, + **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.answer_inline_query`. Args: - results (List[:class:`telegram.InlineQueryResult`]): A list of results for the inline - query. - cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the - result of the inline query may be cached on the server. Defaults to 300. - is_personal (:obj:`bool`, optional): Pass True, if results may be cached on the server - side only for the user that sent the query. By default, results may be returned to - any user who sends the same query. - next_offset (:obj:`str`, optional): Pass the offset that a client should send in the - next query with the same text to receive more results. Pass an empty string if - there are no more results or if you don't support pagination. Offset length can't - exceed 64 bytes. - switch_pm_text (:obj:`str`, optional): If passed, clients will display a button with - specified text that switches the user to a private chat with the bot and sends the - bot a start message with the parameter switch_pm_parameter. - switch_pm_parameter (:obj:`str`, optional): Deep-linking parameter for the /start - message sent to the bot when user presses the switch button. 1-64 characters, - only A-Z, a-z, 0-9, _ and - are allowed. + auto_pagination (:obj:`bool`, optional): If set to :obj:`True`, :attr:`offset` will be + passed as :attr:`current_offset` to :meth:`telegram.Bot.answer_inline_query`. + Defaults to :obj:`False`. + Raises: + TypeError: If both :attr:`current_offset` and :attr:`auto_pagination` are supplied. """ - return self.bot.answer_inline_query(self.id, *args, **kwargs) + if current_offset and auto_pagination: + # We raise TypeError instead of ValueError for backwards compatibility with versions + # which didn't check this here but let Python do the checking + raise TypeError('current_offset and auto_pagination are mutually exclusive!') + return self.bot.answer_inline_query( + inline_query_id=self.id, + current_offset=self.offset if auto_pagination else current_offset, + results=results, + cache_time=cache_time, + is_personal=is_personal, + next_offset=next_offset, + switch_pm_text=switch_pm_text, + switch_pm_parameter=switch_pm_parameter, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + MAX_RESULTS: ClassVar[int] = constants.MAX_INLINE_QUERY_RESULTS + """ + :const:`telegram.constants.MAX_INLINE_QUERY_RESULTS` + + .. versionadded:: 13.2 + """ diff --git a/telegramer/include/telegram/inline/inlinequeryresult.py b/telegramer/include/telegram/inline/inlinequeryresult.py index 6feaf56..f3227d4 100644 --- a/telegramer/include/telegram/inline/inlinequeryresult.py +++ b/telegramer/include/telegram/inline/inlinequeryresult.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,28 +16,56 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 """This module contains the classes that represent Telegram InlineQueryResult.""" +from typing import Any + from telegram import TelegramObject +from telegram.utils.types import JSONDict class InlineQueryResult(TelegramObject): """Baseclass for the InlineQueryResult* classes. - Attributes: - type (:obj:`str`): Type of the result. - id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Note: + All URLs passed in inline query results will be available to end users and therefore must + be assumed to be *public*. Args: type (:obj:`str`): Type of the result. id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): Type of the result. + id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. + """ - def __init__(self, type, id, **kwargs): + __slots__ = ('type', 'id', '_id_attrs') + + def __init__(self, type: str, id: str, **_kwargs: Any): # Required self.type = str(type) - self.id = str(id) + self.id = str(id) # pylint: disable=C0103 self._id_attrs = (self.id,) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + # pylint: disable=E1101 + if ( + hasattr(self, 'caption_entities') + and self.caption_entities # type: ignore[attr-defined] + ): + data['caption_entities'] = [ + ce.to_dict() for ce in self.caption_entities # type: ignore[attr-defined] + ] + + return data diff --git a/telegramer/include/telegram/inline/inlinequeryresultarticle.py b/telegramer/include/telegram/inline/inlinequeryresultarticle.py index 040dad9..ecc975c 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultarticle.py +++ b/telegramer/include/telegram/inline/inlinequeryresultarticle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,28 +18,17 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultArticle.""" +from typing import TYPE_CHECKING, Any + from telegram import InlineQueryResult +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup + class InlineQueryResultArticle(InlineQueryResult): """This object represents a Telegram InlineQueryResultArticle. - Attributes: - type (:obj:`str`): 'article'. - id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. - title (:obj:`str`): Title of the result. - input_message_content (:class:`telegram.InputMessageContent`): Content of the message to - be sent. - reply_markup (:class:`telegram.ReplyMarkup`): Optional. Inline keyboard attached to - the message. - url (:obj:`str`): Optional. URL of the result. - hide_url (:obj:`bool`): Optional. Pass True, if you don't want the URL to be shown in the - message. - description (:obj:`str`): Optional. Short description of the result. - thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. - thumb_width (:obj:`int`): Optional. Thumbnail width. - thumb_height (:obj:`int`): Optional. Thumbnail height. - Args: id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. title (:obj:`str`): Title of the result. @@ -48,46 +37,69 @@ class InlineQueryResultArticle(InlineQueryResult): reply_markup (:class:`telegram.ReplyMarkup`, optional): Inline keyboard attached to the message url (:obj:`str`, optional): URL of the result. - hide_url (:obj:`bool`, optional): Pass True, if you don't want the URL to be shown in the - message. + hide_url (:obj:`bool`, optional): Pass :obj:`True`, if you don't want the URL to be shown + in the message. description (:obj:`str`, optional): Short description of the result. thumb_url (:obj:`str`, optional): Url of the thumbnail for the result. thumb_width (:obj:`int`, optional): Thumbnail width. thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'article'. + id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. + title (:obj:`str`): Title of the result. + input_message_content (:class:`telegram.InputMessageContent`): Content of the message to + be sent. + reply_markup (:class:`telegram.ReplyMarkup`): Optional. Inline keyboard attached to + the message. + url (:obj:`str`): Optional. URL of the result. + hide_url (:obj:`bool`): Optional. Pass :obj:`True`, if you don't want the URL to be shown + in the message. + description (:obj:`str`): Optional. Short description of the result. + thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. + thumb_width (:obj:`int`): Optional. Thumbnail width. + thumb_height (:obj:`int`): Optional. Thumbnail height. + """ - def __init__(self, - id, - title, - input_message_content, - reply_markup=None, - url=None, - hide_url=None, - description=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'thumb_width', + 'thumb_height', + 'hide_url', + 'url', + 'title', + 'description', + 'input_message_content', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + title: str, + input_message_content: 'InputMessageContent', + reply_markup: 'ReplyMarkup' = None, + url: str = None, + hide_url: bool = None, + description: str = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultArticle, self).__init__('article', id) + super().__init__('article', id) self.title = title self.input_message_content = input_message_content # Optional - if reply_markup: - self.reply_markup = reply_markup - if url: - self.url = url - if hide_url: - self.hide_url = hide_url - if description: - self.description = description - if thumb_url: - self.thumb_url = thumb_url - if thumb_width: - self.thumb_width = thumb_width - if thumb_height: - self.thumb_height = thumb_height + self.reply_markup = reply_markup + self.url = url + self.hide_url = hide_url + self.description = description + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegramer/include/telegram/inline/inlinequeryresultaudio.py b/telegramer/include/telegram/inline/inlinequeryresultaudio.py index 0bd2c3c..9f06c62 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultaudio.py +++ b/telegramer/include/telegram/inline/inlinequeryresultaudio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultAudio.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultAudio(InlineQueryResult): @@ -27,67 +34,83 @@ class InlineQueryResultAudio(InlineQueryResult): Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the audio. - Attributes: - type (:obj:`str`): 'audio'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. audio_url (:obj:`str`): A valid URL for the audio file. title (:obj:`str`): Title. - performer (:obj:`str`): Optional. Caption, 0-200 characters. - audio_duration (:obj:`str`): Optional. Performer. - caption (:obj:`str`): Optional. Audio duration in seconds. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + performer (:obj:`str`, optional): Performer. + audio_duration (:obj:`str`, optional): Audio duration in seconds. + caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the audio. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'audio'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. audio_url (:obj:`str`): A valid URL for the audio file. title (:obj:`str`): Title. - performer (:obj:`str`, optional): Caption, 0-200 characters. - audio_duration (:obj:`str`, optional): Performer. - caption (:obj:`str`, optional): Audio duration in seconds. - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + performer (:obj:`str`): Optional. Performer. + audio_duration (:obj:`str`): Optional. Audio duration in seconds. + caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the audio. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - id, - audio_url, - title, - performer=None, - audio_duration=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'parse_mode', + 'audio_url', + 'performer', + 'input_message_content', + 'audio_duration', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + audio_url: str, + title: str, + performer: str = None, + audio_duration: int = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultAudio, self).__init__('audio', id) + super().__init__('audio', id) self.audio_url = audio_url self.title = title # Optionals - if performer: - self.performer = performer - if audio_duration: - self.audio_duration = audio_duration - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.performer = performer + self.audio_duration = audio_duration + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py b/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py index 221616f..f92096a 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,61 +18,83 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedAudio.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedAudio(InlineQueryResult): """ Represents a link to an mp3 audio file stored on the Telegram servers. By default, this audio file will be sent by the user. Alternatively, you can use :attr:`input_message_content` to - send amessage with the specified content instead of the audio. + send a message with the specified content instead of the audio. - Attributes: - type (:obj:`str`): 'audio'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. audio_file_id (:obj:`str`): A valid file identifier for the audio file. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the audio. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'audio'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. audio_file_id (:obj:`str`): A valid file identifier for the audio file. - caption (:obj:`str`, optional): Caption, 0-200 characters - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the audio. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - id, - audio_file_id, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'parse_mode', + 'audio_file_id', + 'input_message_content', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + audio_file_id: str, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedAudio, self).__init__('audio', id) + super().__init__('audio', id) self.audio_file_id = audio_file_id # Optionals - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py b/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py index 49816b5..4b76b74 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,9 +16,17 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 """This module contains the classes that represent Telegram InlineQueryResultCachedDocument.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedDocument(InlineQueryResult): @@ -27,61 +35,79 @@ class InlineQueryResultCachedDocument(InlineQueryResult): by the user with an optional caption. Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the file. - Attributes: - type (:obj:`str`): 'document'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. title (:obj:`str`): Title for the result. document_file_id (:obj:`str`): A valid file identifier for the file. - description (:obj:`str`): Optional. Short description of the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + description (:obj:`str`, optional): Short description of the result. + caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the file. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'document'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. title (:obj:`str`): Title for the result. document_file_id (:obj:`str`): A valid file identifier for the file. - description (:obj:`str`, optional): Short description of the result. - caption (:obj:`str`, optional): Caption, 0-200 characters - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the file. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - id, - title, - document_file_id, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'document_file_id', + 'caption', + 'title', + 'description', + 'parse_mode', + 'input_message_content', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + title: str, + document_file_id: str, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedDocument, self).__init__('document', id) + super().__init__('document', id) self.title = title self.document_file_id = document_file_id # Optionals - if description: - self.description = description - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py b/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py index dd01b33..8cd3ad7 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedGif.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedGif(InlineQueryResult): @@ -28,57 +35,74 @@ class InlineQueryResultCachedGif(InlineQueryResult): use :attr:`input_message_content` to send a message with specified content instead of the animation. - Attributes: - type (:obj:`str`): 'gif'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. gif_file_id (:obj:`str`): A valid file identifier for the GIF file. - title (:obj:`str`): Optional. Title for the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional): + caption (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the gif. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'gif'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. gif_file_id (:obj:`str`): A valid file identifier for the GIF file. - title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional): - caption (:obj:`str`, optional): Caption, 0-200 characters - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + title (:obj:`str`): Optional. Title for the result. + caption (:obj:`str`): Optional. Caption of the GIF file to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the gif. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - id, - gif_file_id, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'input_message_content', + 'parse_mode', + 'gif_file_id', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + gif_file_id: str, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedGif, self).__init__('gif', id) + super().__init__('gif', id) self.gif_file_id = gif_file_id # Optionals - if title: - self.title = title - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py b/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py index 41f099e..8f1d457 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): @@ -28,57 +35,74 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): optional caption. Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the animation. - Attributes: - type (:obj:`str`): 'mpeg4_gif'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file. - title (:obj:`str`): Optional. Title for the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show + title (:obj:`str`, optional): Title for the result. + caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the MPEG-4 file. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'mpeg4_gif'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file. - title (:obj:`str`, optional): Title for the result. - caption (:obj:`str`, optional): Caption, 0-200 characters - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + title (:obj:`str`): Optional. Title for the result. + caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the MPEG-4 file. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - id, - mpeg4_file_id, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'mpeg4_file_id', + 'caption', + 'title', + 'parse_mode', + 'input_message_content', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + mpeg4_file_id: str, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedMpeg4Gif, self).__init__('mpeg4_gif', id) + super().__init__('mpeg4_gif', id) self.mpeg4_file_id = mpeg4_file_id # Optionals - if title: - self.title = title - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py b/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py index 89b8dad..8bba69b 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,9 +16,17 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 """This module contains the classes that represent Telegram InlineQueryResultPhoto""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedPhoto(InlineQueryResult): @@ -28,62 +36,79 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): :attr:`input_message_content` to send a message with the specified content instead of the photo. - Attributes: - type (:obj:`str`): 'photo'. - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - photo_file_id (:obj:`str`): A valid file identifier of the photo. - title (:obj:`str`): Optional. Title for the result. - description (:obj:`str`): Optional. Short description of the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the photo. - Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. photo_file_id (:obj:`str`): A valid file identifier of the photo. title (:obj:`str`, optional): Title for the result. description (:obj:`str`, optional): Short description of the result. - caption (:obj:`str`, optional): Caption, 0-200 characters + caption (:obj:`str`, optional): Caption of the photo to be sent, 0-1024 characters after + entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the photo. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'photo'. + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + photo_file_id (:obj:`str`): A valid file identifier of the photo. + title (:obj:`str`): Optional. Title for the result. + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters after + entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in the media caption. See the constants + in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the photo. + """ - def __init__(self, - id, - photo_file_id, - title=None, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'description', + 'parse_mode', + 'photo_file_id', + 'input_message_content', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + photo_file_id: str, + title: str = None, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedPhoto, self).__init__('photo', id) + super().__init__('photo', id) self.photo_file_id = photo_file_id # Optionals - if title: - self.title = title - if description: - self.description = description - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.title = title + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py b/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py index d9ad8df..0425fa5 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,8 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedSticker.""" +from typing import TYPE_CHECKING, Any + from telegram import InlineQueryResult +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup + class InlineQueryResultCachedSticker(InlineQueryResult): """ @@ -27,6 +32,15 @@ class InlineQueryResultCachedSticker(InlineQueryResult): be sent by the user. Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the sticker. + Args: + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + sticker_file_id (:obj:`str`): A valid file identifier of the sticker. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the sticker. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: type (:obj:`str`): 'sticker`. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. @@ -36,29 +50,22 @@ class InlineQueryResultCachedSticker(InlineQueryResult): input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the sticker. - Args: - id (:obj:`str`): - sticker_file_id (:obj:`str`): - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the - message to be sent instead of the sticker. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. - """ - def __init__(self, - id, - sticker_file_id, - reply_markup=None, - input_message_content=None, - **kwargs): + __slots__ = ('reply_markup', 'input_message_content', 'sticker_file_id') + + def __init__( + self, + id: str, # pylint: disable=W0622 + sticker_file_id: str, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedSticker, self).__init__('sticker', id) + super().__init__('sticker', id) self.sticker_file_id = sticker_file_id # Optionals - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py b/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py index 75cd675..3283794 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedVideo.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedVideo(InlineQueryResult): @@ -28,61 +35,79 @@ class InlineQueryResultCachedVideo(InlineQueryResult): :attr:`input_message_content` to send a message with the specified content instead of the video. - Attributes: - type (:obj:`str`): 'video'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. video_file_id (:obj:`str`): A valid file identifier for the video file. title (:obj:`str`): Title for the result. - description (:obj:`str`): Optional. Short description of the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + description (:obj:`str`, optional): Short description of the result. + caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after + entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the video. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'video'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. video_file_id (:obj:`str`): A valid file identifier for the video file. title (:obj:`str`): Title for the result. - description (:obj:`str`, optional): Short description of the result. - caption (:obj:`str`, optional): Caption, 0-200 characters. - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after + entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the video. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - id, - video_file_id, - title, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'description', + 'parse_mode', + 'input_message_content', + 'video_file_id', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + video_file_id: str, + title: str, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedVideo, self).__init__('video', id) + super().__init__('video', id) self.video_file_id = video_file_id self.title = title # Optionals - if description: - self.description = description - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py b/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py index 503325c..f5ff9bf 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedVoice.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedVoice(InlineQueryResult): @@ -27,56 +34,72 @@ class InlineQueryResultCachedVoice(InlineQueryResult): message will be sent by the user. Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the voice message. - Attributes: - type (:obj:`str`): 'voice'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. voice_file_id (:obj:`str`): A valid file identifier for the voice message. title (:obj:`str`): Voice message title. - caption (:obj:`str`): Optional. Caption, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the voice. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the voice message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'voice'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. voice_file_id (:obj:`str`): A valid file identifier for the voice message. title (:obj:`str`): Voice message title. - caption (:obj:`str`, optional): Caption, 0-200 characters. - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the - message to be sent instead of the voice. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the voice message. """ - def __init__(self, - id, - voice_file_id, - title, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'parse_mode', + 'voice_file_id', + 'input_message_content', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + voice_file_id: str, + title: str, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultCachedVoice, self).__init__('voice', id) + super().__init__('voice', id) self.voice_file_id = voice_file_id self.title = title # Optionals - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultcontact.py b/telegramer/include/telegram/inline/inlinequeryresultcontact.py index 2963cfc..df4f6c9 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultcontact.py +++ b/telegramer/include/telegram/inline/inlinequeryresultcontact.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,8 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultContact.""" +from typing import TYPE_CHECKING, Any + from telegram import InlineQueryResult +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup + class InlineQueryResultContact(InlineQueryResult): """ @@ -27,22 +32,6 @@ class InlineQueryResultContact(InlineQueryResult): Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the contact. - Attributes: - type (:obj:`str`): 'contact'. - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - phone_number (:obj:`str`): Contact's phone number. - first_name (:obj:`str`): Contact's first name. - last_name (:obj:`str`): Optional. Contact's last name. - vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard, - 0-2048 bytes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the contact. - thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. - thumb_width (:obj:`int`): Optional. Thumbnail width. - thumb_height (:obj:`int`): Optional. Thumbnail height. - Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. phone_number (:obj:`str`): Contact's phone number. @@ -59,37 +48,60 @@ class InlineQueryResultContact(InlineQueryResult): thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'contact'. + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + phone_number (:obj:`str`): Contact's phone number. + first_name (:obj:`str`): Contact's first name. + last_name (:obj:`str`): Optional. Contact's last name. + vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard, + 0-2048 bytes. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the contact. + thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. + thumb_width (:obj:`int`): Optional. Thumbnail width. + thumb_height (:obj:`int`): Optional. Thumbnail height. + """ - def __init__(self, - id, - phone_number, - first_name, - last_name=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - vcard=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'thumb_width', + 'thumb_height', + 'vcard', + 'first_name', + 'last_name', + 'phone_number', + 'input_message_content', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + phone_number: str, + first_name: str, + last_name: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + vcard: str = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultContact, self).__init__('contact', id) + super().__init__('contact', id) self.phone_number = phone_number self.first_name = first_name # Optionals - if last_name: - self.last_name = last_name - if vcard: - self.vcard = vcard - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content - if thumb_url: - self.thumb_url = thumb_url - if thumb_width: - self.thumb_width = thumb_width - if thumb_height: - self.thumb_height = thumb_height + self.last_name = last_name + self.vcard = vcard + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegramer/include/telegram/inline/inlinequeryresultdocument.py b/telegramer/include/telegram/inline/inlinequeryresultdocument.py index a4584a9..42555aa 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultdocument.py +++ b/telegramer/include/telegram/inline/inlinequeryresultdocument.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultDocument""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultDocument(InlineQueryResult): @@ -28,82 +35,101 @@ class InlineQueryResultDocument(InlineQueryResult): specified content instead of the file. Currently, only .PDF and .ZIP files can be sent using this method. - Attributes: - type (:obj:`str`): 'document'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. title (:obj:`str`): Title for the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. document_url (:obj:`str`): A valid URL for the file. mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf" or "application/zip". - description (:obj:`str`): Optional. Short description of the result. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + description (:obj:`str`, optional): Short description of the result. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the file. - thumb_url (:obj:`str`): Optional. URL of the thumbnail (jpeg only) for the file. - thumb_width (:obj:`int`): Optional. Thumbnail width. - thumb_height (:obj:`int`): Optional. Thumbnail height. + thumb_url (:obj:`str`, optional): URL of the thumbnail (jpeg only) for the file. + thumb_width (:obj:`int`, optional): Thumbnail width. + thumb_height (:obj:`int`, optional): Thumbnail height. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'document'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. title (:obj:`str`): Title for the result. - caption (:obj:`str`, optional): Caption, 0-200 characters - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. document_url (:obj:`str`): A valid URL for the file. mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf" or "application/zip". - description (:obj:`str`, optional): Short description of the result. + description (:obj:`str`): Optional. Short description of the result. reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the file. - thumb_url (:obj:`str`, optional): URL of the thumbnail (jpeg only) for the file. - thumb_width (:obj:`int`, optional): Thumbnail width. - thumb_height (:obj:`int`, optional): Thumbnail height. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + thumb_url (:obj:`str`): Optional. URL of the thumbnail (jpeg only) for the file. + thumb_width (:obj:`int`): Optional. Thumbnail width. + thumb_height (:obj:`int`): Optional. Thumbnail height. """ - def __init__(self, - id, - document_url, - title, - mime_type, - caption=None, - description=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'document_url', + 'thumb_width', + 'thumb_height', + 'caption', + 'title', + 'description', + 'parse_mode', + 'mime_type', + 'thumb_url', + 'input_message_content', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + document_url: str, + title: str, + mime_type: str, + caption: str = None, + description: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultDocument, self).__init__('document', id) + super().__init__('document', id) self.document_url = document_url self.title = title self.mime_type = mime_type # Optionals - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if description: - self.description = description - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content - if thumb_url: - self.thumb_url = thumb_url - if thumb_width: - self.thumb_width = thumb_width - if thumb_height: - self.thumb_height = thumb_height + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.description = description + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegramer/include/telegram/inline/inlinequeryresultgame.py b/telegramer/include/telegram/inline/inlinequeryresultgame.py index cc06b76..02e4a7d 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultgame.py +++ b/telegramer/include/telegram/inline/inlinequeryresultgame.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,33 +18,45 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultGame.""" +from typing import TYPE_CHECKING, Any + from telegram import InlineQueryResult +if TYPE_CHECKING: + from telegram import ReplyMarkup + class InlineQueryResultGame(InlineQueryResult): - """Represents a Game. + """Represents a :class:`telegram.Game`. - Attributes: - type (:obj:`str`): 'game'. + Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. game_short_name (:obj:`str`): Short name of the game. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: + type (:obj:`str`): 'game'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. game_short_name (:obj:`str`): Short name of the game. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, id, game_short_name, reply_markup=None, **kwargs): + __slots__ = ('reply_markup', 'game_short_name') + + def __init__( + self, + id: str, # pylint: disable=W0622 + game_short_name: str, + reply_markup: 'ReplyMarkup' = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultGame, self).__init__('game', id) - self.id = id + super().__init__('game', id) + self.id = id # pylint: disable=W0622 self.game_short_name = game_short_name - if reply_markup: - self.reply_markup = reply_markup + self.reply_markup = reply_markup diff --git a/telegramer/include/telegram/inline/inlinequeryresultgif.py b/telegramer/include/telegram/inline/inlinequeryresultgif.py index d8b62c6..e879dc0 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultgif.py +++ b/telegramer/include/telegram/inline/inlinequeryresultgif.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,9 +16,17 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 """This module contains the classes that represent Telegram InlineQueryResultGif.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultGif(InlineQueryResult): @@ -27,6 +35,31 @@ class InlineQueryResultGif(InlineQueryResult): the user with optional caption. Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the animation. + Args: + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB. + gif_width (:obj:`int`, optional): Width of the GIF. + gif_height (:obj:`int`, optional): Height of the GIF. + gif_duration (:obj:`int`, optional): Duration of the GIF + thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for + the result. + thumb_mime_type (:obj:`str`, optional): MIME type of the thumbnail, must be one of + ``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``. + title (:obj:`str`, optional): Title for the result. + caption (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in the media caption. See the constants + in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the GIF animation. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: type (:obj:`str`): 'gif'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. @@ -34,70 +67,71 @@ class InlineQueryResultGif(InlineQueryResult): gif_width (:obj:`int`): Optional. Width of the GIF. gif_height (:obj:`int`): Optional. Height of the GIF. gif_duration (:obj:`int`): Optional. Duration of the GIF. - thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif). + thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for + the result. + thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail. title (:obj:`str`): Optional. Title for the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters + caption (:obj:`str`): Optional. Caption of the GIF file to be sent, 0-1024 characters + after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the gif. - - Args: - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB. - gif_width (:obj:`int`, optional): Width of the GIF. - gif_height (:obj:`int`, optional): Height of the GIF. - gif_duration (:obj:`int`, optional): Duration of the GIF - thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif). - title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional): - caption (:obj:`str`, optional): Caption, 0-200 characters - parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the - message to be sent instead of the gif. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + message to be sent instead of the GIF animation. """ - def __init__(self, - id, - gif_url, - thumb_url, - gif_width=None, - gif_height=None, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - gif_duration=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'gif_height', + 'thumb_mime_type', + 'caption_entities', + 'gif_width', + 'title', + 'caption', + 'parse_mode', + 'gif_duration', + 'input_message_content', + 'gif_url', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + gif_url: str, + thumb_url: str, + gif_width: int = None, + gif_height: int = None, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + gif_duration: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb_mime_type: str = None, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultGif, self).__init__('gif', id) + super().__init__('gif', id) self.gif_url = gif_url self.thumb_url = thumb_url # Optionals - if gif_width: - self.gif_width = gif_width - if gif_height: - self.gif_height = gif_height - if gif_duration: - self.gif_duration = gif_duration - if title: - self.title = title - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.gif_width = gif_width + self.gif_height = gif_height + self.gif_duration = gif_duration + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_mime_type = thumb_mime_type diff --git a/telegramer/include/telegram/inline/inlinequeryresultlocation.py b/telegramer/include/telegram/inline/inlinequeryresultlocation.py index e30c271..e55d90b 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultlocation.py +++ b/telegramer/include/telegram/inline/inlinequeryresultlocation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,8 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultLocation.""" +from typing import TYPE_CHECKING, Any + from telegram import InlineQueryResult +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup + class InlineQueryResultLocation(InlineQueryResult): """ @@ -27,29 +32,20 @@ class InlineQueryResultLocation(InlineQueryResult): Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the location. - Attributes: - type (:obj:`str`): 'location'. - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - latitude (:obj:`float`): Location latitude in degrees. - longitude (:obj:`float`): Location longitude in degrees. - title (:obj:`str`): Location title. - live_period (:obj:`int`): Optional. Period in seconds for which the location can be - updated, should be between 60 and 86400. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the location. - thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. - thumb_width (:obj:`int`): Optional. Thumbnail width. - thumb_height (:obj:`int`): Optional. Thumbnail height. - Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. latitude (:obj:`float`): Location latitude in degrees. longitude (:obj:`float`): Location longitude in degrees. title (:obj:`str`): Location title. + horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, + measured in meters; 0-1500. live_period (:obj:`int`, optional): Period in seconds for which the location can be updated, should be between 60 and 86400. + heading (:obj:`int`, optional): For live locations, a direction in which the user is + moving, in degrees. Must be between 1 and 360 if specified. + proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. Must be between 1 + and 100000 if specified. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -59,36 +55,77 @@ class InlineQueryResultLocation(InlineQueryResult): thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'location'. + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + latitude (:obj:`float`): Location latitude in degrees. + longitude (:obj:`float`): Location longitude in degrees. + title (:obj:`str`): Location title. + horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, + measured in meters. + live_period (:obj:`int`): Optional. Period in seconds for which the location can be + updated, should be between 60 and 86400. + heading (:obj:`int`): Optional. For live locations, a direction in which the user is + moving, in degrees. + proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the location. + thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. + thumb_width (:obj:`int`): Optional. Thumbnail width. + thumb_height (:obj:`int`): Optional. Thumbnail height. + """ - def __init__(self, - id, - latitude, - longitude, - title, - live_period=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - **kwargs): + __slots__ = ( + 'longitude', + 'reply_markup', + 'thumb_width', + 'thumb_height', + 'heading', + 'title', + 'live_period', + 'proximity_alert_radius', + 'input_message_content', + 'latitude', + 'horizontal_accuracy', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + latitude: float, + longitude: float, + title: str, + live_period: int = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultLocation, self).__init__('location', id) - self.latitude = latitude - self.longitude = longitude + super().__init__('location', id) + self.latitude = float(latitude) + self.longitude = float(longitude) self.title = title # Optionals - if live_period: - self.live_period = live_period - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content - if thumb_url: - self.thumb_url = thumb_url - if thumb_width: - self.thumb_width = thumb_width - if thumb_height: - self.thumb_height = thumb_height + self.live_period = live_period + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height + self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None + self.heading = int(heading) if heading else None + self.proximity_alert_radius = ( + int(proximity_alert_radius) if proximity_alert_radius else None + ) diff --git a/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py b/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py index 3459d77..5f73218 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py +++ b/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultMpeg4Gif(InlineQueryResult): @@ -28,24 +35,6 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): use :attr:`input_message_content` to send a message with the specified content instead of the animation. - Attributes: - type (:obj:`str`): 'mpeg4_gif'. - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB. - mpeg4_width (:obj:`int`): Optional. Video width. - mpeg4_height (:obj:`int`): Optional. Video height. - mpeg4_duration (:obj:`int`): Optional. Video duration. - thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result. - title (:obj:`str`): Optional. Title for the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the MPEG-4 file. - Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB. @@ -53,52 +42,95 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): mpeg4_height (:obj:`int`, optional): Video height. mpeg4_duration (:obj:`int`, optional): Video duration. thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result. + thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail, must be one of + ``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``. title (:obj:`str`, optional): Title for the result. - caption (:obj:`str`, optional): Caption, 0-200 characters + caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, 0-1024 characters + after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the - message to be sent instead of the MPEG-4 file. + message to be sent instead of the video animation. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'mpeg4_gif'. + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB. + mpeg4_width (:obj:`int`): Optional. Video width. + mpeg4_height (:obj:`int`): Optional. Video height. + mpeg4_duration (:obj:`int`): Optional. Video duration. + thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for + the result. + thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail. + title (:obj:`str`): Optional. Title for the result. + caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters + after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in the media caption. See the constants + in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the video animation. + """ - def __init__(self, - id, - mpeg4_url, - thumb_url, - mpeg4_width=None, - mpeg4_height=None, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - mpeg4_duration=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'thumb_mime_type', + 'caption_entities', + 'mpeg4_duration', + 'mpeg4_width', + 'title', + 'caption', + 'parse_mode', + 'input_message_content', + 'mpeg4_url', + 'mpeg4_height', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + mpeg4_url: str, + thumb_url: str, + mpeg4_width: int = None, + mpeg4_height: int = None, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + mpeg4_duration: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb_mime_type: str = None, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultMpeg4Gif, self).__init__('mpeg4_gif', id) + super().__init__('mpeg4_gif', id) self.mpeg4_url = mpeg4_url self.thumb_url = thumb_url # Optional - if mpeg4_width: - self.mpeg4_width = mpeg4_width - if mpeg4_height: - self.mpeg4_height = mpeg4_height - if mpeg4_duration: - self.mpeg4_duration = mpeg4_duration - if title: - self.title = title - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.mpeg4_width = mpeg4_width + self.mpeg4_height = mpeg4_height + self.mpeg4_duration = mpeg4_duration + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_mime_type = thumb_mime_type diff --git a/telegramer/include/telegram/inline/inlinequeryresultphoto.py b/telegramer/include/telegram/inline/inlinequeryresultphoto.py index efaf95f..e6de727 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultphoto.py +++ b/telegramer/include/telegram/inline/inlinequeryresultphoto.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultPhoto.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultPhoto(InlineQueryResult): @@ -27,25 +34,6 @@ class InlineQueryResultPhoto(InlineQueryResult): caption. Alternatively, you can use :attr:`input_message_content` to send a message with the specified content instead of the photo. - Attributes: - type (:obj:`str`): 'photo'. - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size - must not exceed 5MB. - thumb_url (:obj:`str`): URL of the thumbnail for the photo. - photo_width (:obj:`int`): Optional. Width of the photo. - photo_height (:obj:`int`): Optional. Height of the photo. - title (:obj:`str`): Optional. Title for the result. - description (:obj:`str`): Optional. Short description of the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the photo. - Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size @@ -55,50 +43,87 @@ class InlineQueryResultPhoto(InlineQueryResult): photo_height (:obj:`int`, optional): Height of the photo. title (:obj:`str`, optional): Title for the result. description (:obj:`str`, optional): Short description of the result. - caption (:obj:`str`, optional): Caption, 0-200 characters + caption (:obj:`str`, optional): Caption of the photo to be sent, 0-1024 characters after + entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the message to be sent instead of the photo. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'photo'. + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size + must not exceed 5MB. + thumb_url (:obj:`str`): URL of the thumbnail for the photo. + photo_width (:obj:`int`): Optional. Width of the photo. + photo_height (:obj:`int`): Optional. Height of the photo. + title (:obj:`str`): Optional. Title for the result. + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters after + entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in the media caption. See the constants + in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the photo. + """ - def __init__(self, - id, - photo_url, - thumb_url, - photo_width=None, - photo_height=None, - title=None, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'photo_url', + 'reply_markup', + 'caption_entities', + 'photo_width', + 'caption', + 'title', + 'description', + 'parse_mode', + 'input_message_content', + 'photo_height', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + photo_url: str, + thumb_url: str, + photo_width: int = None, + photo_height: int = None, + title: str = None, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultPhoto, self).__init__('photo', id) + super().__init__('photo', id) self.photo_url = photo_url self.thumb_url = thumb_url # Optionals - if photo_width: - self.photo_width = int(photo_width) - if photo_height: - self.photo_height = int(photo_height) - if title: - self.title = title - if description: - self.description = description - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.photo_width = int(photo_width) if photo_width is not None else None + self.photo_height = int(photo_height) if photo_height is not None else None + self.title = title + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultvenue.py b/telegramer/include/telegram/inline/inlinequeryresultvenue.py index f64fcc2..4a64a10 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultvenue.py +++ b/telegramer/include/telegram/inline/inlinequeryresultvenue.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,8 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultVenue.""" +from typing import TYPE_CHECKING, Any + from telegram import InlineQueryResult +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup + class InlineQueryResultVenue(InlineQueryResult): """ @@ -27,24 +32,9 @@ class InlineQueryResultVenue(InlineQueryResult): use :attr:`input_message_content` to send a message with the specified content instead of the venue. - Attributes: - type (:obj:`str`): 'venue'. - id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. - latitude (:obj:`float`): Latitude of the venue location in degrees. - longitude (:obj:`float`): Longitude of the venue location in degrees. - title (:obj:`str`): Title of the venue. - address (:obj:`str`): Address of the venue. - foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue if known. - foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. - (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or - "food/icecream".) - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the venue. - thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. - thumb_width (:obj:`int`): Optional. Thumbnail width. - thumb_height (:obj:`int`): Optional. Thumbnail height. + Note: + Foursquare details and Google Pace details are mutually exclusive. However, this + behaviour is undocumented and might be changed by Telegram. Args: id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. @@ -56,6 +46,9 @@ class InlineQueryResultVenue(InlineQueryResult): foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) + google_place_id (:obj:`str`, optional): Google Places identifier of the venue. + google_place_type (:obj:`str`, optional): Google Places type of the venue. (See + `supported types `_.) reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -65,42 +58,76 @@ class InlineQueryResultVenue(InlineQueryResult): thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'venue'. + id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. + latitude (:obj:`float`): Latitude of the venue location in degrees. + longitude (:obj:`float`): Longitude of the venue location in degrees. + title (:obj:`str`): Title of the venue. + address (:obj:`str`): Address of the venue. + foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue if known. + foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. + google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. + google_place_type (:obj:`str`): Optional. Google Places type of the venue. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the venue. + thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. + thumb_width (:obj:`int`): Optional. Thumbnail width. + thumb_height (:obj:`int`): Optional. Thumbnail height. + """ - def __init__(self, - id, - latitude, - longitude, - title, - address, - foursquare_id=None, - foursquare_type=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - **kwargs): + __slots__ = ( + 'longitude', + 'reply_markup', + 'google_place_type', + 'thumb_width', + 'thumb_height', + 'title', + 'address', + 'foursquare_id', + 'foursquare_type', + 'google_place_id', + 'input_message_content', + 'latitude', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + latitude: float, + longitude: float, + title: str, + address: str, + foursquare_id: str = None, + foursquare_type: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + google_place_id: str = None, + google_place_type: str = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultVenue, self).__init__('venue', id) + super().__init__('venue', id) self.latitude = latitude self.longitude = longitude self.title = title self.address = address # Optional - if foursquare_id: - self.foursquare_id = foursquare_id - if foursquare_type: - self.foursquare_type = foursquare_type - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content - if thumb_url: - self.thumb_url = thumb_url - if thumb_width: - self.thumb_width = thumb_width - if thumb_height: - self.thumb_height = thumb_height + self.foursquare_id = foursquare_id + self.foursquare_type = foursquare_type + self.google_place_id = google_place_id + self.google_place_type = google_place_type + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegramer/include/telegram/inline/inlinequeryresultvideo.py b/telegramer/include/telegram/inline/inlinequeryresultvideo.py index a21c525..098e3b0 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultvideo.py +++ b/telegramer/include/telegram/inline/inlinequeryresultvideo.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultVideo.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultVideo(InlineQueryResult): @@ -28,25 +35,9 @@ class InlineQueryResultVideo(InlineQueryResult): :attr:`input_message_content` to send a message with the specified content instead of the video. - Attributes: - type (:obj:`str`): 'video'. - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - video_url (:obj:`str`): A valid URL for the embedded video player or video file. - mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4". - thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video. - title (:obj:`str`): Title for the result. - caption (:obj:`str`): Optional. Caption, 0-200 characters - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption. See the constants - in :class:`telegram.ParseMode` for the available modes. - video_width (:obj:`int`): Optional. Video width. - video_height (:obj:`int`): Optional. Video height. - video_duration (:obj:`int`): Optional. Video duration in seconds. - description (:obj:`str`): Optional. Short description of the result. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the video. + Note: + If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), you must + replace its content using :attr:`input_message_content`. Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. @@ -54,10 +45,13 @@ class InlineQueryResultVideo(InlineQueryResult): mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4". thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video. title (:obj:`str`): Title for the result. - caption (:obj:`str`, optional): Caption, 0-200 characters. + caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. video_width (:obj:`int`, optional): Video width. video_height (:obj:`int`, optional): Video height. video_duration (:obj:`int`, optional): Video duration in seconds. @@ -65,48 +59,88 @@ class InlineQueryResultVideo(InlineQueryResult): reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the - message to be sent instead of the video. + message to be sent instead of the video. This field is required if + InlineQueryResultVideo is used to send an HTML-page as a result + (e.g., a YouTube video). **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'video'. + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + video_url (:obj:`str`): A valid URL for the embedded video player or video file. + mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4". + thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video. + title (:obj:`str`): Title for the result. + caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after + entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in the media caption. See the constants + in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + video_width (:obj:`int`): Optional. Video width. + video_height (:obj:`int`): Optional. Video height. + video_duration (:obj:`int`): Optional. Video duration in seconds. + description (:obj:`str`): Optional. Short description of the result. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the video. This field is required if + InlineQueryResultVideo is used to send an HTML-page as a result + (e.g., a YouTube video). + """ - def __init__(self, - id, - video_url, - mime_type, - thumb_url, - title, - caption=None, - video_width=None, - video_height=None, - video_duration=None, - description=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'video_url', + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'description', + 'video_duration', + 'parse_mode', + 'mime_type', + 'input_message_content', + 'video_height', + 'video_width', + 'thumb_url', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + video_url: str, + mime_type: str, + thumb_url: str, + title: str, + caption: str = None, + video_width: int = None, + video_height: int = None, + video_duration: int = None, + description: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultVideo, self).__init__('video', id) + super().__init__('video', id) self.video_url = video_url self.mime_type = mime_type self.thumb_url = thumb_url self.title = title # Optional - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if video_width: - self.video_width = video_width - if video_height: - self.video_height = video_height - if video_duration: - self.video_duration = video_duration - if description: - self.description = description - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.video_width = video_width + self.video_height = video_height + self.video_duration = video_duration + self.description = description + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inlinequeryresultvoice.py b/telegramer/include/telegram/inline/inlinequeryresultvoice.py index 817eca4..960f41b 100644 --- a/telegramer/include/telegram/inline/inlinequeryresultvoice.py +++ b/telegramer/include/telegram/inline/inlinequeryresultvoice.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultVoice.""" -from telegram import InlineQueryResult +from typing import TYPE_CHECKING, Any, Union, Tuple, List + +from telegram import InlineQueryResult, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import ODVInput + +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultVoice(InlineQueryResult): @@ -28,62 +35,78 @@ class InlineQueryResultVoice(InlineQueryResult): :attr:`input_message_content` to send a message with the specified content instead of the the voice message. - Attributes: - type (:obj:`str`): 'voice'. - id (:obj:`str`): Unique identifier for this result, 1-64 bytes. - voice_url (:obj:`str`): A valid URL for the voice recording. - title (:obj:`str`): Voice message title. - caption (:obj:`str`): Optional. Caption, 0-200 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption.. See the constants - in :class:`telegram.ParseMode` for the available modes. - voice_duration (:obj:`int`): Optional. Recording duration in seconds. - reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached - to the message. - input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the - message to be sent instead of the voice. - Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. voice_url (:obj:`str`): A valid URL for the voice recording. - title (:obj:`str`): Voice message title. - caption (:obj:`str`, optional): Caption, 0-200 characters. + title (:obj:`str`): Recording title. + caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in the media caption.. See the constants + bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. voice_duration (:obj:`int`, optional): Recording duration in seconds. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the - message to be sent instead of the voice. + message to be sent instead of the voice recording. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): 'voice'. + id (:obj:`str`): Unique identifier for this result, 1-64 bytes. + voice_url (:obj:`str`): A valid URL for the voice recording. + title (:obj:`str`): Recording title. + caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in the media caption. See the constants + in :class:`telegram.ParseMode` for the available modes. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + voice_duration (:obj:`int`): Optional. Recording duration in seconds. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the voice recording. + """ - def __init__(self, - id, - voice_url, - title, - voice_duration=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=None, - **kwargs): + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'voice_duration', + 'caption', + 'title', + 'voice_url', + 'parse_mode', + 'input_message_content', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + voice_url: str, + title: str, + voice_duration: int = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required - super(InlineQueryResultVoice, self).__init__('voice', id) + super().__init__('voice', id) self.voice_url = voice_url self.title = title # Optional - if voice_duration: - self.voice_duration = voice_duration - if caption: - self.caption = caption - if parse_mode: - self.parse_mode = parse_mode - if reply_markup: - self.reply_markup = reply_markup - if input_message_content: - self.input_message_content = input_message_content + self.voice_duration = voice_duration + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegramer/include/telegram/inline/inputcontactmessagecontent.py b/telegramer/include/telegram/inline/inputcontactmessagecontent.py index c655b0f..fe7b9d7 100644 --- a/telegramer/include/telegram/inline/inputcontactmessagecontent.py +++ b/telegramer/include/telegram/inline/inputcontactmessagecontent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,18 +18,16 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InputContactMessageContent.""" +from typing import Any + from telegram import InputMessageContent class InputContactMessageContent(InputMessageContent): """Represents the content of a contact message to be sent as the result of an inline query. - Attributes: - phone_number (:obj:`str`): Contact's phone number. - first_name (:obj:`str`): Contact's first name. - last_name (:obj:`str`): Optional. Contact's last name. - vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard, - 0-2048 bytes. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`phone_number` is equal. Args: phone_number (:obj:`str`): Contact's phone number. @@ -39,12 +37,30 @@ class InputContactMessageContent(InputMessageContent): 0-2048 bytes. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + phone_number (:obj:`str`): Contact's phone number. + first_name (:obj:`str`): Contact's first name. + last_name (:obj:`str`): Optional. Contact's last name. + vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard, + 0-2048 bytes. + """ - def __init__(self, phone_number, first_name, last_name=None, vcard=None, **kwargs): + __slots__ = ('vcard', 'first_name', 'last_name', 'phone_number', '_id_attrs') + + def __init__( + self, + phone_number: str, + first_name: str, + last_name: str = None, + vcard: str = None, + **_kwargs: Any, + ): # Required self.phone_number = phone_number self.first_name = first_name # Optionals self.last_name = last_name self.vcard = vcard + + self._id_attrs = (self.phone_number,) diff --git a/telegramer/include/telegram/inline/inputinvoicemessagecontent.py b/telegramer/include/telegram/inline/inputinvoicemessagecontent.py new file mode 100644 index 0000000..622f0ed --- /dev/null +++ b/telegramer/include/telegram/inline/inputinvoicemessagecontent.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains a class that represents a Telegram InputInvoiceMessageContent.""" + +from typing import Any, List, Optional, TYPE_CHECKING + +from telegram import InputMessageContent, LabeledPrice +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class InputInvoiceMessageContent(InputMessageContent): + """ + Represents the content of a invoice message to be sent as the result of an inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`title`, :attr:`description`, :attr:`payload`, + :attr:`provider_token`, :attr:`currency` and :attr:`prices` are equal. + + .. versionadded:: 13.5 + + Args: + title (:obj:`str`): Product name, 1-32 characters + description (:obj:`str`): Product description, 1-255 characters + payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed + to the user, use for your internal processes. + provider_token (:obj:`str`): Payment provider token, obtained via + `@Botfather `_. + currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on + `currencies `_ + prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a JSON-serialized list of + components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, + etc.) + max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the smallest + units of the currency (integer, not float/double). For example, for a maximum tip of + US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in + `currencies.json `_, it + shows the number of digits past the decimal point for each currency (2 for the majority + of currencies). Defaults to ``0``. + suggested_tip_amounts (List[:obj:`int`], optional): A JSON-serialized array of suggested + amounts of tip in the smallest units of the currency (integer, not float/double). At + most 4 suggested tip amounts can be specified. The suggested tip amounts must be + positive, passed in a strictly increased order and must not exceed + :attr:`max_tip_amount`. + provider_data (:obj:`str`, optional): A JSON-serialized object for data about the invoice, + which will be shared with the payment provider. A detailed description of the required + fields should be provided by the payment provider. + photo_url (:obj:`str`, optional): URL of the product photo for the invoice. Can be a photo + of the goods or a marketing image for a service. People like it better when they see + what they are paying for. + photo_size (:obj:`int`, optional): Photo size. + photo_width (:obj:`int`, optional): Photo width. + photo_height (:obj:`int`, optional): Photo height. + need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full name to + complete the order. + need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's + phone number to complete the order + need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email + address to complete the order. + need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's + shipping address to complete the order + send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's phone + number should be sent to provider. + send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email address + should be sent to provider. + is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on the + shipping method. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + title (:obj:`str`): Product name, 1-32 characters + description (:obj:`str`): Product description, 1-255 characters + payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed + to the user, use for your internal processes. + provider_token (:obj:`str`): Payment provider token, obtained via + `@Botfather `_. + currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on + `currencies `_ + prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a JSON-serialized list of + components. + max_tip_amount (:obj:`int`): Optional. The maximum accepted amount for tips in the smallest + units of the currency (integer, not float/double). + suggested_tip_amounts (List[:obj:`int`]): Optional. A JSON-serialized array of suggested + amounts of tip in the smallest units of the currency (integer, not float/double). + provider_data (:obj:`str`): Optional. A JSON-serialized object for data about the invoice, + which will be shared with the payment provider. + photo_url (:obj:`str`): Optional. URL of the product photo for the invoice. + photo_size (:obj:`int`): Optional. Photo size. + photo_width (:obj:`int`): Optional. Photo width. + photo_height (:obj:`int`): Optional. Photo height. + need_name (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's full name to + complete the order. + need_phone_number (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's + phone number to complete the order + need_email (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's email + address to complete the order. + need_shipping_address (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's + shipping address to complete the order + send_phone_number_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's phone + number should be sent to provider. + send_email_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's email address + should be sent to provider. + is_flexible (:obj:`bool`): Optional. Pass :obj:`True`, if the final price depends on the + shipping method. + + """ + + __slots__ = ( + 'title', + 'description', + 'payload', + 'provider_token', + 'currency', + 'prices', + 'max_tip_amount', + 'suggested_tip_amounts', + 'provider_data', + 'photo_url', + 'photo_size', + 'photo_width', + 'photo_height', + 'need_name', + 'need_phone_number', + 'need_email', + 'need_shipping_address', + 'send_phone_number_to_provider', + 'send_email_to_provider', + 'is_flexible', + '_id_attrs', + ) + + def __init__( + self, + title: str, + description: str, + payload: str, + provider_token: str, + currency: str, + prices: List[LabeledPrice], + max_tip_amount: int = None, + suggested_tip_amounts: List[int] = None, + provider_data: str = None, + photo_url: str = None, + photo_size: int = None, + photo_width: int = None, + photo_height: int = None, + need_name: bool = None, + need_phone_number: bool = None, + need_email: bool = None, + need_shipping_address: bool = None, + send_phone_number_to_provider: bool = None, + send_email_to_provider: bool = None, + is_flexible: bool = None, + **_kwargs: Any, + ): + # Required + self.title = title + self.description = description + self.payload = payload + self.provider_token = provider_token + self.currency = currency + self.prices = prices + # Optionals + self.max_tip_amount = int(max_tip_amount) if max_tip_amount else None + self.suggested_tip_amounts = ( + [int(sta) for sta in suggested_tip_amounts] if suggested_tip_amounts else None + ) + self.provider_data = provider_data + self.photo_url = photo_url + self.photo_size = int(photo_size) if photo_size else None + self.photo_width = int(photo_width) if photo_width else None + self.photo_height = int(photo_height) if photo_height else None + self.need_name = need_name + self.need_phone_number = need_phone_number + self.need_email = need_email + self.need_shipping_address = need_shipping_address + self.send_phone_number_to_provider = send_phone_number_to_provider + self.send_email_to_provider = send_email_to_provider + self.is_flexible = is_flexible + + self._id_attrs = ( + self.title, + self.description, + self.payload, + self.provider_token, + self.currency, + self.prices, + ) + + def __hash__(self) -> int: + # we override this as self.prices is a list and not hashable + prices = tuple(self.prices) + return hash( + ( + self.title, + self.description, + self.payload, + self.provider_token, + self.currency, + prices, + ) + ) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + data['prices'] = [price.to_dict() for price in self.prices] + + return data + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: 'Bot' + ) -> Optional['InputInvoiceMessageContent']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['prices'] = LabeledPrice.de_list(data.get('prices'), bot) + + return cls(**data, bot=bot) diff --git a/telegramer/include/telegram/inline/inputlocationmessagecontent.py b/telegramer/include/telegram/inline/inputlocationmessagecontent.py index 16010ec..22760bf 100644 --- a/telegramer/include/telegram/inline/inputlocationmessagecontent.py +++ b/telegramer/include/telegram/inline/inputlocationmessagecontent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,28 +18,71 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InputLocationMessageContent.""" +from typing import Any + from telegram import InputMessageContent class InputLocationMessageContent(InputMessageContent): + # fmt: off """ Represents the content of a location message to be sent as the result of an inline query. - Attributes: - latitude (:obj:`float`): Latitude of the location in degrees. - longitude (:obj:`float`): Longitude of the location in degrees. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`latitude` and :attr:`longitude` are equal. Args: latitude (:obj:`float`): Latitude of the location in degrees. longitude (:obj:`float`): Longitude of the location in degrees. + horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, + measured in meters; 0-1500. live_period (:obj:`int`, optional): Period in seconds for which the location can be updated, should be between 60 and 86400. + heading (:obj:`int`, optional): For live locations, a direction in which the user is + moving, in degrees. Must be between 1 and 360 if specified. + proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. Must be between 1 + and 100000 if specified. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + latitude (:obj:`float`): Latitude of the location in degrees. + longitude (:obj:`float`): Longitude of the location in degrees. + horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, + measured in meters. + live_period (:obj:`int`): Optional. Period in seconds for which the location can be + updated. + heading (:obj:`int`): Optional. For live locations, a direction in which the user is + moving, in degrees. + proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. + """ - def __init__(self, latitude, longitude, live_period=None, **kwargs): + __slots__ = ('longitude', 'horizontal_accuracy', 'proximity_alert_radius', 'live_period', + 'latitude', 'heading', '_id_attrs') + # fmt: on + + def __init__( + self, + latitude: float, + longitude: float, + live_period: int = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + **_kwargs: Any, + ): # Required self.latitude = latitude self.longitude = longitude - self.live_period = live_period + + # Optionals + self.live_period = int(live_period) if live_period else None + self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None + self.heading = int(heading) if heading else None + self.proximity_alert_radius = ( + int(proximity_alert_radius) if proximity_alert_radius else None + ) + + self._id_attrs = (self.latitude, self.longitude) diff --git a/telegramer/include/telegram/inline/inputmessagecontent.py b/telegramer/include/telegram/inline/inputmessagecontent.py index f7b7065..4362c72 100644 --- a/telegramer/include/telegram/inline/inputmessagecontent.py +++ b/telegramer/include/telegram/inline/inputmessagecontent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -25,8 +25,10 @@ class InputMessageContent(TelegramObject): """Base class for Telegram InputMessageContent Objects. See: :class:`telegram.InputContactMessageContent`, + :class:`telegram.InputInvoiceMessageContent`, :class:`telegram.InputLocationMessageContent`, :class:`telegram.InputTextMessageContent` and :class:`telegram.InputVenueMessageContent` for more details. """ - pass + + __slots__ = () diff --git a/telegramer/include/telegram/inline/inputtextmessagecontent.py b/telegramer/include/telegram/inline/inputtextmessagecontent.py index 3e08d6b..c068aa7 100644 --- a/telegramer/include/telegram/inline/inputtextmessagecontent.py +++ b/telegramer/include/telegram/inline/inputtextmessagecontent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,34 +18,71 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InputTextMessageContent.""" -from telegram import InputMessageContent +from typing import Any, Union, Tuple, List + +from telegram import InputMessageContent, MessageEntity +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput class InputTextMessageContent(InputMessageContent): """ Represents the content of a text message to be sent as the result of an inline query. - Attributes: - message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters. - parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in your bot's message. - disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in the - sent message. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_text` is equal. Args: - message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters. Also found - as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. + message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters after entities + parsing. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show - bold, italic, fixed-width text or inline URLs in your bot's message. + bold, italic, fixed-width text or inline URLs in your bot's message. See the constants + in :class:`telegram.ParseMode` for the available modes. + entities (List[:class:`telegram.MessageEntity`], optional): List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in the sent message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters after entities + parsing. + parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in your bot's message. See the constants + in :class:`telegram.ParseMode` for the available modes. + entities (List[:class:`telegram.MessageEntity`]): Optional. List of special + entities that appear in the caption, which can be specified instead of + :attr:`parse_mode`. + disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in the + sent message. + """ - def __init__(self, message_text, parse_mode=None, disable_web_page_preview=None, **kwargs): + __slots__ = ('disable_web_page_preview', 'parse_mode', 'entities', 'message_text', '_id_attrs') + + def __init__( + self, + message_text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + **_kwargs: Any, + ): # Required self.message_text = message_text # Optionals self.parse_mode = parse_mode + self.entities = entities self.disable_web_page_preview = disable_web_page_preview + + self._id_attrs = (self.message_text,) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + if self.entities: + data['entities'] = [ce.to_dict() for ce in self.entities] + + return data diff --git a/telegramer/include/telegram/inline/inputvenuemessagecontent.py b/telegramer/include/telegram/inline/inputvenuemessagecontent.py index d0f1fdc..c13107d 100644 --- a/telegramer/include/telegram/inline/inputvenuemessagecontent.py +++ b/telegramer/include/telegram/inline/inputvenuemessagecontent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,21 +18,21 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InputVenueMessageContent.""" +from typing import Any + from telegram import InputMessageContent class InputVenueMessageContent(InputMessageContent): """Represents the content of a venue message to be sent as the result of an inline query. - Attributes: - latitude (:obj:`float`): Latitude of the location in degrees. - longitude (:obj:`float`): Longitude of the location in degrees. - title (:obj:`str`): Name of the venue. - address (:obj:`str`): Address of the venue. - foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue, if known. - foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. - (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or - "food/icecream".) + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`latitude`, :attr:`longitude` and :attr:`title` + are equal. + + Note: + Foursquare details and Google Pace details are mutually exclusive. However, this + behaviour is undocumented and might be changed by Telegram. Args: latitude (:obj:`float`): Latitude of the location in degrees. @@ -43,12 +43,47 @@ class InputVenueMessageContent(InputMessageContent): foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) + google_place_id (:obj:`str`, optional): Google Places identifier of the venue. + google_place_type (:obj:`str`, optional): Google Places type of the venue. (See + `supported types `_.) **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + latitude (:obj:`float`): Latitude of the location in degrees. + longitude (:obj:`float`): Longitude of the location in degrees. + title (:obj:`str`): Name of the venue. + address (:obj:`str`): Address of the venue. + foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue, if known. + foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. + google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. + google_place_type (:obj:`str`): Optional. Google Places type of the venue. + """ - def __init__(self, latitude, longitude, title, address, foursquare_id=None, - foursquare_type=None, **kwargs): + __slots__ = ( + 'longitude', + 'google_place_type', + 'title', + 'address', + 'foursquare_id', + 'foursquare_type', + 'google_place_id', + 'latitude', + '_id_attrs', + ) + + def __init__( + self, + latitude: float, + longitude: float, + title: str, + address: str, + foursquare_id: str = None, + foursquare_type: str = None, + google_place_id: str = None, + google_place_type: str = None, + **_kwargs: Any, + ): # Required self.latitude = latitude self.longitude = longitude @@ -57,3 +92,11 @@ def __init__(self, latitude, longitude, title, address, foursquare_id=None, # Optionals self.foursquare_id = foursquare_id self.foursquare_type = foursquare_type + self.google_place_id = google_place_id + self.google_place_type = google_place_type + + self._id_attrs = ( + self.latitude, + self.longitude, + self.title, + ) diff --git a/telegramer/include/telegram/keyboardbutton.py b/telegramer/include/telegram/keyboardbutton.py index a8ed639..fe2bd87 100644 --- a/telegramer/include/telegram/keyboardbutton.py +++ b/telegramer/include/telegram/keyboardbutton.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,9 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram KeyboardButton.""" -from telegram import TelegramObject +from typing import Any + +from telegram import TelegramObject, KeyboardButtonPollType class KeyboardButton(TelegramObject): @@ -26,31 +28,56 @@ class KeyboardButton(TelegramObject): This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. - Note: - Optional fields are mutually exclusive. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text`, :attr:`request_contact`, :attr:`request_location` and + :attr:`request_poll` are equal. - Attributes: - text (:obj:`str`): Text of the button. - request_contact (:obj:`bool`): Optional. If the user's phone number will be sent. - request_location (:obj:`bool`): Optional. If the user's current location will be sent. + Note: + * Optional fields are mutually exclusive. + * :attr:`request_contact` and :attr:`request_location` options will only work in Telegram + versions released after 9 April, 2016. Older clients will ignore them. + * :attr:`request_poll` option will only work in Telegram versions released after 23 + January, 2020. Older clients will receive unsupported message. Args: text (:obj:`str`): Text of the button. If none of the optional fields are used, it will be sent to the bot as a message when the button is pressed. - request_contact (:obj:`bool`, optional): If True, the user's phone number will be sent as - a contact when the button is pressed. Available in private chats only. - request_location (:obj:`bool`, optional): If True, the user's current location will be sent - when the button is pressed. Available in private chats only. + request_contact (:obj:`bool`, optional): If :obj:`True`, the user's phone number will be + sent as a contact when the button is pressed. Available in private chats only. + request_location (:obj:`bool`, optional): If :obj:`True`, the user's current location will + be sent when the button is pressed. Available in private chats only. + request_poll (:class:`KeyboardButtonPollType`, optional): If specified, the user will be + asked to create a poll and send it to the bot when the button is pressed. Available in + private chats only. - Note: - :attr:`request_contact` and :attr:`request_location` options will only work in Telegram - versions released after 9 April, 2016. Older clients will ignore them. + Attributes: + text (:obj:`str`): Text of the button. + request_contact (:obj:`bool`): Optional. The user's phone number will be sent. + request_location (:obj:`bool`): Optional. The user's current location will be sent. + request_poll (:class:`KeyboardButtonPollType`): Optional. If the user should create a poll. """ - def __init__(self, text, request_contact=None, request_location=None, **kwargs): + __slots__ = ('request_location', 'request_contact', 'request_poll', 'text', '_id_attrs') + + def __init__( + self, + text: str, + request_contact: bool = None, + request_location: bool = None, + request_poll: KeyboardButtonPollType = None, + **_kwargs: Any, + ): # Required self.text = text # Optionals self.request_contact = request_contact self.request_location = request_location + self.request_poll = request_poll + + self._id_attrs = ( + self.text, + self.request_contact, + self.request_location, + self.request_poll, + ) diff --git a/telegramer/include/telegram/keyboardbuttonpolltype.py b/telegramer/include/telegram/keyboardbuttonpolltype.py new file mode 100644 index 0000000..813ce97 --- /dev/null +++ b/telegramer/include/telegram/keyboardbuttonpolltype.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a type of a Telegram Poll.""" +from typing import Any + +from telegram import TelegramObject + + +class KeyboardButtonPollType(TelegramObject): + """This object represents type of a poll, which is allowed to be created + and sent when the corresponding button is pressed. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + Attributes: + type (:obj:`str`): Optional. If :attr:`telegram.Poll.QUIZ` is passed, the user will be + allowed to create only polls in the quiz mode. If :attr:`telegram.Poll.REGULAR` is + passed, only regular polls will be allowed. Otherwise, the user will be allowed to + create a poll of any type. + """ + + __slots__ = ('type', '_id_attrs') + + def __init__(self, type: str = None, **_kwargs: Any): # pylint: disable=W0622 + self.type = type + + self._id_attrs = (self.type,) diff --git a/telegramer/include/telegram/loginurl.py b/telegramer/include/telegram/loginurl.py new file mode 100644 index 0000000..7d6dc56 --- /dev/null +++ b/telegramer/include/telegram/loginurl.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram LoginUrl.""" +from typing import Any + +from telegram import TelegramObject + + +class LoginUrl(TelegramObject): + """This object represents a parameter of the inline keyboard button used to automatically + authorize a user. Serves as a great replacement for the Telegram Login Widget when the user is + coming from Telegram. All the user needs to do is tap/click a button and confirm that they want + to log in. Telegram apps support these buttons as of version 5.7. + + Sample bot: `@discussbot `_ + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`url` is equal. + + Note: + You must always check the hash of the received data to verify the authentication + and the integrity of the data as described in + `Checking authorization `_ + + Args: + url (:obj:`str`): An HTTP URL to be opened with user authorization data added to the query + string when the button is pressed. If the user refuses to provide authorization data, + the original URL without information about the user will be opened. The data added is + the same as described in + `Receiving authorization data + `_ + forward_text (:obj:`str`, optional): New text of the button in forwarded messages. + bot_username (:obj:`str`, optional): Username of a bot, which will be used for user + authorization. See + `Setting up a bot `_ + for more details. If not specified, the current + bot's username will be assumed. The url's domain must be the same as the domain linked + with the bot. See + `Linking your domain to the bot + `_ + for more details. + request_write_access (:obj:`bool`, optional): Pass :obj:`True` to request the permission + for your bot to send messages to the user. + + Attributes: + url (:obj:`str`): An HTTP URL to be opened with user authorization data. + forward_text (:obj:`str`): Optional. New text of the button in forwarded messages. + bot_username (:obj:`str`): Optional. Username of a bot, which will be used for user + authorization. + request_write_access (:obj:`bool`): Optional. Pass :obj:`True` to request the permission + for your bot to send messages to the user. + + """ + + __slots__ = ('bot_username', 'request_write_access', 'url', 'forward_text', '_id_attrs') + + def __init__( + self, + url: str, + forward_text: bool = None, + bot_username: str = None, + request_write_access: bool = None, + **_kwargs: Any, + ): + # Required + self.url = url + # Optional + self.forward_text = forward_text + self.bot_username = bot_username + self.request_write_access = request_write_access + + self._id_attrs = (self.url,) diff --git a/telegramer/include/telegram/message.py b/telegramer/include/telegram/message.py index 09b07d4..74d750f 100644 --- a/telegramer/include/telegram/message.py +++ b/telegramer/include/telegram/message.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -# pylint: disable=R0902,R0912,R0913 +# pylint: disable=R0902,R0913 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,118 +18,118 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Message.""" +import datetime import sys from html import escape - -from telegram import (Animation, Audio, Contact, Document, Chat, Location, PhotoSize, Sticker, - TelegramObject, User, Video, Voice, Venue, MessageEntity, Game, Invoice, - SuccessfulPayment, VideoNote, PassportData) -from telegram import ParseMode -from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp - -_UNDEFINED = object() +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, ClassVar, Tuple + +from telegram import ( + Animation, + Audio, + Chat, + Contact, + Dice, + Document, + Game, + InlineKeyboardMarkup, + Invoice, + Location, + MessageEntity, + ParseMode, + PassportData, + PhotoSize, + Poll, + Sticker, + SuccessfulPayment, + TelegramObject, + User, + Venue, + Video, + VideoNote, + Voice, + VoiceChatStarted, + VoiceChatEnded, + VoiceChatParticipantsInvited, + ProximityAlertTriggered, + ReplyMarkup, + MessageAutoDeleteTimerChanged, + VoiceChatScheduled, +) +from telegram.utils.helpers import ( + escape_markdown, + from_timestamp, + to_timestamp, + DEFAULT_NONE, + DEFAULT_20, +) +from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput + +if TYPE_CHECKING: + from telegram import ( + Bot, + GameHighScore, + InputMedia, + MessageId, + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + LabeledPrice, + ) class Message(TelegramObject): + # fmt: off """This object represents a message. - Note: - * In Python `from` is a reserved word, use `from_user` instead. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` and :attr:`chat` are equal. - Attributes: - message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`): Optional. Sender. - date (:class:`datetime.datetime`): Date the message was sent. - chat (:class:`telegram.Chat`): Conversation the message belongs to. - forward_from (:class:`telegram.User`): Optional. Sender of the original message. - forward_from_chat (:class:`telegram.Chat`): Optional. Information about the original - channel. - forward_from_message_id (:obj:`int`): Optional. Identifier of the original message in the - channel. - forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent. - reply_to_message (:class:`telegram.Message`): Optional. The original message. - edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited. - media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this - message belongs to. - text (:obj:`str`): Optional. The actual UTF-8 text of the message. - entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like - usernames, URLs, bot commands, etc. that appear in the text. See - :attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use - properly. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like - usernames, URLs, bot commands, etc. that appear in the caption. See - :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how - to use properly. - audio (:class:`telegram.Audio`): Optional. Information about the file. - document (:class:`telegram.Document`): Optional. Information about the file. - animation (:class:`telegram.Animation`) Optional. Information about the file. - For backward compatibility, when this field is set, the document field will also be - set. - game (:class:`telegram.Game`): Optional. Information about the game. - photo (List[:class:`telegram.PhotoSize`]): Optional. Available sizes of the photo. - sticker (:class:`telegram.Sticker`): Optional. Information about the sticker. - video (:class:`telegram.Video`): Optional. Information about the video. - voice (:class:`telegram.Voice`): Optional. Information about the file. - video_note (:class:`telegram.VideoNote`): Optional. Information about the video message. - new_chat_members (List[:class:`telegram.User`]): Optional. Information about new members to - the chat. (the bot itself may be one of these members). - caption (:obj:`str`): Optional. Caption for the document, photo or video, 0-200 characters. - contact (:class:`telegram.Contact`): Optional. Information about the contact. - location (:class:`telegram.Location`): Optional. Information about the location. - venue (:class:`telegram.Venue`): Optional. Information about the venue. - left_chat_member (:class:`telegram.User`): Optional. Information about the user that left - the group. (this member may be the bot itself). - new_chat_title (:obj:`str`): Optional. A chat title was changed to this value. - new_chat_photo (List[:class:`telegram.PhotoSize`]): Optional. A chat photo was changed to - this value. - delete_chat_photo (:obj:`bool`): Optional. The chat photo was deleted. - group_chat_created (:obj:`bool`): Optional. The group has been created. - supergroup_chat_created (:obj:`bool`): Optional. The supergroup has been created. - channel_chat_created (:obj:`bool`): Optional. The channel has been created. - migrate_to_chat_id (:obj:`int`): Optional. The group has been migrated to a supergroup with - the specified identifier. - migrate_from_chat_id (:obj:`int`): Optional. The supergroup has been migrated from a group - with the specified identifier. - pinned_message (:class:`telegram.message`): Optional. Specified message was pinned. - invoice (:class:`telegram.Invoice`): Optional. Information about the invoice. - successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Information about the - payment. - connected_website (:obj:`str`): Optional. The domain name of the website on which the user - has logged in. - forward_signature (:obj:`str`): Optional. Signature of the post author for messages - forwarded from channels. - author_signature (:obj:`str`): Optional. Signature of the post author for messages - in channels. - passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Note: + In Python ``from`` is a reserved word, use ``from_user`` instead. Args: message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`, optional): Sender, can be empty for messages sent - to channels. + from_user (:class:`telegram.User`, optional): Sender of the message; empty for messages + sent to channels. For backward compatibility, this will contain a fake sender user in + non-channel chats, if the message was sent on behalf of a chat. + sender_chat (:class:`telegram.Chat`, optional): Sender of the message, sent on behalf of a + chat. For example, the channel itself for channel posts, the supergroup itself for + messages from anonymous group administrators, the linked channel for messages + automatically forwarded to the discussion group. For backward compatibility, + :attr:`from_user` contains a fake sender user in non-channel chats, if the message was + sent on behalf of a chat. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. chat (:class:`telegram.Chat`): Conversation the message belongs to. forward_from (:class:`telegram.User`, optional): For forwarded messages, sender of the original message. - forward_from_chat (:class:`telegram.Chat`, optional): For messages forwarded from a - channel, information about the original channel. + forward_from_chat (:class:`telegram.Chat`, optional): For messages forwarded from channels + or from anonymous administrators, information about the original sender chat. forward_from_message_id (:obj:`int`, optional): For forwarded channel posts, identifier of the original message in the channel. + forward_sender_name (:obj:`str`, optional): Sender's name for messages forwarded from users + who disallow adding a link to their account in forwarded messages. forward_date (:class:`datetime.datetime`, optional): For forwarded messages, date the original message was sent in Unix time. Converted to :class:`datetime.datetime`. + is_automatic_forward (:obj:`bool`, optional): :obj:`True`, if the message is a channel post + that was automatically forwarded to the connected discussion group. + + .. versionadded:: 13.9 reply_to_message (:class:`telegram.Message`, optional): For replies, the original message. - Note that the Message object in this field will not contain further - ``reply_to_message`` fields even if it itself is a reply. edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix time. Converted to :class:`datetime.datetime`. + has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be + forwarded. + + .. versionadded:: 13.9 media_group_id (:obj:`str`, optional): The unique identifier of a media message group this message belongs to. text (str, optional): For text messages, the actual UTF-8 text of the message, 0-4096 characters. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. entities (List[:class:`telegram.MessageEntity`], optional): For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text. See - attr:`parse_entity` and attr:`parse_entities` methods for how to use properly. + :attr:`parse_entity` and :attr:`parse_entities` methods for how to use properly. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. For Messages with a Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` @@ -154,16 +154,19 @@ class Message(TelegramObject): new_chat_members (List[:class:`telegram.User`], optional): New members that were added to the group or supergroup and information about them (the bot itself may be one of these members). - caption (:obj:`str`, optional): Caption for the document, photo or video, 0-200 characters. + caption (:obj:`str`, optional): Caption for the animation, audio, document, photo, video + or voice, 0-1024 characters. contact (:class:`telegram.Contact`, optional): Message is a shared contact, information about the contact. location (:class:`telegram.Location`, optional): Message is a shared location, information about the location. venue (:class:`telegram.Venue`, optional): Message is a venue, information about the venue. + For backward compatibility, when this field is set, the location field will also be + set. left_chat_member (:class:`telegram.User`, optional): A member was removed from the group, information about them (this member may be the bot itself). new_chat_title (:obj:`str`, optional): A chat title was changed to this value. - new_chat_photo (List[:class:`telegram.PhotoSize`], optional): A chat photo was change to + new_chat_photo (List[:class:`telegram.PhotoSize`], optional): A chat photo was changed to this value. delete_chat_photo (:obj:`bool`, optional): Service message: The chat photo was deleted. group_chat_created (:obj:`bool`, optional): Service message: The group has been created. @@ -175,7 +178,11 @@ class Message(TelegramObject): channel_chat_created (:obj:`bool`, optional): Service message: The channel has been created. This field can't be received in a message coming through updates, because bot can't be a member of a channel when it is created. It can only be found in - attr:`reply_to_message` if someone replies to a very first message in a channel. + :attr:`reply_to_message` if someone replies to a very first message in a channel. + message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`, \ + optional): Service message: auto-delete timer settings changed in the chat. + + .. versionadded:: 13.4 migrate_to_chat_id (:obj:`int`, optional): The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than @@ -186,8 +193,8 @@ class Message(TelegramObject): programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. - pinned_message (:class:`telegram.message`, optional): Specified message was pinned. Note - that the Message object in this field will not contain further attr:`reply_to_message` + pinned_message (:class:`telegram.Message`, optional): Specified message was pinned. Note + that the Message object in this field will not contain further :attr:`reply_to_message` fields even if it is itself a reply. invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, information about the invoice. @@ -195,88 +202,343 @@ class Message(TelegramObject): message about a successful payment, information about the payment. connected_website (:obj:`str`, optional): The domain name of the website on which the user has logged in. - forward_signature (:obj:`str`, optional): Signature of the post author for messages + forward_signature (:obj:`str`, optional): For messages forwarded from channels, signature + of the post author if present. + author_signature (:obj:`str`, optional): Signature of the post author for messages in + channels, or the custom title of an anonymous group administrator. + passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data. + poll (:class:`telegram.Poll`, optional): Message is a native poll, + information about the poll. + dice (:class:`telegram.Dice`, optional): Message is a dice with random value from 1 to 6. + via_bot (:class:`telegram.User`, optional): Message was sent through an inline bot. + proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`, optional): Service + message. A user in the chat triggered another user's proximity alert while sharing + Live Location. + voice_chat_scheduled (:class:`telegram.VoiceChatScheduled`, optional): Service message: + voice chat scheduled. + + .. versionadded:: 13.5 + voice_chat_started (:class:`telegram.VoiceChatStarted`, optional): Service message: voice + chat started. + + .. versionadded:: 13.4 + voice_chat_ended (:class:`telegram.VoiceChatEnded`, optional): Service message: voice chat + ended. + + .. versionadded:: 13.4 + voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited` optional): + Service message: new participants invited to a voice chat. + + .. versionadded:: 13.4 + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. ``login_url`` buttons are represented as ordinary url buttons. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + + Attributes: + message_id (:obj:`int`): Unique message identifier inside this chat. + from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages + sent to channels. For backward compatibility, this will contain a fake sender user in + non-channel chats, if the message was sent on behalf of a chat. + sender_chat (:class:`telegram.Chat`): Optional. Sender of the message, sent on behalf of a + chat. For backward compatibility, :attr:`from_user` contains a fake sender user in + non-channel chats, if the message was sent on behalf of a chat. + date (:class:`datetime.datetime`): Date the message was sent. + chat (:class:`telegram.Chat`): Conversation the message belongs to. + forward_from (:class:`telegram.User`): Optional. Sender of the original message. + forward_from_chat (:class:`telegram.Chat`): Optional. For messages forwarded from channels + or from anonymous administrators, information about the original sender chat. + forward_from_message_id (:obj:`int`): Optional. Identifier of the original message in the + channel. + forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent. + is_automatic_forward (:obj:`bool`): Optional. :obj:`True`, if the message is a channel post + that was automatically forwarded to the connected discussion group. + + .. versionadded:: 13.9 + reply_to_message (:class:`telegram.Message`): Optional. For replies, the original message. + Note that the Message object in this field will not contain further + ``reply_to_message`` fields even if it itself is a reply. + edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited. + has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be + forwarded. + + .. versionadded:: 13.9 + media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this + message belongs to. + text (:obj:`str`): Optional. The actual UTF-8 text of the message. + entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like + usernames, URLs, bot commands, etc. that appear in the text. See + :attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use + properly. + caption_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like + usernames, URLs, bot commands, etc. that appear in the caption. See + :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how + to use properly. + audio (:class:`telegram.Audio`): Optional. Information about the file. + document (:class:`telegram.Document`): Optional. Information about the file. + animation (:class:`telegram.Animation`) Optional. Information about the file. + For backward compatibility, when this field is set, the document field will also be + set. + game (:class:`telegram.Game`): Optional. Information about the game. + photo (List[:class:`telegram.PhotoSize`]): Optional. Available sizes of the photo. + sticker (:class:`telegram.Sticker`): Optional. Information about the sticker. + video (:class:`telegram.Video`): Optional. Information about the video. + voice (:class:`telegram.Voice`): Optional. Information about the file. + video_note (:class:`telegram.VideoNote`): Optional. Information about the video message. + new_chat_members (List[:class:`telegram.User`]): Optional. Information about new members to + the chat. (the bot itself may be one of these members). + caption (:obj:`str`): Optional. Caption for the document, photo or video, 0-1024 + characters. + contact (:class:`telegram.Contact`): Optional. Information about the contact. + location (:class:`telegram.Location`): Optional. Information about the location. + venue (:class:`telegram.Venue`): Optional. Information about the venue. + left_chat_member (:class:`telegram.User`): Optional. Information about the user that left + the group. (this member may be the bot itself). + new_chat_title (:obj:`str`): Optional. A chat title was changed to this value. + new_chat_photo (List[:class:`telegram.PhotoSize`]): Optional. A chat photo was changed to + this value. + delete_chat_photo (:obj:`bool`): Optional. The chat photo was deleted. + group_chat_created (:obj:`bool`): Optional. The group has been created. + supergroup_chat_created (:obj:`bool`): Optional. The supergroup has been created. + channel_chat_created (:obj:`bool`): Optional. The channel has been created. + message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`): + Optional. Service message: auto-delete timer settings changed in the chat. + + .. versionadded:: 13.4 + migrate_to_chat_id (:obj:`int`): Optional. The group has been migrated to a supergroup with + the specified identifier. + migrate_from_chat_id (:obj:`int`): Optional. The supergroup has been migrated from a group + with the specified identifier. + pinned_message (:class:`telegram.message`): Optional. Specified message was pinned. + invoice (:class:`telegram.Invoice`): Optional. Information about the invoice. + successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Information about the + payment. + connected_website (:obj:`str`): Optional. The domain name of the website on which the user + has logged in. + forward_signature (:obj:`str`): Optional. Signature of the post author for messages forwarded from channels. - author_signature (:obj:`str`, optional): Signature of the post author for messages - in channels. - passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data + forward_sender_name (:obj:`str`): Optional. Sender's name for messages forwarded from users + who disallow adding a link to their account in forwarded messages. + author_signature (:obj:`str`): Optional. Signature of the post author for messages in + channels, or the custom title of an anonymous group administrator. + passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data. + poll (:class:`telegram.Poll`): Optional. Message is a native poll, + information about the poll. + dice (:class:`telegram.Dice`): Optional. Message is a dice. + via_bot (:class:`telegram.User`): Optional. Bot through which the message was sent. + proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`): Optional. Service + message. A user in the chat triggered another user's proximity alert while sharing + Live Location. + voice_chat_scheduled (:class:`telegram.VoiceChatScheduled`): Optional. Service message: + voice chat scheduled. + + .. versionadded:: 13.5 + voice_chat_started (:class:`telegram.VoiceChatStarted`): Optional. Service message: voice + chat started. + + .. versionadded:: 13.4 + voice_chat_ended (:class:`telegram.VoiceChatEnded`): Optional. Service message: voice chat + ended. + + .. versionadded:: 13.4 + voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited`): Optional. + Service message: new participants invited to a voice chat. + + .. versionadded:: 13.4 + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - _effective_attachment = _UNDEFINED - - ATTACHMENT_TYPES = ['audio', 'game', 'animation', 'document', 'photo', 'sticker', 'video', - 'voice', 'video_note', 'contact', 'location', 'venue', 'invoice', - 'successful_payment'] - MESSAGE_TYPES = ['text', 'new_chat_members', 'new_chat_title', 'new_chat_photo', - 'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created', - 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', - 'pinned_message', 'passport_data'] + ATTACHMENT_TYPES - - def __init__(self, - message_id, - from_user, - date, - chat, - forward_from=None, - forward_from_chat=None, - forward_from_message_id=None, - forward_date=None, - reply_to_message=None, - edit_date=None, - text=None, - entities=None, - caption_entities=None, - audio=None, - document=None, - game=None, - photo=None, - sticker=None, - video=None, - voice=None, - video_note=None, - new_chat_members=None, - caption=None, - contact=None, - location=None, - venue=None, - left_chat_member=None, - new_chat_title=None, - new_chat_photo=None, - delete_chat_photo=False, - group_chat_created=False, - supergroup_chat_created=False, - channel_chat_created=False, - migrate_to_chat_id=None, - migrate_from_chat_id=None, - pinned_message=None, - invoice=None, - successful_payment=None, - forward_signature=None, - author_signature=None, - media_group_id=None, - connected_website=None, - animation=None, - passport_data=None, - bot=None, - **kwargs): + # fmt: on + __slots__ = ( + 'reply_markup', + 'audio', + 'contact', + 'migrate_to_chat_id', + 'forward_signature', + 'chat', + 'successful_payment', + 'game', + 'text', + 'forward_sender_name', + 'document', + 'new_chat_title', + 'forward_date', + 'group_chat_created', + 'media_group_id', + 'caption', + 'video', + 'bot', + 'entities', + 'via_bot', + 'new_chat_members', + 'connected_website', + 'animation', + 'migrate_from_chat_id', + 'forward_from', + 'sticker', + 'location', + 'venue', + 'edit_date', + 'reply_to_message', + 'passport_data', + 'pinned_message', + 'forward_from_chat', + 'new_chat_photo', + 'message_id', + 'delete_chat_photo', + 'from_user', + 'author_signature', + 'proximity_alert_triggered', + 'sender_chat', + 'dice', + 'forward_from_message_id', + 'caption_entities', + 'voice', + 'date', + 'supergroup_chat_created', + 'poll', + 'left_chat_member', + 'photo', + 'channel_chat_created', + 'invoice', + 'video_note', + '_effective_attachment', + 'message_auto_delete_timer_changed', + 'voice_chat_ended', + 'voice_chat_participants_invited', + 'voice_chat_started', + 'voice_chat_scheduled', + 'is_automatic_forward', + 'has_protected_content', + '_id_attrs', + ) + + ATTACHMENT_TYPES: ClassVar[List[str]] = [ + 'audio', + 'game', + 'animation', + 'document', + 'photo', + 'sticker', + 'video', + 'voice', + 'video_note', + 'contact', + 'location', + 'venue', + 'invoice', + 'successful_payment', + ] + MESSAGE_TYPES: ClassVar[List[str]] = [ + 'text', + 'new_chat_members', + 'left_chat_member', + 'new_chat_title', + 'new_chat_photo', + 'delete_chat_photo', + 'group_chat_created', + 'supergroup_chat_created', + 'channel_chat_created', + 'message_auto_delete_timer_changed', + 'migrate_to_chat_id', + 'migrate_from_chat_id', + 'pinned_message', + 'poll', + 'dice', + 'passport_data', + 'proximity_alert_triggered', + 'voice_chat_scheduled', + 'voice_chat_started', + 'voice_chat_ended', + 'voice_chat_participants_invited', + ] + ATTACHMENT_TYPES + + def __init__( + self, + message_id: int, + date: datetime.datetime, + chat: Chat, + from_user: User = None, + forward_from: User = None, + forward_from_chat: Chat = None, + forward_from_message_id: int = None, + forward_date: datetime.datetime = None, + reply_to_message: 'Message' = None, + edit_date: datetime.datetime = None, + text: str = None, + entities: List['MessageEntity'] = None, + caption_entities: List['MessageEntity'] = None, + audio: Audio = None, + document: Document = None, + game: Game = None, + photo: List[PhotoSize] = None, + sticker: Sticker = None, + video: Video = None, + voice: Voice = None, + video_note: VideoNote = None, + new_chat_members: List[User] = None, + caption: str = None, + contact: Contact = None, + location: Location = None, + venue: Venue = None, + left_chat_member: User = None, + new_chat_title: str = None, + new_chat_photo: List[PhotoSize] = None, + delete_chat_photo: bool = False, + group_chat_created: bool = False, + supergroup_chat_created: bool = False, + channel_chat_created: bool = False, + migrate_to_chat_id: int = None, + migrate_from_chat_id: int = None, + pinned_message: 'Message' = None, + invoice: Invoice = None, + successful_payment: SuccessfulPayment = None, + forward_signature: str = None, + author_signature: str = None, + media_group_id: str = None, + connected_website: str = None, + animation: Animation = None, + passport_data: PassportData = None, + poll: Poll = None, + forward_sender_name: str = None, + reply_markup: InlineKeyboardMarkup = None, + bot: 'Bot' = None, + dice: Dice = None, + via_bot: User = None, + proximity_alert_triggered: ProximityAlertTriggered = None, + sender_chat: Chat = None, + voice_chat_started: VoiceChatStarted = None, + voice_chat_ended: VoiceChatEnded = None, + voice_chat_participants_invited: VoiceChatParticipantsInvited = None, + message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged = None, + voice_chat_scheduled: VoiceChatScheduled = None, + is_automatic_forward: bool = None, + has_protected_content: bool = None, + **_kwargs: Any, + ): # Required self.message_id = int(message_id) + # Optionals self.from_user = from_user + self.sender_chat = sender_chat self.date = date self.chat = chat - # Optionals self.forward_from = forward_from self.forward_from_chat = forward_from_chat self.forward_date = forward_date + self.is_automatic_forward = is_automatic_forward self.reply_to_message = reply_to_message self.edit_date = edit_date + self.has_protected_content = has_protected_content self.text = text - self.entities = entities or list() - self.caption_entities = caption_entities or list() + self.entities = entities or [] + self.caption_entities = caption_entities or [] self.audio = audio self.game = game self.document = document - self.photo = photo or list() + self.photo = photo or [] self.sticker = sticker self.video = video self.voice = voice @@ -285,52 +547,72 @@ def __init__(self, self.contact = contact self.location = location self.venue = venue - self.new_chat_members = new_chat_members or list() + self.new_chat_members = new_chat_members or [] self.left_chat_member = left_chat_member self.new_chat_title = new_chat_title - self.new_chat_photo = new_chat_photo or list() + self.new_chat_photo = new_chat_photo or [] self.delete_chat_photo = bool(delete_chat_photo) self.group_chat_created = bool(group_chat_created) self.supergroup_chat_created = bool(supergroup_chat_created) self.migrate_to_chat_id = migrate_to_chat_id self.migrate_from_chat_id = migrate_from_chat_id self.channel_chat_created = bool(channel_chat_created) + self.message_auto_delete_timer_changed = message_auto_delete_timer_changed self.pinned_message = pinned_message self.forward_from_message_id = forward_from_message_id self.invoice = invoice self.successful_payment = successful_payment self.connected_website = connected_website self.forward_signature = forward_signature + self.forward_sender_name = forward_sender_name self.author_signature = author_signature self.media_group_id = media_group_id self.animation = animation self.passport_data = passport_data - + self.poll = poll + self.dice = dice + self.via_bot = via_bot + self.proximity_alert_triggered = proximity_alert_triggered + self.voice_chat_scheduled = voice_chat_scheduled + self.voice_chat_started = voice_chat_started + self.voice_chat_ended = voice_chat_ended + self.voice_chat_participants_invited = voice_chat_participants_invited + self.reply_markup = reply_markup self.bot = bot - self._id_attrs = (self.message_id,) + self._effective_attachment = DEFAULT_NONE + + self._id_attrs = (self.message_id, self.chat) @property - def chat_id(self): + def chat_id(self) -> int: """:obj:`int`: Shortcut for :attr:`telegram.Chat.id` for :attr:`chat`.""" return self.chat.id @property - def link(self): - """:obj:`str`: Convenience property. If the chat of the message is a supergroup or a - channel and has a :attr:`Chat.username`, returns a t.me link of the message.""" - if self.chat.type in (Chat.SUPERGROUP, Chat.CHANNEL) and self.chat.username: - return "https://t.me/{}/{}".format(self.chat.username, self.message_id) + def link(self) -> Optional[str]: + """:obj:`str`: Convenience property. If the chat of the message is not + a private chat or normal group, returns a t.me link of the message. + """ + if self.chat.type not in [Chat.PRIVATE, Chat.GROUP]: + if self.chat.username: + to_link = self.chat.username + else: + # Get rid of leading -100 for supergroups + to_link = f"c/{str(self.chat.id)[4:]}" + return f"https://t.me/{to_link}/{self.message_id}" return None @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Message']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(Message, cls).de_json(data, bot) - data['from_user'] = User.de_json(data.get('from'), bot) + data['sender_chat'] = Chat.de_json(data.get('sender_chat'), bot) data['date'] = from_timestamp(data['date']) data['chat'] = Chat.de_json(data.get('chat'), bot) data['entities'] = MessageEntity.de_list(data.get('entities'), bot) @@ -355,15 +637,49 @@ def de_json(cls, data, bot): data['new_chat_members'] = User.de_list(data.get('new_chat_members'), bot) data['left_chat_member'] = User.de_json(data.get('left_chat_member'), bot) data['new_chat_photo'] = PhotoSize.de_list(data.get('new_chat_photo'), bot) + data['message_auto_delete_timer_changed'] = MessageAutoDeleteTimerChanged.de_json( + data.get('message_auto_delete_timer_changed'), bot + ) data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot) data['invoice'] = Invoice.de_json(data.get('invoice'), bot) data['successful_payment'] = SuccessfulPayment.de_json(data.get('successful_payment'), bot) data['passport_data'] = PassportData.de_json(data.get('passport_data'), bot) - + data['poll'] = Poll.de_json(data.get('poll'), bot) + data['dice'] = Dice.de_json(data.get('dice'), bot) + data['via_bot'] = User.de_json(data.get('via_bot'), bot) + data['proximity_alert_triggered'] = ProximityAlertTriggered.de_json( + data.get('proximity_alert_triggered'), bot + ) + data['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot) + data['voice_chat_scheduled'] = VoiceChatScheduled.de_json( + data.get('voice_chat_scheduled'), bot + ) + data['voice_chat_started'] = VoiceChatStarted.de_json(data.get('voice_chat_started'), bot) + data['voice_chat_ended'] = VoiceChatEnded.de_json(data.get('voice_chat_ended'), bot) + data['voice_chat_participants_invited'] = VoiceChatParticipantsInvited.de_json( + data.get('voice_chat_participants_invited'), bot + ) return cls(bot=bot, **data) @property - def effective_attachment(self): + def effective_attachment( + self, + ) -> Union[ + Contact, + Document, + Animation, + Game, + Invoice, + Location, + List[PhotoSize], + Sticker, + SuccessfulPayment, + Venue, + Video, + VideoNote, + Voice, + None, + ]: """ :class:`telegram.Audio` or :class:`telegram.Contact` @@ -379,11 +695,11 @@ def effective_attachment(self): or :class:`telegram.Video` or :class:`telegram.VideoNote` or :class:`telegram.Voice`: The attachment that this message was sent with. May be - ``None`` if no attachment was sent. + :obj:`None` if no attachment was sent. """ - if self._effective_attachment is not _UNDEFINED: - return self._effective_attachment + if self._effective_attachment is not DEFAULT_NONE: + return self._effective_attachment # type: ignore for i in Message.ATTACHMENT_TYPES: if getattr(self, i, None): @@ -392,16 +708,14 @@ def effective_attachment(self): else: self._effective_attachment = None - return self._effective_attachment + return self._effective_attachment # type: ignore - def __getitem__(self, item): - if item in self.__dict__.keys(): - return self.__dict__[item] - elif item == 'chat_id': - return self.chat.id + def __getitem__(self, item: str) -> Any: # pylint: disable=R1710 + return self.chat.id if item == 'chat_id' else super().__getitem__(item) - def to_dict(self): - data = super(Message, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() # Required data['date'] = to_timestamp(self.date) @@ -423,292 +737,1138 @@ def to_dict(self): return data - def _quote(self, kwargs): + def _quote(self, quote: Optional[bool], reply_to_message_id: Optional[int]) -> Optional[int]: """Modify kwargs for replying with or without quoting.""" - if 'reply_to_message_id' in kwargs: - if 'quote' in kwargs: - del kwargs['quote'] - - elif 'quote' in kwargs: - if kwargs['quote']: - kwargs['reply_to_message_id'] = self.message_id + if reply_to_message_id is not None: + return reply_to_message_id - del kwargs['quote'] + if quote is not None: + if quote: + return self.message_id else: - if self.chat.type != Chat.PRIVATE: - kwargs['reply_to_message_id'] = self.message_id + if self.bot.defaults: + default_quote = self.bot.defaults.quote + else: + default_quote = None + if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote: + return self.message_id - def reply_text(self, *args, **kwargs): + return None + + def reply_text( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_message(update.message.chat_id, *args, **kwargs) + bot.send_message(update.effective_message.chat_id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the message is sent as an actual + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this - parameter will be ignored. Default: ``True`` in group chats and ``False`` in + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. - """ - self._quote(kwargs) - return self.bot.send_message(self.chat_id, *args, **kwargs) + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. - def reply_markdown(self, *args, **kwargs): + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_message( + chat_id=self.chat_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + ) + + def reply_markdown( + self, + text: str, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_message(update.message.chat_id, parse_mode=ParseMode.MARKDOWN, *args, - **kwargs) + bot.send_message( + update.effective_message.chat_id, + parse_mode=ParseMode.MARKDOWN, + *args, + **kwargs, + ) + + Sends a message with Markdown version 1 formatting. - Sends a message with markdown formatting. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + Note: + :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for + backward compatibility. You should use :meth:`reply_markdown_v2` instead. - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the message is sent as an actual + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this - parameter will be ignored. Default: ``True`` in group chats and ``False`` in + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_message( + chat_id=self.chat_id, + text=text, + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + ) + + def reply_markdown_v2( + self, + text: str, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: - kwargs['parse_mode'] = ParseMode.MARKDOWN + bot.send_message( + update.effective_message.chat_id, + parse_mode=ParseMode.MARKDOWN_V2, + *args, + **kwargs, + ) - self._quote(kwargs) + Sends a message with markdown version 2 formatting. - return self.bot.send_message(self.chat_id, *args, **kwargs) + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. - def reply_html(self, *args, **kwargs): + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in + private chats. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_message( + chat_id=self.chat_id, + text=text, + parse_mode=ParseMode.MARKDOWN_V2, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + ) + + def reply_html( + self, + text: str, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_message(update.message.chat_id, parse_mode=ParseMode.HTML, *args, **kwargs) + bot.send_message( + update.effective_message.chat_id, + parse_mode=ParseMode.HTML, + *args, + **kwargs, + ) Sends a message with HTML formatting. - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the message is sent as an actual + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this - parameter will be ignored. Default: ``True`` in group chats and ``False`` in + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. - """ - - kwargs['parse_mode'] = ParseMode.HTML - self._quote(kwargs) - - return self.bot.send_message(self.chat_id, *args, **kwargs) - - def reply_media_group(self, *args, **kwargs): + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_message( + chat_id=self.chat_id, + text=text, + parse_mode=ParseMode.HTML, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + ) + + def reply_media_group( + self, + media: List[ + Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + protect_content: bool = None, + ) -> List['Message']: """Shortcut for:: - bot.reply_media_group(update.message.chat_id, *args, **kwargs) + bot.send_media_group(update.effective_message.chat_id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the media group is sent as an + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the media group is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, - this parameter will be ignored. Default: ``True`` in group chats and ``False`` in - private chats. + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. Returns: List[:class:`telegram.Message`]: An array of the sent Messages. Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - self._quote(kwargs) - return self.bot.send_media_group(self.chat_id, *args, **kwargs) - - def reply_photo(self, *args, **kwargs): + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_media_group( + chat_id=self.chat_id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def reply_photo( + self, + photo: Union[FileInput, 'PhotoSize'], + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_photo(update.message.chat_id, *args, **kwargs) + bot.send_photo(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the photo is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_photo(self.chat_id, *args, **kwargs) - - def reply_audio(self, *args, **kwargs): + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_photo( + chat_id=self.chat_id, + photo=photo, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def reply_audio( + self, + audio: Union[FileInput, 'Audio'], + duration: int = None, + performer: str = None, + title: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_audio(update.message.chat_id, *args, **kwargs) + bot.send_audio(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the audio is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_audio(self.chat_id, *args, **kwargs) - - def reply_document(self, *args, **kwargs): + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_audio( + chat_id=self.chat_id, + audio=audio, + duration=duration, + performer=performer, + title=title, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def reply_document( + self, + document: Union[FileInput, 'Document'], + filename: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + disable_content_type_detection: bool = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_document(update.message.chat_id, *args, **kwargs) + bot.send_document(update.effective_message.chat_id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the document is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in + private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_document(self.chat_id, *args, **kwargs) - - def reply_animation(self, *args, **kwargs): + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_document( + chat_id=self.chat_id, + document=document, + filename=filename, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + thumb=thumb, + api_kwargs=api_kwargs, + disable_content_type_detection=disable_content_type_detection, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + protect_content=protect_content, + ) + + def reply_animation( + self, + animation: Union[FileInput, 'Animation'], + duration: int = None, + width: int = None, + height: int = None, + thumb: FileInput = None, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_animation(update.message.chat_id, *args, **kwargs) + bot.send_animation(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the animation is sent as an + actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_animation(self.chat_id, *args, **kwargs) - - def reply_sticker(self, *args, **kwargs): + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_animation( + chat_id=self.chat_id, + animation=animation, + duration=duration, + width=width, + height=height, + thumb=thumb, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def reply_sticker( + self, + sticker: Union[FileInput, 'Sticker'], + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_sticker(update.message.chat_id, *args, **kwargs) + bot.send_sticker(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the sticker is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in + private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_sticker(self.chat_id, *args, **kwargs) - - def reply_video(self, *args, **kwargs): + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_sticker( + chat_id=self.chat_id, + sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def reply_video( + self, + video: Union[FileInput, 'Video'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + width: int = None, + height: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: bool = None, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_video(update.message.chat_id, *args, **kwargs) + bot.send_video(update.effective_message.chat_id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the video is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in + private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_video(self.chat_id, *args, **kwargs) - - def reply_video_note(self, *args, **kwargs): + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_video( + chat_id=self.chat_id, + video=video, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + width=width, + height=height, + parse_mode=parse_mode, + supports_streaming=supports_streaming, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def reply_video_note( + self, + video_note: Union[FileInput, 'VideoNote'], + duration: int = None, + length: int = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: str = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_video_note(update.message.chat_id, *args, **kwargs) + bot.send_video_note(update.effective_message.chat_id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the video note is sent as an + actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_video_note(self.chat_id, *args, **kwargs) + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_video_note( + chat_id=self.chat_id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + filename=filename, + protect_content=protect_content, + ) + + def reply_voice( + self, + voice: Union[FileInput, 'Voice'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_voice(update.effective_message.chat_id, *args, **kwargs) - def reply_voice(self, *args, **kwargs): + For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the voice note is sent as an + actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_voice( + chat_id=self.chat_id, + voice=voice, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def reply_location( + self, + latitude: float = None, + longitude: float = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + location: Location = None, + live_period: int = None, + api_kwargs: JSONDict = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_voice(update.message.chat_id, *args, **kwargs) + bot.send_location(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the location is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in + private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_voice(self.chat_id, *args, **kwargs) + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_location( + chat_id=self.chat_id, + latitude=latitude, + longitude=longitude, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + location=location, + live_period=live_period, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def reply_venue( + self, + latitude: float = None, + longitude: float = None, + title: str = None, + address: str = None, + foursquare_id: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + venue: Venue = None, + foursquare_type: str = None, + api_kwargs: JSONDict = None, + google_place_id: str = None, + google_place_type: str = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_venue(update.effective_message.chat_id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the venue is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in + private chats. - def reply_location(self, *args, **kwargs): + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_venue( + chat_id=self.chat_id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + venue=venue, + foursquare_type=foursquare_type, + api_kwargs=api_kwargs, + google_place_id=google_place_id, + google_place_type=google_place_type, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def reply_contact( + self, + phone_number: str = None, + first_name: str = None, + last_name: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + contact: Contact = None, + vcard: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_location(update.message.chat_id, *args, **kwargs) + bot.send_contact(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the contact is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in + private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_location(self.chat_id, *args, **kwargs) + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_contact( + chat_id=self.chat_id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + contact=contact, + vcard=vcard, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def reply_poll( + self, + question: str, + options: List[str], + is_anonymous: bool = True, + type: str = Poll.REGULAR, # pylint: disable=W0622 + allows_multiple_answers: bool = False, + correct_option_id: int = None, + is_closed: bool = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + explanation: str = None, + explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, + open_period: int = None, + close_date: Union[int, datetime.datetime] = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_poll(update.effective_message.chat_id, *args, **kwargs) - def reply_venue(self, *args, **kwargs): + For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the poll is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_poll( + chat_id=self.chat_id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + is_closed=is_closed, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + explanation_entities=explanation_entities, + protect_content=protect_content, + ) + + def reply_dice( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + emoji: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_venue(update.message.chat_id, *args, **kwargs) + bot.send_dice(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the dice is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` + in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_venue(self.chat_id, *args, **kwargs) + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_dice( + chat_id=self.chat_id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + emoji=emoji, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def reply_chat_action( + self, + action: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.send_chat_action(update.effective_message.chat_id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. - def reply_contact(self, *args, **kwargs): + .. versionadded:: 13.2 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.send_chat_action( + chat_id=self.chat_id, + action=action, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def reply_game( + self, + game_short_name: str, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_contact(update.message.chat_id, *args, **kwargs) + bot.send_game(update.effective_message.chat_id, *args, **kwargs) - Keyword Args: - quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply - to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter - will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the game is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` + in private chats. + + .. versionadded:: 13.2 Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - self._quote(kwargs) - return self.bot.send_contact(self.chat_id, *args, **kwargs) + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_game( + chat_id=self.chat_id, + game_short_name=game_short_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def reply_invoice( + self, + title: str, + description: str, + payload: str, + provider_token: str, + currency: str, + prices: List['LabeledPrice'], + start_parameter: str = None, + photo_url: str = None, + photo_size: int = None, + photo_width: int = None, + photo_height: int = None, + need_name: bool = None, + need_phone_number: bool = None, + need_email: bool = None, + need_shipping_address: bool = None, + is_flexible: bool = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'InlineKeyboardMarkup' = None, + provider_data: Union[str, object] = None, + send_phone_number_to_provider: bool = None, + send_email_to_provider: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: bool = None, + max_tip_amount: int = None, + suggested_tip_amounts: List[int] = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_invoice(update.effective_message.chat_id, *args, **kwargs) - def forward(self, chat_id, disable_notification=False): + For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. + + Warning: + As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order + of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. + + .. versionadded:: 13.2 + + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter :attr:`start_parameter` is optional. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the invoice is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this + parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` + in private chats. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.send_invoice( + chat_id=self.chat_id, + title=title, + description=description, + payload=payload, + provider_token=provider_token, + currency=currency, + prices=prices, + start_parameter=start_parameter, + photo_url=photo_url, + photo_size=photo_size, + photo_width=photo_width, + photo_height=photo_height, + need_name=need_name, + need_phone_number=need_phone_number, + need_email=need_email, + need_shipping_address=need_shipping_address, + is_flexible=is_flexible, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + provider_data=provider_data, + send_phone_number_to_provider=send_phone_number_to_provider, + send_email_to_provider=send_email_to_provider, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + max_tip_amount=max_tip_amount, + suggested_tip_amounts=suggested_tip_amounts, + protect_content=protect_content, + ) + + def forward( + self, + chat_id: Union[int, str], + disable_notification: DVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: bot.forward_message(chat_id=chat_id, - from_chat_id=update.message.chat_id, - disable_notification=disable_notification, - message_id=update.message.message_id) + from_chat_id=update.effective_message.chat_id, + message_id=update.effective_message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`. + + Note: + Since the release of Bot API 5.5 it can be impossible to forward messages from + some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and + :attr:`telegram.Chat.has_protected_content` to check this. + + As a workaround, it is still possible to use :meth:`copy`. However, this + behaviour is undocumented and might be changed by Telegram. Returns: :class:`telegram.Message`: On success, instance representing the message forwarded. @@ -717,10 +1877,122 @@ def forward(self, chat_id, disable_notification=False): return self.bot.forward_message( chat_id=chat_id, from_chat_id=self.chat_id, + message_id=self.message_id, disable_notification=disable_notification, - message_id=self.message_id) + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def copy( + self, + chat_id: Union[int, str], + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(chat_id=chat_id, + from_chat_id=update.effective_message.chat_id, + message_id=update.effective_message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return self.bot.copy_message( + chat_id=chat_id, + from_chat_id=self.chat_id, + message_id=self.message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def reply_copy( + self, + from_chat_id: Union[str, int], + message_id: int, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: ReplyMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + quote: bool = None, + protect_content: bool = None, + ) -> 'MessageId': + """Shortcut for:: - def edit_text(self, *args, **kwargs): + bot.copy_message(chat_id=message.chat.id, + from_chat_id=from_chat_id, + message_id=message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + Args: + quote (:obj:`bool`, optional): If set to :obj:`True`, the copy is sent as an actual + reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, + this parameter will be ignored. Default: :obj:`True` in group chats and + :obj:`False` in private chats. + + .. versionadded:: 13.1 + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + reply_to_message_id = self._quote(quote, reply_to_message_id) + return self.bot.copy_message( + chat_id=self.chat_id, + from_chat_id=from_chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def edit_text( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + ) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_text(chat_id=message.chat_id, @@ -728,19 +2000,40 @@ def edit_text(self, *args, **kwargs): *args, **kwargs) + For the documentation of the arguments, please see :meth:`telegram.Bot.edit_message_text`. + Note: - You can only edit messages that the bot sent itself, - therefore this method can only be used on the - return value of the ``bot.send_*`` family of methods. + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, instance representing the edited message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_text( - chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - - def edit_caption(self, *args, **kwargs): + chat_id=self.chat_id, + message_id=self.message_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + entities=entities, + inline_message_id=None, + ) + + def edit_caption( + self, + caption: str = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + ) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_caption(chat_id=message.chat_id, @@ -748,40 +2041,74 @@ def edit_caption(self, *args, **kwargs): *args, **kwargs) + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_caption`. + Note: - You can only edit messages that the bot sent itself, - therefore this method can only be used on the - return value of the ``bot.send_*`` family of methods. + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, instance representing the edited message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_caption( - chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - - def edit_media(self, media, *args, **kwargs): + chat_id=self.chat_id, + message_id=self.message_id, + caption=caption, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + caption_entities=caption_entities, + inline_message_id=None, + ) + + def edit_media( + self, + media: 'InputMedia' = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union['Message', bool]: """Shortcut for:: - bot.edit_message_media(chat_id=message.chat_id, - message_id=message.message_id, - *args, - **kwargs) + bot.edit_message_media(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) - Note: - You can only edit messages that the bot sent itself, - therefore this method can only be used on the - return value of the ``bot.send_*`` family of methods. + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_media`. - Returns: - :class:`telegram.Message`: On success, instance representing the edited - message. + Note: + You can only edit messages that the bot sent itself(i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. - """ - return self.bot.edit_message_media( - chat_id=self.chat_id, message_id=self.message_id, media=media, *args, **kwargs) + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. - def edit_reply_markup(self, *args, **kwargs): + """ + return self.bot.edit_message_media( + chat_id=self.chat_id, + message_id=self.message_id, + media=media, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + ) + + def edit_reply_markup( + self, + reply_markup: Optional['InlineKeyboardMarkup'] = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_reply_markup(chat_id=message.chat_id, @@ -789,18 +2116,184 @@ def edit_reply_markup(self, *args, **kwargs): *args, **kwargs) + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_reply_markup`. + Note: - You can only edit messages that the bot sent itself, - therefore this method can only be used on the - return value of the ``bot.send_*`` family of methods. + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, instance representing the edited message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_reply_markup( - chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) + chat_id=self.chat_id, + message_id=self.message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + ) + + def edit_live_location( + self, + latitude: float = None, + longitude: float = None, + location: Location = None, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + ) -> Union['Message', bool]: + """Shortcut for:: + + bot.edit_message_live_location(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) - def delete(self, *args, **kwargs): + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_live_location`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + """ + return self.bot.edit_message_live_location( + chat_id=self.chat_id, + message_id=self.message_id, + latitude=latitude, + longitude=longitude, + location=location, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + inline_message_id=None, + ) + + def stop_live_location( + self, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union['Message', bool]: + """Shortcut for:: + + bot.stop_message_live_location(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.stop_message_live_location`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + """ + return self.bot.stop_message_live_location( + chat_id=self.chat_id, + message_id=self.message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + ) + + def set_game_score( + self, + user_id: Union[int, str], + score: int, + force: bool = None, + disable_edit_message: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Union['Message', bool]: + """Shortcut for:: + + bot.set_game_score(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.set_game_score`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + """ + return self.bot.set_game_score( + chat_id=self.chat_id, + message_id=self.message_id, + user_id=user_id, + score=score, + force=force, + disable_edit_message=disable_edit_message, + timeout=timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + ) + + def get_game_high_scores( + self, + user_id: Union[int, str], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> List['GameHighScore']: + """Shortcut for:: + + bot.get_game_high_scores(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_game_high_scores`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + List[:class:`telegram.GameHighScore`] + """ + return self.bot.get_game_high_scores( + chat_id=self.chat_id, + message_id=self.message_id, + user_id=user_id, + timeout=timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + ) + + def delete( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: bot.delete_message(chat_id=message.chat_id, @@ -808,14 +2301,100 @@ def delete(self, *args, **kwargs): *args, **kwargs) + For the documentation of the arguments, please see :meth:`telegram.Bot.delete_message`. + Returns: - :obj:`bool`: On success, ``True`` is returned. + :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.delete_message( - chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) + chat_id=self.chat_id, + message_id=self.message_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def stop_poll( + self, + reply_markup: InlineKeyboardMarkup = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Poll: + """Shortcut for:: + + bot.stop_poll(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.stop_poll`. + + Returns: + :class:`telegram.Poll`: On success, the stopped Poll with the final results is + returned. + + """ + return self.bot.stop_poll( + chat_id=self.chat_id, + message_id=self.message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def pin( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.pin_chat_message(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.pin_chat_message( + chat_id=self.chat_id, + message_id=self.message_id, + disable_notification=disable_notification, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def unpin( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.unpin_chat_message(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) - def parse_entity(self, entity): + For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_chat_message( + chat_id=self.chat_id, + message_id=self.message_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. Note: @@ -825,22 +2404,27 @@ def parse_entity(self, entity): Args: entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must - be an entity that belongs to this message. + be an entity that belongs to this message. Returns: - :obj:`str`: The text of the given entity + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the message has no text. """ + if not self.text: + raise RuntimeError("This Message has no 'text'.") + # Is it a narrow build, if so we don't need to convert - if sys.maxunicode == 0xffff: - return self.text[entity.offset:entity.offset + entity.length] - else: - entity_text = self.text.encode('utf-16-le') - entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2] + if sys.maxunicode == 0xFFFF: + return self.text[entity.offset : entity.offset + entity.length] + entity_text = self.text.encode('utf-16-le') + entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] return entity_text.decode('utf-16-le') - def parse_caption_entity(self, entity): + def parse_caption_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. Note: @@ -850,22 +2434,27 @@ def parse_caption_entity(self, entity): Args: entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must - be an entity that belongs to this message. + be an entity that belongs to this message. Returns: - :obj:`str`: The text of the given entity + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the message has no caption. """ + if not self.caption: + raise RuntimeError("This Message has no 'caption'.") + # Is it a narrow build, if so we don't need to convert - if sys.maxunicode == 0xffff: - return self.caption[entity.offset:entity.offset + entity.length] - else: - entity_text = self.caption.encode('utf-16-le') - entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2] + if sys.maxunicode == 0xFFFF: + return self.caption[entity.offset : entity.offset + entity.length] + entity_text = self.caption.encode('utf-16-le') + entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] return entity_text.decode('utf-16-le') - def parse_entities(self, types=None): + def parse_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message filtered by their @@ -893,10 +2482,11 @@ def parse_entities(self, types=None): return { entity: self.parse_entity(entity) - for entity in self.entities if entity.type in types + for entity in (self.entities or []) + if entity.type in types } - def parse_caption_entities(self, types=None): + def parse_caption_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message's caption filtered by their @@ -924,59 +2514,126 @@ def parse_caption_entities(self, types=None): return { entity: self.parse_caption_entity(entity) - for entity in self.caption_entities if entity.type in types + for entity in (self.caption_entities or []) + if entity.type in types } @staticmethod - def _parse_html(message_text, entities, urled=False): + def _parse_html( + message_text: Optional[str], + entities: Dict[MessageEntity, str], + urled: bool = False, + offset: int = 0, + ) -> Optional[str]: if message_text is None: return None - if not sys.maxunicode == 0xffff: - message_text = message_text.encode('utf-16-le') + if sys.maxunicode != 0xFFFF: + message_text = message_text.encode('utf-16-le') # type: ignore html_text = '' last_offset = 0 - for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)): - text = escape(text) - - if entity.type == MessageEntity.TEXT_LINK: - insert = '{}'.format(entity.url, text) - elif (entity.type == MessageEntity.URL) and urled: - insert = '{0}'.format(text) - elif entity.type == MessageEntity.BOLD: - insert = '' + text + '' - elif entity.type == MessageEntity.ITALIC: - insert = '' + text + '' - elif entity.type == MessageEntity.CODE: - insert = '' + text + '' - elif entity.type == MessageEntity.PRE: - insert = '
' + text + '
' + sorted_entities = sorted(entities.items(), key=(lambda item: item[0].offset)) + parsed_entities = [] + + for (entity, text) in sorted_entities: + if entity not in parsed_entities: + nested_entities = { + e: t + for (e, t) in sorted_entities + if e.offset >= entity.offset + and e.offset + e.length <= entity.offset + entity.length + and e != entity + } + parsed_entities.extend(list(nested_entities.keys())) + + orig_text = text + text = escape(text) + + if nested_entities: + text = Message._parse_html( + orig_text, nested_entities, urled=urled, offset=entity.offset + ) + + if entity.type == MessageEntity.TEXT_LINK: + insert = f'{text}' + elif entity.type == MessageEntity.TEXT_MENTION and entity.user: + insert = f'{text}' + elif entity.type == MessageEntity.URL and urled: + insert = f'{text}' + elif entity.type == MessageEntity.BOLD: + insert = '' + text + '' + elif entity.type == MessageEntity.ITALIC: + insert = '' + text + '' + elif entity.type == MessageEntity.CODE: + insert = '' + text + '' + elif entity.type == MessageEntity.PRE: + if entity.language: + insert = f'
{text}
' + else: + insert = '
' + text + '
' + elif entity.type == MessageEntity.UNDERLINE: + insert = '' + text + '' + elif entity.type == MessageEntity.STRIKETHROUGH: + insert = '' + text + '' + elif entity.type == MessageEntity.SPOILER: + insert = f'{text}' + else: + insert = text + + if offset == 0: + if sys.maxunicode == 0xFFFF: + html_text += ( + escape(message_text[last_offset : entity.offset - offset]) + insert + ) + else: + html_text += ( + escape( + message_text[ # type: ignore + last_offset * 2 : (entity.offset - offset) * 2 + ].decode('utf-16-le') + ) + + insert + ) + else: + if sys.maxunicode == 0xFFFF: + html_text += message_text[last_offset : entity.offset - offset] + insert + else: + html_text += ( + message_text[ # type: ignore + last_offset * 2 : (entity.offset - offset) * 2 + ].decode('utf-16-le') + + insert + ) + + last_offset = entity.offset - offset + entity.length + + if offset == 0: + if sys.maxunicode == 0xFFFF: + html_text += escape(message_text[last_offset:]) else: - insert = text - - if sys.maxunicode == 0xffff: - html_text += escape(message_text[last_offset:entity.offset]) + insert + html_text += escape( + message_text[last_offset * 2 :].decode('utf-16-le') # type: ignore + ) + else: + if sys.maxunicode == 0xFFFF: + html_text += message_text[last_offset:] else: - html_text += escape(message_text[last_offset * 2:entity.offset * 2] - .decode('utf-16-le')) + insert - - last_offset = entity.offset + entity.length + html_text += message_text[last_offset * 2 :].decode('utf-16-le') # type: ignore - if sys.maxunicode == 0xffff: - html_text += escape(message_text[last_offset:]) - else: - html_text += escape(message_text[last_offset * 2:].decode('utf-16-le')) return html_text @property - def text_html(self): + def text_html(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message. Use this if you want to retrieve the message text with the entities formatted as HTML in the same way the original message was formatted. + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + Returns: :obj:`str`: Message text with entities formatted as HTML. @@ -984,12 +2641,15 @@ def text_html(self): return self._parse_html(self.text, self.parse_entities(), urled=False) @property - def text_html_urled(self): + def text_html_urled(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message. Use this if you want to retrieve the message text with the entities formatted as HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + Returns: :obj:`str`: Message text with entities formatted as HTML. @@ -997,125 +2657,354 @@ def text_html_urled(self): return self._parse_html(self.text, self.parse_entities(), urled=True) @property - def caption_html(self): + def caption_html(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message's caption. Use this if you want to retrieve the message caption with the caption entities formatted as HTML in the same way the original message was formatted. - Returns: - :obj:`str`: Message caption with captionentities formatted as HTML. + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + Returns: + :obj:`str`: Message caption with caption entities formatted as HTML. """ return self._parse_html(self.caption, self.parse_caption_entities(), urled=False) @property - def caption_html_urled(self): + def caption_html_urled(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message's caption. Use this if you want to retrieve the message caption with the caption entities formatted as HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + Returns: :obj:`str`: Message caption with caption entities formatted as HTML. - """ return self._parse_html(self.caption, self.parse_caption_entities(), urled=True) @staticmethod - def _parse_markdown(message_text, entities, urled=False): + def _parse_markdown( + message_text: Optional[str], + entities: Dict[MessageEntity, str], + urled: bool = False, + version: int = 1, + offset: int = 0, + ) -> Optional[str]: + version = int(version) + if message_text is None: return None - if not sys.maxunicode == 0xffff: - message_text = message_text.encode('utf-16-le') + if sys.maxunicode != 0xFFFF: + message_text = message_text.encode('utf-16-le') # type: ignore markdown_text = '' last_offset = 0 - for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)): - text = escape_markdown(text) - - if entity.type == MessageEntity.TEXT_LINK: - insert = '[{}]({})'.format(text, entity.url) - elif (entity.type == MessageEntity.URL) and urled: - insert = '[{0}]({0})'.format(text) - elif entity.type == MessageEntity.BOLD: - insert = '*' + text + '*' - elif entity.type == MessageEntity.ITALIC: - insert = '_' + text + '_' - elif entity.type == MessageEntity.CODE: - insert = '`' + text + '`' - elif entity.type == MessageEntity.PRE: - insert = '```' + text + '```' + sorted_entities = sorted(entities.items(), key=(lambda item: item[0].offset)) + parsed_entities = [] + + for (entity, text) in sorted_entities: + if entity not in parsed_entities: + nested_entities = { + e: t + for (e, t) in sorted_entities + if e.offset >= entity.offset + and e.offset + e.length <= entity.offset + entity.length + and e != entity + } + parsed_entities.extend(list(nested_entities.keys())) + + orig_text = text + text = escape_markdown(text, version=version) + + if nested_entities: + if version < 2: + raise ValueError( + 'Nested entities are not supported for Markdown ' 'version 1' + ) + + text = Message._parse_markdown( + orig_text, + nested_entities, + urled=urled, + offset=entity.offset, + version=version, + ) + + if entity.type == MessageEntity.TEXT_LINK: + if version == 1: + url = entity.url + else: + # Links need special escaping. Also can't have entities nested within + url = escape_markdown( + entity.url, version=version, entity_type=MessageEntity.TEXT_LINK + ) + insert = f'[{text}]({url})' + elif entity.type == MessageEntity.TEXT_MENTION and entity.user: + insert = f'[{text}](tg://user?id={entity.user.id})' + elif entity.type == MessageEntity.URL and urled: + if version == 1: + link = orig_text + else: + link = text + insert = f'[{link}]({orig_text})' + elif entity.type == MessageEntity.BOLD: + insert = '*' + text + '*' + elif entity.type == MessageEntity.ITALIC: + insert = '_' + text + '_' + elif entity.type == MessageEntity.CODE: + # Monospace needs special escaping. Also can't have entities nested within + insert = ( + '`' + + escape_markdown( + orig_text, version=version, entity_type=MessageEntity.CODE + ) + + '`' + ) + elif entity.type == MessageEntity.PRE: + # Monospace needs special escaping. Also can't have entities nested within + code = escape_markdown( + orig_text, version=version, entity_type=MessageEntity.PRE + ) + if entity.language: + prefix = '```' + entity.language + '\n' + else: + if code.startswith('\\'): + prefix = '```' + else: + prefix = '```\n' + insert = prefix + code + '```' + elif entity.type == MessageEntity.UNDERLINE: + if version == 1: + raise ValueError( + 'Underline entities are not supported for Markdown ' 'version 1' + ) + insert = '__' + text + '__' + elif entity.type == MessageEntity.STRIKETHROUGH: + if version == 1: + raise ValueError( + 'Strikethrough entities are not supported for Markdown ' 'version 1' + ) + insert = '~' + text + '~' + elif entity.type == MessageEntity.SPOILER: + if version == 1: + raise ValueError( + "Spoiler entities are not supported for Markdown version 1" + ) + insert = f"||{text}||" + else: + insert = text + + if offset == 0: + if sys.maxunicode == 0xFFFF: + markdown_text += ( + escape_markdown( + message_text[last_offset : entity.offset - offset], version=version + ) + + insert + ) + else: + markdown_text += ( + escape_markdown( + message_text[ # type: ignore + last_offset * 2 : (entity.offset - offset) * 2 + ].decode('utf-16-le'), + version=version, + ) + + insert + ) + else: + if sys.maxunicode == 0xFFFF: + markdown_text += ( + message_text[last_offset : entity.offset - offset] + insert + ) + else: + markdown_text += ( + message_text[ # type: ignore + last_offset * 2 : (entity.offset - offset) * 2 + ].decode('utf-16-le') + + insert + ) + + last_offset = entity.offset - offset + entity.length + + if offset == 0: + if sys.maxunicode == 0xFFFF: + markdown_text += escape_markdown(message_text[last_offset:], version=version) else: - insert = text - if sys.maxunicode == 0xffff: - markdown_text += escape_markdown(message_text[last_offset:entity.offset]) + insert + markdown_text += escape_markdown( + message_text[last_offset * 2 :].decode('utf-16-le'), # type: ignore + version=version, + ) + else: + if sys.maxunicode == 0xFFFF: + markdown_text += message_text[last_offset:] else: - markdown_text += escape_markdown(message_text[last_offset * 2:entity.offset * 2] - .decode('utf-16-le')) + insert - - last_offset = entity.offset + entity.length + markdown_text += message_text[last_offset * 2 :].decode( # type: ignore + 'utf-16-le' + ) - if sys.maxunicode == 0xffff: - markdown_text += escape_markdown(message_text[last_offset:]) - else: - markdown_text += escape_markdown(message_text[last_offset * 2:].decode('utf-16-le')) return markdown_text @property - def text_markdown(self): - """Creates an Markdown-formatted string from the markup entities found in the message. + def text_markdown(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.ParseMode.MARKDOWN`. Use this if you want to retrieve the message text with the entities formatted as Markdown in the same way the original message was formatted. + Note: + :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for + backward compatibility. You should use :meth:`text_markdown_v2` instead. + Returns: :obj:`str`: Message text with entities formatted as Markdown. + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested + entities. + """ return self._parse_markdown(self.text, self.parse_entities(), urled=False) @property - def text_markdown_urled(self): - """Creates an Markdown-formatted string from the markup entities found in the message. + def text_markdown_v2(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message text with the entities formatted as Markdown + in the same way the original message was formatted. + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + Returns: + :obj:`str`: Message text with entities formatted as Markdown. + """ + return self._parse_markdown(self.text, self.parse_entities(), urled=False, version=2) + + @property + def text_markdown_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.ParseMode.MARKDOWN`. Use this if you want to retrieve the message text with the entities formatted as Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + Note: + :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for + backward compatibility. You should use :meth:`text_markdown_v2_urled` instead. + Returns: :obj:`str`: Message text with entities formatted as Markdown. + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested + entities. + """ return self._parse_markdown(self.text, self.parse_entities(), urled=True) @property - def caption_markdown(self): + def text_markdown_v2_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message text with the entities formatted as Markdown. + This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + Returns: + :obj:`str`: Message text with entities formatted as Markdown. + """ + return self._parse_markdown(self.text, self.parse_entities(), urled=True, version=2) + + @property + def caption_markdown(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message's - caption. + caption using :class:`telegram.ParseMode.MARKDOWN`. Use this if you want to retrieve the message caption with the caption entities formatted as Markdown in the same way the original message was formatted. + Note: + :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for + backward compatibility. You should use :meth:`caption_markdown_v2` instead. + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested + entities. + """ return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=False) @property - def caption_markdown_urled(self): + def caption_markdown_v2(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message's - caption. + caption using :class:`telegram.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message caption with the caption entities formatted as + Markdown in the same way the original message was formatted. + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + Returns: + :obj:`str`: Message caption with caption entities formatted as Markdown. + """ + return self._parse_markdown( + self.caption, self.parse_caption_entities(), urled=False, version=2 + ) + + @property + def caption_markdown_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message's + caption using :class:`telegram.ParseMode.MARKDOWN`. Use this if you want to retrieve the message caption with the caption entities formatted as Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + Note: + :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for + backward compatibility. You should use :meth:`caption_markdown_v2_urled` instead. + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested + entities. + """ return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=True) + + @property + def caption_markdown_v2_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message's + caption using :class:`telegram.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message caption with the caption entities formatted as + Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + Returns: + :obj:`str`: Message caption with caption entities formatted as Markdown. + """ + return self._parse_markdown( + self.caption, self.parse_caption_entities(), urled=True, version=2 + ) diff --git a/telegramer/include/telegram/messageautodeletetimerchanged.py b/telegramer/include/telegram/messageautodeletetimerchanged.py new file mode 100644 index 0000000..21af1fb --- /dev/null +++ b/telegramer/include/telegram/messageautodeletetimerchanged.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a change in the Telegram message auto +deletion. +""" + +from typing import Any + +from telegram import TelegramObject + + +class MessageAutoDeleteTimerChanged(TelegramObject): + """This object represents a service message about a change in auto-delete timer settings. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_auto_delete_time` is equal. + + .. versionadded:: 13.4 + + Args: + message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the + chat. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the + chat. + + """ + + __slots__ = ('message_auto_delete_time', '_id_attrs') + + def __init__( + self, + message_auto_delete_time: int, + **_kwargs: Any, + ): + self.message_auto_delete_time = int(message_auto_delete_time) + + self._id_attrs = (self.message_auto_delete_time,) diff --git a/telegramer/include/telegram/messageentity.py b/telegramer/include/telegram/messageentity.py index 9617ee5..d4e1620 100644 --- a/telegramer/include/telegram/messageentity.py +++ b/telegramer/include/telegram/messageentity.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram MessageEntity.""" -from telegram import User, TelegramObject +from typing import TYPE_CHECKING, Any, List, Optional, ClassVar + +from telegram import TelegramObject, User, constants +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class MessageEntity(TelegramObject): @@ -26,27 +32,46 @@ class MessageEntity(TelegramObject): This object represents one special entity in a text message. For example, hashtags, usernames, URLs, etc. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type`, :attr:`offset` and :attr:`length` are equal. + + Args: + type (:obj:`str`): Type of the entity. Currently, can be mention (@username), hashtag, + bot_command, url, email, phone_number, bold (bold text), italic (italic text), + strikethrough, spoiler (spoiler message), code (monowidth string), pre + (monowidth block), text_link (for clickable text URLs), text_mention + (for users without usernames). + offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity. + length (:obj:`int`): Length of the entity in UTF-16 code units. + url (:obj:`str`, optional): For :attr:`TEXT_LINK` only, url that will be opened after + user taps on the text. + user (:class:`telegram.User`, optional): For :attr:`TEXT_MENTION` only, the mentioned + user. + language (:obj:`str`, optional): For :attr:`PRE` only, the programming language of + the entity text. + Attributes: type (:obj:`str`): Type of the entity. offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity. length (:obj:`int`): Length of the entity in UTF-16 code units. url (:obj:`str`): Optional. Url that will be opened after user taps on the text. user (:class:`telegram.User`): Optional. The mentioned user. - - Args: - type (:obj:`str`): Type of the entity. Can be mention (@username), hashtag, bot_command, - url, email, bold (bold text), italic (italic text), code (monowidth string), pre - (monowidth block), text_link (for clickable text URLs), text_mention (for users - without usernames). - offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity. - length (:obj:`int`): Length of the entity in UTF-16 code units. - url (:obj:`str`, optional): For "text_link" only, url that will be opened after usertaps on - the text. - user (:class:`telegram.User`, optional): For "text_mention" only, the mentioned user. + language (:obj:`str`): Optional. Programming language of the entity text. """ - def __init__(self, type, offset, length, url=None, user=None, **kwargs): + __slots__ = ('length', 'url', 'user', 'type', 'language', 'offset', '_id_attrs') + + def __init__( + self, + type: str, # pylint: disable=W0622 + offset: int, + length: int, + url: str = None, + user: User = None, + language: str = None, + **_kwargs: Any, + ): # Required self.type = type self.offset = offset @@ -54,10 +79,14 @@ def __init__(self, type, offset, length, url=None, user=None, **kwargs): # Optionals self.url = url self.user = user + self.language = language + + self._id_attrs = (self.type, self.offset, self.length) @classmethod - def de_json(cls, data, bot): - data = super(MessageEntity, cls).de_json(data, bot) + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MessageEntity']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) if not data: return None @@ -66,45 +95,41 @@ def de_json(cls, data, bot): return cls(**data) - @classmethod - def de_list(cls, data, bot): - if not data: - return list() - - entities = list() - for entity in data: - entities.append(cls.de_json(entity, bot)) - - return entities - - MENTION = 'mention' - """:obj:`str`: 'mention'""" - HASHTAG = 'hashtag' - """:obj:`str`: 'hashtag'""" - CASHTAG = 'cashtag' - """:obj:`str`: 'cashtag'""" - PHONE_NUMBER = 'phone_number' - """:obj:`str`: 'phone_number'""" - BOT_COMMAND = 'bot_command' - """:obj:`str`: 'bot_command'""" - URL = 'url' - """:obj:`str`: 'url'""" - EMAIL = 'email' - """:obj:`str`: 'email'""" - BOLD = 'bold' - """:obj:`str`: 'bold'""" - ITALIC = 'italic' - """:obj:`str`: 'italic'""" - CODE = 'code' - """:obj:`str`: 'code'""" - PRE = 'pre' - """:obj:`str`: 'pre'""" - TEXT_LINK = 'text_link' - """:obj:`str`: 'text_link'""" - TEXT_MENTION = 'text_mention' - """:obj:`str`: 'text_mention'""" - ALL_TYPES = [ - MENTION, HASHTAG, CASHTAG, PHONE_NUMBER, BOT_COMMAND, URL, - EMAIL, BOLD, ITALIC, CODE, PRE, TEXT_LINK, TEXT_MENTION - ] - """List[:obj:`str`]: List of all the types.""" + MENTION: ClassVar[str] = constants.MESSAGEENTITY_MENTION + """:const:`telegram.constants.MESSAGEENTITY_MENTION`""" + HASHTAG: ClassVar[str] = constants.MESSAGEENTITY_HASHTAG + """:const:`telegram.constants.MESSAGEENTITY_HASHTAG`""" + CASHTAG: ClassVar[str] = constants.MESSAGEENTITY_CASHTAG + """:const:`telegram.constants.MESSAGEENTITY_CASHTAG`""" + PHONE_NUMBER: ClassVar[str] = constants.MESSAGEENTITY_PHONE_NUMBER + """:const:`telegram.constants.MESSAGEENTITY_PHONE_NUMBER`""" + BOT_COMMAND: ClassVar[str] = constants.MESSAGEENTITY_BOT_COMMAND + """:const:`telegram.constants.MESSAGEENTITY_BOT_COMMAND`""" + URL: ClassVar[str] = constants.MESSAGEENTITY_URL + """:const:`telegram.constants.MESSAGEENTITY_URL`""" + EMAIL: ClassVar[str] = constants.MESSAGEENTITY_EMAIL + """:const:`telegram.constants.MESSAGEENTITY_EMAIL`""" + BOLD: ClassVar[str] = constants.MESSAGEENTITY_BOLD + """:const:`telegram.constants.MESSAGEENTITY_BOLD`""" + ITALIC: ClassVar[str] = constants.MESSAGEENTITY_ITALIC + """:const:`telegram.constants.MESSAGEENTITY_ITALIC`""" + CODE: ClassVar[str] = constants.MESSAGEENTITY_CODE + """:const:`telegram.constants.MESSAGEENTITY_CODE`""" + PRE: ClassVar[str] = constants.MESSAGEENTITY_PRE + """:const:`telegram.constants.MESSAGEENTITY_PRE`""" + TEXT_LINK: ClassVar[str] = constants.MESSAGEENTITY_TEXT_LINK + """:const:`telegram.constants.MESSAGEENTITY_TEXT_LINK`""" + TEXT_MENTION: ClassVar[str] = constants.MESSAGEENTITY_TEXT_MENTION + """:const:`telegram.constants.MESSAGEENTITY_TEXT_MENTION`""" + UNDERLINE: ClassVar[str] = constants.MESSAGEENTITY_UNDERLINE + """:const:`telegram.constants.MESSAGEENTITY_UNDERLINE`""" + STRIKETHROUGH: ClassVar[str] = constants.MESSAGEENTITY_STRIKETHROUGH + """:const:`telegram.constants.MESSAGEENTITY_STRIKETHROUGH`""" + SPOILER: ClassVar[str] = constants.MESSAGEENTITY_SPOILER + """:const:`telegram.constants.MESSAGEENTITY_SPOILER` + + .. versionadded:: 13.10 + """ + ALL_TYPES: ClassVar[List[str]] = constants.MESSAGEENTITY_ALL_TYPES + """:const:`telegram.constants.MESSAGEENTITY_ALL_TYPES`\n + List of all the types""" diff --git a/telegramer/include/telegram/messageid.py b/telegramer/include/telegram/messageid.py new file mode 100644 index 0000000..df7f47c --- /dev/null +++ b/telegramer/include/telegram/messageid.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents an instance of a Telegram MessageId.""" +from typing import Any + +from telegram import TelegramObject + + +class MessageId(TelegramObject): + """This object represents a unique message identifier. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` is equal. + + Attributes: + message_id (:obj:`int`): Unique message identifier + """ + + __slots__ = ('message_id', '_id_attrs') + + def __init__(self, message_id: int, **_kwargs: Any): + self.message_id = int(message_id) + + self._id_attrs = (self.message_id,) diff --git a/telegramer/include/telegram/parsemode.py b/telegramer/include/telegram/parsemode.py index e998d59..38cf051 100644 --- a/telegramer/include/telegram/parsemode.py +++ b/telegramer/include/telegram/parsemode.py @@ -2,7 +2,7 @@ # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,12 +18,28 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Message Parse Modes.""" +from typing import ClassVar +from telegram import constants +from telegram.utils.deprecate import set_new_attribute_deprecated -class ParseMode(object): + +class ParseMode: """This object represents a Telegram Message Parse Modes.""" - MARKDOWN = 'Markdown' - """:obj:`str`: 'Markdown'""" - HTML = 'HTML' - """:obj:`str`: 'HTML'""" + __slots__ = ('__dict__',) + + MARKDOWN: ClassVar[str] = constants.PARSEMODE_MARKDOWN + """:const:`telegram.constants.PARSEMODE_MARKDOWN`\n + + Note: + :attr:`MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. + You should use :attr:`MARKDOWN_V2` instead. + """ + MARKDOWN_V2: ClassVar[str] = constants.PARSEMODE_MARKDOWN_V2 + """:const:`telegram.constants.PARSEMODE_MARKDOWN_V2`""" + HTML: ClassVar[str] = constants.PARSEMODE_HTML + """:const:`telegram.constants.PARSEMODE_HTML`""" + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) diff --git a/telegramer/include/telegram/passport/credentials.py b/telegramer/include/telegram/passport/credentials.py index 638944d..d27bd31 100644 --- a/telegramer/include/telegram/passport/credentials.py +++ b/telegramer/include/telegram/passport/credentials.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,33 +16,52 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0114, W0622 try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] + from base64 import b64decode +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, no_type_check + +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP + from cryptography.hazmat.primitives.ciphers import Cipher + from cryptography.hazmat.primitives.ciphers.algorithms import AES + from cryptography.hazmat.primitives.ciphers.modes import CBC + from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512, Hash + + CRYPTO_INSTALLED = True +except ImportError: + default_backend = None + MGF1, OAEP, Cipher, AES, CBC = (None, None, None, None, None) # type: ignore[misc] + SHA1, SHA256, SHA512, Hash = (None, None, None, None) # type: ignore[misc] -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1 -from cryptography.hazmat.primitives.ciphers import Cipher -from cryptography.hazmat.primitives.ciphers.algorithms import AES -from cryptography.hazmat.primitives.ciphers.modes import CBC -from cryptography.hazmat.primitives.hashes import SHA512, SHA256, Hash, SHA1 -from future.utils import bord + CRYPTO_INSTALLED = False -from telegram import TelegramObject, TelegramError +from telegram import TelegramError, TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class TelegramDecryptionError(TelegramError): - """ - Something went wrong with decryption. - """ + """Something went wrong with decryption.""" - def __init__(self, message): - super(TelegramDecryptionError, self).__init__("TelegramDecryptionError: " - "{}".format(message)) + __slots__ = ('_msg',) + def __init__(self, message: Union[str, Exception]): + super().__init__(f"TelegramDecryptionError: {message}") + self._msg = str(message) + def __reduce__(self) -> Tuple[type, Tuple[str]]: + return self.__class__, (self._msg,) + + +@no_type_check def decrypt(secret, hash, data): """ Decrypt per telegram docs at https://core.telegram.org/passport. @@ -64,14 +83,19 @@ def decrypt(secret, hash, data): :obj:`bytes`: The decrypted data as bytes. """ + if not CRYPTO_INSTALLED: + raise RuntimeError( + 'To use Telegram Passports, PTB must be installed via `pip install ' + 'python-telegram-bot[passport]`.' + ) # Make a SHA512 hash of secret + update digest = Hash(SHA512(), backend=default_backend()) digest.update(secret + hash) secret_hash_hash = digest.finalize() # First 32 chars is our key, next 16 is the initialisation vector - key, iv = secret_hash_hash[:32], secret_hash_hash[32:32 + 16] + key, init_vector = secret_hash_hash[:32], secret_hash_hash[32 : 32 + 16] # Init a AES-CBC cipher and decrypt the data - cipher = Cipher(AES(key), CBC(iv), backend=default_backend()) + cipher = Cipher(AES(key), CBC(init_vector), backend=default_backend()) decryptor = cipher.decryptor() data = decryptor.update(data) + decryptor.finalize() # Calculate SHA256 hash of the decrypted data @@ -81,11 +105,12 @@ def decrypt(secret, hash, data): # If the newly calculated hash did not match the one telegram gave us if data_hash != hash: # Raise a error that is caught inside telegram.PassportData and transformed into a warning - raise TelegramDecryptionError("Hashes are not equal! {} != {}".format(data_hash, hash)) + raise TelegramDecryptionError(f"Hashes are not equal! {data_hash} != {hash}") # Return data without padding - return data[bord(data[0]):] + return data[data[0] :] +@no_type_check def decrypt_json(secret, hash, data): """Decrypts data using secret and hash and then decodes utf-8 string and loads json""" return json.loads(decrypt(secret, hash, data).decode('utf-8')) @@ -96,28 +121,41 @@ class EncryptedCredentials(TelegramObject): Telegram Passport Documentation for a complete description of the data decryption and authentication processes. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`data`, :attr:`hash` and :attr:`secret` are equal. + + Note: + This object is decrypted only when originating from + :obj:`telegram.PassportData.decrypted_credentials`. + + Args: data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's nonce, data hashes and secrets used for EncryptedPassportElement decryption and authentication or base64 encrypted data. hash (:obj:`str`): Base64-encoded data hash for data authentication. secret (:obj:`str`): Decrypted or encrypted secret used for decryption. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's nonce, data hashes and secrets used for EncryptedPassportElement decryption and authentication or base64 encrypted data. hash (:obj:`str`): Base64-encoded data hash for data authentication. secret (:obj:`str`): Decrypted or encrypted secret used for decryption. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. - - Note: - This object is decrypted only when originating from - :obj:`telegram.PassportData.decrypted_credentials`. """ - def __init__(self, data, hash, secret, bot=None, **kwargs): + __slots__ = ( + 'hash', + 'secret', + 'bot', + 'data', + '_id_attrs', + '_decrypted_secret', + '_decrypted_data', + ) + + def __init__(self, data: str, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any): # Required self.data = data self.hash = hash @@ -127,19 +165,10 @@ def __init__(self, data, hash, secret, bot=None, **kwargs): self.bot = bot self._decrypted_secret = None - self._decrypted_data = None - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - data = super(EncryptedCredentials, cls).de_json(data, bot) - - return cls(bot=bot, **data) + self._decrypted_data: Optional['Credentials'] = None @property - def decrypted_secret(self): + def decrypted_secret(self) -> str: """ :obj:`str`: Lazily decrypt and return secret. @@ -148,6 +177,11 @@ def decrypted_secret(self): private/public key but can also suggest malformed/tampered data. """ if self._decrypted_secret is None: + if not CRYPTO_INSTALLED: + raise RuntimeError( + 'To use Telegram Passports, PTB must be installed via `pip install ' + 'python-telegram-bot[passport]`.' + ) # Try decrypting according to step 1 at # https://core.telegram.org/passport#decrypting-data # We make sure to base64 decode the secret first. @@ -155,18 +189,17 @@ def decrypted_secret(self): # is the default for OAEP, the algorithm is the default for PHP which is what # Telegram's backend servers run. try: - self._decrypted_secret = self.bot.private_key.decrypt(b64decode(self.secret), OAEP( - mgf=MGF1(algorithm=SHA1()), - algorithm=SHA1(), - label=None - )) - except ValueError as e: + self._decrypted_secret = self.bot.private_key.decrypt( + b64decode(self.secret), + OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None), # skipcq + ) + except ValueError as exception: # If decryption fails raise exception - raise TelegramDecryptionError(e) + raise TelegramDecryptionError(exception) from exception return self._decrypted_secret @property - def decrypted_data(self): + def decrypted_data(self) -> 'Credentials': """ :class:`telegram.Credentials`: Lazily decrypt and return credentials data. This object also contains the user specified nonce as @@ -177,10 +210,10 @@ def decrypted_data(self): private/public key but can also suggest malformed/tampered data. """ if self._decrypted_data is None: - self._decrypted_data = Credentials.de_json(decrypt_json(self.decrypted_secret, - b64decode(self.hash), - b64decode(self.data)), - self.bot) + self._decrypted_data = Credentials.de_json( + decrypt_json(self.decrypted_secret, b64decode(self.hash), b64decode(self.data)), + self.bot, + ) return self._decrypted_data @@ -191,7 +224,9 @@ class Credentials(TelegramObject): nonce (:obj:`str`): Bot-specified nonce """ - def __init__(self, secure_data, nonce, bot=None, **kwargs): + __slots__ = ('bot', 'nonce', 'secure_data') + + def __init__(self, secure_data: 'SecureData', nonce: str, bot: 'Bot' = None, **_kwargs: Any): # Required self.secure_data = secure_data self.nonce = nonce @@ -199,7 +234,10 @@ def __init__(self, secure_data, nonce, bot=None, **kwargs): self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Credentials']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None @@ -236,20 +274,37 @@ class SecureData(TelegramObject): temporary registration. """ - def __init__(self, - personal_details=None, - passport=None, - internal_passport=None, - driver_license=None, - identity_card=None, - address=None, - utility_bill=None, - bank_statement=None, - rental_agreement=None, - passport_registration=None, - temporary_registration=None, - bot=None, - **kwargs): + __slots__ = ( + 'bot', + 'utility_bill', + 'personal_details', + 'temporary_registration', + 'address', + 'driver_license', + 'rental_agreement', + 'internal_passport', + 'identity_card', + 'bank_statement', + 'passport', + 'passport_registration', + ) + + def __init__( + self, + personal_details: 'SecureValue' = None, + passport: 'SecureValue' = None, + internal_passport: 'SecureValue' = None, + driver_license: 'SecureValue' = None, + identity_card: 'SecureValue' = None, + address: 'SecureValue' = None, + utility_bill: 'SecureValue' = None, + bank_statement: 'SecureValue' = None, + rental_agreement: 'SecureValue' = None, + passport_registration: 'SecureValue' = None, + temporary_registration: 'SecureValue' = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Optionals self.temporary_registration = temporary_registration self.passport_registration = passport_registration @@ -266,14 +321,19 @@ def __init__(self, self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureData']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data['temporary_registration'] = SecureValue.de_json(data.get('temporary_registration'), - bot=bot) - data['passport_registration'] = SecureValue.de_json(data.get('passport_registration'), - bot=bot) + data['temporary_registration'] = SecureValue.de_json( + data.get('temporary_registration'), bot=bot + ) + data['passport_registration'] = SecureValue.de_json( + data.get('passport_registration'), bot=bot + ) data['rental_agreement'] = SecureValue.de_json(data.get('rental_agreement'), bot=bot) data['bank_statement'] = SecureValue.de_json(data.get('bank_statement'), bot=bot) data['utility_bill'] = SecureValue.de_json(data.get('utility_bill'), bot=bot) @@ -314,15 +374,19 @@ class SecureValue(TelegramObject): """ - def __init__(self, - data=None, - front_side=None, - reverse_side=None, - selfie=None, - files=None, - translation=None, - bot=None, - **kwargs): + __slots__ = ('data', 'front_side', 'reverse_side', 'selfie', 'files', 'translation', 'bot') + + def __init__( + self, + data: 'DataCredentials' = None, + front_side: 'FileCredentials' = None, + reverse_side: 'FileCredentials' = None, + selfie: 'FileCredentials' = None, + files: List['FileCredentials'] = None, + translation: List['FileCredentials'] = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): self.data = data self.front_side = front_side self.reverse_side = reverse_side @@ -333,7 +397,10 @@ def __init__(self, self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureValue']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None @@ -346,8 +413,9 @@ def de_json(cls, data, bot): return cls(bot=bot, **data) - def to_dict(self): - data = super(SecureValue, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['files'] = [p.to_dict() for p in self.files] data['translation'] = [p.to_dict() for p in self.translation] @@ -358,7 +426,9 @@ def to_dict(self): class _CredentialsBase(TelegramObject): """Base class for DataCredentials and FileCredentials.""" - def __init__(self, hash, secret, bot=None, **kwargs): + __slots__ = ('hash', 'secret', 'file_hash', 'data_hash', 'bot') + + def __init__(self, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any): self.hash = hash self.secret = secret @@ -368,24 +438,6 @@ def __init__(self, hash, secret, bot=None, **kwargs): self.bot = bot - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - - @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - credentials = list() - for c in data: - credentials.append(cls.de_json(c, bot=bot)) - - return credentials - class DataCredentials(_CredentialsBase): """ @@ -401,11 +453,14 @@ class DataCredentials(_CredentialsBase): secret (:obj:`str`): Secret of encrypted data """ - def __init__(self, data_hash, secret, **kwargs): - super(DataCredentials, self).__init__(data_hash, secret, **kwargs) + __slots__ = () - def to_dict(self): - data = super(DataCredentials, self).to_dict() + def __init__(self, data_hash: str, secret: str, **_kwargs: Any): + super().__init__(data_hash, secret, **_kwargs) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() del data['file_hash'] del data['hash'] @@ -415,23 +470,26 @@ def to_dict(self): class FileCredentials(_CredentialsBase): """ - These credentials can be used to decrypt encrypted files from the front_side, - reverse_side, selfie and files fields in EncryptedPassportData. + These credentials can be used to decrypt encrypted files from the front_side, + reverse_side, selfie and files fields in EncryptedPassportData. - Args: - file_hash (:obj:`str`): Checksum of encrypted file - secret (:obj:`str`): Secret of encrypted file + Args: + file_hash (:obj:`str`): Checksum of encrypted file + secret (:obj:`str`): Secret of encrypted file - Attributes: - hash (:obj:`str`): Checksum of encrypted file - secret (:obj:`str`): Secret of encrypted file - """ + Attributes: + hash (:obj:`str`): Checksum of encrypted file + secret (:obj:`str`): Secret of encrypted file + """ + + __slots__ = () - def __init__(self, file_hash, secret, **kwargs): - super(FileCredentials, self).__init__(file_hash, secret, **kwargs) + def __init__(self, file_hash: str, secret: str, **_kwargs: Any): + super().__init__(file_hash, secret, **_kwargs) - def to_dict(self): - data = super(FileCredentials, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() del data['data_hash'] del data['hash'] diff --git a/telegramer/include/telegram/passport/data.py b/telegramer/include/telegram/passport/data.py index 08658be..e1d38b5 100644 --- a/telegramer/include/telegram/passport/data.py +++ b/telegramer/include/telegram/passport/data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2017 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,8 +16,14 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0114 +from typing import TYPE_CHECKING, Any + from telegram import TelegramObject +if TYPE_CHECKING: + from telegram import Bot + class PersonalDetails(TelegramObject): """ @@ -32,15 +38,43 @@ class PersonalDetails(TelegramObject): country_code (:obj:`str`): Citizenship (ISO 3166-1 alpha-2 country code). residence_country_code (:obj:`str`): Country of residence (ISO 3166-1 alpha-2 country code). - first_name (:obj:`str`): First Name in the language of the user's country of residence. - middle_name (:obj:`str`): Optional. Middle Name in the language of the user's country of + first_name_native (:obj:`str`): First Name in the language of the user's country of + residence. + middle_name_native (:obj:`str`): Optional. Middle Name in the language of the user's + country of residence. + last_name_native (:obj:`str`): Last Name in the language of the user's country of residence. - last_name (:obj:`str`): Last Name in the language of the user's country of residence. """ - def __init__(self, first_name, last_name, birth_date, gender, country_code, - residence_country_code, first_name_native, last_name_native, middle_name=None, - middle_name_native=None, bot=None, **kwargs): + __slots__ = ( + 'middle_name', + 'first_name_native', + 'last_name_native', + 'residence_country_code', + 'first_name', + 'last_name', + 'country_code', + 'gender', + 'bot', + 'middle_name_native', + 'birth_date', + ) + + def __init__( + self, + first_name: str, + last_name: str, + birth_date: str, + gender: str, + country_code: str, + residence_country_code: str, + first_name_native: str = None, + last_name_native: str = None, + middle_name: str = None, + middle_name_native: str = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.first_name = first_name self.last_name = last_name @@ -55,13 +89,6 @@ def __init__(self, first_name, last_name, birth_date, gender, country_code, self.bot = bot - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - class ResidentialAddress(TelegramObject): """ @@ -76,8 +103,27 @@ class ResidentialAddress(TelegramObject): post_code (:obj:`str`): Address post code. """ - def __init__(self, street_line1, street_line2, city, state, country_code, - post_code, bot=None, **kwargs): + __slots__ = ( + 'post_code', + 'city', + 'country_code', + 'street_line2', + 'street_line1', + 'bot', + 'state', + ) + + def __init__( + self, + street_line1: str, + street_line2: str, + city: str, + state: str, + country_code: str, + post_code: str, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required self.street_line1 = street_line1 self.street_line2 = street_line2 @@ -88,13 +134,6 @@ def __init__(self, street_line1, street_line2, city, state, country_code, self.bot = bot - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - class IdDocumentData(TelegramObject): """ @@ -105,15 +144,10 @@ class IdDocumentData(TelegramObject): expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format. """ - def __init__(self, document_no, expiry_date, bot=None, **kwargs): + __slots__ = ('document_no', 'bot', 'expiry_date') + + def __init__(self, document_no: str, expiry_date: str, bot: 'Bot' = None, **_kwargs: Any): self.document_no = document_no self.expiry_date = expiry_date self.bot = bot - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) diff --git a/telegramer/include/telegram/passport/encryptedpassportelement.py b/telegramer/include/telegram/passport/encryptedpassportelement.py index db7332f..78b6972 100644 --- a/telegramer/include/telegram/passport/encryptedpassportelement.py +++ b/telegramer/include/telegram/passport/encryptedpassportelement.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # flake8: noqa: E501 # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,10 +18,20 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram EncryptedPassportElement.""" from base64 import b64decode +from typing import TYPE_CHECKING, Any, List, Optional -from telegram import (TelegramObject, PassportFile, PersonalDetails, IdDocumentData, - ResidentialAddress) +from telegram import ( + IdDocumentData, + PassportFile, + PersonalDetails, + ResidentialAddress, + TelegramObject, +) from telegram.passport.credentials import decrypt_json +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot, Credentials class EncryptedPassportElement(TelegramObject): @@ -29,92 +39,116 @@ class EncryptedPassportElement(TelegramObject): Contains information about documents or other Telegram Passport elements shared with the bot by the user. The data has been automatically decrypted by python-telegram-bot. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type`, :attr:`data`, :attr:`phone_number`, :attr:`email`, + :attr:`files`, :attr:`front_side`, :attr:`reverse_side` and :attr:`selfie` are equal. + + Note: + This object is decrypted only when originating from + :obj:`telegram.PassportData.decrypted_data`. + + Args: type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license", "identity_card", "internal_passport", "address", "utility_bill", "bank_statement", "rental_agreement", "passport_registration", "temporary_registration", "phone_number", "email". - data (:class:`telegram.PersonalDetails` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :obj:`str`): - Optional. Decrypted or encrypted data, available for "personal_details", "passport", + data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocument` | \ + :class:`telegram.ResidentialAddress` | :obj:`str`, optional): + Decrypted or encrypted data, available for "personal_details", "passport", "driver_license", "identity_card", "identity_passport" and "address" types. - phone_number (:obj:`str`): Optional. User's verified phone number, available only for + phone_number (:obj:`str`, optional): User's verified phone number, available only for "phone_number" type. - email (:obj:`str`): Optional. User's verified email address, available only for "email" + email (:obj:`str`, optional): User's verified email address, available only for "email" type. - files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files + files (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files with documents provided by the user, available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. - front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the + front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the front side of the document, provided by the user. Available for "passport", "driver_license", "identity_card" and "internal_passport". - reverse_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the + reverse_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the reverse side of the document, provided by the user. Available for "driver_license" and "identity_card". - selfie (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the + selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the selfie of the user holding a document, provided by the user; available for "passport", "driver_license", "identity_card" and "internal_passport". - translation (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted + translation (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files with translated versions of documents provided by the user. Available if requested for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. hash (:obj:`str`): Base64-encoded element hash for using in :class:`telegram.PassportElementErrorUnspecified`. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license", "identity_card", "internal_passport", "address", "utility_bill", "bank_statement", "rental_agreement", "passport_registration", "temporary_registration", "phone_number", "email". - data (:class:`telegram.PersonalDetails` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :obj:`str`, optional): - Decrypted or encrypted data, available for "personal_details", "passport", + data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocument` | \ + :class:`telegram.ResidentialAddress` | :obj:`str`): + Optional. Decrypted or encrypted data, available for "personal_details", "passport", "driver_license", "identity_card", "identity_passport" and "address" types. - phone_number (:obj:`str`, optional): User's verified phone number, available only for + phone_number (:obj:`str`): Optional. User's verified phone number, available only for "phone_number" type. - email (:obj:`str`, optional): User's verified email address, available only for "email" + email (:obj:`str`): Optional. User's verified email address, available only for "email" type. - files (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files + files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files with documents provided by the user, available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. - front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the + front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the front side of the document, provided by the user. Available for "passport", "driver_license", "identity_card" and "internal_passport". - reverse_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the + reverse_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the reverse side of the document, provided by the user. Available for "driver_license" and "identity_card". - selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the + selfie (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the selfie of the user holding a document, provided by the user; available for "passport", "driver_license", "identity_card" and "internal_passport". - translation (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted + translation (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files with translated versions of documents provided by the user. Available if requested for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. hash (:obj:`str`): Base64-encoded element hash for using in :class:`telegram.PassportElementErrorUnspecified`. - bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. - Note: - This object is decrypted only when originating from - :obj:`telegram.PassportData.decrypted_data`. """ - def __init__(self, - type, - data=None, - phone_number=None, - email=None, - files=None, - front_side=None, - reverse_side=None, - selfie=None, - translation=None, - hash=None, - bot=None, - credentials=None, - **kwargs): + __slots__ = ( + 'selfie', + 'files', + 'type', + 'translation', + 'email', + 'hash', + 'phone_number', + 'bot', + 'reverse_side', + 'front_side', + 'data', + '_id_attrs', + ) + + def __init__( + self, + type: str, # pylint: disable=W0622 + data: PersonalDetails = None, + phone_number: str = None, + email: str = None, + files: List[PassportFile] = None, + front_side: PassportFile = None, + reverse_side: PassportFile = None, + selfie: PassportFile = None, + translation: List[PassportFile] = None, + hash: str = None, # pylint: disable=W0622 + bot: 'Bot' = None, + credentials: 'Credentials' = None, # pylint: disable=W0613 + **_kwargs: Any, + ): # Required self.type = type # Optionals @@ -128,18 +162,27 @@ def __init__(self, self.translation = translation self.hash = hash - self._id_attrs = (self.type, self.data, self.phone_number, self.email, self.files, - self.front_side, self.reverse_side, self.selfie) + self._id_attrs = ( + self.type, + self.data, + self.phone_number, + self.email, + self.files, + self.front_side, + self.reverse_side, + self.selfie, + ) self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['EncryptedPassportElement']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(EncryptedPassportElement, cls).de_json(data, bot) - data['files'] = PassportFile.de_list(data.get('files'), bot) or None data['front_side'] = PassportFile.de_json(data.get('front_side'), bot) data['reverse_side'] = PassportFile.de_json(data.get('reverse_side'), bot) @@ -149,55 +192,71 @@ def de_json(cls, data, bot): return cls(bot=bot, **data) @classmethod - def de_json_decrypted(cls, data, bot, credentials): + def de_json_decrypted( + cls, data: Optional[JSONDict], bot: 'Bot', credentials: 'Credentials' + ) -> Optional['EncryptedPassportElement']: + """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account + passport credentials. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with this object. + credentials (:class:`telegram.FileCredentials`): The credentials + + Returns: + :class:`telegram.EncryptedPassportElement`: + + """ if not data: return None - data = super(EncryptedPassportElement, cls).de_json(data, bot) - if data['type'] not in ('phone_number', 'email'): secure_data = getattr(credentials.secure_data, data['type']) if secure_data.data is not None: # If not already decrypted if not isinstance(data['data'], dict): - data['data'] = decrypt_json(b64decode(secure_data.data.secret), - b64decode(secure_data.data.hash), - b64decode(data['data'])) + data['data'] = decrypt_json( + b64decode(secure_data.data.secret), + b64decode(secure_data.data.hash), + b64decode(data['data']), + ) if data['type'] == 'personal_details': data['data'] = PersonalDetails.de_json(data['data'], bot=bot) - elif data['type'] in ('passport', 'internal_passport', - 'driver_license', 'identity_card'): + elif data['type'] in ( + 'passport', + 'internal_passport', + 'driver_license', + 'identity_card', + ): data['data'] = IdDocumentData.de_json(data['data'], bot=bot) elif data['type'] == 'address': data['data'] = ResidentialAddress.de_json(data['data'], bot=bot) - data['files'] = PassportFile.de_list_decrypted(data.get('files'), bot, - secure_data.files) or None - data['front_side'] = PassportFile.de_json_decrypted(data.get('front_side'), bot, - secure_data.front_side) - data['reverse_side'] = PassportFile.de_json_decrypted(data.get('reverse_side'), bot, - secure_data.reverse_side) - data['selfie'] = PassportFile.de_json_decrypted(data.get('selfie'), bot, - secure_data.selfie) - data['translation'] = PassportFile.de_list_decrypted(data.get('translation'), bot, - secure_data.translation) or None + data['files'] = ( + PassportFile.de_list_decrypted(data.get('files'), bot, secure_data.files) or None + ) + data['front_side'] = PassportFile.de_json_decrypted( + data.get('front_side'), bot, secure_data.front_side + ) + data['reverse_side'] = PassportFile.de_json_decrypted( + data.get('reverse_side'), bot, secure_data.reverse_side + ) + data['selfie'] = PassportFile.de_json_decrypted( + data.get('selfie'), bot, secure_data.selfie + ) + data['translation'] = ( + PassportFile.de_list_decrypted( + data.get('translation'), bot, secure_data.translation + ) + or None + ) return cls(bot=bot, **data) - @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - encrypted_passport_elements = list() - for element in data: - encrypted_passport_elements.append(cls.de_json(element, bot)) - - return encrypted_passport_elements - - def to_dict(self): - data = super(EncryptedPassportElement, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() if self.files: data['files'] = [p.to_dict() for p in self.files] diff --git a/telegramer/include/telegram/passport/passportdata.py b/telegramer/include/telegram/passport/passportdata.py index 67101da..40f7e72 100644 --- a/telegramer/include/telegram/passport/passportdata.py +++ b/telegramer/include/telegram/passport/passportdata.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,62 +18,78 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """Contains information about Telegram Passport data shared with the bot by the user.""" +from typing import TYPE_CHECKING, Any, List, Optional + from telegram import EncryptedCredentials, EncryptedPassportElement, TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot, Credentials class PassportData(TelegramObject): """Contains information about Telegram Passport data shared with the bot by the user. - Attributes: - data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information - about documents and other Telegram Passport elements that was shared with the bot. - credentials (:class:`telegram.EncryptedCredentials`): Encrypted credentials. - bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + Note: + To be able to decrypt this object, you must pass your ``private_key`` to either + :class:`telegram.Updater` or :class:`telegram.Bot`. Decrypted data is then found in + :attr:`decrypted_data` and the payload can be found in :attr:`decrypted_credentials`'s + attribute :attr:`telegram.Credentials.payload`. Args: data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information about documents and other Telegram Passport elements that was shared with the bot. - credentials (:obj:`str`): Encrypted credentials. + credentials (:class:`telegram.EncryptedCredentials`)): Encrypted credentials. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Note: - To be able to decrypt this object, you must pass your private_key to either - :class:`telegram.Updater` or :class:`telegram.Bot`. Decrypted data is then found in - :attr:`decrypted_data` and the payload can be found in :attr:`decrypted_credentials`'s - attribute :attr:`telegram.Credentials.payload`. + Attributes: + data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information + about documents and other Telegram Passport elements that was shared with the bot. + credentials (:class:`telegram.EncryptedCredentials`): Encrypted credentials. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. """ - def __init__(self, data, credentials, bot=None, **kwargs): + __slots__ = ('bot', 'credentials', 'data', '_decrypted_data', '_id_attrs') + + def __init__( + self, + data: List[EncryptedPassportElement], + credentials: EncryptedCredentials, + bot: 'Bot' = None, + **_kwargs: Any, + ): self.data = data self.credentials = credentials self.bot = bot - self._decrypted_data = None + self._decrypted_data: Optional[List[EncryptedPassportElement]] = None self._id_attrs = tuple([x.type for x in data] + [credentials.hash]) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PassportData']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(PassportData, cls).de_json(data, bot) - data['data'] = EncryptedPassportElement.de_list(data.get('data'), bot) data['credentials'] = EncryptedCredentials.de_json(data.get('credentials'), bot) return cls(bot=bot, **data) - def to_dict(self): - data = super(PassportData, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['data'] = [e.to_dict() for e in self.data] return data @property - def decrypted_data(self): + def decrypted_data(self) -> List[EncryptedPassportElement]: """ List[:class:`telegram.EncryptedPassportElement`]: Lazily decrypt and return information about documents and other Telegram Passport elements which were shared with the bot. @@ -84,15 +100,15 @@ def decrypted_data(self): """ if self._decrypted_data is None: self._decrypted_data = [ - EncryptedPassportElement.de_json_decrypted(element.to_dict(), - self.bot, - self.decrypted_credentials) + EncryptedPassportElement.de_json_decrypted( + element.to_dict(), self.bot, self.decrypted_credentials + ) for element in self.data ] return self._decrypted_data @property - def decrypted_credentials(self): + def decrypted_credentials(self) -> 'Credentials': """ :class:`telegram.Credentials`: Lazily decrypt and return credentials that were used to decrypt the data. This object also contains the user specified payload as diff --git a/telegramer/include/telegram/passport/passportelementerrors.py b/telegramer/include/telegram/passport/passportelementerrors.py index d69935b..fd38ff3 100644 --- a/telegramer/include/telegram/passport/passportelementerrors.py +++ b/telegramer/include/telegram/passport/passportelementerrors.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,27 +16,39 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 """This module contains the classes that represent Telegram PassportElementError.""" +from typing import Any + from telegram import TelegramObject class PassportElementError(TelegramObject): """Baseclass for the PassportElementError* classes. - Attributes: - source (:obj:`str`): Error source. - type (:obj:`str`): The section of the user's Telegram Passport which has the error. - message (:obj:`str`): Error message + This object represents an error in the Telegram Passport element which was submitted that + should be resolved by the user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source` and :attr:`type` are equal. Args: source (:obj:`str`): Error source. type (:obj:`str`): The section of the user's Telegram Passport which has the error. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + source (:obj:`str`): Error source. + type (:obj:`str`): The section of the user's Telegram Passport which has the error. + message (:obj:`str`): Error message. + """ - def __init__(self, source, type, message, **kwargs): + # All subclasses of this class won't have _id_attrs in slots since it's added here. + __slots__ = ('message', 'source', 'type', '_id_attrs') + + def __init__(self, source: str, type: str, message: str, **_kwargs: Any): # Required self.source = str(source) self.type = str(type) @@ -50,33 +62,34 @@ class PassportElementErrorDataField(PassportElementError): Represents an issue in one of the data fields that was provided by the user. The error is considered resolved when the field's value changes. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`field_name`, :attr:`data_hash` + and :attr:`message` are equal. + + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the error, one of - "personal_details", "passport", "driver_license", "identity_card", "internal_passport", - "address". + ``"personal_details"``, ``"passport"``, ``"driver_license"``, ``"identity_card"``, + ``"internal_passport"``, ``"address"``. field_name (:obj:`str`): Name of the data field which has the error. data_hash (:obj:`str`): Base64-encoded data hash. message (:obj:`str`): Error message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the error, one of - "personal_details", "passport", "driver_license", "identity_card", "internal_passport", - "address". + ``"personal_details"``, ``"passport"``, ``"driver_license"``, ``"identity_card"``, + ``"internal_passport"``, ``"address"``. field_name (:obj:`str`): Name of the data field which has the error. data_hash (:obj:`str`): Base64-encoded data hash. message (:obj:`str`): Error message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - type, - field_name, - data_hash, - message, - **kwargs): + __slots__ = ('data_hash', 'field_name') + + def __init__(self, type: str, field_name: str, data_hash: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorDataField, self).__init__('data', type, message) + super().__init__('data', type, message) self.field_name = field_name self.data_hash = data_hash @@ -88,30 +101,32 @@ class PassportElementErrorFile(PassportElementError): Represents an issue with a document scan. The error is considered resolved when the file with the document scan changes. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and + :attr:`message` are equal. + + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration". + ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, + ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded file hash. message (:obj:`str`): Error message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration". + ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, + ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded file hash. message (:obj:`str`): Error message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - type, - file_hash, - message, - **kwargs): + __slots__ = ('file_hash',) + + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorFile, self).__init__('file', type, message) + super().__init__('file', type, message) self.file_hash = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) @@ -119,37 +134,38 @@ def __init__(self, class PassportElementErrorFiles(PassportElementError): """ - Represents an issue with a list of scans. The error is considered resolved when the file with - the document scan changes. + Represents an issue with a list of scans. The error is considered resolved when the list of + files with the document scans changes. - Attributes: - type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration". - file_hash (:obj:`str`): Base64-encoded file hash. - message (:obj:`str`): Error message. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hashes`, and + :attr:`message` are equal. Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration". + ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, + ``"passport_registration"``, ``"temporary_registration"``. file_hashes (List[:obj:`str`]): List of base64-encoded file hashes. message (:obj:`str`): Error message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of + ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, + ``"passport_registration"``, ``"temporary_registration"``. + file_hashes (List[:obj:`str`]): List of base64-encoded file hashes. + message (:obj:`str`): Error message. + """ - def __init__(self, - type, - file_hashes, - message, - **kwargs): + __slots__ = ('file_hashes',) + + def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorFiles, self).__init__('files', type, message) + super().__init__('files', type, message) self.file_hashes = file_hashes - self._id_attrs = ((self.source, self.type, self.message) + - tuple([file_hash for file_hash in file_hashes])) + self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes) class PassportElementErrorFrontSide(PassportElementError): @@ -157,30 +173,32 @@ class PassportElementErrorFrontSide(PassportElementError): Represents an issue with the front side of a document. The error is considered resolved when the file with the front side of the document changes. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and + :attr:`message` are equal. + + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "passport", "driver_license", "identity_card", "internal_passport". + ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the front side of the document. message (:obj:`str`): Error message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "passport", "driver_license", "identity_card", "internal_passport". + ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the front side of the document. message (:obj:`str`): Error message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - type, - file_hash, - message, - **kwargs): + __slots__ = ('file_hash',) + + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorFrontSide, self).__init__('front_side', type, message) + super().__init__('front_side', type, message) self.file_hash = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) @@ -188,33 +206,35 @@ def __init__(self, class PassportElementErrorReverseSide(PassportElementError): """ - Represents an issue with the front side of a document. The error is considered resolved when + Represents an issue with the reverse side of a document. The error is considered resolved when the file with the reverse side of the document changes. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and + :attr:`message` are equal. + + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "passport", "driver_license", "identity_card", "internal_passport". + ``"driver_license"``, ``"identity_card"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the reverse side of the document. message (:obj:`str`): Error message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "driver_license", "identity_card". + ``"driver_license"``, ``"identity_card"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the reverse side of the document. message (:obj:`str`): Error message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - type, - file_hash, - message, - **kwargs): + __slots__ = ('file_hash',) + + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorReverseSide, self).__init__('reverse_side', type, message) + super().__init__('reverse_side', type, message) self.file_hash = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) @@ -225,28 +245,30 @@ class PassportElementErrorSelfie(PassportElementError): Represents an issue with the selfie with a document. The error is considered resolved when the file with the selfie changes. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and + :attr:`message` are equal. + + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "passport", "driver_license", "identity_card", "internal_passport". + ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the selfie. message (:obj:`str`): Error message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of - "passport", "driver_license", "identity_card", "internal_passport". + ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the selfie. message (:obj:`str`): Error message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - type, - file_hash, - message, - **kwargs): + __slots__ = ('file_hash',) + + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorSelfie, self).__init__('selfie', type, message) + super().__init__('selfie', type, message) self.file_hash = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) @@ -257,33 +279,34 @@ class PassportElementErrorTranslationFile(PassportElementError): Represents an issue with one of the files that constitute the translation of a document. The error is considered resolved when the file changes. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and + :attr:`message` are equal. + + Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, - one of "passport", "driver_license", "identity_card", "internal_passport", - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration". + one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, + ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, + ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded hash of the file. message (:obj:`str`): Error message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, - one of "passport", "driver_license", "identity_card", "internal_passport", - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration". + one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, + ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, + ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded hash of the file. message (:obj:`str`): Error message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - type, - file_hash, - message, - **kwargs): + __slots__ = ('file_hash',) + + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorTranslationFile, self).__init__('translation_file', - type, message) + super().__init__('translation_file', type, message) self.file_hash = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) @@ -292,39 +315,39 @@ def __init__(self, class PassportElementErrorTranslationFiles(PassportElementError): """ Represents an issue with the translated version of a document. The error is considered - resolved when a file with the document translation change. + resolved when a file with the document translation changes. - Attributes: - type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, - one of "passport", "driver_license", "identity_card", "internal_passport", - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration" - file_hash (:obj:`str`): Base64-encoded file hash. - message (:obj:`str`): Error message. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hashes`, and + :attr:`message` are equal. Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, - one of "passport", "driver_license", "identity_card", "internal_passport", - "utility_bill", "bank_statement", "rental_agreement", "passport_registration", - "temporary_registration" + one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, + ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, + ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hashes (List[:obj:`str`]): List of base64-encoded file hashes. message (:obj:`str`): Error message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, + one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, + ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, + ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. + file_hashes (List[:obj:`str`]): List of base64-encoded file hashes. + message (:obj:`str`): Error message. + """ - def __init__(self, - type, - file_hashes, - message, - **kwargs): + __slots__ = ('file_hashes',) + + def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorTranslationFiles, self).__init__('translation_files', - type, message) + super().__init__('translation_files', type, message) self.file_hashes = file_hashes - self._id_attrs = ((self.source, self.type, self.message) + - tuple([file_hash for file_hash in file_hashes])) + self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes) class PassportElementErrorUnspecified(PassportElementError): @@ -332,26 +355,28 @@ class PassportElementErrorUnspecified(PassportElementError): Represents an issue in an unspecified place. The error is considered resolved when new data is added. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source`, :attr:`type`, :attr:`element_hash`, + and :attr:`message` are equal. + + Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue. element_hash (:obj:`str`): Base64-encoded element hash. message (:obj:`str`): Error message. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue. element_hash (:obj:`str`): Base64-encoded element hash. message (:obj:`str`): Error message. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - type, - element_hash, - message, - **kwargs): + __slots__ = ('element_hash',) + + def __init__(self, type: str, element_hash: str, message: str, **_kwargs: Any): # Required - super(PassportElementErrorUnspecified, self).__init__('unspecified', type, message) + super().__init__('unspecified', type, message) self.element_hash = element_hash self._id_attrs = (self.source, self.type, self.element_hash, self.message) diff --git a/telegramer/include/telegram/passport/passportfile.py b/telegramer/include/telegram/passport/passportfile.py index 4b82294..59f60c8 100644 --- a/telegramer/include/telegram/passport/passportfile.py +++ b/telegramer/include/telegram/passport/passportfile.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,7 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Encrypted PassportFile.""" +from typing import TYPE_CHECKING, Any, List, Optional + from telegram import TelegramObject +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, File, FileCredentials class PassportFile(TelegramObject): @@ -26,86 +33,128 @@ class PassportFile(TelegramObject): This object represents a file uploaded to Telegram Passport. Currently all Telegram Passport files are in JPEG format when decrypted and don't exceed 10MB. - Attributes: - file_id (:obj:`str`): Unique identifier for this file. - file_size (:obj:`int`): File size. - file_date (:obj:`int`): Unix time when the file was uploaded. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`file_unique_id` is equal. Args: - file_id (:obj:`str`): Unique identifier for this file. + file_id (:obj:`str`): Identifier for this file, which can be used to download + or reuse the file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. file_size (:obj:`int`): File size. file_date (:obj:`int`): Unix time when the file was uploaded. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + file_id (:obj:`str`): Identifier for this file. + file_unique_id (:obj:`str`): Unique identifier for this file, which + is supposed to be the same over time and for different bots. + Can't be used to download or reuse the file. + file_size (:obj:`int`): File size. + file_date (:obj:`int`): Unix time when the file was uploaded. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - def __init__(self, file_id, file_date, file_size=None, bot=None, credentials=None, **kwargs): + __slots__ = ( + 'file_date', + 'bot', + 'file_id', + 'file_size', + '_credentials', + 'file_unique_id', + '_id_attrs', + ) + + def __init__( + self, + file_id: str, + file_unique_id: str, + file_date: int, + file_size: int = None, + bot: 'Bot' = None, + credentials: 'FileCredentials' = None, + **_kwargs: Any, + ): # Required self.file_id = file_id + self.file_unique_id = file_unique_id self.file_size = file_size self.file_date = file_date # Optionals self.bot = bot self._credentials = credentials - self._id_attrs = (self.file_id,) + self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): - if not data: - return None + def de_json_decrypted( + cls, data: Optional[JSONDict], bot: 'Bot', credentials: 'FileCredentials' + ) -> Optional['PassportFile']: + """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account + passport credentials. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with this object. + credentials (:class:`telegram.FileCredentials`): The credentials - data = super(PassportFile, cls).de_json(data, bot) + Returns: + :class:`telegram.PassportFile`: - return cls(bot=bot, **data) + """ + data = cls._parse_data(data) - @classmethod - def de_json_decrypted(cls, data, bot, credentials): if not data: return None - data = super(PassportFile, cls).de_json(data, bot) - data['credentials'] = credentials return cls(bot=bot, **data) @classmethod - def de_list(cls, data, bot): - if not data: - return [] + def de_list_decrypted( + cls, data: Optional[List[JSONDict]], bot: 'Bot', credentials: List['FileCredentials'] + ) -> List[Optional['PassportFile']]: + """Variant of :meth:`telegram.TelegramObject.de_list` that also takes into account + passport credentials. - return [cls.de_json(passport_file, bot) for passport_file in data] + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with these objects. + credentials (:class:`telegram.FileCredentials`): The credentials - @classmethod - def de_list_decrypted(cls, data, bot, credentials): + Returns: + List[:class:`telegram.PassportFile`]: + + """ if not data: return [] - return [cls.de_json_decrypted(passport_file, bot, credentials[i]) - for i, passport_file in enumerate(data)] + return [ + cls.de_json_decrypted(passport_file, bot, credentials[i]) + for i, passport_file in enumerate(data) + ] - def get_file(self, timeout=None, **kwargs): + def get_file( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> 'File': """ Wrapper over :attr:`telegram.Bot.get_file`. Will automatically assign the correct credentials to the returned :class:`telegram.File` if originating from :obj:`telegram.PassportData.decrypted_data`. - Args: - timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as - the read timeout from the server (instead of the one specified during creation of - the connection pool). - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: - :class:`telegram.TelegramError` + :class:`telegram.error.TelegramError` """ - file = self.bot.get_file(self.file_id, timeout=timeout, **kwargs) + file = self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) file.set_credentials(self._credentials) return file diff --git a/telegramer/include/telegram/payment/invoice.py b/telegramer/include/telegram/payment/invoice.py index cefc35a..18fda43 100644 --- a/telegramer/include/telegram/payment/invoice.py +++ b/telegramer/include/telegram/payment/invoice.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,18 +18,17 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Invoice.""" +from typing import Any + from telegram import TelegramObject class Invoice(TelegramObject): """This object contains basic information about an invoice. - Attributes: - title (:obj:`str`): Product name. - description (:obj:`str`): Product description. - start_parameter (:obj:`str`): Unique bot deep-linking parameter. - currency (:obj:`str`): Three-letter ISO 4217 currency code. - total_amount (:obj:`int`): Total price in the smallest units of the currency. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`title`, :attr:`description`, :attr:`start_parameter`, + :attr:`currency` and :attr:`total_amount` are equal. Args: title (:obj:`str`): Product name. @@ -38,21 +37,50 @@ class Invoice(TelegramObject): generate this invoice. currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 pass amount = 145. + float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. See the + :obj:`exp` parameter in + `currencies.json `_, + it shows the number of digits past the decimal point for each currency + (2 for the majority of currencies). **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + title (:obj:`str`): Product name. + description (:obj:`str`): Product description. + start_parameter (:obj:`str`): Unique bot deep-linking parameter. + currency (:obj:`str`): Three-letter ISO 4217 currency code. + total_amount (:obj:`int`): Total price in the smallest units of the currency. + """ - def __init__(self, title, description, start_parameter, currency, total_amount, **kwargs): + __slots__ = ( + 'currency', + 'start_parameter', + 'title', + 'description', + 'total_amount', + '_id_attrs', + ) + + def __init__( + self, + title: str, + description: str, + start_parameter: str, + currency: str, + total_amount: int, + **_kwargs: Any, + ): self.title = title self.description = description self.start_parameter = start_parameter self.currency = currency self.total_amount = total_amount - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) + self._id_attrs = ( + self.title, + self.description, + self.start_parameter, + self.currency, + self.total_amount, + ) diff --git a/telegramer/include/telegram/payment/labeledprice.py b/telegramer/include/telegram/payment/labeledprice.py index 5ee3c27..37b58e3 100644 --- a/telegramer/include/telegram/payment/labeledprice.py +++ b/telegramer/include/telegram/payment/labeledprice.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,26 +18,37 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram LabeledPrice.""" +from typing import Any + from telegram import TelegramObject class LabeledPrice(TelegramObject): """This object represents a portion of the price for goods or services. - Attributes: - label (:obj:`str`): Portion label. - amount (:obj:`int`): Price of the product in the smallest units of the currency. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`label` and :attr:`amount` are equal. Args: - label (:obj:`str`): Portion label + label (:obj:`str`): Portion label. amount (:obj:`int`): Price of the product in the smallest units of the currency (integer, - not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp - parameter in currencies.json, it shows the number of digits past the decimal point for - each currency (2 for the majority of currencies). + not float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. + See the :obj:`exp` parameter in + `currencies.json `_, + it shows the number of digits past the decimal point for each currency + (2 for the majority of currencies). **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + label (:obj:`str`): Portion label. + amount (:obj:`int`): Price of the product in the smallest units of the currency. + """ - def __init__(self, label, amount, **kwargs): + __slots__ = ('label', '_id_attrs', 'amount') + + def __init__(self, label: str, amount: int, **_kwargs: Any): self.label = label self.amount = amount + + self._id_attrs = (self.label, self.amount) diff --git a/telegramer/include/telegram/payment/orderinfo.py b/telegramer/include/telegram/payment/orderinfo.py index 21f8657..902b324 100644 --- a/telegramer/include/telegram/payment/orderinfo.py +++ b/telegramer/include/telegram/payment/orderinfo.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,17 +18,21 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram OrderInfo.""" -from telegram import TelegramObject, ShippingAddress +from typing import TYPE_CHECKING, Any, Optional + +from telegram import ShippingAddress, TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class OrderInfo(TelegramObject): """This object represents information about an order. - Attributes: - name (:obj:`str`): Optional. User name. - phone_number (:obj:`str`): Optional. User's phone number. - email (:obj:`str`): Optional. User email. - shipping_address (:class:`telegram.ShippingAddress`): Optional. User shipping address. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name`, :attr:`phone_number`, :attr:`email` and + :attr:`shipping_address` are equal. Args: name (:obj:`str`, optional): User name. @@ -37,21 +41,39 @@ class OrderInfo(TelegramObject): shipping_address (:class:`telegram.ShippingAddress`, optional): User shipping address. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + name (:obj:`str`): Optional. User name. + phone_number (:obj:`str`): Optional. User's phone number. + email (:obj:`str`): Optional. User email. + shipping_address (:class:`telegram.ShippingAddress`): Optional. User shipping address. + """ - def __init__(self, name=None, phone_number=None, email=None, shipping_address=None, **kwargs): + __slots__ = ('email', 'shipping_address', 'phone_number', 'name', '_id_attrs') + + def __init__( + self, + name: str = None, + phone_number: str = None, + email: str = None, + shipping_address: str = None, + **_kwargs: Any, + ): self.name = name self.phone_number = phone_number self.email = email self.shipping_address = shipping_address + self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address) + @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['OrderInfo']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return cls() - data = super(OrderInfo, cls).de_json(data, bot) - data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot) return cls(**data) diff --git a/telegramer/include/telegram/payment/precheckoutquery.py b/telegramer/include/telegram/payment/precheckoutquery.py index 36eebf6..65b5e30 100644 --- a/telegramer/include/telegram/payment/precheckoutquery.py +++ b/telegramer/include/telegram/payment/precheckoutquery.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,34 +18,35 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram PreCheckoutQuery.""" -from telegram import TelegramObject, User, OrderInfo +from typing import TYPE_CHECKING, Any, Optional + +from telegram import OrderInfo, TelegramObject, User +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot class PreCheckoutQuery(TelegramObject): """This object contains information about an incoming pre-checkout query. - Note: - * In Python `from` is a reserved word, use `from_user` instead. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. - Attributes: - id (:obj:`str`): Unique query identifier. - from_user (:class:`telegram.User`): User who sent the query. - currency (:obj:`str`): Three-letter ISO 4217 currency code. - total_amount (:obj:`int`): Total price in the smallest units of the currency. - invoice_payload (:obj:`str`): Bot specified invoice payload. - shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the - user. - order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + Note: + In Python ``from`` is a reserved word, use ``from_user`` instead. Args: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. - currency (:obj:`str`): Three-letter ISO 4217 currency code + currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp - parameter in currencies.json, it shows the number of digits past the decimal point for - each currency (2 for the majority of currencies). + float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. + See the :obj:`exp` parameter in + `currencies.json `_, + it shows the number of digits past the decimal point for each currency + (2 for the majority of currencies). invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. @@ -53,19 +54,44 @@ class PreCheckoutQuery(TelegramObject): bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + id (:obj:`str`): Unique query identifier. + from_user (:class:`telegram.User`): User who sent the query. + currency (:obj:`str`): Three-letter ISO 4217 currency code. + total_amount (:obj:`int`): Total price in the smallest units of the currency. + invoice_payload (:obj:`str`): Bot specified invoice payload. + shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the + user. + order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + """ - def __init__(self, - id, - from_user, - currency, - total_amount, - invoice_payload, - shipping_option_id=None, - order_info=None, - bot=None, - **kwargs): - self.id = id + __slots__ = ( + 'bot', + 'invoice_payload', + 'shipping_option_id', + 'currency', + 'order_info', + 'total_amount', + 'id', + 'from_user', + '_id_attrs', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + from_user: User, + currency: str, + total_amount: int, + invoice_payload: str, + shipping_option_id: str = None, + order_info: OrderInfo = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): + self.id = id # pylint: disable=C0103 self.from_user = from_user self.currency = currency self.total_amount = total_amount @@ -78,31 +104,37 @@ def __init__(self, self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PreCheckoutQuery']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(PreCheckoutQuery, cls).de_json(data, bot) - data['from_user'] = User.de_json(data.pop('from'), bot) data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot) return cls(bot=bot, **data) - def answer(self, *args, **kwargs): + def answer( # pylint: disable=C0103 + self, + ok: bool, + error_message: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: bot.answer_pre_checkout_query(update.pre_checkout_query.id, *args, **kwargs) - Args: - ok (:obj:`bool`): Specify True if everything is alright (goods are available, etc.) and - the bot is ready to proceed with the order. Use False if there are any problems. - error_message (:obj:`str`, optional): Required if ok is False. Error message in human - readable form that explains the reason for failure to proceed with the checkout - (e.g. "Sorry, somebody just bought the last of our amazing black T-shirts while you - were busy filling out your payment details. Please choose a different color or - garment!"). Telegram will display this message to the user. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + For the documentation of the arguments, please see + :meth:`telegram.Bot.answer_pre_checkout_query`. """ - return self.bot.answer_pre_checkout_query(self.id, *args, **kwargs) + return self.bot.answer_pre_checkout_query( + pre_checkout_query_id=self.id, + ok=ok, + error_message=error_message, + timeout=timeout, + api_kwargs=api_kwargs, + ) diff --git a/telegramer/include/telegram/payment/shippingaddress.py b/telegramer/include/telegram/payment/shippingaddress.py index 6606605..ae16d9f 100644 --- a/telegramer/include/telegram/payment/shippingaddress.py +++ b/telegramer/include/telegram/payment/shippingaddress.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,32 +18,57 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ShippingAddress.""" +from typing import Any + from telegram import TelegramObject class ShippingAddress(TelegramObject): """This object represents a Telegram ShippingAddress. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`country_code`, :attr:`state`, :attr:`city`, + :attr:`street_line1`, :attr:`street_line2` and :attr:`post_cod` are equal. + + Args: country_code (:obj:`str`): ISO 3166-1 alpha-2 country code. state (:obj:`str`): State, if applicable. city (:obj:`str`): City. street_line1 (:obj:`str`): First line for the address. street_line2 (:obj:`str`): Second line for the address. post_code (:obj:`str`): Address post code. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: country_code (:obj:`str`): ISO 3166-1 alpha-2 country code. state (:obj:`str`): State, if applicable. city (:obj:`str`): City. street_line1 (:obj:`str`): First line for the address. street_line2 (:obj:`str`): Second line for the address. post_code (:obj:`str`): Address post code. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, country_code, state, city, street_line1, street_line2, post_code, **kwargs): + __slots__ = ( + 'post_code', + 'city', + '_id_attrs', + 'country_code', + 'street_line2', + 'street_line1', + 'state', + ) + + def __init__( + self, + country_code: str, + state: str, + city: str, + street_line1: str, + street_line2: str, + post_code: str, + **_kwargs: Any, + ): self.country_code = country_code self.state = state self.city = city @@ -51,12 +76,11 @@ def __init__(self, country_code, state, city, street_line1, street_line2, post_c self.street_line2 = street_line2 self.post_code = post_code - self._id_attrs = (self.country_code, self.state, self.city, self.street_line1, - self.street_line2, self.post_code) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) + self._id_attrs = ( + self.country_code, + self.state, + self.city, + self.street_line1, + self.street_line2, + self.post_code, + ) diff --git a/telegramer/include/telegram/payment/shippingoption.py b/telegramer/include/telegram/payment/shippingoption.py index 0695c41..5bca48a 100644 --- a/telegramer/include/telegram/payment/shippingoption.py +++ b/telegramer/include/telegram/payment/shippingoption.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,34 +18,52 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ShippingOption.""" +from typing import TYPE_CHECKING, Any, List + from telegram import TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import LabeledPrice # noqa class ShippingOption(TelegramObject): """This object represents one shipping option. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Args: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. prices (List[:class:`telegram.LabeledPrice`]): List of price portions. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. prices (List[:class:`telegram.LabeledPrice`]): List of price portions. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, id, title, prices, **kwargs): - self.id = id + __slots__ = ('prices', 'title', 'id', '_id_attrs') + + def __init__( + self, + id: str, # pylint: disable=W0622 + title: str, + prices: List['LabeledPrice'], + **_kwargs: Any, + ): + self.id = id # pylint: disable=C0103 self.title = title self.prices = prices self._id_attrs = (self.id,) - def to_dict(self): - data = super(ShippingOption, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['prices'] = [p.to_dict() for p in self.prices] diff --git a/telegramer/include/telegram/payment/shippingquery.py b/telegramer/include/telegram/payment/shippingquery.py index a5b8191..2a0d140 100644 --- a/telegramer/include/telegram/payment/shippingquery.py +++ b/telegramer/include/telegram/payment/shippingquery.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,34 +18,54 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ShippingQuery.""" -from telegram import TelegramObject, User, ShippingAddress +from typing import TYPE_CHECKING, Any, Optional, List + +from telegram import ShippingAddress, TelegramObject, User, ShippingOption +from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot class ShippingQuery(TelegramObject): """This object contains information about an incoming shipping query. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + Note: - * In Python `from` is a reserved word, use `from_user` instead. + In Python ``from`` is a reserved word, use ``from_user`` instead. - Attributes: + Args: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. - bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. - bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. + bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ - def __init__(self, id, from_user, invoice_payload, shipping_address, bot=None, **kwargs): - self.id = id + __slots__ = ('bot', 'invoice_payload', 'shipping_address', 'id', 'from_user', '_id_attrs') + + def __init__( + self, + id: str, # pylint: disable=W0622 + from_user: User, + invoice_payload: str, + shipping_address: ShippingAddress, + bot: 'Bot' = None, + **_kwargs: Any, + ): + self.id = id # pylint: disable=C0103 self.from_user = from_user self.invoice_payload = invoice_payload self.shipping_address = shipping_address @@ -55,32 +75,39 @@ def __init__(self, id, from_user, invoice_payload, shipping_address, bot=None, * self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ShippingQuery']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(ShippingQuery, cls).de_json(data, bot) - data['from_user'] = User.de_json(data.pop('from'), bot) data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot) - return cls(**data) + return cls(bot=bot, **data) - def answer(self, *args, **kwargs): + def answer( # pylint: disable=C0103 + self, + ok: bool, + shipping_options: List[ShippingOption] = None, + error_message: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: """Shortcut for:: bot.answer_shipping_query(update.shipping_query.id, *args, **kwargs) - Args: - ok (:obj:`bool`): Specify True if delivery to the specified address is possible and - False if there are any problems (for example, if delivery to the specified address - is not possible). - shipping_options (List[:class:`telegram.ShippingOption`], optional): Required if ok is - True. A JSON-serialized array of available shipping options. - error_message (:obj:`str`, optional): Required if ok is False. Error message in human - readable form that explains why it is impossible to complete the order (e.g. - "Sorry, delivery to your desired address is unavailable'). Telegram will display - this message to the user. + For the documentation of the arguments, please see + :meth:`telegram.Bot.answer_shipping_query`. """ - return self.bot.answer_shipping_query(self.id, *args, **kwargs) + return self.bot.answer_shipping_query( + shipping_query_id=self.id, + ok=ok, + shipping_options=shipping_options, + error_message=error_message, + timeout=timeout, + api_kwargs=api_kwargs, + ) diff --git a/telegramer/include/telegram/payment/successfulpayment.py b/telegramer/include/telegram/payment/successfulpayment.py index bdda7be..7240fb8 100644 --- a/telegramer/include/telegram/payment/successfulpayment.py +++ b/telegramer/include/telegram/payment/successfulpayment.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,47 +18,72 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram SuccessfulPayment.""" -from telegram import TelegramObject, OrderInfo +from typing import TYPE_CHECKING, Any, Optional + +from telegram import OrderInfo, TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class SuccessfulPayment(TelegramObject): """This object contains basic information about a successful payment. - Attributes: + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`telegram_payment_charge_id` and + :attr:`provider_payment_charge_id` are equal. + + Args: currency (:obj:`str`): Three-letter ISO 4217 currency code. - total_amount (:obj:`int`): Total price in the smallest units of the currency. + total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not + float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. + See the :obj:`exp` parameter in + `currencies.json `_, + it shows the number of digits past the decimal point for each currency + (2 for the majority of currencies). invoice_payload (:obj:`str`): Bot specified invoice payload. - shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the + shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. - order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. + order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. provider_payment_charge_id (:obj:`str`): Provider payment identifier. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. - Args: + Attributes: currency (:obj:`str`): Three-letter ISO 4217 currency code. - total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp - parameter in currencies.json, it shows the number of digits past the decimal point for - each currency (2 for the majority of currencies). + total_amount (:obj:`int`): Total price in the smallest units of the currency. invoice_payload (:obj:`str`): Bot specified invoice payload. - shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the + shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the user. - order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user + order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. provider_payment_charge_id (:obj:`str`): Provider payment identifier. - **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, - currency, - total_amount, - invoice_payload, - telegram_payment_charge_id, - provider_payment_charge_id, - shipping_option_id=None, - order_info=None, - **kwargs): + __slots__ = ( + 'invoice_payload', + 'shipping_option_id', + 'currency', + 'order_info', + 'telegram_payment_charge_id', + 'provider_payment_charge_id', + 'total_amount', + '_id_attrs', + ) + + def __init__( + self, + currency: str, + total_amount: int, + invoice_payload: str, + telegram_payment_charge_id: str, + provider_payment_charge_id: str, + shipping_option_id: str = None, + order_info: OrderInfo = None, + **_kwargs: Any, + ): self.currency = currency self.total_amount = total_amount self.invoice_payload = invoice_payload @@ -70,11 +95,13 @@ def __init__(self, self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SuccessfulPayment']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(SuccessfulPayment, cls).de_json(data, bot) data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot) return cls(**data) diff --git a/telegramer/include/telegram/poll.py b/telegramer/include/telegram/poll.py new file mode 100644 index 0000000..a7c51b2 --- /dev/null +++ b/telegramer/include/telegram/poll.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram Poll.""" + +import datetime +import sys +from typing import TYPE_CHECKING, Any, Dict, List, Optional, ClassVar + +from telegram import MessageEntity, TelegramObject, User, constants +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class PollOption(TelegramObject): + """ + This object contains information about one answer option in a poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text` and :attr:`voter_count` are equal. + + Args: + text (:obj:`str`): Option text, 1-100 characters. + voter_count (:obj:`int`): Number of users that voted for this option. + + Attributes: + text (:obj:`str`): Option text, 1-100 characters. + voter_count (:obj:`int`): Number of users that voted for this option. + + """ + + __slots__ = ('voter_count', 'text', '_id_attrs') + + def __init__(self, text: str, voter_count: int, **_kwargs: Any): + self.text = text + self.voter_count = voter_count + + self._id_attrs = (self.text, self.voter_count) + + MAX_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH + """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`""" + + +class PollAnswer(TelegramObject): + """ + This object represents an answer of a user in a non-anonymous poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`poll_id`, :attr:`user` and :attr:`options_ids` are equal. + + Attributes: + poll_id (:obj:`str`): Unique poll identifier. + user (:class:`telegram.User`): The user, who changed the answer to the poll. + option_ids (List[:obj:`int`]): Identifiers of answer options, chosen by the user. + + Args: + poll_id (:obj:`str`): Unique poll identifier. + user (:class:`telegram.User`): The user, who changed the answer to the poll. + option_ids (List[:obj:`int`]): 0-based identifiers of answer options, chosen by the user. + May be empty if the user retracted their vote. + + """ + + __slots__ = ('option_ids', 'user', 'poll_id', '_id_attrs') + + def __init__(self, poll_id: str, user: User, option_ids: List[int], **_kwargs: Any): + self.poll_id = poll_id + self.user = user + self.option_ids = option_ids + + self._id_attrs = (self.poll_id, self.user, tuple(self.option_ids)) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PollAnswer']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['user'] = User.de_json(data.get('user'), bot) + + return cls(**data) + + +class Poll(TelegramObject): + """ + This object contains information about a poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Attributes: + id (:obj:`str`): Unique poll identifier. + question (:obj:`str`): Poll question, 1-300 characters. + options (List[:class:`PollOption`]): List of poll options. + total_voter_count (:obj:`int`): Total number of users that voted in the poll. + is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. + is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous. + type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. + allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers. + correct_option_id (:obj:`int`): Optional. Identifier of the correct answer option. + explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll. + explanation_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities + like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. + open_period (:obj:`int`): Optional. Amount of time in seconds the poll will be active + after creation. + close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be + automatically closed. + + Args: + id (:obj:`str`): Unique poll identifier. + question (:obj:`str`): Poll question, 1-300 characters. + options (List[:class:`PollOption`]): List of poll options. + is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. + is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous. + type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. + allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers. + correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer option. + Available only for polls in the quiz mode, which are closed, or was sent (not + forwarded) by the bot or to the private chat with the bot. + explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll, 0-200 characters. + explanation_entities (List[:class:`telegram.MessageEntity`], optional): Special entities + like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. + open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active + after creation. + close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the + poll will be automatically closed. Converted to :obj:`datetime.datetime`. + + """ + + __slots__ = ( + 'total_voter_count', + 'allows_multiple_answers', + 'open_period', + 'options', + 'type', + 'explanation_entities', + 'is_anonymous', + 'close_date', + 'is_closed', + 'id', + 'explanation', + 'question', + 'correct_option_id', + '_id_attrs', + ) + + def __init__( + self, + id: str, # pylint: disable=W0622 + question: str, + options: List[PollOption], + total_voter_count: int, + is_closed: bool, + is_anonymous: bool, + type: str, # pylint: disable=W0622 + allows_multiple_answers: bool, + correct_option_id: int = None, + explanation: str = None, + explanation_entities: List[MessageEntity] = None, + open_period: int = None, + close_date: datetime.datetime = None, + **_kwargs: Any, + ): + self.id = id # pylint: disable=C0103 + self.question = question + self.options = options + self.total_voter_count = total_voter_count + self.is_closed = is_closed + self.is_anonymous = is_anonymous + self.type = type + self.allows_multiple_answers = allows_multiple_answers + self.correct_option_id = correct_option_id + self.explanation = explanation + self.explanation_entities = explanation_entities + self.open_period = open_period + self.close_date = close_date + + self._id_attrs = (self.id,) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Poll']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['options'] = [PollOption.de_json(option, bot) for option in data['options']] + data['explanation_entities'] = MessageEntity.de_list(data.get('explanation_entities'), bot) + data['close_date'] = from_timestamp(data.get('close_date')) + + return cls(**data) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + data['options'] = [x.to_dict() for x in self.options] + if self.explanation_entities: + data['explanation_entities'] = [e.to_dict() for e in self.explanation_entities] + data['close_date'] = to_timestamp(data.get('close_date')) + + return data + + def parse_explanation_entity(self, entity: MessageEntity) -> str: + """Returns the text from a given :class:`telegram.MessageEntity`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.text`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to this message. + + Returns: + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the poll has no explanation. + + """ + if not self.explanation: + raise RuntimeError("This Poll has no 'explanation'.") + + # Is it a narrow build, if so we don't need to convert + if sys.maxunicode == 0xFFFF: + return self.explanation[entity.offset : entity.offset + entity.length] + entity_text = self.explanation.encode('utf-16-le') + entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] + + return entity_text.decode('utf-16-le') + + def parse_explanation_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this polls explanation filtered by their ``type`` attribute as + the key, and the text that each entity belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`explanation_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_explanation_entity` for more info. + + Args: + types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``type`` attribute of an entity is contained in this list, it will be returned. + Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`. + + Returns: + Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + + """ + if types is None: + types = MessageEntity.ALL_TYPES + + return { + entity: self.parse_explanation_entity(entity) + for entity in (self.explanation_entities or []) + if entity.type in types + } + + REGULAR: ClassVar[str] = constants.POLL_REGULAR + """:const:`telegram.constants.POLL_REGULAR`""" + QUIZ: ClassVar[str] = constants.POLL_QUIZ + """:const:`telegram.constants.POLL_QUIZ`""" + MAX_QUESTION_LENGTH: ClassVar[int] = constants.MAX_POLL_QUESTION_LENGTH + """:const:`telegram.constants.MAX_POLL_QUESTION_LENGTH`""" + MAX_OPTION_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH + """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`""" diff --git a/telegramer/include/telegram/proximityalerttriggered.py b/telegramer/include/telegram/proximityalerttriggered.py new file mode 100644 index 0000000..df4cb55 --- /dev/null +++ b/telegramer/include/telegram/proximityalerttriggered.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram Proximity Alert.""" +from typing import Any, Optional, TYPE_CHECKING + +from telegram import TelegramObject, User +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ProximityAlertTriggered(TelegramObject): + """ + This object represents the content of a service message, sent whenever a user in the chat + triggers a proximity alert set by another user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`traveler`, :attr:`watcher` and :attr:`distance` are equal. + + Args: + traveler (:class:`telegram.User`): User that triggered the alert + watcher (:class:`telegram.User`): User that set the alert + distance (:obj:`int`): The distance between the users + + Attributes: + traveler (:class:`telegram.User`): User that triggered the alert + watcher (:class:`telegram.User`): User that set the alert + distance (:obj:`int`): The distance between the users + + """ + + __slots__ = ('traveler', 'distance', 'watcher', '_id_attrs') + + def __init__(self, traveler: User, watcher: User, distance: int, **_kwargs: Any): + self.traveler = traveler + self.watcher = watcher + self.distance = distance + + self._id_attrs = (self.traveler, self.watcher, self.distance) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ProximityAlertTriggered']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['traveler'] = User.de_json(data.get('traveler'), bot) + data['watcher'] = User.de_json(data.get('watcher'), bot) + + return cls(bot=bot, **data) diff --git a/telegramer/include/telegram/py.typed b/telegramer/include/telegram/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/telegramer/include/telegram/replykeyboardmarkup.py b/telegramer/include/telegram/replykeyboardmarkup.py index d27572a..cc6f985 100644 --- a/telegramer/include/telegram/replykeyboardmarkup.py +++ b/telegramer/include/telegram/replykeyboardmarkup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,18 +18,17 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ReplyKeyboardMarkup.""" -from telegram import ReplyMarkup +from typing import Any, List, Union, Sequence + +from telegram import KeyboardButton, ReplyMarkup +from telegram.utils.types import JSONDict class ReplyKeyboardMarkup(ReplyMarkup): """This object represents a custom keyboard with reply options. - Attributes: - keyboard (List[List[:class:`telegram.KeyboardButton` | :obj:`str`]]): Array of button rows. - resize_keyboard (:obj:`bool`): Optional. Requests clients to resize the keyboard. - one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as - it's been used. - selective (:obj:`bool`): Optional. Show the keyboard to specific users only. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their the size of :attr:`keyboard` and all the buttons are equal. Example: A user requests to change the bot's language, bot replies to the request with a keyboard @@ -40,48 +39,253 @@ class ReplyKeyboardMarkup(ReplyMarkup): each represented by an Array of :class:`telegram.KeyboardButton` objects. resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of - buttons). Defaults to false, in which case the custom keyboard is always of the same - height as the app's standard keyboard. Defaults to ``False`` + buttons). Defaults to :obj:`False`, in which case the custom keyboard is always of the + same height as the app's standard keyboard. one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as soon as it's been used. The keyboard will still be available, but clients will automatically display the usual letter-keyboard in the chat - the user can press a special button in - the input field to see the custom keyboard again. Defaults to ``False``. + the input field to see the custom keyboard again. Defaults to :obj:`False`. selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to specific users only. Targets: - 1) users that are @mentioned in the text of the Message object - 2) if the bot's message is a reply (has reply_to_message_id), sender of the original - message. + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the + original message. - Defaults to ``False``. + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input + field when the keyboard is active; 1-64 characters. + + .. versionadded:: 13.7 **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + keyboard (List[List[:class:`telegram.KeyboardButton` | :obj:`str`]]): Array of button rows. + resize_keyboard (:obj:`bool`): Optional. Requests clients to resize the keyboard. + one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as + it's been used. + selective (:obj:`bool`): Optional. Show the keyboard to specific users only. + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + """ - def __init__(self, - keyboard, - resize_keyboard=False, - one_time_keyboard=False, - selective=False, - **kwargs): + __slots__ = ( + 'selective', + 'keyboard', + 'resize_keyboard', + 'one_time_keyboard', + 'input_field_placeholder', + '_id_attrs', + ) + + def __init__( + self, + keyboard: Sequence[Sequence[Union[str, KeyboardButton]]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + input_field_placeholder: str = None, + **_kwargs: Any, + ): # Required - self.keyboard = keyboard + self.keyboard = [] + for row in keyboard: + button_row = [] + for button in row: + if isinstance(button, KeyboardButton): + button_row.append(button) # telegram.KeyboardButton + else: + button_row.append(KeyboardButton(button)) # str + self.keyboard.append(button_row) + # Optionals self.resize_keyboard = bool(resize_keyboard) self.one_time_keyboard = bool(one_time_keyboard) self.selective = bool(selective) + self.input_field_placeholder = input_field_placeholder + + self._id_attrs = (self.keyboard,) - def to_dict(self): - data = super(ReplyKeyboardMarkup, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['keyboard'] = [] for row in self.keyboard: - r = [] - for button in row: - if hasattr(button, 'to_dict'): - r.append(button.to_dict()) # telegram.KeyboardButton - else: - r.append(button) # str - data['keyboard'].append(r) + data['keyboard'].append([button.to_dict() for button in row]) return data + + @classmethod + def from_button( + cls, + button: Union[KeyboardButton, str], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + input_field_placeholder: str = None, + **kwargs: object, + ) -> 'ReplyKeyboardMarkup': + """Shortcut for:: + + ReplyKeyboardMarkup([[button]], **kwargs) + + Return a ReplyKeyboardMarkup from a single KeyboardButton. + + Args: + button (:class:`telegram.KeyboardButton` | :obj:`str`): The button to use in + the markup. + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard + vertically for optimal fit (e.g., make the keyboard smaller if there are just two + rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is + always of the same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as + soon as it's been used. The keyboard will still be available, but clients will + automatically display the usual letter-keyboard in the chat - the user can press + a special button in the input field to see the custom keyboard again. + Defaults to :obj:`False`. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard + to specific users only. Targets: + + 1) Users that are @mentioned in the text of the Message object. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the + original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + """ + return cls( + [[button]], + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + input_field_placeholder=input_field_placeholder, + **kwargs, + ) + + @classmethod + def from_row( + cls, + button_row: List[Union[str, KeyboardButton]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + input_field_placeholder: str = None, + **kwargs: object, + ) -> 'ReplyKeyboardMarkup': + """Shortcut for:: + + ReplyKeyboardMarkup([button_row], **kwargs) + + Return a ReplyKeyboardMarkup from a single row of KeyboardButtons. + + Args: + button_row (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use in + the markup. + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard + vertically for optimal fit (e.g., make the keyboard smaller if there are just two + rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is + always of the same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as + soon as it's been used. The keyboard will still be available, but clients will + automatically display the usual letter-keyboard in the chat - the user can press + a special button in the input field to see the custom keyboard again. + Defaults to :obj:`False`. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard + to specific users only. Targets: + + 1) Users that are @mentioned in the text of the Message object. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the + original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + return cls( + [button_row], + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + input_field_placeholder=input_field_placeholder, + **kwargs, + ) + + @classmethod + def from_column( + cls, + button_column: List[Union[str, KeyboardButton]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + input_field_placeholder: str = None, + **kwargs: object, + ) -> 'ReplyKeyboardMarkup': + """Shortcut for:: + + ReplyKeyboardMarkup([[button] for button in button_column], **kwargs) + + Return a ReplyKeyboardMarkup from a single column of KeyboardButtons. + + Args: + button_column (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use + in the markup. + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard + vertically for optimal fit (e.g., make the keyboard smaller if there are just two + rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is + always of the same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as + soon as it's been used. The keyboard will still be available, but clients will + automatically display the usual letter-keyboard in the chat - the user can press + a special button in the input field to see the custom keyboard again. + Defaults to :obj:`False`. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard + to specific users only. Targets: + + 1) Users that are @mentioned in the text of the Message object. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the + original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + button_grid = [[button] for button in button_column] + return cls( + button_grid, + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + input_field_placeholder=input_field_placeholder, + **kwargs, + ) + + def __hash__(self) -> int: + return hash( + ( + tuple(tuple(button for button in row) for row in self.keyboard), + self.resize_keyboard, + self.one_time_keyboard, + self.selective, + ) + ) diff --git a/telegramer/include/telegram/replykeyboardremove.py b/telegramer/include/telegram/replykeyboardremove.py index 6d31bd4..2762141 100644 --- a/telegramer/include/telegram/replykeyboardremove.py +++ b/telegramer/include/telegram/replykeyboardremove.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,6 +17,8 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ReplyKeyboardRemove.""" +from typing import Any + from telegram import ReplyMarkup @@ -27,29 +29,35 @@ class ReplyKeyboardRemove(ReplyMarkup): until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see :class:`telegram.ReplyKeyboardMarkup`). - Attributes: - remove_keyboard (:obj:`True`): Requests clients to remove the custom keyboard. - selective (:obj:`bool`): Optional. Use this parameter if you want to remove the keyboard - for specific users only. - Example: A user votes in a poll, bot returns confirmation message in reply to the vote and removes the keyboard for that user, while still showing the keyboard with poll options to users who haven't voted yet. + Note: + User will not be able to summon this keyboard; if you want to hide the keyboard from + sight but keep it accessible, use :attr:`telegram.ReplyKeyboardMarkup.one_time_keyboard`. + Args: selective (:obj:`bool`, optional): Use this parameter if you want to remove the keyboard for specific users only. Targets: - 1) users that are @mentioned in the text of the Message object - 2) if the bot's message is a reply (has reply_to_message_id), sender of the original + 1) Users that are @mentioned in the text of the :class:`telegram.Message` object. + 2) If the bot's message is a reply (has `reply_to_message_id`), sender of the original message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + remove_keyboard (:obj:`True`): Requests clients to remove the custom keyboard. + selective (:obj:`bool`): Optional. Use this parameter if you want to remove the keyboard + for specific users only. + """ - def __init__(self, selective=False, **kwargs): + __slots__ = ('selective', 'remove_keyboard') + + def __init__(self, selective: bool = False, **_kwargs: Any): # Required self.remove_keyboard = True # Optionals diff --git a/telegramer/include/telegram/replymarkup.py b/telegramer/include/telegram/replymarkup.py index 47928b7..081a73a 100644 --- a/telegramer/include/telegram/replymarkup.py +++ b/telegramer/include/telegram/replymarkup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -24,8 +24,10 @@ class ReplyMarkup(TelegramObject): """Base class for Telegram ReplyMarkup Objects. - See :class:`telegram.ReplyKeyboardMarkup` and :class:`telegram.InlineKeyboardMarkup` for + See :class:`telegram.InlineKeyboardMarkup`, :class:`telegram.ReplyKeyboardMarkup`, + :class:`telegram.ReplyKeyboardRemove` and :class:`telegram.ForceReply` for detailed use. """ - pass + + __slots__ = () diff --git a/telegramer/include/telegram/update.py b/telegramer/include/telegram/update.py index e69fd3a..7c7b210 100644 --- a/telegramer/include/telegram/update.py +++ b/telegramer/include/telegram/update.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,35 +18,44 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Update.""" -from telegram import (Message, TelegramObject, InlineQuery, ChosenInlineResult, - CallbackQuery, ShippingQuery, PreCheckoutQuery) +from typing import TYPE_CHECKING, Any, Optional + +from telegram import ( + CallbackQuery, + ChosenInlineResult, + InlineQuery, + Message, + Poll, + PreCheckoutQuery, + ShippingQuery, + TelegramObject, + ChatMemberUpdated, + constants, + ChatJoinRequest, +) +from telegram.poll import PollAnswer +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot, Chat, User # noqa class Update(TelegramObject): """This object represents an incoming update. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`update_id` is equal. + Note: At most one of the optional parameters can be present in any given update. - Attributes: - update_id (:obj:`int`): The update's unique identifier. - message (:class:`telegram.Message`): Optional. New incoming message. - edited_message (:class:`telegram.Message`): Optional. New version of a message. - channel_post (:class:`telegram.Message`): Optional. New incoming channel post. - edited_channel_post (:class:`telegram.Message`): Optional. New version of a channel post. - inline_query (:class:`telegram.InlineQuery`): Optional. New incoming inline query. - chosen_inline_result (:class:`telegram.ChosenInlineResult`): Optional. The result of an - inline query that was chosen by a user. - callback_query (:class:`telegram.CallbackQuery`): Optional. New incoming callback query. - shipping_query (:class:`telegram.ShippingQuery`): Optional. New incoming shipping query. - pre_checkout_query (:class:`telegram.PreCheckoutQuery`): Optional. New incoming - pre-checkout query. - Args: update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if you're using Webhooks, since it allows you to ignore repeated updates or to restore the - correct update sequence, should they get out of order. + correct update sequence, should they get out of order. If there are no new updates for + at least a week, then identifier of the next update will be chosen randomly instead of + sequentially. message (:class:`telegram.Message`, optional): New incoming message of any kind - text, photo, sticker, etc. edited_message (:class:`telegram.Message`, optional): New version of a message that is @@ -62,23 +71,174 @@ class Update(TelegramObject): shipping_query (:class:`telegram.ShippingQuery`, optional): New incoming shipping query. Only for invoices with flexible price. pre_checkout_query (:class:`telegram.PreCheckoutQuery`, optional): New incoming - pre-checkout query. Contains full information about checkout + pre-checkout query. Contains full information about checkout. + poll (:class:`telegram.Poll`, optional): New poll state. Bots receive only updates about + stopped polls and polls, which are sent by the bot. + poll_answer (:class:`telegram.PollAnswer`, optional): A user changed their answer + in a non-anonymous poll. Bots receive new votes only in polls that were sent + by the bot itself. + my_chat_member (:class:`telegram.ChatMemberUpdated`, optional): The bot's chat member + status was updated in a chat. For private chats, this update is received only when the + bot is blocked or unblocked by the user. + + .. versionadded:: 13.4 + chat_member (:class:`telegram.ChatMemberUpdated`, optional): A chat member's status was + updated in a chat. The bot must be an administrator in the chat and must explicitly + specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Updater.start_polling` and + :meth:`telegram.ext.Updater.start_webhook`). + + .. versionadded:: 13.4 + chat_join_request (:class:`telegram.ChatJoinRequest`, optional): A request to join the + chat has been sent. The bot must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat to + receive these updates. + + .. versionadded:: 13.8 **kwargs (:obj:`dict`): Arbitrary keyword arguments. + Attributes: + update_id (:obj:`int`): The update's unique identifier. + message (:class:`telegram.Message`): Optional. New incoming message. + edited_message (:class:`telegram.Message`): Optional. New version of a message. + channel_post (:class:`telegram.Message`): Optional. New incoming channel post. + edited_channel_post (:class:`telegram.Message`): Optional. New version of a channel post. + inline_query (:class:`telegram.InlineQuery`): Optional. New incoming inline query. + chosen_inline_result (:class:`telegram.ChosenInlineResult`): Optional. The result of an + inline query that was chosen by a user. + callback_query (:class:`telegram.CallbackQuery`): Optional. New incoming callback query. + shipping_query (:class:`telegram.ShippingQuery`): Optional. New incoming shipping query. + pre_checkout_query (:class:`telegram.PreCheckoutQuery`): Optional. New incoming + pre-checkout query. + poll (:class:`telegram.Poll`): Optional. New poll state. Bots receive only updates + about stopped polls and polls, which are sent by the bot. + poll_answer (:class:`telegram.PollAnswer`): Optional. A user changed their answer + in a non-anonymous poll. Bots receive new votes only in polls that were sent + by the bot itself. + my_chat_member (:class:`telegram.ChatMemberUpdated`): Optional. The bot's chat member + status was updated in a chat. For private chats, this update is received only when the + bot is blocked or unblocked by the user. + + .. versionadded:: 13.4 + chat_member (:class:`telegram.ChatMemberUpdated`): Optional. A chat member's status was + updated in a chat. The bot must be an administrator in the chat and must explicitly + specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Updater.start_polling` and + :meth:`telegram.ext.Updater.start_webhook`). + + .. versionadded:: 13.4 + chat_join_request (:class:`telegram.ChatJoinRequest`): Optional. A request to join the + chat has been sent. The bot must have the ``'can_invite_users'`` administrator + right in the chat to receive these updates. + + .. versionadded:: 13.8 + """ - def __init__(self, - update_id, - message=None, - edited_message=None, - channel_post=None, - edited_channel_post=None, - inline_query=None, - chosen_inline_result=None, - callback_query=None, - shipping_query=None, - pre_checkout_query=None, - **kwargs): + __slots__ = ( + 'callback_query', + 'chosen_inline_result', + 'pre_checkout_query', + 'inline_query', + 'update_id', + 'message', + 'shipping_query', + 'poll', + 'poll_answer', + 'channel_post', + 'edited_channel_post', + 'edited_message', + '_effective_user', + '_effective_chat', + '_effective_message', + 'my_chat_member', + 'chat_member', + 'chat_join_request', + '_id_attrs', + ) + + MESSAGE = constants.UPDATE_MESSAGE + """:const:`telegram.constants.UPDATE_MESSAGE` + + .. versionadded:: 13.5""" + EDITED_MESSAGE = constants.UPDATE_EDITED_MESSAGE + """:const:`telegram.constants.UPDATE_EDITED_MESSAGE` + + .. versionadded:: 13.5""" + CHANNEL_POST = constants.UPDATE_CHANNEL_POST + """:const:`telegram.constants.UPDATE_CHANNEL_POST` + + .. versionadded:: 13.5""" + EDITED_CHANNEL_POST = constants.UPDATE_EDITED_CHANNEL_POST + """:const:`telegram.constants.UPDATE_EDITED_CHANNEL_POST` + + .. versionadded:: 13.5""" + INLINE_QUERY = constants.UPDATE_INLINE_QUERY + """:const:`telegram.constants.UPDATE_INLINE_QUERY` + + .. versionadded:: 13.5""" + CHOSEN_INLINE_RESULT = constants.UPDATE_CHOSEN_INLINE_RESULT + """:const:`telegram.constants.UPDATE_CHOSEN_INLINE_RESULT` + + .. versionadded:: 13.5""" + CALLBACK_QUERY = constants.UPDATE_CALLBACK_QUERY + """:const:`telegram.constants.UPDATE_CALLBACK_QUERY` + + .. versionadded:: 13.5""" + SHIPPING_QUERY = constants.UPDATE_SHIPPING_QUERY + """:const:`telegram.constants.UPDATE_SHIPPING_QUERY` + + .. versionadded:: 13.5""" + PRE_CHECKOUT_QUERY = constants.UPDATE_PRE_CHECKOUT_QUERY + """:const:`telegram.constants.UPDATE_PRE_CHECKOUT_QUERY` + + .. versionadded:: 13.5""" + POLL = constants.UPDATE_POLL + """:const:`telegram.constants.UPDATE_POLL` + + .. versionadded:: 13.5""" + POLL_ANSWER = constants.UPDATE_POLL_ANSWER + """:const:`telegram.constants.UPDATE_POLL_ANSWER` + + .. versionadded:: 13.5""" + MY_CHAT_MEMBER = constants.UPDATE_MY_CHAT_MEMBER + """:const:`telegram.constants.UPDATE_MY_CHAT_MEMBER` + + .. versionadded:: 13.5""" + CHAT_MEMBER = constants.UPDATE_CHAT_MEMBER + """:const:`telegram.constants.UPDATE_CHAT_MEMBER` + + .. versionadded:: 13.5""" + CHAT_JOIN_REQUEST = constants.UPDATE_CHAT_JOIN_REQUEST + """:const:`telegram.constants.UPDATE_CHAT_JOIN_REQUEST` + + .. versionadded:: 13.8""" + ALL_TYPES = constants.UPDATE_ALL_TYPES + """:const:`telegram.constants.UPDATE_ALL_TYPES` + + .. versionadded:: 13.5""" + + def __init__( + self, + update_id: int, + message: Message = None, + edited_message: Message = None, + channel_post: Message = None, + edited_channel_post: Message = None, + inline_query: InlineQuery = None, + chosen_inline_result: ChosenInlineResult = None, + callback_query: CallbackQuery = None, + shipping_query: ShippingQuery = None, + pre_checkout_query: PreCheckoutQuery = None, + poll: Poll = None, + poll_answer: PollAnswer = None, + my_chat_member: ChatMemberUpdated = None, + chat_member: ChatMemberUpdated = None, + chat_join_request: ChatJoinRequest = None, + **_kwargs: Any, + ): # Required self.update_id = int(update_id) # Optionals @@ -91,18 +251,23 @@ def __init__(self, self.pre_checkout_query = pre_checkout_query self.channel_post = channel_post self.edited_channel_post = edited_channel_post + self.poll = poll + self.poll_answer = poll_answer + self.my_chat_member = my_chat_member + self.chat_member = chat_member + self.chat_join_request = chat_join_request - self._effective_user = None - self._effective_chat = None - self._effective_message = None + self._effective_user: Optional['User'] = None + self._effective_chat: Optional['Chat'] = None + self._effective_message: Optional[Message] = None self._id_attrs = (self.update_id,) @property - def effective_user(self): + def effective_user(self) -> Optional['User']: """ :class:`telegram.User`: The user that sent this update, no matter what kind of update this - is. Will be ``None`` for :attr:`channel_post`. + is. Will be :obj:`None` for :attr:`channel_post` and :attr:`poll`. """ if self._effective_user: @@ -131,16 +296,29 @@ def effective_user(self): elif self.pre_checkout_query: user = self.pre_checkout_query.from_user + elif self.poll_answer: + user = self.poll_answer.user + + elif self.my_chat_member: + user = self.my_chat_member.from_user + + elif self.chat_member: + user = self.chat_member.from_user + + elif self.chat_join_request: + user = self.chat_join_request.from_user + self._effective_user = user return user @property - def effective_chat(self): + def effective_chat(self) -> Optional['Chat']: """ :class:`telegram.Chat`: The chat that this update was sent in, no matter what kind of - update this is. Will be ``None`` for :attr:`inline_query`, + update this is. Will be :obj:`None` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, - :attr:`shipping_query` and :attr:`pre_checkout_query`. + :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` and + :attr:`poll_answer`. """ if self._effective_chat: @@ -163,16 +341,28 @@ def effective_chat(self): elif self.edited_channel_post: chat = self.edited_channel_post.chat + elif self.my_chat_member: + chat = self.my_chat_member.chat + + elif self.chat_member: + chat = self.chat_member.chat + + elif self.chat_join_request: + chat = self.chat_join_request.chat + self._effective_chat = chat return chat @property - def effective_message(self): + def effective_message(self) -> Optional[Message]: """ :class:`telegram.Message`: The message included in this update, no matter what kind of - update this is. Will be ``None`` for :attr:`inline_query`, + update this is. Will be :obj:`None` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, - :attr:`shipping_query` and :attr:`pre_checkout_query`. + :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, + :attr:`poll_answer`, :attr:`my_chat_member`, :attr:`chat_member` as well as + :attr:`chat_join_request` in case the bot is missing the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat. """ if self._effective_message: @@ -199,21 +389,28 @@ def effective_message(self): return message @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Update']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(Update, cls).de_json(data, bot) - data['message'] = Message.de_json(data.get('message'), bot) data['edited_message'] = Message.de_json(data.get('edited_message'), bot) data['inline_query'] = InlineQuery.de_json(data.get('inline_query'), bot) data['chosen_inline_result'] = ChosenInlineResult.de_json( - data.get('chosen_inline_result'), bot) + data.get('chosen_inline_result'), bot + ) data['callback_query'] = CallbackQuery.de_json(data.get('callback_query'), bot) data['shipping_query'] = ShippingQuery.de_json(data.get('shipping_query'), bot) data['pre_checkout_query'] = PreCheckoutQuery.de_json(data.get('pre_checkout_query'), bot) data['channel_post'] = Message.de_json(data.get('channel_post'), bot) data['edited_channel_post'] = Message.de_json(data.get('edited_channel_post'), bot) + data['poll'] = Poll.de_json(data.get('poll'), bot) + data['poll_answer'] = PollAnswer.de_json(data.get('poll_answer'), bot) + data['my_chat_member'] = ChatMemberUpdated.de_json(data.get('my_chat_member'), bot) + data['chat_member'] = ChatMemberUpdated.de_json(data.get('chat_member'), bot) + data['chat_join_request'] = ChatJoinRequest.de_json(data.get('chat_join_request'), bot) return cls(**data) diff --git a/telegramer/include/telegram/user.py b/telegramer/include/telegram/user.py index e140cfc..0a50895 100644 --- a/telegramer/include/telegram/user.py +++ b/telegramer/include/telegram/user.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -# pylint: disable=C0103,W0622 +# pylint: disable=W0622 # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,127 +18,210 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram User.""" - -from telegram import TelegramObject -from telegram.utils.helpers import mention_html as util_mention_html +from datetime import datetime +from typing import TYPE_CHECKING, Any, List, Optional, Union, Tuple + +from telegram import TelegramObject, constants +from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton +from telegram.utils.helpers import ( + mention_html as util_mention_html, + DEFAULT_NONE, + DEFAULT_20, +) from telegram.utils.helpers import mention_markdown as util_mention_markdown +from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput + +if TYPE_CHECKING: + from telegram import ( + Bot, + Message, + UserProfilePhotos, + MessageId, + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + MessageEntity, + ReplyMarkup, + PhotoSize, + Audio, + Contact, + Document, + InlineKeyboardMarkup, + LabeledPrice, + Location, + Animation, + Sticker, + Video, + Venue, + VideoNote, + Voice, + ) class User(TelegramObject): """This object represents a Telegram user or bot. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Args: + id (:obj:`int`): Unique identifier for this user or bot. + is_bot (:obj:`bool`): :obj:`True`, if this user is a bot. + first_name (:obj:`str`): User's or bots first name. + last_name (:obj:`str`, optional): User's or bots last name. + username (:obj:`str`, optional): User's or bots username. + language_code (:obj:`str`, optional): IETF language tag of the user's language. + can_join_groups (:obj:`str`, optional): :obj:`True`, if the bot can be invited to groups. + Returned only in :attr:`telegram.Bot.get_me` requests. + can_read_all_group_messages (:obj:`str`, optional): :obj:`True`, if privacy mode is + disabled for the bot. Returned only in :attr:`telegram.Bot.get_me` requests. + supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline + queries. Returned only in :attr:`telegram.Bot.get_me` requests. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + Attributes: id (:obj:`int`): Unique identifier for this user or bot. - is_bot (:obj:`bool`): True, if this user is a bot + is_bot (:obj:`bool`): :obj:`True`, if this user is a bot. first_name (:obj:`str`): User's or bot's first name. last_name (:obj:`str`): Optional. User's or bot's last name. username (:obj:`str`): Optional. User's or bot's username. language_code (:obj:`str`): Optional. IETF language tag of the user's language. + can_join_groups (:obj:`str`): Optional. :obj:`True`, if the bot can be invited to groups. + Returned only in :attr:`telegram.Bot.get_me` requests. + can_read_all_group_messages (:obj:`str`): Optional. :obj:`True`, if privacy mode is + disabled for the bot. Returned only in :attr:`telegram.Bot.get_me` requests. + supports_inline_queries (:obj:`str`): Optional. :obj:`True`, if the bot supports inline + queries. Returned only in :attr:`telegram.Bot.get_me` requests. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. - Args: - id (:obj:`int`): Unique identifier for this user or bot. - is_bot (:obj:`bool`): True, if this user is a bot - first_name (:obj:`str`): User's or bot's first name. - last_name (:obj:`str`, optional): User's or bot's last name. - username (:obj:`str`, optional): User's or bot's username. - language_code (:obj:`str`, optional): IETF language tag of the user's language. - bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. - """ - def __init__(self, - id, - first_name, - is_bot, - last_name=None, - username=None, - language_code=None, - bot=None, - **kwargs): + __slots__ = ( + 'is_bot', + 'can_read_all_group_messages', + 'username', + 'first_name', + 'last_name', + 'can_join_groups', + 'supports_inline_queries', + 'id', + 'bot', + 'language_code', + '_id_attrs', + ) + + def __init__( + self, + id: int, + first_name: str, + is_bot: bool, + last_name: str = None, + username: str = None, + language_code: str = None, + can_join_groups: bool = None, + can_read_all_group_messages: bool = None, + supports_inline_queries: bool = None, + bot: 'Bot' = None, + **_kwargs: Any, + ): # Required - self.id = int(id) + self.id = int(id) # pylint: disable=C0103 self.first_name = first_name self.is_bot = is_bot # Optionals self.last_name = last_name self.username = username self.language_code = language_code - + self.can_join_groups = can_join_groups + self.can_read_all_group_messages = can_read_all_group_messages + self.supports_inline_queries = supports_inline_queries self.bot = bot self._id_attrs = (self.id,) @property - def name(self): + def name(self) -> str: """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` - prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`.""" + prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`. + """ if self.username: - return '@{}'.format(self.username) + return f'@{self.username}' return self.full_name @property - def full_name(self): + def full_name(self) -> str: """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if - available) :attr:`last_name`.""" - + available) :attr:`last_name`. + """ if self.last_name: - return u'{} {}'.format(self.first_name, self.last_name) + return f'{self.first_name} {self.last_name}' return self.first_name @property - def link(self): + def link(self) -> Optional[str]: """:obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link - of the user.""" - + of the user. + """ if self.username: - return "https://t.me/{}".format(self.username) + return f"https://t.me/{self.username}" return None - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - data = super(User, cls).de_json(data, bot) - - return cls(bot=bot, **data) - - def get_profile_photos(self, *args, **kwargs): + def get_profile_photos( + self, + offset: int = None, + limit: int = 100, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> Optional['UserProfilePhotos']: """ Shortcut for:: - bot.get_user_profile_photos(update.message.from_user.id, *args, **kwargs) + bot.get_user_profile_photos(update.effective_user.id, *args, **kwargs) - """ + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_user_profile_photos`. - return self.bot.get_user_profile_photos(self.id, *args, **kwargs) + """ + return self.bot.get_user_profile_photos( + user_id=self.id, + offset=offset, + limit=limit, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def mention_markdown(self, name: str = None) -> str: + """ + Note: + :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for + backward compatibility. You should use :meth:`mention_markdown_v2` instead. - @classmethod - def de_list(cls, data, bot): - if not data: - return [] + Args: + name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. - users = list() - for user in data: - users.append(cls.de_json(user, bot)) + Returns: + :obj:`str`: The inline mention for the user as markdown (version 1). - return users + """ + if name: + return util_mention_markdown(self.id, name) + return util_mention_markdown(self.id, self.full_name) - def mention_markdown(self, name=None): + def mention_markdown_v2(self, name: str = None) -> str: """ Args: name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. Returns: - :obj:`str`: The inline mention for the user as markdown. + :obj:`str`: The inline mention for the user as markdown (version 2). """ if name: - return util_mention_markdown(self.id, name) - return util_mention_markdown(self.id, self.full_name) + return util_mention_markdown(self.id, name, version=2) + return util_mention_markdown(self.id, self.full_name, version=2) - def mention_html(self, name=None): + def mention_html(self, name: str = None) -> str: """ Args: name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. @@ -151,119 +234,1010 @@ def mention_html(self, name=None): return util_mention_html(self.id, name) return util_mention_html(self.id, self.full_name) - def send_message(self, *args, **kwargs): + def mention_button(self, name: str = None) -> InlineKeyboardButton: + """ + Shortcut for:: + + InlineKeyboardButton(text=name, url=f"tg://user?id={update.effective_user.id}") + + .. versionadded:: 13.9 + + Args: + name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. + + Returns: + :class:`telegram.InlineKeyboardButton`: InlineButton with url set to the user mention + """ + return InlineKeyboardButton(text=name or self.full_name, url=f"tg://user?id={self.id}") + + def pin_message( + self, + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.pin_chat_message(chat_id=update.effective_user.id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.pin_chat_message( + chat_id=self.id, + message_id=message_id, + disable_notification=disable_notification, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def unpin_message( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + message_id: int = None, + ) -> bool: + """Shortcut for:: + + bot.unpin_chat_message(chat_id=update.effective_user.id, + *args, + **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_chat_message( + chat_id=self.id, + timeout=timeout, + api_kwargs=api_kwargs, + message_id=message_id, + ) + + def unpin_all_messages( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.unpin_all_chat_messages(chat_id=update.effective_user.id, + *args, + **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_chat_messages`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.unpin_all_chat_messages( + chat_id=self.id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def send_message( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_message(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_message( + chat_id=self.id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + ) + + def send_photo( + self, + photo: Union[FileInput, 'PhotoSize'], + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_message(User.id, *args, **kwargs) + bot.send_photo(update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_message(self.id, *args, **kwargs) + return self.bot.send_photo( + chat_id=self.id, + photo=photo, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_media_group( + self, + media: List[ + Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> List['Message']: + """Shortcut for:: + + bot.send_media_group(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. - def send_photo(self, *args, **kwargs): + Returns: + List[:class:`telegram.Message`:] On success, instance representing the message posted. + + """ + return self.bot.send_media_group( + chat_id=self.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_audio( + self, + audio: Union[FileInput, 'Audio'], + duration: int = None, + performer: str = None, + title: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_photo(User.id, *args, **kwargs) + bot.send_audio(update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_photo(self.id, *args, **kwargs) + return self.bot.send_audio( + chat_id=self.id, + audio=audio, + duration=duration, + performer=performer, + title=title, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_chat_action( + self, + action: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.send_chat_action(update.effective_user.id, *args, **kwargs) - def send_audio(self, *args, **kwargs): + For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. + + Returns: + :obj:`True`: On success. + + """ + return self.bot.send_chat_action( + chat_id=self.id, + action=action, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + send_action = send_chat_action + """Alias for :attr:`send_chat_action`""" + + def send_contact( + self, + phone_number: str = None, + first_name: str = None, + last_name: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + contact: 'Contact' = None, + vcard: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_contact(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_contact( + chat_id=self.id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + contact=contact, + vcard=vcard, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_dice( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + emoji: str = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_dice(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_dice( + chat_id=self.id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + emoji=emoji, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_document( + self, + document: Union[FileInput, 'Document'], + filename: str = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + disable_content_type_detection: bool = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_audio(User.id, *args, **kwargs) + bot.send_document(update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_audio(self.id, *args, **kwargs) + return self.bot.send_document( + chat_id=self.id, + document=document, + filename=filename, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + thumb=thumb, + api_kwargs=api_kwargs, + disable_content_type_detection=disable_content_type_detection, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + protect_content=protect_content, + ) + + def send_game( + self, + game_short_name: str, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'InlineKeyboardMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_game(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. - def send_document(self, *args, **kwargs): + """ + return self.bot.send_game( + chat_id=self.id, + game_short_name=game_short_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_invoice( + self, + title: str, + description: str, + payload: str, + provider_token: str, + currency: str, + prices: List['LabeledPrice'], + start_parameter: str = None, + photo_url: str = None, + photo_size: int = None, + photo_width: int = None, + photo_height: int = None, + need_name: bool = None, + need_phone_number: bool = None, + need_email: bool = None, + need_shipping_address: bool = None, + is_flexible: bool = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'InlineKeyboardMarkup' = None, + provider_data: Union[str, object] = None, + send_phone_number_to_provider: bool = None, + send_email_to_provider: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + max_tip_amount: int = None, + suggested_tip_amounts: List[int] = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_document(User.id, *args, **kwargs) + bot.send_invoice(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. - Where User is the current instance. + Warning: + As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order + of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. + + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter :attr:`start_parameter` is optional. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_document(self.id, *args, **kwargs) + return self.bot.send_invoice( + chat_id=self.id, + title=title, + description=description, + payload=payload, + provider_token=provider_token, + currency=currency, + prices=prices, + start_parameter=start_parameter, + photo_url=photo_url, + photo_size=photo_size, + photo_width=photo_width, + photo_height=photo_height, + need_name=need_name, + need_phone_number=need_phone_number, + need_email=need_email, + need_shipping_address=need_shipping_address, + is_flexible=is_flexible, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + provider_data=provider_data, + send_phone_number_to_provider=send_phone_number_to_provider, + send_email_to_provider=send_email_to_provider, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + max_tip_amount=max_tip_amount, + suggested_tip_amounts=suggested_tip_amounts, + protect_content=protect_content, + ) + + def send_location( + self, + latitude: float = None, + longitude: float = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + location: 'Location' = None, + live_period: int = None, + api_kwargs: JSONDict = None, + horizontal_accuracy: float = None, + heading: int = None, + proximity_alert_radius: int = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: - def send_animation(self, *args, **kwargs): + bot.send_location(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_location( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + location=location, + live_period=live_period, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_animation( + self, + animation: Union[FileInput, 'Animation'], + duration: int = None, + width: int = None, + height: int = None, + thumb: FileInput = None, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_animation(User.id, *args, **kwargs) + bot.send_animation(update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_animation(self.id, *args, **kwargs) + return self.bot.send_animation( + chat_id=self.id, + animation=animation, + duration=duration, + width=width, + height=height, + thumb=thumb, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_sticker( + self, + sticker: Union[FileInput, 'Sticker'], + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_sticker(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. - def send_sticker(self, *args, **kwargs): + """ + return self.bot.send_sticker( + chat_id=self.id, + sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_video( + self, + video: Union[FileInput, 'Video'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + width: int = None, + height: int = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: bool = None, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_sticker(User.id, *args, **kwargs) + bot.send_video(update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_sticker(self.id, *args, **kwargs) + return self.bot.send_video( + chat_id=self.id, + video=video, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + width=width, + height=height, + parse_mode=parse_mode, + supports_streaming=supports_streaming, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_venue( + self, + latitude: float = None, + longitude: float = None, + title: str = None, + address: str = None, + foursquare_id: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + venue: 'Venue' = None, + foursquare_type: str = None, + api_kwargs: JSONDict = None, + google_place_id: str = None, + google_place_type: str = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_venue(update.effective_user.id, *args, **kwargs) - def send_video(self, *args, **kwargs): + For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.send_venue( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + venue=venue, + foursquare_type=foursquare_type, + api_kwargs=api_kwargs, + google_place_id=google_place_id, + google_place_type=google_place_type, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + ) + + def send_video_note( + self, + video_note: Union[FileInput, 'VideoNote'], + duration: int = None, + length: int = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + thumb: FileInput = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_video(User.id, *args, **kwargs) + bot.send_video_note(update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_video(self.id, *args, **kwargs) + return self.bot.send_video_note( + chat_id=self.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + thumb=thumb, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + filename=filename, + protect_content=protect_content, + ) + + def send_voice( + self, + voice: Union[FileInput, 'Voice'], + duration: int = None, + caption: str = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: DVInput[float] = DEFAULT_20, + parse_mode: ODVInput[str] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + filename: str = None, + protect_content: bool = None, + ) -> 'Message': + """Shortcut for:: + + bot.send_voice(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. - def send_video_note(self, *args, **kwargs): + """ + return self.bot.send_voice( + chat_id=self.id, + voice=voice, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + ) + + def send_poll( + self, + question: str, + options: List[str], + is_anonymous: bool = True, + # We use constant.POLL_REGULAR instead of Poll.REGULAR here to avoid circular imports + type: str = constants.POLL_REGULAR, # pylint: disable=W0622 + allows_multiple_answers: bool = False, + correct_option_id: int = None, + is_closed: bool = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + explanation: str = None, + explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, + open_period: int = None, + close_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, + protect_content: bool = None, + ) -> 'Message': """Shortcut for:: - bot.send_video_note(User.id, *args, **kwargs) + bot.send_poll(update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_video_note(self.id, *args, **kwargs) + return self.bot.send_poll( + chat_id=self.id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, # pylint=pylint, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + is_closed=is_closed, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + explanation_entities=explanation_entities, + protect_content=protect_content, + ) + + def send_copy( + self, + from_chat_id: Union[str, int], + message_id: int, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> 'MessageId': + """Shortcut for:: + + bot.copy_message(chat_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. - def send_voice(self, *args, **kwargs): + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return self.bot.copy_message( + chat_id=self.id, + from_chat_id=from_chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def copy_message( + self, + chat_id: Union[int, str], + message_id: int, + caption: str = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, + disable_notification: DVInput[bool] = DEFAULT_NONE, + reply_to_message_id: int = None, + allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, + reply_markup: 'ReplyMarkup' = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + protect_content: bool = None, + ) -> 'MessageId': """Shortcut for:: - bot.send_voice(User.id, *args, **kwargs) + bot.copy_message(from_chat_id=update.effective_user.id, *args, **kwargs) - Where User is the current instance. + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ - return self.bot.send_voice(self.id, *args, **kwargs) + return self.bot.copy_message( + from_chat_id=self.id, + chat_id=chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + timeout=timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + ) + + def approve_join_request( + self, + chat_id: Union[int, str], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.approve_chat_join_request(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.approve_chat_join_request( + user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def decline_join_request( + self, + chat_id: Union[int, str], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.decline_chat_join_request(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.decline_chat_join_request( + user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs + ) diff --git a/telegramer/include/telegram/userprofilephotos.py b/telegramer/include/telegram/userprofilephotos.py index ec94a0a..aa3e6ec 100644 --- a/telegramer/include/telegram/userprofilephotos.py +++ b/telegramer/include/telegram/userprofilephotos.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,44 +18,62 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram UserProfilePhotos.""" +from typing import TYPE_CHECKING, Any, List, Optional + from telegram import PhotoSize, TelegramObject +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot class UserProfilePhotos(TelegramObject): """This object represent a user's profile pictures. - Attributes: - total_count (:obj:`int`): Total number of profile pictures. - photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`total_count` and :attr:`photos` are equal. Args: total_count (:obj:`int`): Total number of profile pictures the target user has. photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up to 4 sizes each). + Attributes: + total_count (:obj:`int`): Total number of profile pictures. + photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures. + """ - def __init__(self, total_count, photos, **kwargs): + __slots__ = ('photos', 'total_count', '_id_attrs') + + def __init__(self, total_count: int, photos: List[List[PhotoSize]], **_kwargs: Any): # Required self.total_count = int(total_count) self.photos = photos + self._id_attrs = (self.total_count, self.photos) + @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['UserProfilePhotos']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + if not data: return None - data = super(UserProfilePhotos, cls).de_json(data, bot) - data['photos'] = [PhotoSize.de_list(photo, bot) for photo in data['photos']] return cls(**data) - def to_dict(self): - data = super(UserProfilePhotos, self).to_dict() + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() data['photos'] = [] for photo in self.photos: data['photos'].append([x.to_dict() for x in photo]) return data + + def __hash__(self) -> int: + return hash(tuple(tuple(p for p in photo) for photo in self.photos)) diff --git a/telegramer/include/telegram/utils/deprecate.py b/telegramer/include/telegram/utils/deprecate.py index effd4df..bc9f519 100644 --- a/telegramer/include/telegram/utils/deprecate.py +++ b/telegramer/include/telegram/utils/deprecate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -25,21 +25,23 @@ # seem like it's the user that issued the warning # We name it something else so that you don't get confused when you attempt to suppress it class TelegramDeprecationWarning(Warning): - pass - - -def warn_deprecate_obj(old, new, stacklevel=3): - warnings.warn( - '{0} is being deprecated, please use {1} from now on.'.format(old, new), - category=TelegramDeprecationWarning, - stacklevel=stacklevel) - - -def deprecate(func, old, new): - """Warn users invoking old to switch to the new function.""" - - def f(*args, **kwargs): - warn_deprecate_obj(old, new) - return func(*args, **kwargs) - - return f + """Custom warning class for deprecations in this library.""" + + __slots__ = () + + +# Function to warn users that setting custom attributes is deprecated (Use only in __setattr__!) +# Checks if a custom attribute is added by checking length of dictionary before & after +# assigning attribute. This is the fastest way to do it (I hope!). +def set_new_attribute_deprecated(self: object, key: str, value: object) -> None: + """Warns the user if they set custom attributes on PTB objects.""" + org = len(self.__dict__) + object.__setattr__(self, key, value) + new = len(self.__dict__) + if new > org: + warnings.warn( + f"Setting custom attributes such as {key!r} on objects such as " + f"{self.__class__.__name__!r} of the PTB library is deprecated.", + TelegramDeprecationWarning, + stacklevel=3, + ) diff --git a/telegramer/include/telegram/utils/helpers.py b/telegramer/include/telegram/utils/helpers.py index 0290579..194a461 100644 --- a/telegramer/include/telegram/utils/helpers.py +++ b/telegramer/include/telegram/utils/helpers.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -17,125 +17,580 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains helper functions.""" -from html import escape +import datetime as dtm # dtm = "DateTime Module" import re import signal -from datetime import datetime +import time + +from collections import defaultdict +from html import escape +from pathlib import Path + +from typing import ( + TYPE_CHECKING, + Any, + DefaultDict, + Dict, + Optional, + Tuple, + Union, + Type, + cast, + IO, + TypeVar, + Generic, + overload, +) + +from telegram.utils.types import JSONDict, FileInput + +if TYPE_CHECKING: + from telegram import Message, Update, TelegramObject, InputFile + +# in PTB-Raw we don't have pytz, so we make a little workaround here +DTM_UTC = dtm.timezone.utc +try: + import pytz + + UTC = pytz.utc +except ImportError: + UTC = DTM_UTC # type: ignore[assignment] + +try: + import ujson as json +except ImportError: + import json # type: ignore[no-redef] + # From https://stackoverflow.com/questions/2549939/get-signal-names-from-numbers-in-python -_signames = {v: k - for k, v in reversed(sorted(vars(signal).items())) - if k.startswith('SIG') and not k.startswith('SIG_')} +_signames = { + v: k + for k, v in reversed(sorted(vars(signal).items())) + if k.startswith('SIG') and not k.startswith('SIG_') +} -def get_signal_name(signum): +def get_signal_name(signum: int) -> str: """Returns the signal name of the given signal number.""" return _signames[signum] -# Not using future.backports.datetime here as datetime value might be an input from the user, -# making every isinstace() call more delicate. So we just use our own compat layer. -if hasattr(datetime, 'timestamp'): - # Python 3.3+ - def _timestamp(dt_obj): - return dt_obj.timestamp() -else: - # Python < 3.3 (incl 2.7) - from time import mktime +def is_local_file(obj: Optional[Union[str, Path]]) -> bool: + """ + Checks if a given string is a file on local system. - def _timestamp(dt_obj): - return mktime(dt_obj.timetuple()) + Args: + obj (:obj:`str`): The string to check. + """ + if obj is None: + return False + + path = Path(obj) + try: + return path.is_file() + except Exception: + return False + + +def parse_file_input( + file_input: Union[FileInput, 'TelegramObject'], + tg_type: Type['TelegramObject'] = None, + attach: bool = None, + filename: str = None, +) -> Union[str, 'InputFile', Any]: + """ + Parses input for sending files: + + * For string input, if the input is an absolute path of a local file, + adds the ``file://`` prefix. If the input is a relative path of a local file, computes the + absolute path and adds the ``file://`` prefix. Returns the input unchanged, otherwise. + * :class:`pathlib.Path` objects are treated the same way as strings. + * For IO and bytes input, returns an :class:`telegram.InputFile`. + * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id`` + attribute. + + Args: + file_input (:obj:`str` | :obj:`bytes` | `filelike object` | Telegram media object): The + input to parse. + tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g. + :class:`telegram.Animation`. + attach (:obj:`bool`, optional): Whether this file should be send as one file or is part of + a collection of files. Only relevant in case an :class:`telegram.InputFile` is + returned. + filename (:obj:`str`, optional): The filename. Only relevant in case an + :class:`telegram.InputFile` is returned. + + Returns: + :obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched + :attr:`file_input`, in case it's no valid file input. + """ + # Importing on file-level yields cyclic Import Errors + from telegram import InputFile # pylint: disable=C0415 + + if isinstance(file_input, str) and file_input.startswith('file://'): + return file_input + if isinstance(file_input, (str, Path)): + if is_local_file(file_input): + out = Path(file_input).absolute().as_uri() + else: + out = file_input # type: ignore[assignment] + return out + if isinstance(file_input, bytes): + return InputFile(file_input, attach=attach, filename=filename) + if InputFile.is_file(file_input): + file_input = cast(IO, file_input) + return InputFile(file_input, attach=attach, filename=filename) + if tg_type and isinstance(file_input, tg_type): + return file_input.file_id # type: ignore[attr-defined] + return file_input + + +def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: + """ + Helper function to escape telegram markup symbols. + + Args: + text (:obj:`str`): The text. + version (:obj:`int` | :obj:`str`): Use to specify the version of telegrams Markdown. + Either ``1`` or ``2``. Defaults to ``1``. + entity_type (:obj:`str`, optional): For the entity types ``PRE``, ``CODE`` and the link + part of ``TEXT_LINKS``, only certain characters need to be escaped in ``MarkdownV2``. + See the official API documentation for details. Only valid in combination with + ``version=2``, will be ignored else. + """ + if int(version) == 1: + escape_chars = r'_*`[' + elif int(version) == 2: + if entity_type in ['pre', 'code']: + escape_chars = r'\`' + elif entity_type == 'text_link': + escape_chars = r'\)' + else: + escape_chars = r'_*[]()~`>#+-=|{}.!' + else: + raise ValueError('Markdown version must be either 1 or 2!') + + return re.sub(f'([{re.escape(escape_chars)}])', r'\\\1', text) + + +# -------- date/time related helpers -------- +def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float: + """ + Converts a datetime object to a float timestamp (with sub-second precision). + If the datetime object is timezone-naive, it is assumed to be in UTC. + """ + if dt_obj.tzinfo is None: + dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc) + return dt_obj.timestamp() -def escape_markdown(text): - """Helper function to escape telegram markup symbols.""" - escape_chars = '\*_`\[' - return re.sub(r'([%s])' % escape_chars, r'\\\1', text) +def _localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime: + """Localize the datetime, where UTC is handled depending on whether pytz is available or not""" + if tzinfo is DTM_UTC: + return datetime.replace(tzinfo=DTM_UTC) + return tzinfo.localize(datetime) # type: ignore[attr-defined] -def to_timestamp(dt_obj): +def to_float_timestamp( + time_object: Union[int, float, dtm.timedelta, dtm.datetime, dtm.time], + reference_timestamp: float = None, + tzinfo: dtm.tzinfo = None, +) -> float: """ + Converts a given time object to a float POSIX timestamp. + Used to convert different time specifications to a common format. The time object + can be relative (i.e. indicate a time increment, or a time of day) or absolute. + object objects from the :class:`datetime` module that are timezone-naive will be assumed + to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`. + Args: - dt_obj (:class:`datetime.datetime`): + time_object (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ + :obj:`datetime.datetime` | :obj:`datetime.time`): + Time value to convert. The semantics of this parameter will depend on its type: + + * :obj:`int` or :obj:`float` will be interpreted as "seconds from ``reference_t``" + * :obj:`datetime.timedelta` will be interpreted as + "time increment from ``reference_t``" + * :obj:`datetime.datetime` will be interpreted as an absolute date/time value + * :obj:`datetime.time` will be interpreted as a specific time of day + + reference_timestamp (:obj:`float`, optional): POSIX timestamp that indicates the absolute + time from which relative calculations are to be performed (e.g. when ``t`` is given as + an :obj:`int`, indicating "seconds from ``reference_t``"). Defaults to now (the time at + which this function is called). + + If ``t`` is given as an absolute representation of date & time (i.e. a + :obj:`datetime.datetime` object), ``reference_timestamp`` is not relevant and so its + value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised. + tzinfo (:obj:`pytz.BaseTzInfo`, optional): If ``t`` is a naive object from the + :class:`datetime` module, it will be interpreted as this timezone. Defaults to + ``pytz.utc``. + + Note: + Only to be used by ``telegram.ext``. + Returns: - int: + :obj:`float` | :obj:`None`: + The return value depends on the type of argument ``t``. + If ``t`` is given as a time increment (i.e. as a :obj:`int`, :obj:`float` or + :obj:`datetime.timedelta`), then the return value will be ``reference_t`` + ``t``. + + Else if it is given as an absolute date/time value (i.e. a :obj:`datetime.datetime` + object), the equivalent value as a POSIX timestamp will be returned. + Finally, if it is a time of the day without date (i.e. a :obj:`datetime.time` + object), the return value is the nearest future occurrence of that time of day. + + Raises: + TypeError: If ``t``'s type is not one of those described above. + ValueError: If ``t`` is a :obj:`datetime.datetime` and :obj:`reference_timestamp` is not + :obj:`None`. """ - if not dt_obj: - return None + if reference_timestamp is None: + reference_timestamp = time.time() + elif isinstance(time_object, dtm.datetime): + raise ValueError('t is an (absolute) datetime while reference_timestamp is not None') + + if isinstance(time_object, dtm.timedelta): + return reference_timestamp + time_object.total_seconds() + if isinstance(time_object, (int, float)): + return reference_timestamp + time_object + + if tzinfo is None: + tzinfo = UTC + + if isinstance(time_object, dtm.time): + reference_dt = dtm.datetime.fromtimestamp( + reference_timestamp, tz=time_object.tzinfo or tzinfo + ) + reference_date = reference_dt.date() + reference_time = reference_dt.timetz() + + aware_datetime = dtm.datetime.combine(reference_date, time_object) + if aware_datetime.tzinfo is None: + aware_datetime = _localize(aware_datetime, tzinfo) + + # if the time of day has passed today, use tomorrow + if reference_time > aware_datetime.timetz(): + aware_datetime += dtm.timedelta(days=1) + return _datetime_to_float_timestamp(aware_datetime) + if isinstance(time_object, dtm.datetime): + if time_object.tzinfo is None: + time_object = _localize(time_object, tzinfo) + return _datetime_to_float_timestamp(time_object) + + raise TypeError(f'Unable to convert {type(time_object).__name__} object to timestamp') + + +def to_timestamp( + dt_obj: Union[int, float, dtm.timedelta, dtm.datetime, dtm.time, None], + reference_timestamp: float = None, + tzinfo: dtm.tzinfo = None, +) -> Optional[int]: + """ + Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated + down to the nearest integer). - return int(_timestamp(dt_obj)) + See the documentation for :func:`to_float_timestamp` for more details. + """ + return ( + int(to_float_timestamp(dt_obj, reference_timestamp, tzinfo)) + if dt_obj is not None + else None + ) -def from_timestamp(unixtime): +def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optional[dtm.datetime]: """ + Converts an (integer) unix timestamp to a timezone aware datetime object. + :obj:`None` s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`). + Args: - unixtime (int): + unixtime (:obj:`int`): Integer POSIX timestamp. + tzinfo (:obj:`datetime.tzinfo`, optional): The timezone to which the timestamp is to be + converted to. Defaults to UTC. Returns: - datetime.datetime: - + Timezone aware equivalent :obj:`datetime.datetime` value if ``unixtime`` is not + :obj:`None`; else :obj:`None`. """ - if not unixtime: + if unixtime is None: return None - return datetime.fromtimestamp(unixtime) + if tzinfo is not None: + return dtm.datetime.fromtimestamp(unixtime, tz=tzinfo) + return dtm.datetime.utcfromtimestamp(unixtime) -def mention_html(user_id, name): +# -------- end -------- + + +def mention_html(user_id: Union[int, str], name: str) -> str: """ Args: - user_id (:obj:`int`) The user's id which you want to mention. - name (:obj:`str`) The name the mention is showing. + user_id (:obj:`int`): The user's id which you want to mention. + name (:obj:`str`): The name the mention is showing. Returns: - :obj:`str`: The inline mention for the user as html. + :obj:`str`: The inline mention for the user as HTML. """ - if isinstance(user_id, int): - return u'{}'.format(user_id, escape(name)) + return f'{escape(name)}' -def mention_markdown(user_id, name): +def mention_markdown(user_id: Union[int, str], name: str, version: int = 1) -> str: """ Args: - user_id (:obj:`int`) The user's id which you want to mention. - name (:obj:`str`) The name the mention is showing. + user_id (:obj:`int`): The user's id which you want to mention. + name (:obj:`str`): The name the mention is showing. + version (:obj:`int` | :obj:`str`): Use to specify the version of Telegram's Markdown. + Either ``1`` or ``2``. Defaults to ``1``. Returns: - :obj:`str`: The inline mention for the user as markdown. + :obj:`str`: The inline mention for the user as Markdown. """ - if isinstance(user_id, int): - return u'[{}](tg://user?id={})'.format(escape_markdown(name), user_id) + return f'[{escape_markdown(name, version=version)}](tg://user?id={user_id})' -def effective_message_type(entity): +def effective_message_type(entity: Union['Message', 'Update']) -> Optional[str]: """ Extracts the type of message as a string identifier from a :class:`telegram.Message` or a :class:`telegram.Update`. Args: - entity (:obj:`Update` | :obj:`Message`) The ``update`` or ``message`` to extract from + entity (:class:`telegram.Update` | :class:`telegram.Message`): The ``update`` or + ``message`` to extract from. Returns: - str: One of ``Message.MESSAGE_TYPES`` + :obj:`str`: One of ``Message.MESSAGE_TYPES`` """ - # Importing on file-level yields cyclic Import Errors - from telegram import Message - from telegram import Update + from telegram import Message, Update # pylint: disable=C0415 if isinstance(entity, Message): message = entity elif isinstance(entity, Update): - message = entity.effective_message + message = entity.effective_message # type: ignore[assignment] else: - raise TypeError("entity is not Message or Update (got: {})".format(type(entity))) + raise TypeError(f"entity is not Message or Update (got: {type(entity)})") for i in Message.MESSAGE_TYPES: if getattr(message, i, None): return i return None + + +def create_deep_linked_url(bot_username: str, payload: str = None, group: bool = False) -> str: + """ + Creates a deep-linked URL for this ``bot_username`` with the specified ``payload``. + See https://core.telegram.org/bots#deep-linking to learn more. + + The ``payload`` may consist of the following characters: ``A-Z, a-z, 0-9, _, -`` + + Note: + Works well in conjunction with + ``CommandHandler("start", callback, filters = Filters.regex('payload'))`` + + Examples: + ``create_deep_linked_url(bot.get_me().username, "some-params")`` + + Args: + bot_username (:obj:`str`): The username to link to + payload (:obj:`str`, optional): Parameters to encode in the created URL + group (:obj:`bool`, optional): If :obj:`True` the user is prompted to select a group to + add the bot to. If :obj:`False`, opens a one-on-one conversation with the bot. + Defaults to :obj:`False`. + + Returns: + :obj:`str`: An URL to start the bot with specific parameters + """ + if bot_username is None or len(bot_username) <= 3: + raise ValueError("You must provide a valid bot_username.") + + base_url = f'https://t.me/{bot_username}' + if not payload: + return base_url + + if len(payload) > 64: + raise ValueError("The deep-linking payload must not exceed 64 characters.") + + if not re.match(r'^[A-Za-z0-9_-]+$', payload): + raise ValueError( + "Only the following characters are allowed for deep-linked " + "URLs: A-Z, a-z, 0-9, _ and -" + ) + + if group: + key = 'startgroup' + else: + key = 'start' + + return f'{base_url}?{key}={payload}' + + +def encode_conversations_to_json(conversations: Dict[str, Dict[Tuple, object]]) -> str: + """Helper method to encode a conversations dict (that uses tuples as keys) to a + JSON-serializable way. Use :meth:`decode_conversations_from_json` to decode. + + Args: + conversations (:obj:`dict`): The conversations dict to transform to JSON. + + Returns: + :obj:`str`: The JSON-serialized conversations dict + """ + tmp: Dict[str, JSONDict] = {} + for handler, states in conversations.items(): + tmp[handler] = {} + for key, state in states.items(): + tmp[handler][json.dumps(key)] = state + return json.dumps(tmp) + + +def decode_conversations_from_json(json_string: str) -> Dict[str, Dict[Tuple, object]]: + """Helper method to decode a conversations dict (that uses tuples as keys) from a + JSON-string created with :meth:`encode_conversations_to_json`. + + Args: + json_string (:obj:`str`): The conversations dict as JSON string. + + Returns: + :obj:`dict`: The conversations dict after decoding + """ + tmp = json.loads(json_string) + conversations: Dict[str, Dict[Tuple, object]] = {} + for handler, states in tmp.items(): + conversations[handler] = {} + for key, state in states.items(): + conversations[handler][tuple(json.loads(key))] = state + return conversations + + +def decode_user_chat_data_from_json(data: str) -> DefaultDict[int, Dict[object, object]]: + """Helper method to decode chat or user data (that uses ints as keys) from a + JSON-string. + + Args: + data (:obj:`str`): The user/chat_data dict as JSON string. + + Returns: + :obj:`dict`: The user/chat_data defaultdict after decoding + """ + tmp: DefaultDict[int, Dict[object, object]] = defaultdict(dict) + decoded_data = json.loads(data) + for user, user_data in decoded_data.items(): + user = int(user) + tmp[user] = {} + for key, value in user_data.items(): + try: + key = int(key) + except ValueError: + pass + tmp[user][key] = value + return tmp + + +DVType = TypeVar('DVType', bound=object) +OT = TypeVar('OT', bound=object) + + +class DefaultValue(Generic[DVType]): + """Wrapper for immutable default arguments that allows to check, if the default value was set + explicitly. Usage:: + + DefaultOne = DefaultValue(1) + def f(arg=DefaultOne): + if arg is DefaultOne: + print('`arg` is the default') + arg = arg.value + else: + print('`arg` was set explicitly') + print(f'`arg` = {str(arg)}') + + This yields:: + + >>> f() + `arg` is the default + `arg` = 1 + >>> f(1) + `arg` was set explicitly + `arg` = 1 + >>> f(2) + `arg` was set explicitly + `arg` = 2 + + Also allows to evaluate truthiness:: + + default = DefaultValue(value) + if default: + ... + + is equivalent to:: + + default = DefaultValue(value) + if value: + ... + + ``repr(DefaultValue(value))`` returns ``repr(value)`` and ``str(DefaultValue(value))`` returns + ``f'DefaultValue({value})'``. + + Args: + value (:obj:`obj`): The value of the default argument + + Attributes: + value (:obj:`obj`): The value of the default argument + + """ + + __slots__ = ('value', '__dict__') + + def __init__(self, value: DVType = None): + self.value = value + + def __bool__(self) -> bool: + return bool(self.value) + + @overload + @staticmethod + def get_value(obj: 'DefaultValue[OT]') -> OT: + ... + + @overload + @staticmethod + def get_value(obj: OT) -> OT: + ... + + @staticmethod + def get_value(obj: Union[OT, 'DefaultValue[OT]']) -> OT: + """ + Shortcut for:: + + return obj.value if isinstance(obj, DefaultValue) else obj + + Args: + obj (:obj:`object`): The object to process + + Returns: + Same type as input, or the value of the input: The value + """ + return obj.value if isinstance(obj, DefaultValue) else obj # type: ignore[return-value] + + # This is mostly here for readability during debugging + def __str__(self) -> str: + return f'DefaultValue({self.value})' + + # This is here to have the default instances nicely rendered in the docs + def __repr__(self) -> str: + return repr(self.value) + + +DEFAULT_NONE: DefaultValue = DefaultValue(None) +""":class:`DefaultValue`: Default :obj:`None`""" + +DEFAULT_FALSE: DefaultValue = DefaultValue(False) +""":class:`DefaultValue`: Default :obj:`False`""" + +DEFAULT_20: DefaultValue = DefaultValue(20) +""":class:`DefaultValue`: Default :obj:`20`""" diff --git a/telegramer/include/telegram/utils/promise.py b/telegramer/include/telegram/utils/promise.py index 4e3ebf9..01bef33 100644 --- a/telegramer/include/telegram/utils/promise.py +++ b/telegramer/include/telegram/utils/promise.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,77 +16,23 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -"""This module contains the Promise class.""" - -import logging -from threading import Event - - -logger = logging.getLogger(__name__) -logger.addHandler(logging.NullHandler()) - - -class Promise(object): - """A simple Promise implementation for use with the run_async decorator, DelayQueue etc. - - Args: - pooled_function (:obj:`callable`): The callable that will be called concurrently. - args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`. - kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`. - - Attributes: - pooled_function (:obj:`callable`): The callable that will be called concurrently. - args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`. - kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`. - done (:obj:`threading.Event`): Is set when the result is available. - - """ - - def __init__(self, pooled_function, args, kwargs): - self.pooled_function = pooled_function - self.args = args - self.kwargs = kwargs - self.done = Event() - self._result = None - self._exception = None - - def run(self): - """Calls the :attr:`pooled_function` callable.""" - - try: - self._result = self.pooled_function(*self.args, **self.kwargs) - - except Exception as exc: - logger.exception('An uncaught error was raised while running the promise') - self._exception = exc - - finally: - self.done.set() - - def __call__(self): - self.run() - - def result(self, timeout=None): - """Return the result of the ``Promise``. - - Args: - timeout (:obj:`float`, optional): Maximum time in seconds to wait for the result to be - calculated. ``None`` means indefinite. Default is ``None``. - - Returns: - Returns the return value of :attr:`pooled_function` or ``None`` if the ``timeout`` - expires. - - Raises: - Any exception raised by :attr:`pooled_function`. - """ - self.done.wait(timeout=timeout) - if self._exception is not None: - raise self._exception # pylint: disable=raising-bad-type - return self._result - - @property - def exception(self): - """The exception raised by :attr:`pooled_function` or ``None`` if no exception has been - raised (yet).""" - return self._exception +"""This module contains the :class:`telegram.ext.utils.promise.Promise` class for backwards +compatibility. +""" +import warnings + +import telegram.ext.utils.promise as promise +from telegram.utils.deprecate import TelegramDeprecationWarning + +warnings.warn( + 'telegram.utils.promise is deprecated. Please use telegram.ext.utils.promise instead.', + TelegramDeprecationWarning, +) + +Promise = promise.Promise +""" +:class:`telegram.ext.utils.promise.Promise` + +.. deprecated:: v13.2 + Use :class:`telegram.ext.utils.promise.Promise` instead. +""" diff --git a/telegramer/include/telegram/utils/request.py b/telegramer/include/telegram/utils/request.py index e97cfbe..4b8de7b 100644 --- a/telegramer/include/telegram/utils/request.py +++ b/telegramer/include/telegram/utils/request.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -22,117 +22,136 @@ import socket import sys import warnings -from builtins import str # For PY2 - -import time -import traceback -from deluge.log import LOG as log try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] -try: - import certifi -except Exception as e: - log.error(str(e) + '\n' + traceback.format_exc()) +from typing import Any, Union + +import certifi try: import telegram.vendor.ptb_urllib3.urllib3 as urllib3 import telegram.vendor.ptb_urllib3.urllib3.contrib.appengine as appengine from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection + from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout except ImportError: # pragma: no cover - warnings.warn("python-telegram-bot wasn't properly installed. Please refer to README.rst on " - "how to properly install.") - raise + try: + import urllib3 # type: ignore[no-redef] + import urllib3.contrib.appengine as appengine # type: ignore[no-redef] + from urllib3.connection import HTTPConnection # type: ignore[no-redef] + from urllib3.fields import RequestField # type: ignore[no-redef] + from urllib3.util.timeout import Timeout # type: ignore[no-redef] + + warnings.warn( + 'python-telegram-bot is using upstream urllib3. This is allowed but not ' + 'supported by python-telegram-bot maintainers.' + ) + except ImportError: + warnings.warn( + "python-telegram-bot wasn't properly installed. Please refer to README.rst on " + "how to properly install." + ) + raise + +# pylint: disable=C0412 +from telegram import InputFile, TelegramError +from telegram.error import ( + BadRequest, + ChatMigrated, + Conflict, + InvalidToken, + NetworkError, + RetryAfter, + TimedOut, + Unauthorized, +) +from telegram.utils.types import JSONDict +from telegram.utils.deprecate import set_new_attribute_deprecated + + +def _render_part(self: RequestField, name: str, value: str) -> str: # pylint: disable=W0613 + r""" + Monkey patch urllib3.urllib3.fields.RequestField to make it *not* support RFC2231 compliant + Content-Disposition headers since telegram servers don't understand it. Instead just escape + \\ and " and replace any \n and \r with a space. + """ + value = value.replace('\\', '\\\\').replace('"', '\\"') + value = value.replace('\r', ' ').replace('\n', ' ') + return f'{name}="{value}"' -from telegram import (InputFile, TelegramError, InputMedia) -from telegram.error import (Unauthorized, NetworkError, TimedOut, BadRequest, ChatMigrated, - RetryAfter, InvalidToken) -logging.getLogger('urllib3').setLevel(logging.WARNING) +RequestField._render_part = _render_part # type: ignore # pylint: disable=W0212 -USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + \ - '(KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36' +logging.getLogger('telegram.vendor.ptb_urllib3.urllib3').setLevel(logging.WARNING) +USER_AGENT = 'Python Telegram Bot (https://github.com/python-telegram-bot/python-telegram-bot)' -class Request(object): + +class Request: """ Helper class for python-telegram-bot which provides methods to perform POST & GET towards - telegram servers. + Telegram servers. Args: - con_pool_size (int): Number of connections to keep in the connection pool. - proxy_url (str): The URL to the proxy server. For example: `http://127.0.0.1:3128`. - urllib3_proxy_kwargs (dict): Arbitrary arguments passed as-is to `urllib3.ProxyManager`. - This value will be ignored if proxy_url is not set. - connect_timeout (int|float): The maximum amount of time (in seconds) to wait for a - connection attempt to a server to succeed. None will set an infinite timeout for - connection attempts. (default: 5.) - read_timeout (int|float): The maximum amount of time (in seconds) to wait between - consecutive read operations for a response from the server. None will set an infinite - timeout. This value is usually overridden by the various ``telegram.Bot`` methods. - (default: 5.) + con_pool_size (:obj:`int`): Number of connections to keep in the connection pool. + proxy_url (:obj:`str`): The URL to the proxy server. For example: `http://127.0.0.1:3128`. + urllib3_proxy_kwargs (:obj:`dict`): Arbitrary arguments passed as-is to + :obj:`urllib3.ProxyManager`. This value will be ignored if :attr:`proxy_url` is not + set. + connect_timeout (:obj:`int` | :obj:`float`): The maximum amount of time (in seconds) to + wait for a connection attempt to a server to succeed. :obj:`None` will set an + infinite timeout for connection attempts. Defaults to ``5.0``. + read_timeout (:obj:`int` | :obj:`float`): The maximum amount of time (in seconds) to wait + between consecutive read operations for a response from the server. :obj:`None` will + set an infinite timeout. This value is usually overridden by the various + :class:`telegram.Bot` methods. Defaults to ``5.0``. """ - def __init__(self, - con_pool_size=1, - proxy_url=None, - urllib3_proxy_kwargs=None, - connect_timeout=5., - read_timeout=5.): + __slots__ = ('_connect_timeout', '_con_pool_size', '_con_pool', '__dict__') + + def __init__( + self, + con_pool_size: int = 1, + proxy_url: str = None, + urllib3_proxy_kwargs: JSONDict = None, + connect_timeout: float = 5.0, + read_timeout: float = 5.0, + ): if urllib3_proxy_kwargs is None: - urllib3_proxy_kwargs = dict() + urllib3_proxy_kwargs = {} self._connect_timeout = connect_timeout sockopts = HTTPConnection.default_socket_options + [ - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + ] # TODO: Support other platforms like mac and windows. if 'linux' in sys.platform: - sockopts.append((socket.IPPROTO_TCP, - socket.TCP_KEEPIDLE, 120)) # pylint: disable=no-member - sockopts.append((socket.IPPROTO_TCP, - socket.TCP_KEEPINTVL, 30)) # pylint: disable=no-member - sockopts.append((socket.IPPROTO_TCP, - socket.TCP_KEEPCNT, 8)) # pylint: disable=no-member + sockopts.append( + (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120) # pylint: disable=no-member + ) + sockopts.append( + (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) # pylint: disable=no-member + ) + sockopts.append( + (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8) # pylint: disable=no-member + ) self._con_pool_size = con_pool_size - # This was performed on Windows only, but it shouldn't be a problem - # managing cacert.pem on Linux as well - # if os.name == 'nt': - try: - import urllib2 - import tempfile - capath = os.path.join(tempfile.gettempdir(), 'tg-cacert.pem') - # Check if tg-cacert.pem exists and if it's older than 7 days - if not os.path.exists(capath) or (os.path.exists(capath) - and (time.time() - os.path.getctime(capath)) // (24 * 3600) >= 7): - CACERT_URL = "https://curl.haxx.se/ca/cacert.pem" - request = urllib2.Request(CACERT_URL) - file_contents = urllib2.urlopen(request).read() - log.debug("## Telegramer downloaded "+os.path.realpath(capath)) - cafile = open(os.path.realpath(capath), 'wb') - cafile.write(file_contents) - cafile.close() - except Exception as e: - try: - capath = certifi.where() - except Exception as e: - capath = os.path.join(tempfile.gettempdir(), 'tg-cacert.pem') - kwargs = dict( maxsize=con_pool_size, cert_reqs='CERT_REQUIRED', - ca_certs=capath, # certifi.where(), + ca_certs=certifi.where(), socket_options=sockopts, - timeout=urllib3.Timeout( - connect=self._connect_timeout, read=read_timeout, total=None)) + timeout=urllib3.Timeout(connect=self._connect_timeout, read=read_timeout, total=None), + ) # Set a proxy according to the following order: # * proxy defined in proxy_url (+ urllib3_proxy_kwargs) @@ -143,20 +162,27 @@ def __init__(self, if not proxy_url: proxy_url = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy') + self._con_pool: Union[ + urllib3.PoolManager, + appengine.AppEngineManager, + 'SOCKSProxyManager', # noqa: F821 + urllib3.ProxyManager, + ] = None # type: ignore if not proxy_url: if appengine.is_appengine_sandbox(): # Use URLFetch service if running in App Engine - mgr = appengine.AppEngineManager() + self._con_pool = appengine.AppEngineManager() else: - mgr = urllib3.PoolManager(**kwargs) + self._con_pool = urllib3.PoolManager(**kwargs) else: kwargs.update(urllib3_proxy_kwargs) if proxy_url.startswith('socks'): try: + # pylint: disable=C0415 from telegram.vendor.ptb_urllib3.urllib3.contrib.socks import SOCKSProxyManager - except ImportError: - raise RuntimeError('PySocks is missing') - mgr = SOCKSProxyManager(proxy_url, **kwargs) + except ImportError as exc: + raise RuntimeError('PySocks is missing') from exc + self._con_pool = SOCKSProxyManager(proxy_url, **kwargs) else: mgr = urllib3.proxy_from_url(proxy_url, **kwargs) if mgr.proxy.auth: @@ -164,34 +190,33 @@ def __init__(self, auth_hdrs = urllib3.make_headers(proxy_basic_auth=mgr.proxy.auth) mgr.proxy_headers.update(auth_hdrs) - self._con_pool = mgr + self._con_pool = mgr + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) @property - def con_pool_size(self): + def con_pool_size(self) -> int: """The size of the connection pool used.""" return self._con_pool_size - def stop(self): - self._con_pool.clear() + def stop(self) -> None: + """Performs cleanup on shutdown.""" + self._con_pool.clear() # type: ignore @staticmethod - def _parse(json_data): + def _parse(json_data: bytes) -> Union[JSONDict, bool]: """Try and parse the JSON returned from Telegram. Returns: dict: A JSON parsed as Python dict with results - on error this dict will be empty. """ - + decoded_s = json_data.decode('utf-8', 'replace') try: - decoded_s = json_data.decode('utf-8') data = json.loads(decoded_s) - except UnicodeDecodeError: - logging.getLogger(__name__).debug( - 'Logging raw invalid UTF-8 response:\n%r', json_data) - raise TelegramError('Server response could not be decoded using UTF-8') - except ValueError: - raise TelegramError('Invalid server response') + except ValueError as exc: + raise TelegramError('Invalid server response') from exc if not data.get('ok'): # pragma: no cover description = data.get('description') @@ -208,15 +233,15 @@ def _parse(json_data): return data['result'] - def _request_wrapper(self, *args, **kwargs): + def _request_wrapper(self, *args: object, **kwargs: Any) -> bytes: """Wraps urllib3 request for handling known exceptions. Args: args: unnamed arguments, passed to urllib3 request. - kwargs: keyword arguments, passed tp urllib3 request. + kwargs: keyword arguments, passed to urllib3 request. Returns: - str: A non-parsed JSON text. + bytes: A non-parsed JSON text. Raises: TelegramError @@ -232,45 +257,48 @@ def _request_wrapper(self, *args, **kwargs): try: resp = self._con_pool.request(*args, **kwargs) - except urllib3.exceptions.TimeoutError: - raise TimedOut() + except urllib3.exceptions.TimeoutError as error: + raise TimedOut() from error except urllib3.exceptions.HTTPError as error: # HTTPError must come last as its the base urllib3 exception class # TODO: do something smart here; for now just raise NetworkError - raise NetworkError('urllib3 HTTPError {0}'.format(error)) + raise NetworkError(f'urllib3 HTTPError {error}') from error if 200 <= resp.status <= 299: # 200-299 range are HTTP success statuses return resp.data try: - message = self._parse(resp.data) + message = str(self._parse(resp.data)) except ValueError: message = 'Unknown HTTPError' if resp.status in (401, 403): raise Unauthorized(message) - elif resp.status == 400: + if resp.status == 400: raise BadRequest(message) - elif resp.status == 404: + if resp.status == 404: raise InvalidToken() - elif resp.status == 413: - raise NetworkError('File too large. Check telegram api limits ' - 'https://core.telegram.org/bots/api#senddocument') - - elif resp.status == 502: + if resp.status == 409: + raise Conflict(message) + if resp.status == 413: + raise NetworkError( + 'File too large. Check telegram api limits ' + 'https://core.telegram.org/bots/api#senddocument' + ) + if resp.status == 502: raise NetworkError('Bad Gateway') - else: - raise NetworkError('{0} ({1})'.format(message, resp.status)) + raise NetworkError(f'{message} ({resp.status})') - def get(self, url, timeout=None): + def post(self, url: str, data: JSONDict, timeout: float = None) -> Union[JSONDict, bool]: """Request an URL. Args: url (:obj:`str`): The web location we want to retrieve. - timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read - timeout from the server (instead of the one specified during creation of the - connection pool). + data (Dict[:obj:`str`, :obj:`str` | :obj:`int`], optional): A dict of key/value pairs. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). Returns: A JSON object. @@ -281,31 +309,13 @@ def get(self, url, timeout=None): if timeout is not None: urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout) - result = self._request_wrapper('GET', url, **urlopen_kwargs) - return self._parse(result) - - def post(self, url, data, timeout=None): - """Request an URL. - - Args: - url (:obj:`str`): The web location we want to retrieve. - data (dict[str, str|int]): A dict of key/value pairs. Note: On py2.7 value is unicode. - timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read - timeout from the server (instead of the one specified during creation of the - connection pool). - - Returns: - A JSON object. - - """ - urlopen_kwargs = {} - - if timeout is not None: - urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout) + if data is None: + data = {} # Are we uploading files? files = False + # pylint: disable=R1702 for key, val in data.copy().items(): if isinstance(val, InputFile): # Convert the InputFile to urllib3 field format @@ -315,33 +325,50 @@ def post(self, url, data, timeout=None): # Urllib3 doesn't like floats it seems data[key] = str(val) elif key == 'media': - # One media or multiple - if isinstance(val, InputMedia): - # Attach and set val to attached name - data[key] = val.to_json() - if isinstance(val.media, InputFile): - data[val.media.attach] = val.media.field_tuple - else: + files = True + # List of media + if isinstance(val, list): # Attach and set val to attached name for all media = [] - for m in val: - media.append(m.to_dict()) - if isinstance(m.media, InputFile): - data[m.media.attach] = m.media.field_tuple + for med in val: + media_dict = med.to_dict() + media.append(media_dict) + if isinstance(med.media, InputFile): + data[med.media.attach] = med.media.field_tuple + # if the file has a thumb, we also need to attach it to the data + if "thumb" in media_dict: + data[med.thumb.attach] = med.thumb.field_tuple data[key] = json.dumps(media) - files = True + # Single media + else: + # Attach and set val to attached name + media_dict = val.to_dict() + if isinstance(val.media, InputFile): + data[val.media.attach] = val.media.field_tuple + # if the file has a thumb, we also need to attach it to the data + if "thumb" in media_dict: + data[val.thumb.attach] = val.thumb.field_tuple + data[key] = json.dumps(media_dict) + elif isinstance(val, list): + # In case we're sending files, we need to json-dump lists manually + # As we can't know if that's the case, we just json-dump here + data[key] = json.dumps(val) # Use multipart upload if we're uploading files, otherwise use JSON if files: result = self._request_wrapper('POST', url, fields=data, **urlopen_kwargs) else: - result = self._request_wrapper('POST', url, - body=json.dumps(data).encode('utf-8'), - headers={'Content-Type': 'application/json'}) + result = self._request_wrapper( + 'POST', + url, + body=json.dumps(data).encode('utf-8'), + headers={'Content-Type': 'application/json'}, + **urlopen_kwargs, + ) return self._parse(result) - def retrieve(self, url, timeout=None): + def retrieve(self, url: str, timeout: float = None) -> bytes: """Retrieve the contents of a file by its URL. Args: @@ -357,17 +384,15 @@ def retrieve(self, url, timeout=None): return self._request_wrapper('GET', url, **urlopen_kwargs) - def download(self, url, filename, timeout=None): + def download(self, url: str, filename: str, timeout: float = None) -> None: """Download a file by its URL. Args: - url (str): The web location we want to retrieve. - timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read - timeout from the server (instead of the one specified during creation of the - connection pool). - - filename: - The filename within the path to download the file. + url (:obj:`str`): The web location we want to retrieve. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + filename (:obj:`str`): The filename within the path to download the file. """ buf = self.retrieve(url, timeout=timeout) diff --git a/telegramer/include/telegram/utils/types.py b/telegramer/include/telegram/utils/types.py new file mode 100644 index 0000000..a3e1f42 --- /dev/null +++ b/telegramer/include/telegram/utils/types.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains custom typing aliases.""" +from pathlib import Path +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Tuple, + TypeVar, + Union, +) + +if TYPE_CHECKING: + from telegram import InputFile # noqa: F401 + from telegram.utils.helpers import DefaultValue # noqa: F401 + +FileLike = Union[IO, 'InputFile'] +"""Either an open file handler or a :class:`telegram.InputFile`.""" + +FileInput = Union[str, bytes, FileLike, Path] +"""Valid input for passing files to Telegram. Either a file id as string, a file like object, +a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`.""" + +JSONDict = Dict[str, Any] +"""Dictionary containing response from Telegram or data to send to the API.""" + +DVType = TypeVar('DVType') +ODVInput = Optional[Union['DefaultValue[DVType]', DVType]] +"""Generic type for bot method parameters which can have defaults. ``ODVInput[type]`` is the same +as ``Optional[Union[DefaultValue, type]]``.""" +DVInput = Union['DefaultValue[DVType]', DVType] +"""Generic type for bot method parameters which can have defaults. ``DVInput[type]`` is the same +as ``Union[DefaultValue, type]``.""" + +RT = TypeVar("RT") +SLT = Union[RT, List[RT], Tuple[RT, ...]] +"""Single instance or list/tuple of instances.""" diff --git a/telegramer/include/telegram/utils/webhookhandler.py b/telegramer/include/telegram/utils/webhookhandler.py index 8adc897..88e3c5a 100644 --- a/telegramer/include/telegram/utils/webhookhandler.py +++ b/telegramer/include/telegram/utils/webhookhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,137 +16,20 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -import logging - -from telegram import Update -# REMREM from future.utils import bytes_to_native_str -try: - from future.utils import bytes_to_native_str -except Exception as e: - pass -from threading import Lock -try: - import ujson as json -except ImportError: - import json -try: - import BaseHTTPServer -except ImportError: - import http.server as BaseHTTPServer - -logging.getLogger(__name__).addHandler(logging.NullHandler()) - - -class _InvalidPost(Exception): - - def __init__(self, http_code): - self.http_code = http_code - super(_InvalidPost, self).__init__() - - -class WebhookServer(BaseHTTPServer.HTTPServer, object): - - def __init__(self, server_address, RequestHandlerClass, update_queue, webhook_path, bot): - super(WebhookServer, self).__init__(server_address, RequestHandlerClass) - self.logger = logging.getLogger(__name__) - self.update_queue = update_queue - self.webhook_path = webhook_path - self.bot = bot - self.is_running = False - self.server_lock = Lock() - self.shutdown_lock = Lock() - - def serve_forever(self, poll_interval=0.5): - with self.server_lock: - self.is_running = True - self.logger.debug('Webhook Server started.') - super(WebhookServer, self).serve_forever(poll_interval) - self.logger.debug('Webhook Server stopped.') - - def shutdown(self): - with self.shutdown_lock: - if not self.is_running: - self.logger.warning('Webhook Server already stopped.') - return - else: - super(WebhookServer, self).shutdown() - self.is_running = False - - def handle_error(self, request, client_address): - """Handle an error gracefully.""" - self.logger.debug('Exception happened during processing of request from %s', - client_address, exc_info=True) - - -# WebhookHandler, process webhook calls -# Based on: https://github.com/eternnoir/pyTelegramBotAPI/blob/master/ -# examples/webhook_examples/webhook_cpython_echo_bot.py -class WebhookHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): - server_version = 'WebhookHandler/1.0' - - def __init__(self, request, client_address, server): - self.logger = logging.getLogger(__name__) - super(WebhookHandler, self).__init__(request, client_address, server) - - def do_HEAD(self): - self.send_response(200) - self.end_headers() - - def do_GET(self): - self.send_response(200) - self.end_headers() - - def do_POST(self): - self.logger.debug('Webhook triggered') - try: - self._validate_post() - clen = self._get_content_len() - except _InvalidPost as e: - self.send_error(e.http_code) - self.end_headers() - else: - buf = self.rfile.read(clen) - json_string = bytes_to_native_str(buf) - - self.send_response(200) - self.end_headers() - - self.logger.debug('Webhook received data: ' + json_string) - - update = Update.de_json(json.loads(json_string), self.server.bot) - - self.logger.debug('Received Update with ID %d on Webhook' % update.update_id) - self.server.update_queue.put(update) - - def _validate_post(self): - if not (self.path == self.server.webhook_path and 'content-type' in self.headers and - self.headers['content-type'] == 'application/json'): - raise _InvalidPost(403) - - def _get_content_len(self): - clen = self.headers.get('content-length') - if clen is None: - raise _InvalidPost(411) - try: - clen = int(clen) - except ValueError: - raise _InvalidPost(403) - if clen < 0: - raise _InvalidPost(403) - return clen - - def log_message(self, format, *args): - """Log an arbitrary message. - - This is used by all other logging functions. - - It overrides ``BaseHTTPRequestHandler.log_message``, which logs to ``sys.stderr``. - - The first argument, FORMAT, is a format string for the message to be logged. If the format - string contains any % escapes requiring parameters, they should be specified as subsequent - arguments (it's just like printf!). - - The client ip is prefixed to every message. - - """ - self.logger.debug("%s - - %s" % (self.address_string(), format % args)) +"""This module contains the :class:`telegram.ext.utils.webhookhandler.WebhookHandler` class for +backwards compatibility. +""" +import warnings + +import telegram.ext.utils.webhookhandler as webhook_handler +from telegram.utils.deprecate import TelegramDeprecationWarning + +warnings.warn( + 'telegram.utils.webhookhandler is deprecated. Please use telegram.ext.utils.webhookhandler ' + 'instead.', + TelegramDeprecationWarning, +) + +WebhookHandler = webhook_handler.WebhookHandler +WebhookServer = webhook_handler.WebhookServer +WebhookAppClass = webhook_handler.WebhookAppClass diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py index 7dc5a70..0cd5e34 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py @@ -1,96 +1,96 @@ -""" -urllib3 - Thread-safe connection pooling and re-using. -""" -from __future__ import absolute_import -import warnings - -from .connectionpool import ( - HTTPConnectionPool, - HTTPSConnectionPool, - connection_from_url -) - -from . import exceptions -from .filepost import encode_multipart_formdata -from .poolmanager import PoolManager, ProxyManager, proxy_from_url -from .response import HTTPResponse -from .util.request import make_headers -from .util.url import get_host -from .util.timeout import Timeout -from .util.retry import Retry - - -# Set default logging handler to avoid "No handler found" warnings. -import logging -try: # Python 2.7+ - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - -__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)' -__license__ = 'MIT' -__version__ = 'dev' - -__all__ = ( - 'HTTPConnectionPool', - 'HTTPSConnectionPool', - 'PoolManager', - 'ProxyManager', - 'HTTPResponse', - 'Retry', - 'Timeout', - 'add_stderr_logger', - 'connection_from_url', - 'disable_warnings', - 'encode_multipart_formdata', - 'get_host', - 'make_headers', - 'proxy_from_url', -) - -logging.getLogger(__name__).addHandler(NullHandler()) - - -def add_stderr_logger(level=logging.DEBUG): - """ - Helper for quickly adding a StreamHandler to the logger. Useful for - debugging. - - Returns the handler after adding it. - """ - # This method needs to be in this __init__.py to get the __name__ correct - # even if urllib3 is vendored within another package. - logger = logging.getLogger(__name__) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) - logger.addHandler(handler) - logger.setLevel(level) - logger.debug('Added a stderr logging handler to logger: %s', __name__) - return handler - - -# ... Clean up. -del NullHandler - - -# All warning filters *must* be appended unless you're really certain that they -# shouldn't be: otherwise, it's very hard for users to use most Python -# mechanisms to silence them. -# SecurityWarning's always go off by default. -warnings.simplefilter('always', exceptions.SecurityWarning, append=True) -# SubjectAltNameWarning's should go off once per host -warnings.simplefilter('default', exceptions.SubjectAltNameWarning, append=True) -# InsecurePlatformWarning's don't vary between requests, so we keep it default. -warnings.simplefilter('default', exceptions.InsecurePlatformWarning, - append=True) -# SNIMissingWarnings should go off only once. -warnings.simplefilter('default', exceptions.SNIMissingWarning, append=True) - - -def disable_warnings(category=exceptions.HTTPWarning): - """ - Helper for quickly disabling all urllib3 warnings. - """ - warnings.simplefilter('ignore', category) +""" +urllib3 - Thread-safe connection pooling and re-using. +""" +from __future__ import absolute_import +import warnings + +from .connectionpool import ( + HTTPConnectionPool, + HTTPSConnectionPool, + connection_from_url +) + +from . import exceptions +from .filepost import encode_multipart_formdata +from .poolmanager import PoolManager, ProxyManager, proxy_from_url +from .response import HTTPResponse +from .util.request import make_headers +from .util.url import get_host +from .util.timeout import Timeout +from .util.retry import Retry + + +# Set default logging handler to avoid "No handler found" warnings. +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)' +__license__ = 'MIT' +__version__ = 'dev' + +__all__ = ( + 'HTTPConnectionPool', + 'HTTPSConnectionPool', + 'PoolManager', + 'ProxyManager', + 'HTTPResponse', + 'Retry', + 'Timeout', + 'add_stderr_logger', + 'connection_from_url', + 'disable_warnings', + 'encode_multipart_formdata', + 'get_host', + 'make_headers', + 'proxy_from_url', +) + +logging.getLogger(__name__).addHandler(NullHandler()) + + +def add_stderr_logger(level=logging.DEBUG): + """ + Helper for quickly adding a StreamHandler to the logger. Useful for + debugging. + + Returns the handler after adding it. + """ + # This method needs to be in this __init__.py to get the __name__ correct + # even if urllib3 is vendored within another package. + logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) + logger.addHandler(handler) + logger.setLevel(level) + logger.debug('Added a stderr logging handler to logger: %s', __name__) + return handler + + +# ... Clean up. +del NullHandler + + +# All warning filters *must* be appended unless you're really certain that they +# shouldn't be: otherwise, it's very hard for users to use most Python +# mechanisms to silence them. +# SecurityWarning's always go off by default. +warnings.simplefilter('always', exceptions.SecurityWarning, append=True) +# SubjectAltNameWarning's should go off once per host +warnings.simplefilter('default', exceptions.SubjectAltNameWarning, append=True) +# InsecurePlatformWarning's don't vary between requests, so we keep it default. +warnings.simplefilter('default', exceptions.InsecurePlatformWarning, + append=True) +# SNIMissingWarnings should go off only once. +warnings.simplefilter('default', exceptions.SNIMissingWarning, append=True) + + +def disable_warnings(category=exceptions.HTTPWarning): + """ + Helper for quickly disabling all urllib3 warnings. + """ + warnings.simplefilter('ignore', category) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py index 23db3a3..4fcd2ab 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py @@ -1,324 +1,327 @@ -from __future__ import absolute_import -from collections import Mapping, MutableMapping -try: - from threading import RLock -except ImportError: # Platform-specific: No threads available - class RLock: - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_value, traceback): - pass - - -try: # Python 2.7+ - from collections import OrderedDict -except ImportError: - from .packages.ordered_dict import OrderedDict -from .packages.six import iterkeys, itervalues, PY3 - - -__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] - - -_Null = object() - - -class RecentlyUsedContainer(MutableMapping): - """ - Provides a thread-safe dict-like container which maintains up to - ``maxsize`` keys while throwing away the least-recently-used keys beyond - ``maxsize``. - - :param maxsize: - Maximum number of recent elements to retain. - - :param dispose_func: - Every time an item is evicted from the container, - ``dispose_func(value)`` is called. Callback which will get called - """ - - ContainerCls = OrderedDict - - def __init__(self, maxsize=10, dispose_func=None): - self._maxsize = maxsize - self.dispose_func = dispose_func - - self._container = self.ContainerCls() - self.lock = RLock() - - def __getitem__(self, key): - # Re-insert the item, moving it to the end of the eviction line. - with self.lock: - item = self._container.pop(key) - self._container[key] = item - return item - - def __setitem__(self, key, value): - evicted_value = _Null - with self.lock: - # Possibly evict the existing value of 'key' - evicted_value = self._container.get(key, _Null) - self._container[key] = value - - # If we didn't evict an existing value, we might have to evict the - # least recently used item from the beginning of the container. - if len(self._container) > self._maxsize: - _key, evicted_value = self._container.popitem(last=False) - - if self.dispose_func and evicted_value is not _Null: - self.dispose_func(evicted_value) - - def __delitem__(self, key): - with self.lock: - value = self._container.pop(key) - - if self.dispose_func: - self.dispose_func(value) - - def __len__(self): - with self.lock: - return len(self._container) - - def __iter__(self): - raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.') - - def clear(self): - with self.lock: - # Copy pointers to all values, then wipe the mapping - values = list(itervalues(self._container)) - self._container.clear() - - if self.dispose_func: - for value in values: - self.dispose_func(value) - - def keys(self): - with self.lock: - return list(iterkeys(self._container)) - - -class HTTPHeaderDict(MutableMapping): - """ - :param headers: - An iterable of field-value pairs. Must not contain multiple field names - when compared case-insensitively. - - :param kwargs: - Additional field-value pairs to pass in to ``dict.update``. - - A ``dict`` like container for storing HTTP Headers. - - Field names are stored and compared case-insensitively in compliance with - RFC 7230. Iteration provides the first case-sensitive key seen for each - case-insensitive pair. - - Using ``__setitem__`` syntax overwrites fields that compare equal - case-insensitively in order to maintain ``dict``'s api. For fields that - compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` - in a loop. - - If multiple fields that are equal case-insensitively are passed to the - constructor or ``.update``, the behavior is undefined and some will be - lost. - - >>> headers = HTTPHeaderDict() - >>> headers.add('Set-Cookie', 'foo=bar') - >>> headers.add('set-cookie', 'baz=quxx') - >>> headers['content-length'] = '7' - >>> headers['SET-cookie'] - 'foo=bar, baz=quxx' - >>> headers['Content-Length'] - '7' - """ - - def __init__(self, headers=None, **kwargs): - super(HTTPHeaderDict, self).__init__() - self._container = OrderedDict() - if headers is not None: - if isinstance(headers, HTTPHeaderDict): - self._copy_from(headers) - else: - self.extend(headers) - if kwargs: - self.extend(kwargs) - - def __setitem__(self, key, val): - self._container[key.lower()] = (key, val) - return self._container[key.lower()] - - def __getitem__(self, key): - val = self._container[key.lower()] - return ', '.join(val[1:]) - - def __delitem__(self, key): - del self._container[key.lower()] - - def __contains__(self, key): - return key.lower() in self._container - - def __eq__(self, other): - if not isinstance(other, Mapping) and not hasattr(other, 'keys'): - return False - if not isinstance(other, type(self)): - other = type(self)(other) - return (dict((k.lower(), v) for k, v in self.itermerged()) == - dict((k.lower(), v) for k, v in other.itermerged())) - - def __ne__(self, other): - return not self.__eq__(other) - - if not PY3: # Python 2 - iterkeys = MutableMapping.iterkeys - itervalues = MutableMapping.itervalues - - __marker = object() - - def __len__(self): - return len(self._container) - - def __iter__(self): - # Only provide the originally cased names - for vals in self._container.values(): - yield vals[0] - - def pop(self, key, default=__marker): - '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value. - If key is not found, d is returned if given, otherwise KeyError is raised. - ''' - # Using the MutableMapping function directly fails due to the private marker. - # Using ordinary dict.pop would expose the internal structures. - # So let's reinvent the wheel. - try: - value = self[key] - except KeyError: - if default is self.__marker: - raise - return default - else: - del self[key] - return value - - def discard(self, key): - try: - del self[key] - except KeyError: - pass - - def add(self, key, val): - """Adds a (name, value) pair, doesn't overwrite the value if it already - exists. - - >>> headers = HTTPHeaderDict(foo='bar') - >>> headers.add('Foo', 'baz') - >>> headers['foo'] - 'bar, baz' - """ - key_lower = key.lower() - new_vals = key, val - # Keep the common case aka no item present as fast as possible - vals = self._container.setdefault(key_lower, new_vals) - if new_vals is not vals: - # new_vals was not inserted, as there was a previous one - if isinstance(vals, list): - # If already several items got inserted, we have a list - vals.append(val) - else: - # vals should be a tuple then, i.e. only one item so far - # Need to convert the tuple to list for further extension - self._container[key_lower] = [vals[0], vals[1], val] - - def extend(self, *args, **kwargs): - """Generic import function for any type of header-like object. - Adapted version of MutableMapping.update in order to insert items - with self.add instead of self.__setitem__ - """ - if len(args) > 1: - raise TypeError("extend() takes at most 1 positional " - "arguments ({0} given)".format(len(args))) - other = args[0] if len(args) >= 1 else () - - if isinstance(other, HTTPHeaderDict): - for key, val in other.iteritems(): - self.add(key, val) - elif isinstance(other, Mapping): - for key in other: - self.add(key, other[key]) - elif hasattr(other, "keys"): - for key in other.keys(): - self.add(key, other[key]) - else: - for key, value in other: - self.add(key, value) - - for key, value in kwargs.items(): - self.add(key, value) - - def getlist(self, key): - """Returns a list of all the values for the named field. Returns an - empty list if the key doesn't exist.""" - try: - vals = self._container[key.lower()] - except KeyError: - return [] - else: - if isinstance(vals, tuple): - return [vals[1]] - else: - return vals[1:] - - # Backwards compatibility for httplib - getheaders = getlist - getallmatchingheaders = getlist - iget = getlist - - def __repr__(self): - return "%s(%s)" % (type(self).__name__, dict(self.itermerged())) - - def _copy_from(self, other): - for key in other: - val = other.getlist(key) - if isinstance(val, list): - # Don't need to convert tuples - val = list(val) - self._container[key.lower()] = [key] + val - - def copy(self): - clone = type(self)() - clone._copy_from(self) - return clone - - def iteritems(self): - """Iterate over all header lines, including duplicate ones.""" - for key in self: - vals = self._container[key.lower()] - for val in vals[1:]: - yield vals[0], val - - def itermerged(self): - """Iterate over all headers, merging duplicate ones together.""" - for key in self: - val = self._container[key.lower()] - yield val[0], ', '.join(val[1:]) - - def items(self): - return list(self.iteritems()) - - @classmethod - def from_httplib(cls, message): # Python 2 - """Read headers from a Python 2 httplib message object.""" - # python2.7 does not expose a proper API for exporting multiheaders - # efficiently. This function re-reads raw lines from the message - # object and extracts the multiheaders properly. - headers = [] - - for line in message.headers: - if line.startswith((' ', '\t')): - key, value = headers[-1] - headers[-1] = (key, value + '\r\n' + line.rstrip()) - continue - - key, value = line.split(':', 1) - headers.append((key, value.strip())) - - return cls(headers) +from __future__ import absolute_import +try: + from collections.abc import Mapping, MutableMapping +except ImportError: + from collections import Mapping, MutableMapping +try: + from threading import RLock +except ImportError: # Platform-specific: No threads available + class RLock: + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +try: # Python 2.7+ + from collections import OrderedDict +except ImportError: + from .packages.ordered_dict import OrderedDict +from .packages.six import iterkeys, itervalues, PY3 + + +__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] + + +_Null = object() + + +class RecentlyUsedContainer(MutableMapping): + """ + Provides a thread-safe dict-like container which maintains up to + ``maxsize`` keys while throwing away the least-recently-used keys beyond + ``maxsize``. + + :param maxsize: + Maximum number of recent elements to retain. + + :param dispose_func: + Every time an item is evicted from the container, + ``dispose_func(value)`` is called. Callback which will get called + """ + + ContainerCls = OrderedDict + + def __init__(self, maxsize=10, dispose_func=None): + self._maxsize = maxsize + self.dispose_func = dispose_func + + self._container = self.ContainerCls() + self.lock = RLock() + + def __getitem__(self, key): + # Re-insert the item, moving it to the end of the eviction line. + with self.lock: + item = self._container.pop(key) + self._container[key] = item + return item + + def __setitem__(self, key, value): + evicted_value = _Null + with self.lock: + # Possibly evict the existing value of 'key' + evicted_value = self._container.get(key, _Null) + self._container[key] = value + + # If we didn't evict an existing value, we might have to evict the + # least recently used item from the beginning of the container. + if len(self._container) > self._maxsize: + _key, evicted_value = self._container.popitem(last=False) + + if self.dispose_func and evicted_value is not _Null: + self.dispose_func(evicted_value) + + def __delitem__(self, key): + with self.lock: + value = self._container.pop(key) + + if self.dispose_func: + self.dispose_func(value) + + def __len__(self): + with self.lock: + return len(self._container) + + def __iter__(self): + raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.') + + def clear(self): + with self.lock: + # Copy pointers to all values, then wipe the mapping + values = list(itervalues(self._container)) + self._container.clear() + + if self.dispose_func: + for value in values: + self.dispose_func(value) + + def keys(self): + with self.lock: + return list(iterkeys(self._container)) + + +class HTTPHeaderDict(MutableMapping): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 7230. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + """ + + def __init__(self, headers=None, **kwargs): + super(HTTPHeaderDict, self).__init__() + self._container = OrderedDict() + if headers is not None: + if isinstance(headers, HTTPHeaderDict): + self._copy_from(headers) + else: + self.extend(headers) + if kwargs: + self.extend(kwargs) + + def __setitem__(self, key, val): + self._container[key.lower()] = (key, val) + return self._container[key.lower()] + + def __getitem__(self, key): + val = self._container[key.lower()] + return ', '.join(val[1:]) + + def __delitem__(self, key): + del self._container[key.lower()] + + def __contains__(self, key): + return key.lower() in self._container + + def __eq__(self, other): + if not isinstance(other, Mapping) and not hasattr(other, 'keys'): + return False + if not isinstance(other, type(self)): + other = type(self)(other) + return (dict((k.lower(), v) for k, v in self.itermerged()) == + dict((k.lower(), v) for k, v in other.itermerged())) + + def __ne__(self, other): + return not self.__eq__(other) + + if not PY3: # Python 2 + iterkeys = MutableMapping.iterkeys + itervalues = MutableMapping.itervalues + + __marker = object() + + def __len__(self): + return len(self._container) + + def __iter__(self): + # Only provide the originally cased names + for vals in self._container.values(): + yield vals[0] + + def pop(self, key, default=__marker): + '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + ''' + # Using the MutableMapping function directly fails due to the private marker. + # Using ordinary dict.pop would expose the internal structures. + # So let's reinvent the wheel. + try: + value = self[key] + except KeyError: + if default is self.__marker: + raise + return default + else: + del self[key] + return value + + def discard(self, key): + try: + del self[key] + except KeyError: + pass + + def add(self, key, val): + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + """ + key_lower = key.lower() + new_vals = key, val + # Keep the common case aka no item present as fast as possible + vals = self._container.setdefault(key_lower, new_vals) + if new_vals is not vals: + # new_vals was not inserted, as there was a previous one + if isinstance(vals, list): + # If already several items got inserted, we have a list + vals.append(val) + else: + # vals should be a tuple then, i.e. only one item so far + # Need to convert the tuple to list for further extension + self._container[key_lower] = [vals[0], vals[1], val] + + def extend(self, *args, **kwargs): + """Generic import function for any type of header-like object. + Adapted version of MutableMapping.update in order to insert items + with self.add instead of self.__setitem__ + """ + if len(args) > 1: + raise TypeError("extend() takes at most 1 positional " + "arguments ({0} given)".format(len(args))) + other = args[0] if len(args) >= 1 else () + + if isinstance(other, HTTPHeaderDict): + for key, val in other.iteritems(): + self.add(key, val) + elif isinstance(other, Mapping): + for key in other: + self.add(key, other[key]) + elif hasattr(other, "keys"): + for key in other.keys(): + self.add(key, other[key]) + else: + for key, value in other: + self.add(key, value) + + for key, value in kwargs.items(): + self.add(key, value) + + def getlist(self, key): + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + try: + vals = self._container[key.lower()] + except KeyError: + return [] + else: + if isinstance(vals, tuple): + return [vals[1]] + else: + return vals[1:] + + # Backwards compatibility for httplib + getheaders = getlist + getallmatchingheaders = getlist + iget = getlist + + def __repr__(self): + return "%s(%s)" % (type(self).__name__, dict(self.itermerged())) + + def _copy_from(self, other): + for key in other: + val = other.getlist(key) + if isinstance(val, list): + # Don't need to convert tuples + val = list(val) + self._container[key.lower()] = [key] + val + + def copy(self): + clone = type(self)() + clone._copy_from(self) + return clone + + def iteritems(self): + """Iterate over all header lines, including duplicate ones.""" + for key in self: + vals = self._container[key.lower()] + for val in vals[1:]: + yield vals[0], val + + def itermerged(self): + """Iterate over all headers, merging duplicate ones together.""" + for key in self: + val = self._container[key.lower()] + yield val[0], ', '.join(val[1:]) + + def items(self): + return list(self.iteritems()) + + @classmethod + def from_httplib(cls, message): # Python 2 + """Read headers from a Python 2 httplib message object.""" + # python2.7 does not expose a proper API for exporting multiheaders + # efficiently. This function re-reads raw lines from the message + # object and extracts the multiheaders properly. + headers = [] + + for line in message.headers: + if line.startswith((' ', '\t')): + key, value = headers[-1] + headers[-1] = (key, value + '\r\n' + line.rstrip()) + continue + + key, value = line.split(':', 1) + headers.append((key, value.strip())) + + return cls(headers) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py index e05ae0b..9f06c39 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py @@ -1,369 +1,369 @@ -from __future__ import absolute_import -import datetime -import logging -import os -import sys -import socket -from socket import error as SocketError, timeout as SocketTimeout -import warnings -from .packages import six -from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection -from .packages.six.moves.http_client import HTTPException # noqa: F401 - -try: # Compiled with SSL? - import ssl - BaseSSLError = ssl.SSLError -except (ImportError, AttributeError): # Platform-specific: No SSL. - ssl = None - - class BaseSSLError(BaseException): - pass - - -try: # Python 3: - # Not a no-op, we're adding this to the namespace so it can be imported. - ConnectionError = ConnectionError -except NameError: # Python 2: - class ConnectionError(Exception): - pass - - -from .exceptions import ( - NewConnectionError, - ConnectTimeoutError, - SubjectAltNameWarning, - SystemTimeWarning, -) -from .packages.ssl_match_hostname import match_hostname, CertificateError - -from .util.ssl_ import ( - resolve_cert_reqs, - resolve_ssl_version, - assert_fingerprint, - create_urllib3_context, - ssl_wrap_socket -) - - -from .util import connection - -from ._collections import HTTPHeaderDict - -log = logging.getLogger(__name__) - -port_by_scheme = { - 'http': 80, - 'https': 443, -} - -# When updating RECENT_DATE, move it to -# within two years of the current date, and no -# earlier than 6 months ago. -RECENT_DATE = datetime.date(2016, 1, 1) - - -class DummyConnection(object): - """Used to detect a failed ConnectionCls import.""" - pass - - -class HTTPConnection(_HTTPConnection, object): - """ - Based on httplib.HTTPConnection but provides an extra constructor - backwards-compatibility layer between older and newer Pythons. - - Additional keyword parameters are used to configure attributes of the connection. - Accepted parameters include: - - - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool` - - ``source_address``: Set the source address for the current connection. - - .. note:: This is ignored for Python 2.6. It is only applied for 2.7 and 3.x - - - ``socket_options``: Set specific options on the underlying socket. If not specified, then - defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling - Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. - - For example, if you wish to enable TCP Keep Alive in addition to the defaults, - you might pass:: - - HTTPConnection.default_socket_options + [ - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ] - - Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). - """ - - default_port = port_by_scheme['http'] - - #: Disable Nagle's algorithm by default. - #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` - default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)] - - #: Whether this connection verifies the host's certificate. - is_verified = False - - def __init__(self, *args, **kw): - if six.PY3: # Python 3 - kw.pop('strict', None) - - # Pre-set source_address in case we have an older Python like 2.6. - self.source_address = kw.get('source_address') - - if sys.version_info < (2, 7): # Python 2.6 - # _HTTPConnection on Python 2.6 will balk at this keyword arg, but - # not newer versions. We can still use it when creating a - # connection though, so we pop it *after* we have saved it as - # self.source_address. - kw.pop('source_address', None) - - #: The socket options provided by the user. If no options are - #: provided, we use the default options. - self.socket_options = kw.pop('socket_options', self.default_socket_options) - - # Superclass also sets self.source_address in Python 2.7+. - _HTTPConnection.__init__(self, *args, **kw) - - def _new_conn(self): - """ Establish a socket connection and set nodelay settings on it. - - :return: New socket connection. - """ - extra_kw = {} - if self.source_address: - extra_kw['source_address'] = self.source_address - - if self.socket_options: - extra_kw['socket_options'] = self.socket_options - - try: - conn = connection.create_connection( - (self.host, self.port), self.timeout, **extra_kw) - - except SocketTimeout as e: - raise ConnectTimeoutError( - self, "Connection to %s timed out. (connect timeout=%s)" % - (self.host, self.timeout)) - - except SocketError as e: - raise NewConnectionError( - self, "Failed to establish a new connection: %s" % e) - - return conn - - def _prepare_conn(self, conn): - self.sock = conn - # the _tunnel_host attribute was added in python 2.6.3 (via - # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do - # not have them. - if getattr(self, '_tunnel_host', None): - # TODO: Fix tunnel so it doesn't depend on self.sock state. - self._tunnel() - # Mark this connection as not reusable - self.auto_open = 0 - - def connect(self): - conn = self._new_conn() - self._prepare_conn(conn) - - def request_chunked(self, method, url, body=None, headers=None): - """ - Alternative to the common request method, which sends the - body with chunked encoding and not as one block - """ - headers = HTTPHeaderDict(headers if headers is not None else {}) - skip_accept_encoding = 'accept-encoding' in headers - skip_host = 'host' in headers - self.putrequest( - method, - url, - skip_accept_encoding=skip_accept_encoding, - skip_host=skip_host - ) - for header, value in headers.items(): - self.putheader(header, value) - if 'transfer-encoding' not in headers: - self.putheader('Transfer-Encoding', 'chunked') - self.endheaders() - - if body is not None: - stringish_types = six.string_types + (six.binary_type,) - if isinstance(body, stringish_types): - body = (body,) - for chunk in body: - if not chunk: - continue - if not isinstance(chunk, six.binary_type): - chunk = chunk.encode('utf8') - len_str = hex(len(chunk))[2:] - self.send(len_str.encode('utf-8')) - self.send(b'\r\n') - self.send(chunk) - self.send(b'\r\n') - - # After the if clause, to always have a closed body - self.send(b'0\r\n\r\n') - - -class HTTPSConnection(HTTPConnection): - default_port = port_by_scheme['https'] - - ssl_version = None - - def __init__(self, host, port=None, key_file=None, cert_file=None, - strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - ssl_context=None, **kw): - - HTTPConnection.__init__(self, host, port, strict=strict, - timeout=timeout, **kw) - - self.key_file = key_file - self.cert_file = cert_file - self.ssl_context = ssl_context - - # Required property for Google AppEngine 1.9.0 which otherwise causes - # HTTPS requests to go out as HTTP. (See Issue #356) - self._protocol = 'https' - - def connect(self): - conn = self._new_conn() - self._prepare_conn(conn) - - if self.ssl_context is None: - self.ssl_context = create_urllib3_context( - ssl_version=resolve_ssl_version(None), - cert_reqs=resolve_cert_reqs(None), - ) - - self.sock = ssl_wrap_socket( - sock=conn, - keyfile=self.key_file, - certfile=self.cert_file, - ssl_context=self.ssl_context, - ) - - -class VerifiedHTTPSConnection(HTTPSConnection): - """ - Based on httplib.HTTPSConnection but wraps the socket with - SSL certification. - """ - cert_reqs = None - ca_certs = None - ca_cert_dir = None - ssl_version = None - assert_fingerprint = None - - def set_cert(self, key_file=None, cert_file=None, - cert_reqs=None, ca_certs=None, - assert_hostname=None, assert_fingerprint=None, - ca_cert_dir=None): - """ - This method should only be called once, before the connection is used. - """ - # If cert_reqs is not provided, we can try to guess. If the user gave - # us a cert database, we assume they want to use it: otherwise, if - # they gave us an SSL Context object we should use whatever is set for - # it. - if cert_reqs is None: - if ca_certs or ca_cert_dir: - cert_reqs = 'CERT_REQUIRED' - elif self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - - def connect(self): - # Add certificate verification - conn = self._new_conn() - - hostname = self.host - if getattr(self, '_tunnel_host', None): - # _tunnel_host was added in Python 2.6.3 - # (See: http://hg.python.org/cpython/rev/0f57b30a152f) - - self.sock = conn - # Calls self._set_hostport(), so self.host is - # self._tunnel_host below. - self._tunnel() - # Mark this connection as not reusable - self.auto_open = 0 - - # Override the host with the one we're requesting data from. - hostname = self._tunnel_host - - is_time_off = datetime.date.today() < RECENT_DATE - if is_time_off: - warnings.warn(( - 'System time is way off (before {0}). This will probably ' - 'lead to SSL verification errors').format(RECENT_DATE), - SystemTimeWarning - ) - - # Wrap socket using verification with the root certs in - # trusted_root_certs - if self.ssl_context is None: - self.ssl_context = create_urllib3_context( - ssl_version=resolve_ssl_version(self.ssl_version), - cert_reqs=resolve_cert_reqs(self.cert_reqs), - ) - - context = self.ssl_context - context.verify_mode = resolve_cert_reqs(self.cert_reqs) - self.sock = ssl_wrap_socket( - sock=conn, - keyfile=self.key_file, - certfile=self.cert_file, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - server_hostname=hostname, - ssl_context=context) - - if self.assert_fingerprint: - assert_fingerprint(self.sock.getpeercert(binary_form=True), - self.assert_fingerprint) - elif context.verify_mode != ssl.CERT_NONE \ - and self.assert_hostname is not False: - cert = self.sock.getpeercert() - if not cert.get('subjectAltName', ()): - warnings.warn(( - 'Certificate for {0} has no `subjectAltName`, falling back to check for a ' - '`commonName` for now. This feature is being removed by major browsers and ' - 'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 ' - 'for details.)'.format(hostname)), - SubjectAltNameWarning - ) - _match_hostname(cert, self.assert_hostname or hostname) - - self.is_verified = ( - context.verify_mode == ssl.CERT_REQUIRED or - self.assert_fingerprint is not None - ) - - -def _match_hostname(cert, asserted_hostname): - try: - match_hostname(cert, asserted_hostname) - except CertificateError as e: - log.error( - 'Certificate did not match expected hostname: %s. ' - 'Certificate: %s', asserted_hostname, cert - ) - # Add cert to exception and reraise so client code can inspect - # the cert when catching the exception, if they want to - e._peer_cert = cert - raise - - -if ssl: - # Make a copy for testing. - UnverifiedHTTPSConnection = HTTPSConnection - HTTPSConnection = VerifiedHTTPSConnection -else: - HTTPSConnection = DummyConnection +from __future__ import absolute_import +import datetime +import logging +import os +import sys +import socket +from socket import error as SocketError, timeout as SocketTimeout +import warnings +from .packages import six +from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection +from .packages.six.moves.http_client import HTTPException # noqa: F401 + +try: # Compiled with SSL? + import ssl + BaseSSLError = ssl.SSLError +except (ImportError, AttributeError): # Platform-specific: No SSL. + ssl = None + + class BaseSSLError(BaseException): + pass + + +try: # Python 3: + # Not a no-op, we're adding this to the namespace so it can be imported. + ConnectionError = ConnectionError +except NameError: # Python 2: + class ConnectionError(Exception): + pass + + +from .exceptions import ( + NewConnectionError, + ConnectTimeoutError, + SubjectAltNameWarning, + SystemTimeWarning, +) +from .packages.ssl_match_hostname import match_hostname, CertificateError + +from .util.ssl_ import ( + resolve_cert_reqs, + resolve_ssl_version, + assert_fingerprint, + create_urllib3_context, + ssl_wrap_socket +) + + +from .util import connection + +from ._collections import HTTPHeaderDict + +log = logging.getLogger(__name__) + +port_by_scheme = { + 'http': 80, + 'https': 443, +} + +# When updating RECENT_DATE, move it to +# within two years of the current date, and no +# earlier than 6 months ago. +RECENT_DATE = datetime.date(2016, 1, 1) + + +class DummyConnection(object): + """Used to detect a failed ConnectionCls import.""" + pass + + +class HTTPConnection(_HTTPConnection, object): + """ + Based on httplib.HTTPConnection but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + + Additional keyword parameters are used to configure attributes of the connection. + Accepted parameters include: + + - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool` + - ``source_address``: Set the source address for the current connection. + + .. note:: This is ignored for Python 2.6. It is only applied for 2.7 and 3.x + + - ``socket_options``: Set specific options on the underlying socket. If not specified, then + defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling + Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. + + For example, if you wish to enable TCP Keep Alive in addition to the defaults, + you might pass:: + + HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). + """ + + default_port = port_by_scheme['http'] + + #: Disable Nagle's algorithm by default. + #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` + default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)] + + #: Whether this connection verifies the host's certificate. + is_verified = False + + def __init__(self, *args, **kw): + if six.PY3: # Python 3 + kw.pop('strict', None) + + # Pre-set source_address in case we have an older Python like 2.6. + self.source_address = kw.get('source_address') + + if sys.version_info < (2, 7): # Python 2.6 + # _HTTPConnection on Python 2.6 will balk at this keyword arg, but + # not newer versions. We can still use it when creating a + # connection though, so we pop it *after* we have saved it as + # self.source_address. + kw.pop('source_address', None) + + #: The socket options provided by the user. If no options are + #: provided, we use the default options. + self.socket_options = kw.pop('socket_options', self.default_socket_options) + + # Superclass also sets self.source_address in Python 2.7+. + _HTTPConnection.__init__(self, *args, **kw) + + def _new_conn(self): + """ Establish a socket connection and set nodelay settings on it. + + :return: New socket connection. + """ + extra_kw = {} + if self.source_address: + extra_kw['source_address'] = self.source_address + + if self.socket_options: + extra_kw['socket_options'] = self.socket_options + + try: + conn = connection.create_connection( + (self.host, self.port), self.timeout, **extra_kw) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, "Connection to %s timed out. (connect timeout=%s)" % + (self.host, self.timeout)) + + except SocketError as e: + raise NewConnectionError( + self, "Failed to establish a new connection: %s" % e) + + return conn + + def _prepare_conn(self, conn): + self.sock = conn + # the _tunnel_host attribute was added in python 2.6.3 (via + # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do + # not have them. + if getattr(self, '_tunnel_host', None): + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + # Mark this connection as not reusable + self.auto_open = 0 + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + + def request_chunked(self, method, url, body=None, headers=None): + """ + Alternative to the common request method, which sends the + body with chunked encoding and not as one block + """ + headers = HTTPHeaderDict(headers if headers is not None else {}) + skip_accept_encoding = 'accept-encoding' in headers + skip_host = 'host' in headers + self.putrequest( + method, + url, + skip_accept_encoding=skip_accept_encoding, + skip_host=skip_host + ) + for header, value in headers.items(): + self.putheader(header, value) + if 'transfer-encoding' not in headers: + self.putheader('Transfer-Encoding', 'chunked') + self.endheaders() + + if body is not None: + stringish_types = six.string_types + (six.binary_type,) + if isinstance(body, stringish_types): + body = (body,) + for chunk in body: + if not chunk: + continue + if not isinstance(chunk, six.binary_type): + chunk = chunk.encode('utf8') + len_str = hex(len(chunk))[2:] + self.send(len_str.encode('utf-8')) + self.send(b'\r\n') + self.send(chunk) + self.send(b'\r\n') + + # After the if clause, to always have a closed body + self.send(b'0\r\n\r\n') + + +class HTTPSConnection(HTTPConnection): + default_port = port_by_scheme['https'] + + ssl_version = None + + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + ssl_context=None, **kw): + + HTTPConnection.__init__(self, host, port, strict=strict, + timeout=timeout, **kw) + + self.key_file = key_file + self.cert_file = cert_file + self.ssl_context = ssl_context + + # Required property for Google AppEngine 1.9.0 which otherwise causes + # HTTPS requests to go out as HTTP. (See Issue #356) + self._protocol = 'https' + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + + if self.ssl_context is None: + self.ssl_context = create_urllib3_context( + ssl_version=resolve_ssl_version(None), + cert_reqs=resolve_cert_reqs(None), + ) + + self.sock = ssl_wrap_socket( + sock=conn, + keyfile=self.key_file, + certfile=self.cert_file, + ssl_context=self.ssl_context, + ) + + +class VerifiedHTTPSConnection(HTTPSConnection): + """ + Based on httplib.HTTPSConnection but wraps the socket with + SSL certification. + """ + cert_reqs = None + ca_certs = None + ca_cert_dir = None + ssl_version = None + assert_fingerprint = None + + def set_cert(self, key_file=None, cert_file=None, + cert_reqs=None, ca_certs=None, + assert_hostname=None, assert_fingerprint=None, + ca_cert_dir=None): + """ + This method should only be called once, before the connection is used. + """ + # If cert_reqs is not provided, we can try to guess. If the user gave + # us a cert database, we assume they want to use it: otherwise, if + # they gave us an SSL Context object we should use whatever is set for + # it. + if cert_reqs is None: + if ca_certs or ca_cert_dir: + cert_reqs = 'CERT_REQUIRED' + elif self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + + def connect(self): + # Add certificate verification + conn = self._new_conn() + + hostname = self.host + if getattr(self, '_tunnel_host', None): + # _tunnel_host was added in Python 2.6.3 + # (See: http://hg.python.org/cpython/rev/0f57b30a152f) + + self.sock = conn + # Calls self._set_hostport(), so self.host is + # self._tunnel_host below. + self._tunnel() + # Mark this connection as not reusable + self.auto_open = 0 + + # Override the host with the one we're requesting data from. + hostname = self._tunnel_host + + is_time_off = datetime.date.today() < RECENT_DATE + if is_time_off: + warnings.warn(( + 'System time is way off (before {0}). This will probably ' + 'lead to SSL verification errors').format(RECENT_DATE), + SystemTimeWarning + ) + + # Wrap socket using verification with the root certs in + # trusted_root_certs + if self.ssl_context is None: + self.ssl_context = create_urllib3_context( + ssl_version=resolve_ssl_version(self.ssl_version), + cert_reqs=resolve_cert_reqs(self.cert_reqs), + ) + + context = self.ssl_context + context.verify_mode = resolve_cert_reqs(self.cert_reqs) + self.sock = ssl_wrap_socket( + sock=conn, + keyfile=self.key_file, + certfile=self.cert_file, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + server_hostname=hostname, + ssl_context=context) + + if self.assert_fingerprint: + assert_fingerprint(self.sock.getpeercert(binary_form=True), + self.assert_fingerprint) + elif context.verify_mode != ssl.CERT_NONE \ + and self.assert_hostname is not False: + cert = self.sock.getpeercert() + if not cert.get('subjectAltName', ()): + warnings.warn(( + 'Certificate for {0} has no `subjectAltName`, falling back to check for a ' + '`commonName` for now. This feature is being removed by major browsers and ' + 'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 ' + 'for details.)'.format(hostname)), + SubjectAltNameWarning + ) + _match_hostname(cert, self.assert_hostname or hostname) + + self.is_verified = ( + context.verify_mode == ssl.CERT_REQUIRED or + self.assert_fingerprint is not None + ) + + +def _match_hostname(cert, asserted_hostname): + try: + match_hostname(cert, asserted_hostname) + except CertificateError as e: + log.error( + 'Certificate did not match expected hostname: %s. ' + 'Certificate: %s', asserted_hostname, cert + ) + # Add cert to exception and reraise so client code can inspect + # the cert when catching the exception, if they want to + e._peer_cert = cert + raise + + +if ssl: + # Make a copy for testing. + UnverifiedHTTPSConnection = HTTPSConnection + HTTPSConnection = VerifiedHTTPSConnection +else: + HTTPSConnection = DummyConnection diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py index 1073a1f..a958f99 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py @@ -1,912 +1,912 @@ -from __future__ import absolute_import -import errno -import logging -import sys -import warnings - -from socket import error as SocketError, timeout as SocketTimeout -import socket - - -from .exceptions import ( - ClosedPoolError, - ProtocolError, - EmptyPoolError, - HeaderParsingError, - HostChangedError, - LocationValueError, - MaxRetryError, - ProxyError, - ReadTimeoutError, - SSLError, - TimeoutError, - InsecureRequestWarning, - NewConnectionError, - ConnectTimeoutError, -) -from .packages.ssl_match_hostname import CertificateError -from .packages import six -from .packages.six.moves import queue -from .connection import ( - port_by_scheme, - DummyConnection, - HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection, - HTTPException, BaseSSLError, -) -from .request import RequestMethods -from .response import HTTPResponse - -from .util.connection import is_connection_dropped -from .util.request import set_file_position -from .util.response import assert_header_parsing -from .util.retry import Retry -from .util.timeout import Timeout -from .util.url import get_host, Url - - -if six.PY2: - # Queue is imported for side effects on MS Windows - import Queue as _unused_module_Queue # noqa: F401 - -xrange = six.moves.xrange - -log = logging.getLogger(__name__) - -_Default = object() - - -# Pool objects -class ConnectionPool(object): - """ - Base class for all connection pools, such as - :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. - """ - - scheme = None - QueueCls = queue.LifoQueue - - def __init__(self, host, port=None): - if not host: - raise LocationValueError("No host specified.") - - self.host = _ipv6_host(host).lower() - self.port = port - - def __str__(self): - return '%s(host=%r, port=%r)' % (type(self).__name__, - self.host, self.port) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - # Return False to re-raise any potential exceptions - return False - - def close(self): - """ - Close all pooled connections and disable the pool. - """ - pass - - -# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 -_blocking_errnos = set([errno.EAGAIN, errno.EWOULDBLOCK]) - - -class HTTPConnectionPool(ConnectionPool, RequestMethods): - """ - Thread-safe connection pool for one host. - - :param host: - Host used for this HTTP Connection (e.g. "localhost"), passed into - :class:`httplib.HTTPConnection`. - - :param port: - Port used for this HTTP Connection (None is equivalent to 80), passed - into :class:`httplib.HTTPConnection`. - - :param strict: - Causes BadStatusLine to be raised if the status line can't be parsed - as a valid HTTP/1.0 or 1.1 status line, passed into - :class:`httplib.HTTPConnection`. - - .. note:: - Only works in Python 2. This parameter is ignored in Python 3. - - :param timeout: - Socket timeout in seconds for each individual connection. This can - be a float or integer, which sets the timeout for the HTTP request, - or an instance of :class:`urllib3.util.Timeout` which gives you more - fine-grained control over request timeouts. After the constructor has - been parsed, this is always a `urllib3.util.Timeout` object. - - :param maxsize: - Number of connections to save that can be reused. More than 1 is useful - in multithreaded situations. If ``block`` is set to False, more - connections will be created but they will not be saved once they've - been used. - - :param block: - If set to True, no more than ``maxsize`` connections will be used at - a time. When no free connections are available, the call will block - until a connection has been released. This is a useful side effect for - particular multithreaded situations where one does not want to use more - than maxsize connections per host to prevent flooding. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param retries: - Retry configuration to use by default with requests in this pool. - - :param _proxy: - Parsed proxy URL, should not be used directly, instead, see - :class:`urllib3.connectionpool.ProxyManager`" - - :param _proxy_headers: - A dictionary with proxy headers, should not be used directly, - instead, see :class:`urllib3.connectionpool.ProxyManager`" - - :param \\**conn_kw: - Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, - :class:`urllib3.connection.HTTPSConnection` instances. - """ - - scheme = 'http' - ConnectionCls = HTTPConnection - ResponseCls = HTTPResponse - - def __init__(self, host, port=None, strict=False, - timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, - headers=None, retries=None, - _proxy=None, _proxy_headers=None, - **conn_kw): - ConnectionPool.__init__(self, host, port) - RequestMethods.__init__(self, headers) - - self.strict = strict - - if not isinstance(timeout, Timeout): - timeout = Timeout.from_float(timeout) - - if retries is None: - retries = Retry.DEFAULT - - self.timeout = timeout - self.retries = retries - - self.pool = self.QueueCls(maxsize) - self.block = block - - self.proxy = _proxy - self.proxy_headers = _proxy_headers or {} - - # Fill the queue up so that doing get() on it will block properly - for _ in xrange(maxsize): - self.pool.put(None) - - # These are mostly for testing and debugging purposes. - self.num_connections = 0 - self.num_requests = 0 - self.conn_kw = conn_kw - - if self.proxy: - # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. - # We cannot know if the user has added default socket options, so we cannot replace the - # list. - self.conn_kw.setdefault('socket_options', []) - - def _new_conn(self): - """ - Return a fresh :class:`HTTPConnection`. - """ - self.num_connections += 1 - log.debug("Starting new HTTP connection (%d): %s", - self.num_connections, self.host) - - conn = self.ConnectionCls(host=self.host, port=self.port, - timeout=self.timeout.connect_timeout, - strict=self.strict, **self.conn_kw) - return conn - - def _get_conn(self, timeout=None): - """ - Get a connection. Will return a pooled connection if one is available. - - If no connections are available and :prop:`.block` is ``False``, then a - fresh connection is returned. - - :param timeout: - Seconds to wait before giving up and raising - :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and - :prop:`.block` is ``True``. - """ - conn = None - try: - conn = self.pool.get(block=self.block, timeout=timeout) - - except AttributeError: # self.pool is None - raise ClosedPoolError(self, "Pool is closed.") - - except queue.Empty: - if self.block: - raise EmptyPoolError(self, - "Pool reached maximum size and no more " - "connections are allowed.") - pass # Oh well, we'll create a new connection then - - # If this is a persistent connection, check if it got disconnected - if conn and is_connection_dropped(conn): - log.debug("Resetting dropped connection: %s", self.host) - conn.close() - if getattr(conn, 'auto_open', 1) == 0: - # This is a proxied connection that has been mutated by - # httplib._tunnel() and cannot be reused (since it would - # attempt to bypass the proxy) - conn = None - - return conn or self._new_conn() - - def _put_conn(self, conn): - """ - Put a connection back into the pool. - - :param conn: - Connection object for the current host and port as returned by - :meth:`._new_conn` or :meth:`._get_conn`. - - If the pool is already full, the connection is closed and discarded - because we exceeded maxsize. If connections are discarded frequently, - then maxsize should be increased. - - If the pool is closed, then the connection will be closed and discarded. - """ - try: - self.pool.put(conn, block=False) - return # Everything is dandy, done. - except AttributeError: - # self.pool is None. - pass - except queue.Full: - # This should never happen if self.block == True - log.warning( - "Connection pool is full, discarding connection: %s", - self.host) - - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - def _validate_conn(self, conn): - """ - Called right before a request is made, after the socket is created. - """ - # Force connect early to allow us to set read timeout in time - if not getattr(conn, 'sock', None): # AppEngine might not have `.sock` - conn.connect() - - def _prepare_proxy(self, conn): - # Nothing to do for HTTP connections. - pass - - def _get_timeout(self, timeout): - """ Helper that always returns a :class:`urllib3.util.Timeout` """ - if timeout is _Default: - return self.timeout.clone() - - if isinstance(timeout, Timeout): - return timeout.clone() - else: - # User passed us an int/float. This is for backwards compatibility, - # can be removed later - return Timeout.from_float(timeout) - - def _raise_timeout(self, err, url, timeout_value, exc_cls): - """Is the error actually a timeout? Will raise a ReadTimeout or pass""" - - # exc_cls is either ReadTimeoutError or ConnectTimeoutError - # Only ReadTimeoutError requires the url (preserving old behaviour) - args = [self] - if exc_cls is ReadTimeoutError: - args.append(url) - desc = 'Read' - else: - desc = 'Connect' - - if isinstance(err, SocketTimeout): - args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value)) - raise exc_cls(*args) - - # See the above comment about EAGAIN in Python 3. In Python 2 we have - # to specifically catch it and throw the timeout error - elif hasattr(err, 'errno') and err.errno in _blocking_errnos: - args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value)) - raise exc_cls(*args) - - # Catch possible read timeouts thrown as SSL errors. If not the - # case, rethrow the original. We need to do this because of: - # http://bugs.python.org/issue10272 - elif 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python 2.6 - args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value)) - raise exc_cls(*args) - - def _make_request(self, conn, method, url, timeout=_Default, chunked=False, - **httplib_request_kw): - """ - Perform a request on a given urllib connection object taken from our - pool. - - :param conn: - a connection from one of our connection pools - - :param timeout: - Socket timeout in seconds for the request. This can be a - float or integer, which will set the same timeout value for - the socket connect and the socket read, or an instance of - :class:`urllib3.util.Timeout`, which gives you more fine-grained - control over your timeouts. - """ - self.num_requests += 1 - - timeout_obj = self._get_timeout(timeout) - timeout_obj.start_connect() - conn.timeout = timeout_obj.connect_timeout - - # Trigger any extra validation we need to do. - try: - self._validate_conn(conn) - except (SocketTimeout, BaseSSLError) as e: - # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout. - self._raise_timeout(err=e, url=url, timeout_value=conn.timeout, - exc_cls=ConnectTimeoutError) - raise - - # Reset the timeout for the recv() on the socket - read_timeout = timeout_obj.read_timeout - - # App Engine doesn't have a sock attr - if getattr(conn, 'sock', None): - # In Python 3 socket.py will catch EAGAIN and return None when you - # try and read into the file pointer created by http.client, which - # instead raises a BadStatusLine exception. Instead of catching - # the exception and assuming all BadStatusLine exceptions are read - # timeouts, check for a zero timeout before making the request. - if read_timeout == 0: - raise ReadTimeoutError( - self, url, "Read timed out. (read timeout=%s)" % read_timeout) - if read_timeout is Timeout.DEFAULT_TIMEOUT: - conn.sock.settimeout(socket.getdefaulttimeout()) - else: # None or a value - conn.sock.settimeout(read_timeout) - - # conn.request() calls httplib.*.request, not the method in - # urllib3.request. It also calls makefile (recv) on the socket. - if chunked: - conn.request_chunked(method, url, **httplib_request_kw) - else: - conn.request(method, url, **httplib_request_kw) - - # Receive the response from the server - try: - try: # Python 2.7, use buffering of HTTP responses - httplib_response = conn.getresponse(buffering=True) - except TypeError: # Python 2.6 and older, Python 3 - try: - httplib_response = conn.getresponse() - except Exception as e: - # Remove the TypeError from the exception chain in Python 3; - # otherwise it looks like a programming error was the cause. - six.raise_from(e, None) - except (SocketTimeout, BaseSSLError, SocketError) as e: - self._raise_timeout(err=e, url=url, timeout_value=read_timeout, - exc_cls=ReadTimeoutError) - raise - - # AppEngine doesn't have a version attr. - http_version = getattr(conn, '_http_vsn_str', 'HTTP/?') - log.debug("%s://%s:%s \"%s %s %s\" %s %s", self.scheme, self.host, self.port, - method, url, http_version, httplib_response.status, - httplib_response.length) - - try: - assert_header_parsing(httplib_response.msg) - except HeaderParsingError as hpe: # Platform-specific: Python 3 - log.warning( - 'Failed to parse headers (url=%s): %s', - self._absolute_url(url), hpe, exc_info=True) - - return httplib_response - - def _absolute_url(self, path): - return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url - - def close(self): - """ - Close all pooled connections and disable the pool. - """ - # Disable access to the pool - old_pool, self.pool = self.pool, None - - try: - while True: - conn = old_pool.get(block=False) - if conn: - conn.close() - - except queue.Empty: - pass # Done. - - def is_same_host(self, url): - """ - Check if the given ``url`` is a member of the same host as this - connection pool. - """ - if url.startswith('/'): - return True - - # TODO: Add optional support for socket.gethostbyname checking. - scheme, host, port = get_host(url) - - host = _ipv6_host(host).lower() - - # Use explicit default port for comparison when none is given - if self.port and not port: - port = port_by_scheme.get(scheme) - elif not self.port and port == port_by_scheme.get(scheme): - port = None - - return (scheme, host, port) == (self.scheme, self.host, self.port) - - def urlopen(self, method, url, body=None, headers=None, retries=None, - redirect=True, assert_same_host=True, timeout=_Default, - pool_timeout=None, release_conn=None, chunked=False, - body_pos=None, **response_kw): - """ - Get a connection from the pool and perform an HTTP request. This is the - lowest level call for making a request, so you'll need to specify all - the raw details. - - .. note:: - - More commonly, it's appropriate to use a convenience method provided - by :class:`.RequestMethods`, such as :meth:`request`. - - .. note:: - - `release_conn` will only behave as expected if - `preload_content=False` because we want to make - `preload_content=False` the default behaviour someday soon without - breaking backwards compatibility. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param body: - Data to send in the request body (useful for creating - POST requests, see HTTPConnectionPool.post_url for - more convenience). - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - Pass ``None`` to retry until you receive a response. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param assert_same_host: - If ``True``, will make sure that the host of the pool requests is - consistent else will raise HostChangedError. When False, you can - use the pool on an HTTP proxy and request foreign hosts. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param pool_timeout: - If set and the pool is set to block=True, then this method will - block for ``pool_timeout`` seconds and raise EmptyPoolError if no - connection is available within the time period. - - :param release_conn: - If False, then the urlopen call will not release the connection - back into the pool once a response is received (but will release if - you read the entire contents of the response such as when - `preload_content=True`). This is useful if you're not preloading - the response's content immediately. You will need to call - ``r.release_conn()`` on the response ``r`` to return the connection - back into the pool. If None, it takes the value of - ``response_kw.get('preload_content', True)``. - - :param chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param int body_pos: - Position to seek to in file-like body in the event of a retry or - redirect. Typically this won't need to be set because urllib3 will - auto-populate the value when needed. - - :param \\**response_kw: - Additional parameters are passed to - :meth:`urllib3.response.HTTPResponse.from_httplib` - """ - if headers is None: - headers = self.headers - - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect, default=self.retries) - - if release_conn is None: - release_conn = response_kw.get('preload_content', True) - - # Check host - if assert_same_host and not self.is_same_host(url): - raise HostChangedError(self, url, retries) - - conn = None - - # Track whether `conn` needs to be released before - # returning/raising/recursing. Update this variable if necessary, and - # leave `release_conn` constant throughout the function. That way, if - # the function recurses, the original value of `release_conn` will be - # passed down into the recursive call, and its value will be respected. - # - # See issue #651 [1] for details. - # - # [1] - release_this_conn = release_conn - - # Merge the proxy headers. Only do this in HTTP. We have to copy the - # headers dict so we can safely change it without those changes being - # reflected in anyone else's copy. - if self.scheme == 'http': - headers = headers.copy() - headers.update(self.proxy_headers) - - # Must keep the exception bound to a separate variable or else Python 3 - # complains about UnboundLocalError. - err = None - - # Keep track of whether we cleanly exited the except block. This - # ensures we do proper cleanup in finally. - clean_exit = False - - # Rewind body position, if needed. Record current position - # for future rewinds in the event of a redirect/retry. - body_pos = set_file_position(body, body_pos) - - try: - # Request a connection from the queue. - timeout_obj = self._get_timeout(timeout) - conn = self._get_conn(timeout=pool_timeout) - - conn.timeout = timeout_obj.connect_timeout - - is_new_proxy_conn = self.proxy is not None and not getattr(conn, 'sock', None) - if is_new_proxy_conn: - self._prepare_proxy(conn) - - # Make the request on the httplib connection object. - httplib_response = self._make_request(conn, method, url, - timeout=timeout_obj, - body=body, headers=headers, - chunked=chunked) - - # If we're going to release the connection in ``finally:``, then - # the response doesn't need to know about the connection. Otherwise - # it will also try to release it and we'll have a double-release - # mess. - response_conn = conn if not release_conn else None - - # Pass method to Response for length checking - response_kw['request_method'] = method - - # Import httplib's response into our own wrapper object - response = self.ResponseCls.from_httplib(httplib_response, - pool=self, - connection=response_conn, - retries=retries, - **response_kw) - - # Everything went great! - clean_exit = True - - except queue.Empty: - # Timed out by queue. - raise EmptyPoolError(self, "No pool connections are available.") - - except (BaseSSLError, CertificateError) as e: - # Close the connection. If a connection is reused on which there - # was a Certificate error, the next request will certainly raise - # another Certificate error. - clean_exit = False - raise SSLError(e) - - except SSLError: - # Treat SSLError separately from BaseSSLError to preserve - # traceback. - clean_exit = False - raise - - except (TimeoutError, HTTPException, SocketError, ProtocolError) as e: - # Discard the connection for these exceptions. It will be - # be replaced during the next _get_conn() call. - clean_exit = False - - if isinstance(e, (SocketError, NewConnectionError)) and self.proxy: - e = ProxyError('Cannot connect to proxy.', e) - elif isinstance(e, (SocketError, HTTPException)): - e = ProtocolError('Connection aborted.', e) - - retries = retries.increment(method, url, error=e, _pool=self, - _stacktrace=sys.exc_info()[2]) - retries.sleep() - - # Keep track of the error for the retry warning. - err = e - - finally: - if not clean_exit: - # We hit some kind of exception, handled or otherwise. We need - # to throw the connection away unless explicitly told not to. - # Close the connection, set the variable to None, and make sure - # we put the None back in the pool to avoid leaking it. - conn = conn and conn.close() - release_this_conn = True - - if release_this_conn: - # Put the connection back to be reused. If the connection is - # expired then it will be None, which will get replaced with a - # fresh connection during _get_conn. - self._put_conn(conn) - - if not conn: - # Try again - log.warning("Retrying (%r) after connection " - "broken by '%r': %s", retries, err, url) - return self.urlopen(method, url, body, headers, retries, - redirect, assert_same_host, - timeout=timeout, pool_timeout=pool_timeout, - release_conn=release_conn, body_pos=body_pos, - **response_kw) - - # Handle redirect? - redirect_location = redirect and response.get_redirect_location() - if redirect_location: - if response.status == 303: - method = 'GET' - - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_redirect: - # Release the connection for this response, since we're not - # returning it to be released manually. - response.release_conn() - raise - return response - - retries.sleep_for_retry(response) - log.debug("Redirecting %s -> %s", url, redirect_location) - return self.urlopen( - method, redirect_location, body, headers, - retries=retries, redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, pool_timeout=pool_timeout, - release_conn=release_conn, body_pos=body_pos, - **response_kw) - - # Check if we should retry the HTTP response. - has_retry_after = bool(response.getheader('Retry-After')) - if retries.is_retry(method, response.status, has_retry_after): - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_status: - # Release the connection for this response, since we're not - # returning it to be released manually. - response.release_conn() - raise - return response - retries.sleep(response) - log.debug("Retry: %s", url) - return self.urlopen( - method, url, body, headers, - retries=retries, redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, pool_timeout=pool_timeout, - release_conn=release_conn, - body_pos=body_pos, **response_kw) - - return response - - -class HTTPSConnectionPool(HTTPConnectionPool): - """ - Same as :class:`.HTTPConnectionPool`, but HTTPS. - - When Python is compiled with the :mod:`ssl` module, then - :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates, - instead of :class:`.HTTPSConnection`. - - :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``, - ``assert_hostname`` and ``host`` in this order to verify connections. - If ``assert_hostname`` is False, no verification is done. - - The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, - ``ca_cert_dir``, and ``ssl_version`` are only used if :mod:`ssl` is - available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade - the connection socket into an SSL socket. - """ - - scheme = 'https' - ConnectionCls = HTTPSConnection - - def __init__(self, host, port=None, - strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, - block=False, headers=None, retries=None, - _proxy=None, _proxy_headers=None, - key_file=None, cert_file=None, cert_reqs=None, - ca_certs=None, ssl_version=None, - assert_hostname=None, assert_fingerprint=None, - ca_cert_dir=None, **conn_kw): - - HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize, - block, headers, retries, _proxy, _proxy_headers, - **conn_kw) - - if ca_certs and cert_reqs is None: - cert_reqs = 'CERT_REQUIRED' - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.ca_certs = ca_certs - self.ca_cert_dir = ca_cert_dir - self.ssl_version = ssl_version - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - - def _prepare_conn(self, conn): - """ - Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket` - and establish the tunnel if proxy is used. - """ - - if isinstance(conn, VerifiedHTTPSConnection): - conn.set_cert(key_file=self.key_file, - cert_file=self.cert_file, - cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint) - conn.ssl_version = self.ssl_version - return conn - - def _prepare_proxy(self, conn): - """ - Establish tunnel connection early, because otherwise httplib - would improperly set Host: header to proxy's IP:port. - """ - # Python 2.7+ - try: - set_tunnel = conn.set_tunnel - except AttributeError: # Platform-specific: Python 2.6 - set_tunnel = conn._set_tunnel - - if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older - set_tunnel(self.host, self.port) - else: - set_tunnel(self.host, self.port, self.proxy_headers) - - conn.connect() - - def _new_conn(self): - """ - Return a fresh :class:`httplib.HTTPSConnection`. - """ - self.num_connections += 1 - log.debug("Starting new HTTPS connection (%d): %s", - self.num_connections, self.host) - - if not self.ConnectionCls or self.ConnectionCls is DummyConnection: - raise SSLError("Can't connect to HTTPS URL because the SSL " - "module is not available.") - - actual_host = self.host - actual_port = self.port - if self.proxy is not None: - actual_host = self.proxy.host - actual_port = self.proxy.port - - conn = self.ConnectionCls(host=actual_host, port=actual_port, - timeout=self.timeout.connect_timeout, - strict=self.strict, **self.conn_kw) - - return self._prepare_conn(conn) - - def _validate_conn(self, conn): - """ - Called right before a request is made, after the socket is created. - """ - super(HTTPSConnectionPool, self)._validate_conn(conn) - - if not conn.is_verified: - warnings.warn(( - 'Unverified HTTPS request is being made. ' - 'Adding certificate verification is strongly advised. See: ' - 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' - '#ssl-warnings'), - InsecureRequestWarning) - - -def connection_from_url(url, **kw): - """ - Given a url, return an :class:`.ConnectionPool` instance of its host. - - This is a shortcut for not having to parse out the scheme, host, and port - of the url before creating an :class:`.ConnectionPool` instance. - - :param url: - Absolute URL string that must include the scheme. Port is optional. - - :param \\**kw: - Passes additional parameters to the constructor of the appropriate - :class:`.ConnectionPool`. Useful for specifying things like - timeout, maxsize, headers, etc. - - Example:: - - >>> conn = connection_from_url('http://google.com/') - >>> r = conn.request('GET', '/') - """ - scheme, host, port = get_host(url) - port = port or port_by_scheme.get(scheme, 80) - if scheme == 'https': - return HTTPSConnectionPool(host, port=port, **kw) - else: - return HTTPConnectionPool(host, port=port, **kw) - - -def _ipv6_host(host): - """ - Process IPv6 address literals - """ - - # httplib doesn't like it when we include brackets in IPv6 addresses - # Specifically, if we include brackets but also pass the port then - # httplib crazily doubles up the square brackets on the Host header. - # Instead, we need to make sure we never pass ``None`` as the port. - # However, for backward compatibility reasons we can't actually - # *assert* that. See http://bugs.python.org/issue28539 - # - # Also if an IPv6 address literal has a zone identifier, the - # percent sign might be URIencoded, convert it back into ASCII - if host.startswith('[') and host.endswith(']'): - host = host.replace('%25', '%').strip('[]') - return host +from __future__ import absolute_import +import errno +import logging +import sys +import warnings + +from socket import error as SocketError, timeout as SocketTimeout +import socket + + +from .exceptions import ( + ClosedPoolError, + ProtocolError, + EmptyPoolError, + HeaderParsingError, + HostChangedError, + LocationValueError, + MaxRetryError, + ProxyError, + ReadTimeoutError, + SSLError, + TimeoutError, + InsecureRequestWarning, + NewConnectionError, + ConnectTimeoutError, +) +from .packages.ssl_match_hostname import CertificateError +from .packages import six +from .packages.six.moves import queue +from .connection import ( + port_by_scheme, + DummyConnection, + HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection, + HTTPException, BaseSSLError, +) +from .request import RequestMethods +from .response import HTTPResponse + +from .util.connection import is_connection_dropped +from .util.request import set_file_position +from .util.response import assert_header_parsing +from .util.retry import Retry +from .util.timeout import Timeout +from .util.url import get_host, Url + + +if six.PY2: + # Queue is imported for side effects on MS Windows + import Queue as _unused_module_Queue # noqa: F401 + +xrange = six.moves.xrange + +log = logging.getLogger(__name__) + +_Default = object() + + +# Pool objects +class ConnectionPool(object): + """ + Base class for all connection pools, such as + :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + """ + + scheme = None + QueueCls = queue.LifoQueue + + def __init__(self, host, port=None): + if not host: + raise LocationValueError("No host specified.") + + self.host = _ipv6_host(host).lower() + self.port = port + + def __str__(self): + return '%s(host=%r, port=%r)' % (type(self).__name__, + self.host, self.port) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + # Return False to re-raise any potential exceptions + return False + + def close(self): + """ + Close all pooled connections and disable the pool. + """ + pass + + +# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 +_blocking_errnos = set([errno.EAGAIN, errno.EWOULDBLOCK]) + + +class HTTPConnectionPool(ConnectionPool, RequestMethods): + """ + Thread-safe connection pool for one host. + + :param host: + Host used for this HTTP Connection (e.g. "localhost"), passed into + :class:`httplib.HTTPConnection`. + + :param port: + Port used for this HTTP Connection (None is equivalent to 80), passed + into :class:`httplib.HTTPConnection`. + + :param strict: + Causes BadStatusLine to be raised if the status line can't be parsed + as a valid HTTP/1.0 or 1.1 status line, passed into + :class:`httplib.HTTPConnection`. + + .. note:: + Only works in Python 2. This parameter is ignored in Python 3. + + :param timeout: + Socket timeout in seconds for each individual connection. This can + be a float or integer, which sets the timeout for the HTTP request, + or an instance of :class:`urllib3.util.Timeout` which gives you more + fine-grained control over request timeouts. After the constructor has + been parsed, this is always a `urllib3.util.Timeout` object. + + :param maxsize: + Number of connections to save that can be reused. More than 1 is useful + in multithreaded situations. If ``block`` is set to False, more + connections will be created but they will not be saved once they've + been used. + + :param block: + If set to True, no more than ``maxsize`` connections will be used at + a time. When no free connections are available, the call will block + until a connection has been released. This is a useful side effect for + particular multithreaded situations where one does not want to use more + than maxsize connections per host to prevent flooding. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param retries: + Retry configuration to use by default with requests in this pool. + + :param _proxy: + Parsed proxy URL, should not be used directly, instead, see + :class:`urllib3.connectionpool.ProxyManager`" + + :param _proxy_headers: + A dictionary with proxy headers, should not be used directly, + instead, see :class:`urllib3.connectionpool.ProxyManager`" + + :param \\**conn_kw: + Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, + :class:`urllib3.connection.HTTPSConnection` instances. + """ + + scheme = 'http' + ConnectionCls = HTTPConnection + ResponseCls = HTTPResponse + + def __init__(self, host, port=None, strict=False, + timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, + headers=None, retries=None, + _proxy=None, _proxy_headers=None, + **conn_kw): + ConnectionPool.__init__(self, host, port) + RequestMethods.__init__(self, headers) + + self.strict = strict + + if not isinstance(timeout, Timeout): + timeout = Timeout.from_float(timeout) + + if retries is None: + retries = Retry.DEFAULT + + self.timeout = timeout + self.retries = retries + + self.pool = self.QueueCls(maxsize) + self.block = block + + self.proxy = _proxy + self.proxy_headers = _proxy_headers or {} + + # Fill the queue up so that doing get() on it will block properly + for _ in xrange(maxsize): + self.pool.put(None) + + # These are mostly for testing and debugging purposes. + self.num_connections = 0 + self.num_requests = 0 + self.conn_kw = conn_kw + + if self.proxy: + # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. + # We cannot know if the user has added default socket options, so we cannot replace the + # list. + self.conn_kw.setdefault('socket_options', []) + + def _new_conn(self): + """ + Return a fresh :class:`HTTPConnection`. + """ + self.num_connections += 1 + log.debug("Starting new HTTP connection (%d): %s", + self.num_connections, self.host) + + conn = self.ConnectionCls(host=self.host, port=self.port, + timeout=self.timeout.connect_timeout, + strict=self.strict, **self.conn_kw) + return conn + + def _get_conn(self, timeout=None): + """ + Get a connection. Will return a pooled connection if one is available. + + If no connections are available and :prop:`.block` is ``False``, then a + fresh connection is returned. + + :param timeout: + Seconds to wait before giving up and raising + :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and + :prop:`.block` is ``True``. + """ + conn = None + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise ClosedPoolError(self, "Pool is closed.") + + except queue.Empty: + if self.block: + raise EmptyPoolError(self, + "Pool reached maximum size and no more " + "connections are allowed.") + pass # Oh well, we'll create a new connection then + + # If this is a persistent connection, check if it got disconnected + if conn and is_connection_dropped(conn): + log.debug("Resetting dropped connection: %s", self.host) + conn.close() + if getattr(conn, 'auto_open', 1) == 0: + # This is a proxied connection that has been mutated by + # httplib._tunnel() and cannot be reused (since it would + # attempt to bypass the proxy) + conn = None + + return conn or self._new_conn() + + def _put_conn(self, conn): + """ + Put a connection back into the pool. + + :param conn: + Connection object for the current host and port as returned by + :meth:`._new_conn` or :meth:`._get_conn`. + + If the pool is already full, the connection is closed and discarded + because we exceeded maxsize. If connections are discarded frequently, + then maxsize should be increased. + + If the pool is closed, then the connection will be closed and discarded. + """ + try: + self.pool.put(conn, block=False) + return # Everything is dandy, done. + except AttributeError: + # self.pool is None. + pass + except queue.Full: + # This should never happen if self.block == True + log.warning( + "Connection pool is full, discarding connection: %s", + self.host) + + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + def _validate_conn(self, conn): + """ + Called right before a request is made, after the socket is created. + """ + # Force connect early to allow us to set read timeout in time + if not getattr(conn, 'sock', None): # AppEngine might not have `.sock` + conn.connect() + + def _prepare_proxy(self, conn): + # Nothing to do for HTTP connections. + pass + + def _get_timeout(self, timeout): + """ Helper that always returns a :class:`urllib3.util.Timeout` """ + if timeout is _Default: + return self.timeout.clone() + + if isinstance(timeout, Timeout): + return timeout.clone() + else: + # User passed us an int/float. This is for backwards compatibility, + # can be removed later + return Timeout.from_float(timeout) + + def _raise_timeout(self, err, url, timeout_value, exc_cls): + """Is the error actually a timeout? Will raise a ReadTimeout or pass""" + + # exc_cls is either ReadTimeoutError or ConnectTimeoutError + # Only ReadTimeoutError requires the url (preserving old behaviour) + args = [self] + if exc_cls is ReadTimeoutError: + args.append(url) + desc = 'Read' + else: + desc = 'Connect' + + if isinstance(err, SocketTimeout): + args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value)) + raise exc_cls(*args) + + # See the above comment about EAGAIN in Python 3. In Python 2 we have + # to specifically catch it and throw the timeout error + elif hasattr(err, 'errno') and err.errno in _blocking_errnos: + args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value)) + raise exc_cls(*args) + + # Catch possible read timeouts thrown as SSL errors. If not the + # case, rethrow the original. We need to do this because of: + # http://bugs.python.org/issue10272 + elif 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python 2.6 + args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value)) + raise exc_cls(*args) + + def _make_request(self, conn, method, url, timeout=_Default, chunked=False, + **httplib_request_kw): + """ + Perform a request on a given urllib connection object taken from our + pool. + + :param conn: + a connection from one of our connection pools + + :param timeout: + Socket timeout in seconds for the request. This can be a + float or integer, which will set the same timeout value for + the socket connect and the socket read, or an instance of + :class:`urllib3.util.Timeout`, which gives you more fine-grained + control over your timeouts. + """ + self.num_requests += 1 + + timeout_obj = self._get_timeout(timeout) + timeout_obj.start_connect() + conn.timeout = timeout_obj.connect_timeout + + # Trigger any extra validation we need to do. + try: + self._validate_conn(conn) + except (SocketTimeout, BaseSSLError) as e: + # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout. + self._raise_timeout(err=e, url=url, timeout_value=conn.timeout, + exc_cls=ConnectTimeoutError) + raise + + # Reset the timeout for the recv() on the socket + read_timeout = timeout_obj.read_timeout + + # App Engine doesn't have a sock attr + if getattr(conn, 'sock', None): + # In Python 3 socket.py will catch EAGAIN and return None when you + # try and read into the file pointer created by http.client, which + # instead raises a BadStatusLine exception. Instead of catching + # the exception and assuming all BadStatusLine exceptions are read + # timeouts, check for a zero timeout before making the request. + if read_timeout == 0: + raise ReadTimeoutError( + self, url, "Read timed out. (read timeout=%s)" % read_timeout) + if read_timeout is Timeout.DEFAULT_TIMEOUT: + conn.sock.settimeout(socket.getdefaulttimeout()) + else: # None or a value + conn.sock.settimeout(read_timeout) + + # conn.request() calls httplib.*.request, not the method in + # urllib3.request. It also calls makefile (recv) on the socket. + if chunked: + conn.request_chunked(method, url, **httplib_request_kw) + else: + conn.request(method, url, **httplib_request_kw) + + # Receive the response from the server + try: + try: # Python 2.7, use buffering of HTTP responses + httplib_response = conn.getresponse(buffering=True) + except TypeError: # Python 2.6 and older, Python 3 + try: + httplib_response = conn.getresponse() + except Exception as e: + # Remove the TypeError from the exception chain in Python 3; + # otherwise it looks like a programming error was the cause. + six.raise_from(e, None) + except (SocketTimeout, BaseSSLError, SocketError) as e: + self._raise_timeout(err=e, url=url, timeout_value=read_timeout, + exc_cls=ReadTimeoutError) + raise + + # AppEngine doesn't have a version attr. + http_version = getattr(conn, '_http_vsn_str', 'HTTP/?') + log.debug("%s://%s:%s \"%s %s %s\" %s %s", self.scheme, self.host, self.port, + method, url, http_version, httplib_response.status, + httplib_response.length) + + try: + assert_header_parsing(httplib_response.msg) + except HeaderParsingError as hpe: # Platform-specific: Python 3 + log.warning( + 'Failed to parse headers (url=%s): %s', + self._absolute_url(url), hpe, exc_info=True) + + return httplib_response + + def _absolute_url(self, path): + return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url + + def close(self): + """ + Close all pooled connections and disable the pool. + """ + # Disable access to the pool + old_pool, self.pool = self.pool, None + + try: + while True: + conn = old_pool.get(block=False) + if conn: + conn.close() + + except queue.Empty: + pass # Done. + + def is_same_host(self, url): + """ + Check if the given ``url`` is a member of the same host as this + connection pool. + """ + if url.startswith('/'): + return True + + # TODO: Add optional support for socket.gethostbyname checking. + scheme, host, port = get_host(url) + + host = _ipv6_host(host).lower() + + # Use explicit default port for comparison when none is given + if self.port and not port: + port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None + + return (scheme, host, port) == (self.scheme, self.host, self.port) + + def urlopen(self, method, url, body=None, headers=None, retries=None, + redirect=True, assert_same_host=True, timeout=_Default, + pool_timeout=None, release_conn=None, chunked=False, + body_pos=None, **response_kw): + """ + Get a connection from the pool and perform an HTTP request. This is the + lowest level call for making a request, so you'll need to specify all + the raw details. + + .. note:: + + More commonly, it's appropriate to use a convenience method provided + by :class:`.RequestMethods`, such as :meth:`request`. + + .. note:: + + `release_conn` will only behave as expected if + `preload_content=False` because we want to make + `preload_content=False` the default behaviour someday soon without + breaking backwards compatibility. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param body: + Data to send in the request body (useful for creating + POST requests, see HTTPConnectionPool.post_url for + more convenience). + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + Pass ``None`` to retry until you receive a response. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param assert_same_host: + If ``True``, will make sure that the host of the pool requests is + consistent else will raise HostChangedError. When False, you can + use the pool on an HTTP proxy and request foreign hosts. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param pool_timeout: + If set and the pool is set to block=True, then this method will + block for ``pool_timeout`` seconds and raise EmptyPoolError if no + connection is available within the time period. + + :param release_conn: + If False, then the urlopen call will not release the connection + back into the pool once a response is received (but will release if + you read the entire contents of the response such as when + `preload_content=True`). This is useful if you're not preloading + the response's content immediately. You will need to call + ``r.release_conn()`` on the response ``r`` to return the connection + back into the pool. If None, it takes the value of + ``response_kw.get('preload_content', True)``. + + :param chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param int body_pos: + Position to seek to in file-like body in the event of a retry or + redirect. Typically this won't need to be set because urllib3 will + auto-populate the value when needed. + + :param \\**response_kw: + Additional parameters are passed to + :meth:`urllib3.response.HTTPResponse.from_httplib` + """ + if headers is None: + headers = self.headers + + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) + + if release_conn is None: + release_conn = response_kw.get('preload_content', True) + + # Check host + if assert_same_host and not self.is_same_host(url): + raise HostChangedError(self, url, retries) + + conn = None + + # Track whether `conn` needs to be released before + # returning/raising/recursing. Update this variable if necessary, and + # leave `release_conn` constant throughout the function. That way, if + # the function recurses, the original value of `release_conn` will be + # passed down into the recursive call, and its value will be respected. + # + # See issue #651 [1] for details. + # + # [1] + release_this_conn = release_conn + + # Merge the proxy headers. Only do this in HTTP. We have to copy the + # headers dict so we can safely change it without those changes being + # reflected in anyone else's copy. + if self.scheme == 'http': + headers = headers.copy() + headers.update(self.proxy_headers) + + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + + # Keep track of whether we cleanly exited the except block. This + # ensures we do proper cleanup in finally. + clean_exit = False + + # Rewind body position, if needed. Record current position + # for future rewinds in the event of a redirect/retry. + body_pos = set_file_position(body, body_pos) + + try: + # Request a connection from the queue. + timeout_obj = self._get_timeout(timeout) + conn = self._get_conn(timeout=pool_timeout) + + conn.timeout = timeout_obj.connect_timeout + + is_new_proxy_conn = self.proxy is not None and not getattr(conn, 'sock', None) + if is_new_proxy_conn: + self._prepare_proxy(conn) + + # Make the request on the httplib connection object. + httplib_response = self._make_request(conn, method, url, + timeout=timeout_obj, + body=body, headers=headers, + chunked=chunked) + + # If we're going to release the connection in ``finally:``, then + # the response doesn't need to know about the connection. Otherwise + # it will also try to release it and we'll have a double-release + # mess. + response_conn = conn if not release_conn else None + + # Pass method to Response for length checking + response_kw['request_method'] = method + + # Import httplib's response into our own wrapper object + response = self.ResponseCls.from_httplib(httplib_response, + pool=self, + connection=response_conn, + retries=retries, + **response_kw) + + # Everything went great! + clean_exit = True + + except queue.Empty: + # Timed out by queue. + raise EmptyPoolError(self, "No pool connections are available.") + + except (BaseSSLError, CertificateError) as e: + # Close the connection. If a connection is reused on which there + # was a Certificate error, the next request will certainly raise + # another Certificate error. + clean_exit = False + raise SSLError(e) + + except SSLError: + # Treat SSLError separately from BaseSSLError to preserve + # traceback. + clean_exit = False + raise + + except (TimeoutError, HTTPException, SocketError, ProtocolError) as e: + # Discard the connection for these exceptions. It will be + # be replaced during the next _get_conn() call. + clean_exit = False + + if isinstance(e, (SocketError, NewConnectionError)) and self.proxy: + e = ProxyError('Cannot connect to proxy.', e) + elif isinstance(e, (SocketError, HTTPException)): + e = ProtocolError('Connection aborted.', e) + + retries = retries.increment(method, url, error=e, _pool=self, + _stacktrace=sys.exc_info()[2]) + retries.sleep() + + # Keep track of the error for the retry warning. + err = e + + finally: + if not clean_exit: + # We hit some kind of exception, handled or otherwise. We need + # to throw the connection away unless explicitly told not to. + # Close the connection, set the variable to None, and make sure + # we put the None back in the pool to avoid leaking it. + conn = conn and conn.close() + release_this_conn = True + + if release_this_conn: + # Put the connection back to be reused. If the connection is + # expired then it will be None, which will get replaced with a + # fresh connection during _get_conn. + self._put_conn(conn) + + if not conn: + # Try again + log.warning("Retrying (%r) after connection " + "broken by '%r': %s", retries, err, url) + return self.urlopen(method, url, body, headers, retries, + redirect, assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, body_pos=body_pos, + **response_kw) + + # Handle redirect? + redirect_location = redirect and response.get_redirect_location() + if redirect_location: + if response.status == 303: + method = 'GET' + + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + # Release the connection for this response, since we're not + # returning it to be released manually. + response.release_conn() + raise + return response + + retries.sleep_for_retry(response) + log.debug("Redirecting %s -> %s", url, redirect_location) + return self.urlopen( + method, redirect_location, body, headers, + retries=retries, redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, body_pos=body_pos, + **response_kw) + + # Check if we should retry the HTTP response. + has_retry_after = bool(response.getheader('Retry-After')) + if retries.is_retry(method, response.status, has_retry_after): + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_status: + # Release the connection for this response, since we're not + # returning it to be released manually. + response.release_conn() + raise + return response + retries.sleep(response) + log.debug("Retry: %s", url) + return self.urlopen( + method, url, body, headers, + retries=retries, redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, + body_pos=body_pos, **response_kw) + + return response + + +class HTTPSConnectionPool(HTTPConnectionPool): + """ + Same as :class:`.HTTPConnectionPool`, but HTTPS. + + When Python is compiled with the :mod:`ssl` module, then + :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates, + instead of :class:`.HTTPSConnection`. + + :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``, + ``assert_hostname`` and ``host`` in this order to verify connections. + If ``assert_hostname`` is False, no verification is done. + + The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, + ``ca_cert_dir``, and ``ssl_version`` are only used if :mod:`ssl` is + available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade + the connection socket into an SSL socket. + """ + + scheme = 'https' + ConnectionCls = HTTPSConnection + + def __init__(self, host, port=None, + strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, + block=False, headers=None, retries=None, + _proxy=None, _proxy_headers=None, + key_file=None, cert_file=None, cert_reqs=None, + ca_certs=None, ssl_version=None, + assert_hostname=None, assert_fingerprint=None, + ca_cert_dir=None, **conn_kw): + + HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize, + block, headers, retries, _proxy, _proxy_headers, + **conn_kw) + + if ca_certs and cert_reqs is None: + cert_reqs = 'CERT_REQUIRED' + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.ca_certs = ca_certs + self.ca_cert_dir = ca_cert_dir + self.ssl_version = ssl_version + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def _prepare_conn(self, conn): + """ + Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket` + and establish the tunnel if proxy is used. + """ + + if isinstance(conn, VerifiedHTTPSConnection): + conn.set_cert(key_file=self.key_file, + cert_file=self.cert_file, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint) + conn.ssl_version = self.ssl_version + return conn + + def _prepare_proxy(self, conn): + """ + Establish tunnel connection early, because otherwise httplib + would improperly set Host: header to proxy's IP:port. + """ + # Python 2.7+ + try: + set_tunnel = conn.set_tunnel + except AttributeError: # Platform-specific: Python 2.6 + set_tunnel = conn._set_tunnel + + if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older + set_tunnel(self.host, self.port) + else: + set_tunnel(self.host, self.port, self.proxy_headers) + + conn.connect() + + def _new_conn(self): + """ + Return a fresh :class:`httplib.HTTPSConnection`. + """ + self.num_connections += 1 + log.debug("Starting new HTTPS connection (%d): %s", + self.num_connections, self.host) + + if not self.ConnectionCls or self.ConnectionCls is DummyConnection: + raise SSLError("Can't connect to HTTPS URL because the SSL " + "module is not available.") + + actual_host = self.host + actual_port = self.port + if self.proxy is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + conn = self.ConnectionCls(host=actual_host, port=actual_port, + timeout=self.timeout.connect_timeout, + strict=self.strict, **self.conn_kw) + + return self._prepare_conn(conn) + + def _validate_conn(self, conn): + """ + Called right before a request is made, after the socket is created. + """ + super(HTTPSConnectionPool, self)._validate_conn(conn) + + if not conn.is_verified: + warnings.warn(( + 'Unverified HTTPS request is being made. ' + 'Adding certificate verification is strongly advised. See: ' + 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' + '#ssl-warnings'), + InsecureRequestWarning) + + +def connection_from_url(url, **kw): + """ + Given a url, return an :class:`.ConnectionPool` instance of its host. + + This is a shortcut for not having to parse out the scheme, host, and port + of the url before creating an :class:`.ConnectionPool` instance. + + :param url: + Absolute URL string that must include the scheme. Port is optional. + + :param \\**kw: + Passes additional parameters to the constructor of the appropriate + :class:`.ConnectionPool`. Useful for specifying things like + timeout, maxsize, headers, etc. + + Example:: + + >>> conn = connection_from_url('http://google.com/') + >>> r = conn.request('GET', '/') + """ + scheme, host, port = get_host(url) + port = port or port_by_scheme.get(scheme, 80) + if scheme == 'https': + return HTTPSConnectionPool(host, port=port, **kw) + else: + return HTTPConnectionPool(host, port=port, **kw) + + +def _ipv6_host(host): + """ + Process IPv6 address literals + """ + + # httplib doesn't like it when we include brackets in IPv6 addresses + # Specifically, if we include brackets but also pass the port then + # httplib crazily doubles up the square brackets on the Host header. + # Instead, we need to make sure we never pass ``None`` as the port. + # However, for backward compatibility reasons we can't actually + # *assert* that. See http://bugs.python.org/issue28539 + # + # Also if an IPv6 address literal has a zone identifier, the + # percent sign might be URIencoded, convert it back into ASCII + if host.startswith('[') and host.endswith(']'): + host = host.replace('%25', '%').strip('[]') + return host diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py index 6eb75a0..814b022 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py @@ -1,296 +1,296 @@ -""" -This module provides a pool manager that uses Google App Engine's -`URLFetch Service `_. - -Example usage:: - - from urllib3 import PoolManager - from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox - - if is_appengine_sandbox(): - # AppEngineManager uses AppEngine's URLFetch API behind the scenes - http = AppEngineManager() - else: - # PoolManager uses a socket-level API behind the scenes - http = PoolManager() - - r = http.request('GET', 'https://google.com/') - -There are `limitations `_ to the URLFetch service and it may not be -the best choice for your application. There are three options for using -urllib3 on Google App Engine: - -1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is - cost-effective in many circumstances as long as your usage is within the - limitations. -2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets. - Sockets also have `limitations and restrictions - `_ and have a lower free quota than URLFetch. - To use sockets, be sure to specify the following in your ``app.yaml``:: - - env_variables: - GAE_USE_SOCKETS_HTTPLIB : 'true' - -3. If you are using `App Engine Flexible -`_, you can use the standard -:class:`PoolManager` without any configuration or special environment variables. -""" - -from __future__ import absolute_import -import logging -import os -import warnings -from ..packages.six.moves.urllib.parse import urljoin - -from ..exceptions import ( - HTTPError, - HTTPWarning, - MaxRetryError, - ProtocolError, - TimeoutError, - SSLError -) - -from ..packages.six import BytesIO -from ..request import RequestMethods -from ..response import HTTPResponse -from ..util.timeout import Timeout -from ..util.retry import Retry - -try: - from google.appengine.api import urlfetch -except ImportError: - urlfetch = None - - -log = logging.getLogger(__name__) - - -class AppEnginePlatformWarning(HTTPWarning): - pass - - -class AppEnginePlatformError(HTTPError): - pass - - -class AppEngineManager(RequestMethods): - """ - Connection manager for Google App Engine sandbox applications. - - This manager uses the URLFetch service directly instead of using the - emulated httplib, and is subject to URLFetch limitations as described in - the App Engine documentation `here - `_. - - Notably it will raise an :class:`AppEnginePlatformError` if: - * URLFetch is not available. - * If you attempt to use this on App Engine Flexible, as full socket - support is available. - * If a request size is more than 10 megabytes. - * If a response size is more than 32 megabtyes. - * If you use an unsupported request method such as OPTIONS. - - Beyond those cases, it will raise normal urllib3 errors. - """ - - def __init__(self, headers=None, retries=None, validate_certificate=True, - urlfetch_retries=True): - if not urlfetch: - raise AppEnginePlatformError( - "URLFetch is not available in this environment.") - - if is_prod_appengine_mvms(): - raise AppEnginePlatformError( - "Use normal urllib3.PoolManager instead of AppEngineManager" - "on Managed VMs, as using URLFetch is not necessary in " - "this environment.") - - warnings.warn( - "urllib3 is using URLFetch on Google App Engine sandbox instead " - "of sockets. To use sockets directly instead of URLFetch see " - "https://urllib3.readthedocs.io/en/latest/reference/urllib3.contrib.html.", - AppEnginePlatformWarning) - - RequestMethods.__init__(self, headers) - self.validate_certificate = validate_certificate - self.urlfetch_retries = urlfetch_retries - - self.retries = retries or Retry.DEFAULT - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - # Return False to re-raise any potential exceptions - return False - - def urlopen(self, method, url, body=None, headers=None, - retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT, - **response_kw): - - retries = self._get_retries(retries, redirect) - - try: - follow_redirects = ( - redirect and - retries.redirect != 0 and - retries.total) - response = urlfetch.fetch( - url, - payload=body, - method=method, - headers=headers or {}, - allow_truncated=False, - follow_redirects=self.urlfetch_retries and follow_redirects, - deadline=self._get_absolute_timeout(timeout), - validate_certificate=self.validate_certificate, - ) - except urlfetch.DeadlineExceededError as e: - raise TimeoutError(self, e) - - except urlfetch.InvalidURLError as e: - if 'too large' in str(e): - raise AppEnginePlatformError( - "URLFetch request too large, URLFetch only " - "supports requests up to 10mb in size.", e) - raise ProtocolError(e) - - except urlfetch.DownloadError as e: - if 'Too many redirects' in str(e): - raise MaxRetryError(self, url, reason=e) - raise ProtocolError(e) - - except urlfetch.ResponseTooLargeError as e: - raise AppEnginePlatformError( - "URLFetch response too large, URLFetch only supports" - "responses up to 32mb in size.", e) - - except urlfetch.SSLCertificateError as e: - raise SSLError(e) - - except urlfetch.InvalidMethodError as e: - raise AppEnginePlatformError( - "URLFetch does not support method: %s" % method, e) - - http_response = self._urlfetch_response_to_http_response( - response, retries=retries, **response_kw) - - # Handle redirect? - redirect_location = redirect and http_response.get_redirect_location() - if redirect_location: - # Check for redirect response - if (self.urlfetch_retries and retries.raise_on_redirect): - raise MaxRetryError(self, url, "too many redirects") - else: - if http_response.status == 303: - method = 'GET' - - try: - retries = retries.increment(method, url, response=http_response, _pool=self) - except MaxRetryError: - if retries.raise_on_redirect: - raise MaxRetryError(self, url, "too many redirects") - return http_response - - retries.sleep_for_retry(http_response) - log.debug("Redirecting %s -> %s", url, redirect_location) - redirect_url = urljoin(url, redirect_location) - return self.urlopen( - method, redirect_url, body, headers, - retries=retries, redirect=redirect, - timeout=timeout, **response_kw) - - # Check if we should retry the HTTP response. - has_retry_after = bool(http_response.getheader('Retry-After')) - if retries.is_retry(method, http_response.status, has_retry_after): - retries = retries.increment( - method, url, response=http_response, _pool=self) - log.debug("Retry: %s", url) - retries.sleep(http_response) - return self.urlopen( - method, url, - body=body, headers=headers, - retries=retries, redirect=redirect, - timeout=timeout, **response_kw) - - return http_response - - def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw): - - if is_prod_appengine(): - # Production GAE handles deflate encoding automatically, but does - # not remove the encoding header. - content_encoding = urlfetch_resp.headers.get('content-encoding') - - if content_encoding == 'deflate': - del urlfetch_resp.headers['content-encoding'] - - transfer_encoding = urlfetch_resp.headers.get('transfer-encoding') - # We have a full response's content, - # so let's make sure we don't report ourselves as chunked data. - if transfer_encoding == 'chunked': - encodings = transfer_encoding.split(",") - encodings.remove('chunked') - urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings) - - return HTTPResponse( - # In order for decoding to work, we must present the content as - # a file-like object. - body=BytesIO(urlfetch_resp.content), - headers=urlfetch_resp.headers, - status=urlfetch_resp.status_code, - **response_kw - ) - - def _get_absolute_timeout(self, timeout): - if timeout is Timeout.DEFAULT_TIMEOUT: - return None # Defer to URLFetch's default. - if isinstance(timeout, Timeout): - if timeout._read is not None or timeout._connect is not None: - warnings.warn( - "URLFetch does not support granular timeout settings, " - "reverting to total or default URLFetch timeout.", - AppEnginePlatformWarning) - return timeout.total - return timeout - - def _get_retries(self, retries, redirect): - if not isinstance(retries, Retry): - retries = Retry.from_int( - retries, redirect=redirect, default=self.retries) - - if retries.connect or retries.read or retries.redirect: - warnings.warn( - "URLFetch only supports total retries and does not " - "recognize connect, read, or redirect retry parameters.", - AppEnginePlatformWarning) - - return retries - - -def is_appengine(): - return (is_local_appengine() or - is_prod_appengine() or - is_prod_appengine_mvms()) - - -def is_appengine_sandbox(): - return is_appengine() and not is_prod_appengine_mvms() - - -def is_local_appengine(): - return ('APPENGINE_RUNTIME' in os.environ and - 'Development/' in os.environ['SERVER_SOFTWARE']) - - -def is_prod_appengine(): - return ('APPENGINE_RUNTIME' in os.environ and - 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and - not is_prod_appengine_mvms()) - - -def is_prod_appengine_mvms(): - return os.environ.get('GAE_VM', False) == 'true' +""" +This module provides a pool manager that uses Google App Engine's +`URLFetch Service `_. + +Example usage:: + + from urllib3 import PoolManager + from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox + + if is_appengine_sandbox(): + # AppEngineManager uses AppEngine's URLFetch API behind the scenes + http = AppEngineManager() + else: + # PoolManager uses a socket-level API behind the scenes + http = PoolManager() + + r = http.request('GET', 'https://google.com/') + +There are `limitations `_ to the URLFetch service and it may not be +the best choice for your application. There are three options for using +urllib3 on Google App Engine: + +1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is + cost-effective in many circumstances as long as your usage is within the + limitations. +2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets. + Sockets also have `limitations and restrictions + `_ and have a lower free quota than URLFetch. + To use sockets, be sure to specify the following in your ``app.yaml``:: + + env_variables: + GAE_USE_SOCKETS_HTTPLIB : 'true' + +3. If you are using `App Engine Flexible +`_, you can use the standard +:class:`PoolManager` without any configuration or special environment variables. +""" + +from __future__ import absolute_import +import logging +import os +import warnings +from ..packages.six.moves.urllib.parse import urljoin + +from ..exceptions import ( + HTTPError, + HTTPWarning, + MaxRetryError, + ProtocolError, + TimeoutError, + SSLError +) + +from ..packages.six import BytesIO +from ..request import RequestMethods +from ..response import HTTPResponse +from ..util.timeout import Timeout +from ..util.retry import Retry + +try: + from google.appengine.api import urlfetch +except ImportError: + urlfetch = None + + +log = logging.getLogger(__name__) + + +class AppEnginePlatformWarning(HTTPWarning): + pass + + +class AppEnginePlatformError(HTTPError): + pass + + +class AppEngineManager(RequestMethods): + """ + Connection manager for Google App Engine sandbox applications. + + This manager uses the URLFetch service directly instead of using the + emulated httplib, and is subject to URLFetch limitations as described in + the App Engine documentation `here + `_. + + Notably it will raise an :class:`AppEnginePlatformError` if: + * URLFetch is not available. + * If you attempt to use this on App Engine Flexible, as full socket + support is available. + * If a request size is more than 10 megabytes. + * If a response size is more than 32 megabtyes. + * If you use an unsupported request method such as OPTIONS. + + Beyond those cases, it will raise normal urllib3 errors. + """ + + def __init__(self, headers=None, retries=None, validate_certificate=True, + urlfetch_retries=True): + if not urlfetch: + raise AppEnginePlatformError( + "URLFetch is not available in this environment.") + + if is_prod_appengine_mvms(): + raise AppEnginePlatformError( + "Use normal urllib3.PoolManager instead of AppEngineManager" + "on Managed VMs, as using URLFetch is not necessary in " + "this environment.") + + warnings.warn( + "urllib3 is using URLFetch on Google App Engine sandbox instead " + "of sockets. To use sockets directly instead of URLFetch see " + "https://urllib3.readthedocs.io/en/latest/reference/urllib3.contrib.html.", + AppEnginePlatformWarning) + + RequestMethods.__init__(self, headers) + self.validate_certificate = validate_certificate + self.urlfetch_retries = urlfetch_retries + + self.retries = retries or Retry.DEFAULT + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Return False to re-raise any potential exceptions + return False + + def urlopen(self, method, url, body=None, headers=None, + retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT, + **response_kw): + + retries = self._get_retries(retries, redirect) + + try: + follow_redirects = ( + redirect and + retries.redirect != 0 and + retries.total) + response = urlfetch.fetch( + url, + payload=body, + method=method, + headers=headers or {}, + allow_truncated=False, + follow_redirects=self.urlfetch_retries and follow_redirects, + deadline=self._get_absolute_timeout(timeout), + validate_certificate=self.validate_certificate, + ) + except urlfetch.DeadlineExceededError as e: + raise TimeoutError(self, e) + + except urlfetch.InvalidURLError as e: + if 'too large' in str(e): + raise AppEnginePlatformError( + "URLFetch request too large, URLFetch only " + "supports requests up to 10mb in size.", e) + raise ProtocolError(e) + + except urlfetch.DownloadError as e: + if 'Too many redirects' in str(e): + raise MaxRetryError(self, url, reason=e) + raise ProtocolError(e) + + except urlfetch.ResponseTooLargeError as e: + raise AppEnginePlatformError( + "URLFetch response too large, URLFetch only supports" + "responses up to 32mb in size.", e) + + except urlfetch.SSLCertificateError as e: + raise SSLError(e) + + except urlfetch.InvalidMethodError as e: + raise AppEnginePlatformError( + "URLFetch does not support method: %s" % method, e) + + http_response = self._urlfetch_response_to_http_response( + response, retries=retries, **response_kw) + + # Handle redirect? + redirect_location = redirect and http_response.get_redirect_location() + if redirect_location: + # Check for redirect response + if (self.urlfetch_retries and retries.raise_on_redirect): + raise MaxRetryError(self, url, "too many redirects") + else: + if http_response.status == 303: + method = 'GET' + + try: + retries = retries.increment(method, url, response=http_response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + raise MaxRetryError(self, url, "too many redirects") + return http_response + + retries.sleep_for_retry(http_response) + log.debug("Redirecting %s -> %s", url, redirect_location) + redirect_url = urljoin(url, redirect_location) + return self.urlopen( + method, redirect_url, body, headers, + retries=retries, redirect=redirect, + timeout=timeout, **response_kw) + + # Check if we should retry the HTTP response. + has_retry_after = bool(http_response.getheader('Retry-After')) + if retries.is_retry(method, http_response.status, has_retry_after): + retries = retries.increment( + method, url, response=http_response, _pool=self) + log.debug("Retry: %s", url) + retries.sleep(http_response) + return self.urlopen( + method, url, + body=body, headers=headers, + retries=retries, redirect=redirect, + timeout=timeout, **response_kw) + + return http_response + + def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw): + + if is_prod_appengine(): + # Production GAE handles deflate encoding automatically, but does + # not remove the encoding header. + content_encoding = urlfetch_resp.headers.get('content-encoding') + + if content_encoding == 'deflate': + del urlfetch_resp.headers['content-encoding'] + + transfer_encoding = urlfetch_resp.headers.get('transfer-encoding') + # We have a full response's content, + # so let's make sure we don't report ourselves as chunked data. + if transfer_encoding == 'chunked': + encodings = transfer_encoding.split(",") + encodings.remove('chunked') + urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings) + + return HTTPResponse( + # In order for decoding to work, we must present the content as + # a file-like object. + body=BytesIO(urlfetch_resp.content), + headers=urlfetch_resp.headers, + status=urlfetch_resp.status_code, + **response_kw + ) + + def _get_absolute_timeout(self, timeout): + if timeout is Timeout.DEFAULT_TIMEOUT: + return None # Defer to URLFetch's default. + if isinstance(timeout, Timeout): + if timeout._read is not None or timeout._connect is not None: + warnings.warn( + "URLFetch does not support granular timeout settings, " + "reverting to total or default URLFetch timeout.", + AppEnginePlatformWarning) + return timeout.total + return timeout + + def _get_retries(self, retries, redirect): + if not isinstance(retries, Retry): + retries = Retry.from_int( + retries, redirect=redirect, default=self.retries) + + if retries.connect or retries.read or retries.redirect: + warnings.warn( + "URLFetch only supports total retries and does not " + "recognize connect, read, or redirect retry parameters.", + AppEnginePlatformWarning) + + return retries + + +def is_appengine(): + return (is_local_appengine() or + is_prod_appengine() or + is_prod_appengine_mvms()) + + +def is_appengine_sandbox(): + return is_appengine() and not is_prod_appengine_mvms() + + +def is_local_appengine(): + return ('APPENGINE_RUNTIME' in os.environ and + 'Development/' in os.environ['SERVER_SOFTWARE']) + + +def is_prod_appengine(): + return ('APPENGINE_RUNTIME' in os.environ and + 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and + not is_prod_appengine_mvms()) + + +def is_prod_appengine_mvms(): + return os.environ.get('GAE_VM', False) == 'true' diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py index 888e0ad..642e99e 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py @@ -1,112 +1,112 @@ -""" -NTLM authenticating pool, contributed by erikcederstran - -Issue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10 -""" -from __future__ import absolute_import - -from logging import getLogger -from ntlm import ntlm - -from .. import HTTPSConnectionPool -from ..packages.six.moves.http_client import HTTPSConnection - - -log = getLogger(__name__) - - -class NTLMConnectionPool(HTTPSConnectionPool): - """ - Implements an NTLM authentication version of an urllib3 connection pool - """ - - scheme = 'https' - - def __init__(self, user, pw, authurl, *args, **kwargs): - """ - authurl is a random URL on the server that is protected by NTLM. - user is the Windows user, probably in the DOMAIN\\username format. - pw is the password for the user. - """ - super(NTLMConnectionPool, self).__init__(*args, **kwargs) - self.authurl = authurl - self.rawuser = user - user_parts = user.split('\\', 1) - self.domain = user_parts[0].upper() - self.user = user_parts[1] - self.pw = pw - - def _new_conn(self): - # Performs the NTLM handshake that secures the connection. The socket - # must be kept open while requests are performed. - self.num_connections += 1 - log.debug('Starting NTLM HTTPS connection no. %d: https://%s%s', - self.num_connections, self.host, self.authurl) - - headers = {} - headers['Connection'] = 'Keep-Alive' - req_header = 'Authorization' - resp_header = 'www-authenticate' - - conn = HTTPSConnection(host=self.host, port=self.port) - - # Send negotiation message - headers[req_header] = ( - 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.rawuser)) - log.debug('Request headers: %s', headers) - conn.request('GET', self.authurl, None, headers) - res = conn.getresponse() - reshdr = dict(res.getheaders()) - log.debug('Response status: %s %s', res.status, res.reason) - log.debug('Response headers: %s', reshdr) - log.debug('Response data: %s [...]', res.read(100)) - - # Remove the reference to the socket, so that it can not be closed by - # the response object (we want to keep the socket open) - res.fp = None - - # Server should respond with a challenge message - auth_header_values = reshdr[resp_header].split(', ') - auth_header_value = None - for s in auth_header_values: - if s[:5] == 'NTLM ': - auth_header_value = s[5:] - if auth_header_value is None: - raise Exception('Unexpected %s response header: %s' % - (resp_header, reshdr[resp_header])) - - # Send authentication message - ServerChallenge, NegotiateFlags = \ - ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value) - auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, - self.user, - self.domain, - self.pw, - NegotiateFlags) - headers[req_header] = 'NTLM %s' % auth_msg - log.debug('Request headers: %s', headers) - conn.request('GET', self.authurl, None, headers) - res = conn.getresponse() - log.debug('Response status: %s %s', res.status, res.reason) - log.debug('Response headers: %s', dict(res.getheaders())) - log.debug('Response data: %s [...]', res.read()[:100]) - if res.status != 200: - if res.status == 401: - raise Exception('Server rejected request: wrong ' - 'username or password') - raise Exception('Wrong server response: %s %s' % - (res.status, res.reason)) - - res.fp = None - log.debug('Connection established') - return conn - - def urlopen(self, method, url, body=None, headers=None, retries=3, - redirect=True, assert_same_host=True): - if headers is None: - headers = {} - headers['Connection'] = 'Keep-Alive' - return super(NTLMConnectionPool, self).urlopen(method, url, body, - headers, retries, - redirect, - assert_same_host) +""" +NTLM authenticating pool, contributed by erikcederstran + +Issue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10 +""" +from __future__ import absolute_import + +from logging import getLogger +from ntlm import ntlm + +from .. import HTTPSConnectionPool +from ..packages.six.moves.http_client import HTTPSConnection + + +log = getLogger(__name__) + + +class NTLMConnectionPool(HTTPSConnectionPool): + """ + Implements an NTLM authentication version of an urllib3 connection pool + """ + + scheme = 'https' + + def __init__(self, user, pw, authurl, *args, **kwargs): + """ + authurl is a random URL on the server that is protected by NTLM. + user is the Windows user, probably in the DOMAIN\\username format. + pw is the password for the user. + """ + super(NTLMConnectionPool, self).__init__(*args, **kwargs) + self.authurl = authurl + self.rawuser = user + user_parts = user.split('\\', 1) + self.domain = user_parts[0].upper() + self.user = user_parts[1] + self.pw = pw + + def _new_conn(self): + # Performs the NTLM handshake that secures the connection. The socket + # must be kept open while requests are performed. + self.num_connections += 1 + log.debug('Starting NTLM HTTPS connection no. %d: https://%s%s', + self.num_connections, self.host, self.authurl) + + headers = {} + headers['Connection'] = 'Keep-Alive' + req_header = 'Authorization' + resp_header = 'www-authenticate' + + conn = HTTPSConnection(host=self.host, port=self.port) + + # Send negotiation message + headers[req_header] = ( + 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.rawuser)) + log.debug('Request headers: %s', headers) + conn.request('GET', self.authurl, None, headers) + res = conn.getresponse() + reshdr = dict(res.getheaders()) + log.debug('Response status: %s %s', res.status, res.reason) + log.debug('Response headers: %s', reshdr) + log.debug('Response data: %s [...]', res.read(100)) + + # Remove the reference to the socket, so that it can not be closed by + # the response object (we want to keep the socket open) + res.fp = None + + # Server should respond with a challenge message + auth_header_values = reshdr[resp_header].split(', ') + auth_header_value = None + for s in auth_header_values: + if s[:5] == 'NTLM ': + auth_header_value = s[5:] + if auth_header_value is None: + raise Exception('Unexpected %s response header: %s' % + (resp_header, reshdr[resp_header])) + + # Send authentication message + ServerChallenge, NegotiateFlags = \ + ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value) + auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, + self.user, + self.domain, + self.pw, + NegotiateFlags) + headers[req_header] = 'NTLM %s' % auth_msg + log.debug('Request headers: %s', headers) + conn.request('GET', self.authurl, None, headers) + res = conn.getresponse() + log.debug('Response status: %s %s', res.status, res.reason) + log.debug('Response headers: %s', dict(res.getheaders())) + log.debug('Response data: %s [...]', res.read()[:100]) + if res.status != 200: + if res.status == 401: + raise Exception('Server rejected request: wrong ' + 'username or password') + raise Exception('Wrong server response: %s %s' % + (res.status, res.reason)) + + res.fp = None + log.debug('Connection established') + return conn + + def urlopen(self, method, url, body=None, headers=None, retries=3, + redirect=True, assert_same_host=True): + if headers is None: + headers = {} + headers['Connection'] = 'Keep-Alive' + return super(NTLMConnectionPool, self).urlopen(method, url, body, + headers, retries, + redirect, + assert_same_host) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py index 9c789f0..eb4d476 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py @@ -1,450 +1,450 @@ -""" -SSL with SNI_-support for Python 2. Follow these instructions if you would -like to verify SSL certificates in Python 2. Note, the default libraries do -*not* do certificate checking; you need to do additional work to validate -certificates yourself. - -This needs the following packages installed: - -* pyOpenSSL (tested with 16.0.0) -* cryptography (minimum 1.3.4, from pyopenssl) -* idna (minimum 2.0, from cryptography) - -However, pyopenssl depends on cryptography, which depends on idna, so while we -use all three directly here we end up having relatively few packages required. - -You can install them with the following command: - - pip install pyopenssl cryptography idna - -To activate certificate checking, call -:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code -before you begin making HTTP requests. This can be done in a ``sitecustomize`` -module, or at any other time before your application begins using ``urllib3``, -like this:: - - try: - import urllib3.contrib.pyopenssl - urllib3.contrib.pyopenssl.inject_into_urllib3() - except ImportError: - pass - -Now you can use :mod:`urllib3` as you normally would, and it will support SNI -when the required modules are installed. - -Activating this module also has the positive side effect of disabling SSL/TLS -compression in Python 2 (see `CRIME attack`_). - -If you want to configure the default list of supported cipher suites, you can -set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable. - -.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication -.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit) -""" -from __future__ import absolute_import - -import OpenSSL.SSL -from cryptography import x509 -from cryptography.hazmat.backends.openssl import backend as openssl_backend -from cryptography.hazmat.backends.openssl.x509 import _Certificate - -from socket import timeout, error as SocketError -from io import BytesIO - -try: # Platform-specific: Python 2 - from socket import _fileobject -except ImportError: # Platform-specific: Python 3 - _fileobject = None - from ..packages.backports.makefile import backport_makefile - -import logging -import ssl -import six -import sys - -from .. import util - -__all__ = ['inject_into_urllib3', 'extract_from_urllib3'] - -# SNI always works. -HAS_SNI = True - -# Map from urllib3 to PyOpenSSL compatible parameter-values. -_openssl_versions = { - ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD, - ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, -} - -if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'): - _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD - -if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): - _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD - -try: - _openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD}) -except AttributeError: - pass - -_stdlib_to_openssl_verify = { - ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, - ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, - ssl.CERT_REQUIRED: - OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, -} -_openssl_to_stdlib_verify = dict( - (v, k) for k, v in _stdlib_to_openssl_verify.items() -) - -# OpenSSL will only write 16K at a time -SSL_WRITE_BLOCKSIZE = 16384 - -orig_util_HAS_SNI = util.HAS_SNI -orig_util_SSLContext = util.ssl_.SSLContext - - -log = logging.getLogger(__name__) - - -def inject_into_urllib3(): - 'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.' - - _validate_dependencies_met() - - util.ssl_.SSLContext = PyOpenSSLContext - util.HAS_SNI = HAS_SNI - util.ssl_.HAS_SNI = HAS_SNI - util.IS_PYOPENSSL = True - util.ssl_.IS_PYOPENSSL = True - - -def extract_from_urllib3(): - 'Undo monkey-patching by :func:`inject_into_urllib3`.' - - util.ssl_.SSLContext = orig_util_SSLContext - util.HAS_SNI = orig_util_HAS_SNI - util.ssl_.HAS_SNI = orig_util_HAS_SNI - util.IS_PYOPENSSL = False - util.ssl_.IS_PYOPENSSL = False - - -def _validate_dependencies_met(): - """ - Verifies that PyOpenSSL's package-level dependencies have been met. - Throws `ImportError` if they are not met. - """ - # Method added in `cryptography==1.1`; not available in older versions - from cryptography.x509.extensions import Extensions - if getattr(Extensions, "get_extension_for_class", None) is None: - raise ImportError("'cryptography' module missing required functionality. " - "Try upgrading to v1.3.4 or newer.") - - # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 - # attribute is only present on those versions. - from OpenSSL.crypto import X509 - x509 = X509() - if getattr(x509, "_x509", None) is None: - raise ImportError("'pyOpenSSL' module missing required functionality. " - "Try upgrading to v0.14 or newer.") - - -def _dnsname_to_stdlib(name): - """ - Converts a dNSName SubjectAlternativeName field to the form used by the - standard library on the given Python version. - - Cryptography produces a dNSName as a unicode string that was idna-decoded - from ASCII bytes. We need to idna-encode that string to get it back, and - then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib - uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). - """ - def idna_encode(name): - """ - Borrowed wholesale from the Python Cryptography Project. It turns out - that we can't just safely call `idna.encode`: it can explode for - wildcard names. This avoids that problem. - """ - import idna - - for prefix in [u'*.', u'.']: - if name.startswith(prefix): - name = name[len(prefix):] - return prefix.encode('ascii') + idna.encode(name) - return idna.encode(name) - - name = idna_encode(name) - if sys.version_info >= (3, 0): - name = name.decode('utf-8') - return name - - -def get_subj_alt_name(peer_cert): - """ - Given an PyOpenSSL certificate, provides all the subject alternative names. - """ - # Pass the cert to cryptography, which has much better APIs for this. - # This is technically using private APIs, but should work across all - # relevant versions until PyOpenSSL gets something proper for this. - cert = _Certificate(openssl_backend, peer_cert._x509) - - # We want to find the SAN extension. Ask Cryptography to locate it (it's - # faster than looping in Python) - try: - ext = cert.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ).value - except x509.ExtensionNotFound: - # No such extension, return the empty list. - return [] - except (x509.DuplicateExtension, x509.UnsupportedExtension, - x509.UnsupportedGeneralNameType, UnicodeError) as e: - # A problem has been found with the quality of the certificate. Assume - # no SAN field is present. - log.warning( - "A problem was encountered with the certificate that prevented " - "urllib3 from finding the SubjectAlternativeName field. This can " - "affect certificate validation. The error was %s", - e, - ) - return [] - - # We want to return dNSName and iPAddress fields. We need to cast the IPs - # back to strings because the match_hostname function wants them as - # strings. - # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 - # decoded. This is pretty frustrating, but that's what the standard library - # does with certificates, and so we need to attempt to do the same. - names = [ - ('DNS', _dnsname_to_stdlib(name)) - for name in ext.get_values_for_type(x509.DNSName) - ] - names.extend( - ('IP Address', str(name)) - for name in ext.get_values_for_type(x509.IPAddress) - ) - - return names - - -class WrappedSocket(object): - '''API-compatibility wrapper for Python OpenSSL's Connection-class. - - Note: _makefile_refs, _drop() and _reuse() are needed for the garbage - collector of pypy. - ''' - - def __init__(self, connection, socket, suppress_ragged_eofs=True): - self.connection = connection - self.socket = socket - self.suppress_ragged_eofs = suppress_ragged_eofs - self._makefile_refs = 0 - self._closed = False - - def fileno(self): - return self.socket.fileno() - - # Copy-pasted from Python 3.5 source code - def _decref_socketios(self): - if self._makefile_refs > 0: - self._makefile_refs -= 1 - if self._closed: - self.close() - - def recv(self, *args, **kwargs): - try: - data = self.connection.recv(*args, **kwargs) - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): - return b'' - else: - raise SocketError(str(e)) - except OpenSSL.SSL.ZeroReturnError as e: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return b'' - else: - raise - except OpenSSL.SSL.WantReadError: - rd = util.wait_for_read(self.socket, self.socket.gettimeout()) - if not rd: - raise timeout('The read operation timed out') - else: - return self.recv(*args, **kwargs) - else: - return data - - def recv_into(self, *args, **kwargs): - try: - return self.connection.recv_into(*args, **kwargs) - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): - return 0 - else: - raise SocketError(str(e)) - except OpenSSL.SSL.ZeroReturnError as e: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return 0 - else: - raise - except OpenSSL.SSL.WantReadError: - rd = util.wait_for_read(self.socket, self.socket.gettimeout()) - if not rd: - raise timeout('The read operation timed out') - else: - return self.recv_into(*args, **kwargs) - - def settimeout(self, timeout): - return self.socket.settimeout(timeout) - - def _send_until_done(self, data): - while True: - try: - return self.connection.send(data) - except OpenSSL.SSL.WantWriteError: - wr = util.wait_for_write(self.socket, self.socket.gettimeout()) - if not wr: - raise timeout() - continue - - def sendall(self, data): - total_sent = 0 - while total_sent < len(data): - sent = self._send_until_done(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE]) - total_sent += sent - - def shutdown(self): - # FIXME rethrow compatible exceptions should we ever use this - self.connection.shutdown() - - def close(self): - if self._makefile_refs < 1: - try: - self._closed = True - return self.connection.close() - except OpenSSL.SSL.Error: - return - else: - self._makefile_refs -= 1 - - def getpeercert(self, binary_form=False): - x509 = self.connection.get_peer_certificate() - - if not x509: - return x509 - - if binary_form: - return OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, - x509) - - return { - 'subject': ( - (('commonName', x509.get_subject().CN),), - ), - 'subjectAltName': get_subj_alt_name(x509) - } - - def _reuse(self): - self._makefile_refs += 1 - - def _drop(self): - if self._makefile_refs < 1: - self.close() - else: - self._makefile_refs -= 1 - - -if _fileobject: # Platform-specific: Python 2 - def makefile(self, mode, bufsize=-1): - self._makefile_refs += 1 - return _fileobject(self, mode, bufsize, close=True) -else: # Platform-specific: Python 3 - makefile = backport_makefile - -WrappedSocket.makefile = makefile - - -class PyOpenSSLContext(object): - """ - I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible - for translating the interface of the standard library ``SSLContext`` object - to calls into PyOpenSSL. - """ - def __init__(self, protocol): - self.protocol = _openssl_versions[protocol] - self._ctx = OpenSSL.SSL.Context(self.protocol) - self._options = 0 - self.check_hostname = False - - @property - def options(self): - return self._options - - @options.setter - def options(self, value): - self._options = value - self._ctx.set_options(value) - - @property - def verify_mode(self): - return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] - - @verify_mode.setter - def verify_mode(self, value): - self._ctx.set_verify( - _stdlib_to_openssl_verify[value], - _verify_callback - ) - - def set_default_verify_paths(self): - self._ctx.set_default_verify_paths() - - def set_ciphers(self, ciphers): - if isinstance(ciphers, six.text_type): - ciphers = ciphers.encode('utf-8') - self._ctx.set_cipher_list(ciphers) - - def load_verify_locations(self, cafile=None, capath=None, cadata=None): - if cafile is not None: - cafile = cafile.encode('utf-8') - if capath is not None: - capath = capath.encode('utf-8') - self._ctx.load_verify_locations(cafile, capath) - if cadata is not None: - self._ctx.load_verify_locations(BytesIO(cadata)) - - def load_cert_chain(self, certfile, keyfile=None, password=None): - self._ctx.use_certificate_file(certfile) - if password is not None: - self._ctx.set_passwd_cb(lambda max_length, prompt_twice, userdata: password) - self._ctx.use_privatekey_file(keyfile or certfile) - - def wrap_socket(self, sock, server_side=False, - do_handshake_on_connect=True, suppress_ragged_eofs=True, - server_hostname=None): - cnx = OpenSSL.SSL.Connection(self._ctx, sock) - - if isinstance(server_hostname, six.text_type): # Platform-specific: Python 3 - server_hostname = server_hostname.encode('utf-8') - - if server_hostname is not None: - cnx.set_tlsext_host_name(server_hostname) - - cnx.set_connect_state() - - while True: - try: - cnx.do_handshake() - except OpenSSL.SSL.WantReadError: - rd = util.wait_for_read(sock, sock.gettimeout()) - if not rd: - raise timeout('select timed out') - continue - except OpenSSL.SSL.Error as e: - raise ssl.SSLError('bad handshake: %r' % e) - break - - return WrappedSocket(cnx, sock) - - -def _verify_callback(cnx, x509, err_no, err_depth, return_code): - return err_no == 0 +""" +SSL with SNI_-support for Python 2. Follow these instructions if you would +like to verify SSL certificates in Python 2. Note, the default libraries do +*not* do certificate checking; you need to do additional work to validate +certificates yourself. + +This needs the following packages installed: + +* pyOpenSSL (tested with 16.0.0) +* cryptography (minimum 1.3.4, from pyopenssl) +* idna (minimum 2.0, from cryptography) + +However, pyopenssl depends on cryptography, which depends on idna, so while we +use all three directly here we end up having relatively few packages required. + +You can install them with the following command: + + pip install pyopenssl cryptography idna + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this:: + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +Now you can use :mod:`urllib3` as you normally would, and it will support SNI +when the required modules are installed. + +Activating this module also has the positive side effect of disabling SSL/TLS +compression in Python 2 (see `CRIME attack`_). + +If you want to configure the default list of supported cipher suites, you can +set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable. + +.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication +.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit) +""" +from __future__ import absolute_import + +import OpenSSL.SSL +from cryptography import x509 +from cryptography.hazmat.backends.openssl import backend as openssl_backend +from cryptography.hazmat.backends.openssl.x509 import _Certificate + +from socket import timeout, error as SocketError +from io import BytesIO + +try: # Platform-specific: Python 2 + from socket import _fileobject +except ImportError: # Platform-specific: Python 3 + _fileobject = None + from ..packages.backports.makefile import backport_makefile + +import logging +import ssl +import six +import sys + +from .. import util + +__all__ = ['inject_into_urllib3', 'extract_from_urllib3'] + +# SNI always works. +HAS_SNI = True + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions = { + ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD, + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} + +if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'): + _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD + +if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): + _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD + +try: + _openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD}) +except AttributeError: + pass + +_stdlib_to_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: + OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} +_openssl_to_stdlib_verify = dict( + (v, k) for k, v in _stdlib_to_openssl_verify.items() +) + +# OpenSSL will only write 16K at a time +SSL_WRITE_BLOCKSIZE = 16384 + +orig_util_HAS_SNI = util.HAS_SNI +orig_util_SSLContext = util.ssl_.SSLContext + + +log = logging.getLogger(__name__) + + +def inject_into_urllib3(): + 'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.' + + _validate_dependencies_met() + + util.ssl_.SSLContext = PyOpenSSLContext + util.HAS_SNI = HAS_SNI + util.ssl_.HAS_SNI = HAS_SNI + util.IS_PYOPENSSL = True + util.ssl_.IS_PYOPENSSL = True + + +def extract_from_urllib3(): + 'Undo monkey-patching by :func:`inject_into_urllib3`.' + + util.ssl_.SSLContext = orig_util_SSLContext + util.HAS_SNI = orig_util_HAS_SNI + util.ssl_.HAS_SNI = orig_util_HAS_SNI + util.IS_PYOPENSSL = False + util.ssl_.IS_PYOPENSSL = False + + +def _validate_dependencies_met(): + """ + Verifies that PyOpenSSL's package-level dependencies have been met. + Throws `ImportError` if they are not met. + """ + # Method added in `cryptography==1.1`; not available in older versions + from cryptography.x509.extensions import Extensions + if getattr(Extensions, "get_extension_for_class", None) is None: + raise ImportError("'cryptography' module missing required functionality. " + "Try upgrading to v1.3.4 or newer.") + + # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 + # attribute is only present on those versions. + from OpenSSL.crypto import X509 + x509 = X509() + if getattr(x509, "_x509", None) is None: + raise ImportError("'pyOpenSSL' module missing required functionality. " + "Try upgrading to v0.14 or newer.") + + +def _dnsname_to_stdlib(name): + """ + Converts a dNSName SubjectAlternativeName field to the form used by the + standard library on the given Python version. + + Cryptography produces a dNSName as a unicode string that was idna-decoded + from ASCII bytes. We need to idna-encode that string to get it back, and + then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib + uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). + """ + def idna_encode(name): + """ + Borrowed wholesale from the Python Cryptography Project. It turns out + that we can't just safely call `idna.encode`: it can explode for + wildcard names. This avoids that problem. + """ + import idna + + for prefix in [u'*.', u'.']: + if name.startswith(prefix): + name = name[len(prefix):] + return prefix.encode('ascii') + idna.encode(name) + return idna.encode(name) + + name = idna_encode(name) + if sys.version_info >= (3, 0): + name = name.decode('utf-8') + return name + + +def get_subj_alt_name(peer_cert): + """ + Given an PyOpenSSL certificate, provides all the subject alternative names. + """ + # Pass the cert to cryptography, which has much better APIs for this. + # This is technically using private APIs, but should work across all + # relevant versions until PyOpenSSL gets something proper for this. + cert = _Certificate(openssl_backend, peer_cert._x509) + + # We want to find the SAN extension. Ask Cryptography to locate it (it's + # faster than looping in Python) + try: + ext = cert.extensions.get_extension_for_class( + x509.SubjectAlternativeName + ).value + except x509.ExtensionNotFound: + # No such extension, return the empty list. + return [] + except (x509.DuplicateExtension, x509.UnsupportedExtension, + x509.UnsupportedGeneralNameType, UnicodeError) as e: + # A problem has been found with the quality of the certificate. Assume + # no SAN field is present. + log.warning( + "A problem was encountered with the certificate that prevented " + "urllib3 from finding the SubjectAlternativeName field. This can " + "affect certificate validation. The error was %s", + e, + ) + return [] + + # We want to return dNSName and iPAddress fields. We need to cast the IPs + # back to strings because the match_hostname function wants them as + # strings. + # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 + # decoded. This is pretty frustrating, but that's what the standard library + # does with certificates, and so we need to attempt to do the same. + names = [ + ('DNS', _dnsname_to_stdlib(name)) + for name in ext.get_values_for_type(x509.DNSName) + ] + names.extend( + ('IP Address', str(name)) + for name in ext.get_values_for_type(x509.IPAddress) + ) + + return names + + +class WrappedSocket(object): + '''API-compatibility wrapper for Python OpenSSL's Connection-class. + + Note: _makefile_refs, _drop() and _reuse() are needed for the garbage + collector of pypy. + ''' + + def __init__(self, connection, socket, suppress_ragged_eofs=True): + self.connection = connection + self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs + self._makefile_refs = 0 + self._closed = False + + def fileno(self): + return self.socket.fileno() + + # Copy-pasted from Python 3.5 source code + def _decref_socketios(self): + if self._makefile_refs > 0: + self._makefile_refs -= 1 + if self._closed: + self.close() + + def recv(self, *args, **kwargs): + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): + return b'' + else: + raise SocketError(str(e)) + except OpenSSL.SSL.ZeroReturnError as e: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return b'' + else: + raise + except OpenSSL.SSL.WantReadError: + rd = util.wait_for_read(self.socket, self.socket.gettimeout()) + if not rd: + raise timeout('The read operation timed out') + else: + return self.recv(*args, **kwargs) + else: + return data + + def recv_into(self, *args, **kwargs): + try: + return self.connection.recv_into(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): + return 0 + else: + raise SocketError(str(e)) + except OpenSSL.SSL.ZeroReturnError as e: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return 0 + else: + raise + except OpenSSL.SSL.WantReadError: + rd = util.wait_for_read(self.socket, self.socket.gettimeout()) + if not rd: + raise timeout('The read operation timed out') + else: + return self.recv_into(*args, **kwargs) + + def settimeout(self, timeout): + return self.socket.settimeout(timeout) + + def _send_until_done(self, data): + while True: + try: + return self.connection.send(data) + except OpenSSL.SSL.WantWriteError: + wr = util.wait_for_write(self.socket, self.socket.gettimeout()) + if not wr: + raise timeout() + continue + + def sendall(self, data): + total_sent = 0 + while total_sent < len(data): + sent = self._send_until_done(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE]) + total_sent += sent + + def shutdown(self): + # FIXME rethrow compatible exceptions should we ever use this + self.connection.shutdown() + + def close(self): + if self._makefile_refs < 1: + try: + self._closed = True + return self.connection.close() + except OpenSSL.SSL.Error: + return + else: + self._makefile_refs -= 1 + + def getpeercert(self, binary_form=False): + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 + + if binary_form: + return OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_ASN1, + x509) + + return { + 'subject': ( + (('commonName', x509.get_subject().CN),), + ), + 'subjectAltName': get_subj_alt_name(x509) + } + + def _reuse(self): + self._makefile_refs += 1 + + def _drop(self): + if self._makefile_refs < 1: + self.close() + else: + self._makefile_refs -= 1 + + +if _fileobject: # Platform-specific: Python 2 + def makefile(self, mode, bufsize=-1): + self._makefile_refs += 1 + return _fileobject(self, mode, bufsize, close=True) +else: # Platform-specific: Python 3 + makefile = backport_makefile + +WrappedSocket.makefile = makefile + + +class PyOpenSSLContext(object): + """ + I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible + for translating the interface of the standard library ``SSLContext`` object + to calls into PyOpenSSL. + """ + def __init__(self, protocol): + self.protocol = _openssl_versions[protocol] + self._ctx = OpenSSL.SSL.Context(self.protocol) + self._options = 0 + self.check_hostname = False + + @property + def options(self): + return self._options + + @options.setter + def options(self, value): + self._options = value + self._ctx.set_options(value) + + @property + def verify_mode(self): + return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] + + @verify_mode.setter + def verify_mode(self, value): + self._ctx.set_verify( + _stdlib_to_openssl_verify[value], + _verify_callback + ) + + def set_default_verify_paths(self): + self._ctx.set_default_verify_paths() + + def set_ciphers(self, ciphers): + if isinstance(ciphers, six.text_type): + ciphers = ciphers.encode('utf-8') + self._ctx.set_cipher_list(ciphers) + + def load_verify_locations(self, cafile=None, capath=None, cadata=None): + if cafile is not None: + cafile = cafile.encode('utf-8') + if capath is not None: + capath = capath.encode('utf-8') + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + + def load_cert_chain(self, certfile, keyfile=None, password=None): + self._ctx.use_certificate_file(certfile) + if password is not None: + self._ctx.set_passwd_cb(lambda max_length, prompt_twice, userdata: password) + self._ctx.use_privatekey_file(keyfile or certfile) + + def wrap_socket(self, sock, server_side=False, + do_handshake_on_connect=True, suppress_ragged_eofs=True, + server_hostname=None): + cnx = OpenSSL.SSL.Connection(self._ctx, sock) + + if isinstance(server_hostname, six.text_type): # Platform-specific: Python 3 + server_hostname = server_hostname.encode('utf-8') + + if server_hostname is not None: + cnx.set_tlsext_host_name(server_hostname) + + cnx.set_connect_state() + + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError: + rd = util.wait_for_read(sock, sock.gettimeout()) + if not rd: + raise timeout('select timed out') + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError('bad handshake: %r' % e) + break + + return WrappedSocket(cnx, sock) + + +def _verify_callback(cnx, x509, err_no, err_depth, return_code): + return err_no == 0 diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py index a2d1b46..811e312 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py @@ -1,178 +1,192 @@ -# -*- coding: utf-8 -*- -""" -This module contains provisional support for SOCKS proxies from within -urllib3. This module supports SOCKS4 (specifically the SOCKS4A variant) and -SOCKS5. To enable its functionality, either install PySocks or install this -module with the ``socks`` extra. - -The SOCKS implementation supports the full range of urllib3 features. It also -supports the following SOCKS features: - -- SOCKS4 -- SOCKS4a -- SOCKS5 -- Usernames and passwords for the SOCKS proxy - -Known Limitations: - -- Currently PySocks does not support contacting remote websites via literal - IPv6 addresses. Any such connection attempt will fail. You must use a domain - name. -- Currently PySocks does not support IPv6 connections to the SOCKS proxy. Any - such connection attempt will fail. -""" -from __future__ import absolute_import - -try: - import socks -except ImportError: - import warnings - from ..exceptions import DependencyWarning - - warnings.warn(( - 'SOCKS support in urllib3 requires the installation of optional ' - 'dependencies: specifically, PySocks. For more information, see ' - 'https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies' - ), - DependencyWarning - ) - raise - -from socket import error as SocketError, timeout as SocketTimeout - -from ..connection import ( - HTTPConnection, HTTPSConnection -) -from ..connectionpool import ( - HTTPConnectionPool, HTTPSConnectionPool -) -from ..exceptions import ConnectTimeoutError, NewConnectionError -from ..poolmanager import PoolManager -from ..util.url import parse_url - -try: - import ssl -except ImportError: - ssl = None - - -class SOCKSConnection(HTTPConnection): - """ - A plain-text HTTP connection that connects via a SOCKS proxy. - """ - def __init__(self, *args, **kwargs): - self._socks_options = kwargs.pop('_socks_options') - super(SOCKSConnection, self).__init__(*args, **kwargs) - - def _new_conn(self): - """ - Establish a new connection via the SOCKS proxy. - """ - extra_kw = {} - if self.source_address: - extra_kw['source_address'] = self.source_address - - if self.socket_options: - extra_kw['socket_options'] = self.socket_options - - try: - conn = socks.create_connection( - (self.host, self.port), - proxy_type=self._socks_options['socks_version'], - proxy_addr=self._socks_options['proxy_host'], - proxy_port=self._socks_options['proxy_port'], - proxy_username=self._socks_options['username'], - proxy_password=self._socks_options['password'], - timeout=self.timeout, - **extra_kw - ) - - except SocketTimeout as e: - raise ConnectTimeoutError( - self, "Connection to %s timed out. (connect timeout=%s)" % - (self.host, self.timeout)) - - except socks.ProxyError as e: - # This is fragile as hell, but it seems to be the only way to raise - # useful errors here. - if e.socket_err: - error = e.socket_err - if isinstance(error, SocketTimeout): - raise ConnectTimeoutError( - self, - "Connection to %s timed out. (connect timeout=%s)" % - (self.host, self.timeout) - ) - else: - raise NewConnectionError( - self, - "Failed to establish a new connection: %s" % error - ) - else: - raise NewConnectionError( - self, - "Failed to establish a new connection: %s" % e - ) - - except SocketError as e: # Defensive: PySocks should catch all these. - raise NewConnectionError( - self, "Failed to establish a new connection: %s" % e) - - return conn - - -# We don't need to duplicate the Verified/Unverified distinction from -# urllib3/connection.py here because the HTTPSConnection will already have been -# correctly set to either the Verified or Unverified form by that module. This -# means the SOCKSHTTPSConnection will automatically be the correct type. -class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): - pass - - -class SOCKSHTTPConnectionPool(HTTPConnectionPool): - ConnectionCls = SOCKSConnection - - -class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): - ConnectionCls = SOCKSHTTPSConnection - - -class SOCKSProxyManager(PoolManager): - """ - A version of the urllib3 ProxyManager that routes connections via the - defined SOCKS proxy. - """ - pool_classes_by_scheme = { - 'http': SOCKSHTTPConnectionPool, - 'https': SOCKSHTTPSConnectionPool, - } - - def __init__(self, proxy_url, username=None, password=None, - num_pools=10, headers=None, **connection_pool_kw): - parsed = parse_url(proxy_url) - - if parsed.scheme == 'socks5': - socks_version = socks.PROXY_TYPE_SOCKS5 - elif parsed.scheme == 'socks4': - socks_version = socks.PROXY_TYPE_SOCKS4 - else: - raise ValueError( - "Unable to determine SOCKS version from %s" % proxy_url - ) - - self.proxy_url = proxy_url - - socks_options = { - 'socks_version': socks_version, - 'proxy_host': parsed.host, - 'proxy_port': parsed.port, - 'username': username, - 'password': password, - } - connection_pool_kw['_socks_options'] = socks_options - - super(SOCKSProxyManager, self).__init__( - num_pools, headers, **connection_pool_kw - ) - - self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme +# -*- coding: utf-8 -*- +""" +This module contains provisional support for SOCKS proxies from within +urllib3. This module supports SOCKS4 (specifically the SOCKS4A variant) and +SOCKS5. To enable its functionality, either install PySocks or install this +module with the ``socks`` extra. + +The SOCKS implementation supports the full range of urllib3 features. It also +supports the following SOCKS features: + +- SOCKS4 +- SOCKS4a +- SOCKS5 +- Usernames and passwords for the SOCKS proxy + +Known Limitations: + +- Currently PySocks does not support contacting remote websites via literal + IPv6 addresses. Any such connection attempt will fail. You must use a domain + name. +- Currently PySocks does not support IPv6 connections to the SOCKS proxy. Any + such connection attempt will fail. +""" +from __future__ import absolute_import + +try: + import socks +except ImportError: + import warnings + from ..exceptions import DependencyWarning + + warnings.warn(( + 'SOCKS support in urllib3 requires the installation of optional ' + 'dependencies: specifically, PySocks. For more information, see ' + 'https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies' + ), + DependencyWarning + ) + raise + +from socket import error as SocketError, timeout as SocketTimeout + +from ..connection import ( + HTTPConnection, HTTPSConnection +) +from ..connectionpool import ( + HTTPConnectionPool, HTTPSConnectionPool +) +from ..exceptions import ConnectTimeoutError, NewConnectionError +from ..poolmanager import PoolManager +from ..util.url import parse_url + +try: + import ssl +except ImportError: + ssl = None + + +class SOCKSConnection(HTTPConnection): + """ + A plain-text HTTP connection that connects via a SOCKS proxy. + """ + def __init__(self, *args, **kwargs): + self._socks_options = kwargs.pop('_socks_options') + super(SOCKSConnection, self).__init__(*args, **kwargs) + + def _new_conn(self): + """ + Establish a new connection via the SOCKS proxy. + """ + extra_kw = {} + if self.source_address: + extra_kw['source_address'] = self.source_address + + if self.socket_options: + extra_kw['socket_options'] = self.socket_options + + try: + conn = socks.create_connection( + (self.host, self.port), + proxy_type=self._socks_options['socks_version'], + proxy_addr=self._socks_options['proxy_host'], + proxy_port=self._socks_options['proxy_port'], + proxy_username=self._socks_options['username'], + proxy_password=self._socks_options['password'], + proxy_rdns=self._socks_options['rdns'], + timeout=self.timeout, + **extra_kw + ) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, "Connection to %s timed out. (connect timeout=%s)" % + (self.host, self.timeout)) + + except socks.ProxyError as e: + # This is fragile as hell, but it seems to be the only way to raise + # useful errors here. + if e.socket_err: + error = e.socket_err + if isinstance(error, SocketTimeout): + raise ConnectTimeoutError( + self, + "Connection to %s timed out. (connect timeout=%s)" % + (self.host, self.timeout) + ) + else: + raise NewConnectionError( + self, + "Failed to establish a new connection: %s" % error + ) + else: + raise NewConnectionError( + self, + "Failed to establish a new connection: %s" % e + ) + + except SocketError as e: # Defensive: PySocks should catch all these. + raise NewConnectionError( + self, "Failed to establish a new connection: %s" % e) + + return conn + + +# We don't need to duplicate the Verified/Unverified distinction from +# urllib3/connection.py here because the HTTPSConnection will already have been +# correctly set to either the Verified or Unverified form by that module. This +# means the SOCKSHTTPSConnection will automatically be the correct type. +class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): + pass + + +class SOCKSHTTPConnectionPool(HTTPConnectionPool): + ConnectionCls = SOCKSConnection + + +class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): + ConnectionCls = SOCKSHTTPSConnection + + +class SOCKSProxyManager(PoolManager): + """ + A version of the urllib3 ProxyManager that routes connections via the + defined SOCKS proxy. + """ + pool_classes_by_scheme = { + 'http': SOCKSHTTPConnectionPool, + 'https': SOCKSHTTPSConnectionPool, + } + + def __init__(self, proxy_url, username=None, password=None, + num_pools=10, headers=None, **connection_pool_kw): + parsed = parse_url(proxy_url) + + if username is None and password is None and parsed.auth is not None: + split = parsed.auth.split(':') + if len(split) == 2: + username, password = split + if parsed.scheme == 'socks5': + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = False + elif parsed.scheme == 'socks5h': + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = True + elif parsed.scheme == 'socks4': + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = False + elif parsed.scheme == 'socks4a': + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = True + else: + raise ValueError( + "Unable to determine SOCKS version from %s" % proxy_url + ) + + self.proxy_url = proxy_url + + socks_options = { + 'socks_version': socks_version, + 'proxy_host': parsed.host, + 'proxy_port': parsed.port, + 'username': username, + 'password': password, + 'rdns': rdns + } + connection_pool_kw['_socks_options'] = socks_options + + super(SOCKSProxyManager, self).__init__( + num_pools, headers, **connection_pool_kw + ) + + self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py index 670a63e..6c4be58 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py @@ -1,246 +1,246 @@ -from __future__ import absolute_import -from .packages.six.moves.http_client import ( - IncompleteRead as httplib_IncompleteRead -) -# Base Exceptions - - -class HTTPError(Exception): - "Base exception used by this module." - pass - - -class HTTPWarning(Warning): - "Base warning used by this module." - pass - - -class PoolError(HTTPError): - "Base exception for errors caused within a pool." - def __init__(self, pool, message): - self.pool = pool - HTTPError.__init__(self, "%s: %s" % (pool, message)) - - def __reduce__(self): - # For pickling purposes. - return self.__class__, (None, None) - - -class RequestError(PoolError): - "Base exception for PoolErrors that have associated URLs." - def __init__(self, pool, url, message): - self.url = url - PoolError.__init__(self, pool, message) - - def __reduce__(self): - # For pickling purposes. - return self.__class__, (None, self.url, None) - - -class SSLError(HTTPError): - "Raised when SSL certificate fails in an HTTPS connection." - pass - - -class ProxyError(HTTPError): - "Raised when the connection to a proxy fails." - pass - - -class DecodeError(HTTPError): - "Raised when automatic decoding based on Content-Type fails." - pass - - -class ProtocolError(HTTPError): - "Raised when something unexpected happens mid-request/response." - pass - - -#: Renamed to ProtocolError but aliased for backwards compatibility. -ConnectionError = ProtocolError - - -# Leaf Exceptions - -class MaxRetryError(RequestError): - """Raised when the maximum number of retries is exceeded. - - :param pool: The connection pool - :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` - :param string url: The requested Url - :param exceptions.Exception reason: The underlying error - - """ - - def __init__(self, pool, url, reason=None): - self.reason = reason - - message = "Max retries exceeded with url: %s (Caused by %r)" % ( - url, reason) - - RequestError.__init__(self, pool, url, message) - - -class HostChangedError(RequestError): - "Raised when an existing pool gets a request for a foreign host." - - def __init__(self, pool, url, retries=3): - message = "Tried to open a foreign host with url: %s" % url - RequestError.__init__(self, pool, url, message) - self.retries = retries - - -class TimeoutStateError(HTTPError): - """ Raised when passing an invalid state to a timeout """ - pass - - -class TimeoutError(HTTPError): - """ Raised when a socket timeout error occurs. - - Catching this error will catch both :exc:`ReadTimeoutErrors - ` and :exc:`ConnectTimeoutErrors `. - """ - pass - - -class ReadTimeoutError(TimeoutError, RequestError): - "Raised when a socket timeout occurs while receiving data from a server" - pass - - -# This timeout error does not have a URL attached and needs to inherit from the -# base HTTPError -class ConnectTimeoutError(TimeoutError): - "Raised when a socket timeout occurs while connecting to a server" - pass - - -class NewConnectionError(ConnectTimeoutError, PoolError): - "Raised when we fail to establish a new connection. Usually ECONNREFUSED." - pass - - -class EmptyPoolError(PoolError): - "Raised when a pool runs out of connections and no more are allowed." - pass - - -class ClosedPoolError(PoolError): - "Raised when a request enters a pool after the pool has been closed." - pass - - -class LocationValueError(ValueError, HTTPError): - "Raised when there is something wrong with a given URL input." - pass - - -class LocationParseError(LocationValueError): - "Raised when get_host or similar fails to parse the URL input." - - def __init__(self, location): - message = "Failed to parse: %s" % location - HTTPError.__init__(self, message) - - self.location = location - - -class ResponseError(HTTPError): - "Used as a container for an error reason supplied in a MaxRetryError." - GENERIC_ERROR = 'too many error responses' - SPECIFIC_ERROR = 'too many {status_code} error responses' - - -class SecurityWarning(HTTPWarning): - "Warned when perfoming security reducing actions" - pass - - -class SubjectAltNameWarning(SecurityWarning): - "Warned when connecting to a host with a certificate missing a SAN." - pass - - -class InsecureRequestWarning(SecurityWarning): - "Warned when making an unverified HTTPS request." - pass - - -class SystemTimeWarning(SecurityWarning): - "Warned when system time is suspected to be wrong" - pass - - -class InsecurePlatformWarning(SecurityWarning): - "Warned when certain SSL configuration is not available on a platform." - pass - - -class SNIMissingWarning(HTTPWarning): - "Warned when making a HTTPS request without SNI available." - pass - - -class DependencyWarning(HTTPWarning): - """ - Warned when an attempt is made to import a module with missing optional - dependencies. - """ - pass - - -class ResponseNotChunked(ProtocolError, ValueError): - "Response needs to be chunked in order to read it as chunks." - pass - - -class BodyNotHttplibCompatible(HTTPError): - """ - Body should be httplib.HTTPResponse like (have an fp attribute which - returns raw chunks) for read_chunked(). - """ - pass - - -class IncompleteRead(HTTPError, httplib_IncompleteRead): - """ - Response length doesn't match expected Content-Length - - Subclass of http_client.IncompleteRead to allow int value - for `partial` to avoid creating large objects on streamed - reads. - """ - def __init__(self, partial, expected): - super(IncompleteRead, self).__init__(partial, expected) - - def __repr__(self): - return ('IncompleteRead(%i bytes read, ' - '%i more expected)' % (self.partial, self.expected)) - - -class InvalidHeader(HTTPError): - "The header provided was somehow invalid." - pass - - -class ProxySchemeUnknown(AssertionError, ValueError): - "ProxyManager does not support the supplied scheme" - # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. - - def __init__(self, scheme): - message = "Not supported proxy scheme %s" % scheme - super(ProxySchemeUnknown, self).__init__(message) - - -class HeaderParsingError(HTTPError): - "Raised by assert_header_parsing, but we convert it to a log.warning statement." - def __init__(self, defects, unparsed_data): - message = '%s, unparsed data: %r' % (defects or 'Unknown', unparsed_data) - super(HeaderParsingError, self).__init__(message) - - -class UnrewindableBodyError(HTTPError): - "urllib3 encountered an error when trying to rewind a body" - pass +from __future__ import absolute_import +from .packages.six.moves.http_client import ( + IncompleteRead as httplib_IncompleteRead +) +# Base Exceptions + + +class HTTPError(Exception): + "Base exception used by this module." + pass + + +class HTTPWarning(Warning): + "Base warning used by this module." + pass + + +class PoolError(HTTPError): + "Base exception for errors caused within a pool." + def __init__(self, pool, message): + self.pool = pool + HTTPError.__init__(self, "%s: %s" % (pool, message)) + + def __reduce__(self): + # For pickling purposes. + return self.__class__, (None, None) + + +class RequestError(PoolError): + "Base exception for PoolErrors that have associated URLs." + def __init__(self, pool, url, message): + self.url = url + PoolError.__init__(self, pool, message) + + def __reduce__(self): + # For pickling purposes. + return self.__class__, (None, self.url, None) + + +class SSLError(HTTPError): + "Raised when SSL certificate fails in an HTTPS connection." + pass + + +class ProxyError(HTTPError): + "Raised when the connection to a proxy fails." + pass + + +class DecodeError(HTTPError): + "Raised when automatic decoding based on Content-Type fails." + pass + + +class ProtocolError(HTTPError): + "Raised when something unexpected happens mid-request/response." + pass + + +#: Renamed to ProtocolError but aliased for backwards compatibility. +ConnectionError = ProtocolError + + +# Leaf Exceptions + +class MaxRetryError(RequestError): + """Raised when the maximum number of retries is exceeded. + + :param pool: The connection pool + :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` + :param string url: The requested Url + :param exceptions.Exception reason: The underlying error + + """ + + def __init__(self, pool, url, reason=None): + self.reason = reason + + message = "Max retries exceeded with url: %s (Caused by %r)" % ( + url, reason) + + RequestError.__init__(self, pool, url, message) + + +class HostChangedError(RequestError): + "Raised when an existing pool gets a request for a foreign host." + + def __init__(self, pool, url, retries=3): + message = "Tried to open a foreign host with url: %s" % url + RequestError.__init__(self, pool, url, message) + self.retries = retries + + +class TimeoutStateError(HTTPError): + """ Raised when passing an invalid state to a timeout """ + pass + + +class TimeoutError(HTTPError): + """ Raised when a socket timeout error occurs. + + Catching this error will catch both :exc:`ReadTimeoutErrors + ` and :exc:`ConnectTimeoutErrors `. + """ + pass + + +class ReadTimeoutError(TimeoutError, RequestError): + "Raised when a socket timeout occurs while receiving data from a server" + pass + + +# This timeout error does not have a URL attached and needs to inherit from the +# base HTTPError +class ConnectTimeoutError(TimeoutError): + "Raised when a socket timeout occurs while connecting to a server" + pass + + +class NewConnectionError(ConnectTimeoutError, PoolError): + "Raised when we fail to establish a new connection. Usually ECONNREFUSED." + pass + + +class EmptyPoolError(PoolError): + "Raised when a pool runs out of connections and no more are allowed." + pass + + +class ClosedPoolError(PoolError): + "Raised when a request enters a pool after the pool has been closed." + pass + + +class LocationValueError(ValueError, HTTPError): + "Raised when there is something wrong with a given URL input." + pass + + +class LocationParseError(LocationValueError): + "Raised when get_host or similar fails to parse the URL input." + + def __init__(self, location): + message = "Failed to parse: %s" % location + HTTPError.__init__(self, message) + + self.location = location + + +class ResponseError(HTTPError): + "Used as a container for an error reason supplied in a MaxRetryError." + GENERIC_ERROR = 'too many error responses' + SPECIFIC_ERROR = 'too many {status_code} error responses' + + +class SecurityWarning(HTTPWarning): + "Warned when perfoming security reducing actions" + pass + + +class SubjectAltNameWarning(SecurityWarning): + "Warned when connecting to a host with a certificate missing a SAN." + pass + + +class InsecureRequestWarning(SecurityWarning): + "Warned when making an unverified HTTPS request." + pass + + +class SystemTimeWarning(SecurityWarning): + "Warned when system time is suspected to be wrong" + pass + + +class InsecurePlatformWarning(SecurityWarning): + "Warned when certain SSL configuration is not available on a platform." + pass + + +class SNIMissingWarning(HTTPWarning): + "Warned when making a HTTPS request without SNI available." + pass + + +class DependencyWarning(HTTPWarning): + """ + Warned when an attempt is made to import a module with missing optional + dependencies. + """ + pass + + +class ResponseNotChunked(ProtocolError, ValueError): + "Response needs to be chunked in order to read it as chunks." + pass + + +class BodyNotHttplibCompatible(HTTPError): + """ + Body should be httplib.HTTPResponse like (have an fp attribute which + returns raw chunks) for read_chunked(). + """ + pass + + +class IncompleteRead(HTTPError, httplib_IncompleteRead): + """ + Response length doesn't match expected Content-Length + + Subclass of http_client.IncompleteRead to allow int value + for `partial` to avoid creating large objects on streamed + reads. + """ + def __init__(self, partial, expected): + super(IncompleteRead, self).__init__(partial, expected) + + def __repr__(self): + return ('IncompleteRead(%i bytes read, ' + '%i more expected)' % (self.partial, self.expected)) + + +class InvalidHeader(HTTPError): + "The header provided was somehow invalid." + pass + + +class ProxySchemeUnknown(AssertionError, ValueError): + "ProxyManager does not support the supplied scheme" + # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. + + def __init__(self, scheme): + message = "Not supported proxy scheme %s" % scheme + super(ProxySchemeUnknown, self).__init__(message) + + +class HeaderParsingError(HTTPError): + "Raised by assert_header_parsing, but we convert it to a log.warning statement." + def __init__(self, defects, unparsed_data): + message = '%s, unparsed data: %r' % (defects or 'Unknown', unparsed_data) + super(HeaderParsingError, self).__init__(message) + + +class UnrewindableBodyError(HTTPError): + "urllib3 encountered an error when trying to rewind a body" + pass diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py index 8e15621..19b0ae0 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py @@ -1,178 +1,178 @@ -from __future__ import absolute_import -import email.utils -import mimetypes - -from .packages import six - - -def guess_content_type(filename, default='application/octet-stream'): - """ - Guess the "Content-Type" of a file. - - :param filename: - The filename to guess the "Content-Type" of using :mod:`mimetypes`. - :param default: - If no "Content-Type" can be guessed, default to `default`. - """ - if filename: - return mimetypes.guess_type(filename)[0] or default - return default - - -def format_header_param(name, value): - """ - Helper function to format and quote a single header parameter. - - Particularly useful for header parameters which might contain - non-ASCII values, like file names. This follows RFC 2231, as - suggested by RFC 2388 Section 4.4. - - :param name: - The name of the parameter, a string expected to be ASCII only. - :param value: - The value of the parameter, provided as a unicode string. - """ - if not any(ch in value for ch in '"\\\r\n'): - result = '%s="%s"' % (name, value) - try: - result.encode('ascii') - except (UnicodeEncodeError, UnicodeDecodeError): - pass - else: - return result - if not six.PY3 and isinstance(value, six.text_type): # Python 2: - value = value.encode('utf-8') - value = email.utils.encode_rfc2231(value, 'utf-8') - value = '%s*=%s' % (name, value) - return value - - -class RequestField(object): - """ - A data container for request body parameters. - - :param name: - The name of this request field. - :param data: - The data/value body. - :param filename: - An optional filename of the request field. - :param headers: - An optional dict-like object of headers to initially use for the field. - """ - def __init__(self, name, data, filename=None, headers=None): - self._name = name - self._filename = filename - self.data = data - self.headers = {} - if headers: - self.headers = dict(headers) - - @classmethod - def from_tuples(cls, fieldname, value): - """ - A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. - - Supports constructing :class:`~urllib3.fields.RequestField` from - parameter of key/value strings AND key/filetuple. A filetuple is a - (filename, data, MIME type) tuple where the MIME type is optional. - For example:: - - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - - Field names and filenames must be unicode. - """ - if isinstance(value, tuple): - if len(value) == 3: - filename, data, content_type = value - else: - filename, data = value - content_type = guess_content_type(filename) - else: - filename = None - content_type = None - data = value - - request_param = cls(fieldname, data, filename=filename) - request_param.make_multipart(content_type=content_type) - - return request_param - - def _render_part(self, name, value): - """ - Overridable helper function to format a single header parameter. - - :param name: - The name of the parameter, a string expected to be ASCII only. - :param value: - The value of the parameter, provided as a unicode string. - """ - return format_header_param(name, value) - - def _render_parts(self, header_parts): - """ - Helper function to format and quote a single header. - - Useful for single headers that are composed of multiple items. E.g., - 'Content-Disposition' fields. - - :param header_parts: - A sequence of (k, v) typles or a :class:`dict` of (k, v) to format - as `k1="v1"; k2="v2"; ...`. - """ - parts = [] - iterable = header_parts - if isinstance(header_parts, dict): - iterable = header_parts.items() - - for name, value in iterable: - if value is not None: - parts.append(self._render_part(name, value)) - - return '; '.join(parts) - - def render_headers(self): - """ - Renders the headers for this request field. - """ - lines = [] - - sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location'] - for sort_key in sort_keys: - if self.headers.get(sort_key, False): - lines.append('%s: %s' % (sort_key, self.headers[sort_key])) - - for header_name, header_value in self.headers.items(): - if header_name not in sort_keys: - if header_value: - lines.append('%s: %s' % (header_name, header_value)) - - lines.append('\r\n') - return '\r\n'.join(lines) - - def make_multipart(self, content_disposition=None, content_type=None, - content_location=None): - """ - Makes this request field into a multipart request field. - - This method overrides "Content-Disposition", "Content-Type" and - "Content-Location" headers to the request parameter. - - :param content_type: - The 'Content-Type' of the request body. - :param content_location: - The 'Content-Location' of the request body. - - """ - self.headers['Content-Disposition'] = content_disposition or 'form-data' - self.headers['Content-Disposition'] += '; '.join([ - '', self._render_parts( - (('name', self._name), ('filename', self._filename)) - ) - ]) - self.headers['Content-Type'] = content_type - self.headers['Content-Location'] = content_location +from __future__ import absolute_import +import email.utils +import mimetypes + +from .packages import six + + +def guess_content_type(filename, default='application/octet-stream'): + """ + Guess the "Content-Type" of a file. + + :param filename: + The filename to guess the "Content-Type" of using :mod:`mimetypes`. + :param default: + If no "Content-Type" can be guessed, default to `default`. + """ + if filename: + return mimetypes.guess_type(filename)[0] or default + return default + + +def format_header_param(name, value): + """ + Helper function to format and quote a single header parameter. + + Particularly useful for header parameters which might contain + non-ASCII values, like file names. This follows RFC 2231, as + suggested by RFC 2388 Section 4.4. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as a unicode string. + """ + if not any(ch in value for ch in '"\\\r\n'): + result = '%s="%s"' % (name, value) + try: + result.encode('ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + pass + else: + return result + if not six.PY3 and isinstance(value, six.text_type): # Python 2: + value = value.encode('utf-8') + value = email.utils.encode_rfc2231(value, 'utf-8') + value = '%s*=%s' % (name, value) + return value + + +class RequestField(object): + """ + A data container for request body parameters. + + :param name: + The name of this request field. + :param data: + The data/value body. + :param filename: + An optional filename of the request field. + :param headers: + An optional dict-like object of headers to initially use for the field. + """ + def __init__(self, name, data, filename=None, headers=None): + self._name = name + self._filename = filename + self.data = data + self.headers = {} + if headers: + self.headers = dict(headers) + + @classmethod + def from_tuples(cls, fieldname, value): + """ + A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. + + Supports constructing :class:`~urllib3.fields.RequestField` from + parameter of key/value strings AND key/filetuple. A filetuple is a + (filename, data, MIME type) tuple where the MIME type is optional. + For example:: + + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + + Field names and filenames must be unicode. + """ + if isinstance(value, tuple): + if len(value) == 3: + filename, data, content_type = value + else: + filename, data = value + content_type = guess_content_type(filename) + else: + filename = None + content_type = None + data = value + + request_param = cls(fieldname, data, filename=filename) + request_param.make_multipart(content_type=content_type) + + return request_param + + def _render_part(self, name, value): + """ + Overridable helper function to format a single header parameter. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as a unicode string. + """ + return format_header_param(name, value) + + def _render_parts(self, header_parts): + """ + Helper function to format and quote a single header. + + Useful for single headers that are composed of multiple items. E.g., + 'Content-Disposition' fields. + + :param header_parts: + A sequence of (k, v) typles or a :class:`dict` of (k, v) to format + as `k1="v1"; k2="v2"; ...`. + """ + parts = [] + iterable = header_parts + if isinstance(header_parts, dict): + iterable = header_parts.items() + + for name, value in iterable: + if value is not None: + parts.append(self._render_part(name, value)) + + return '; '.join(parts) + + def render_headers(self): + """ + Renders the headers for this request field. + """ + lines = [] + + sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location'] + for sort_key in sort_keys: + if self.headers.get(sort_key, False): + lines.append('%s: %s' % (sort_key, self.headers[sort_key])) + + for header_name, header_value in self.headers.items(): + if header_name not in sort_keys: + if header_value: + lines.append('%s: %s' % (header_name, header_value)) + + lines.append('\r\n') + return '\r\n'.join(lines) + + def make_multipart(self, content_disposition=None, content_type=None, + content_location=None): + """ + Makes this request field into a multipart request field. + + This method overrides "Content-Disposition", "Content-Type" and + "Content-Location" headers to the request parameter. + + :param content_type: + The 'Content-Type' of the request body. + :param content_location: + The 'Content-Location' of the request body. + + """ + self.headers['Content-Disposition'] = content_disposition or 'form-data' + self.headers['Content-Disposition'] += '; '.join([ + '', self._render_parts( + (('name', self._name), ('filename', self._filename)) + ) + ]) + self.headers['Content-Type'] = content_type + self.headers['Content-Location'] = content_location diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py index e53dedc..cd11cee 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py @@ -1,94 +1,94 @@ -from __future__ import absolute_import -import codecs - -from uuid import uuid4 -from io import BytesIO - -from .packages import six -from .packages.six import b -from .fields import RequestField - -writer = codecs.lookup('utf-8')[3] - - -def choose_boundary(): - """ - Our embarrassingly-simple replacement for mimetools.choose_boundary. - """ - return uuid4().hex - - -def iter_field_objects(fields): - """ - Iterate over fields. - - Supports list of (k, v) tuples and dicts, and lists of - :class:`~urllib3.fields.RequestField`. - - """ - if isinstance(fields, dict): - i = six.iteritems(fields) - else: - i = iter(fields) - - for field in i: - if isinstance(field, RequestField): - yield field - else: - yield RequestField.from_tuples(*field) - - -def iter_fields(fields): - """ - .. deprecated:: 1.6 - - Iterate over fields. - - The addition of :class:`~urllib3.fields.RequestField` makes this function - obsolete. Instead, use :func:`iter_field_objects`, which returns - :class:`~urllib3.fields.RequestField` objects. - - Supports list of (k, v) tuples and dicts. - """ - if isinstance(fields, dict): - return ((k, v) for k, v in six.iteritems(fields)) - - return ((k, v) for k, v in fields) - - -def encode_multipart_formdata(fields, boundary=None): - """ - Encode a dictionary of ``fields`` using the multipart/form-data MIME format. - - :param fields: - Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). - - :param boundary: - If not specified, then a random boundary will be generated using - :func:`mimetools.choose_boundary`. - """ - body = BytesIO() - if boundary is None: - boundary = choose_boundary() - - for field in iter_field_objects(fields): - body.write(b('--%s\r\n' % (boundary))) - - writer(body).write(field.render_headers()) - data = field.data - - if isinstance(data, int): - data = str(data) # Backwards compatibility - - if isinstance(data, six.text_type): - writer(body).write(data) - else: - body.write(data) - - body.write(b'\r\n') - - body.write(b('--%s--\r\n' % (boundary))) - - content_type = str('multipart/form-data; boundary=%s' % boundary) - - return body.getvalue(), content_type +from __future__ import absolute_import +import codecs + +from uuid import uuid4 +from io import BytesIO + +from .packages import six +from .packages.six import b +from .fields import RequestField + +writer = codecs.lookup('utf-8')[3] + + +def choose_boundary(): + """ + Our embarrassingly-simple replacement for mimetools.choose_boundary. + """ + return uuid4().hex + + +def iter_field_objects(fields): + """ + Iterate over fields. + + Supports list of (k, v) tuples and dicts, and lists of + :class:`~urllib3.fields.RequestField`. + + """ + if isinstance(fields, dict): + i = six.iteritems(fields) + else: + i = iter(fields) + + for field in i: + if isinstance(field, RequestField): + yield field + else: + yield RequestField.from_tuples(*field) + + +def iter_fields(fields): + """ + .. deprecated:: 1.6 + + Iterate over fields. + + The addition of :class:`~urllib3.fields.RequestField` makes this function + obsolete. Instead, use :func:`iter_field_objects`, which returns + :class:`~urllib3.fields.RequestField` objects. + + Supports list of (k, v) tuples and dicts. + """ + if isinstance(fields, dict): + return ((k, v) for k, v in six.iteritems(fields)) + + return ((k, v) for k, v in fields) + + +def encode_multipart_formdata(fields, boundary=None): + """ + Encode a dictionary of ``fields`` using the multipart/form-data MIME format. + + :param fields: + Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`mimetools.choose_boundary`. + """ + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for field in iter_field_objects(fields): + body.write(b('--%s\r\n' % (boundary))) + + writer(body).write(field.render_headers()) + data = field.data + + if isinstance(data, int): + data = str(data) # Backwards compatibility + + if isinstance(data, six.text_type): + writer(body).write(data) + else: + body.write(data) + + body.write(b'\r\n') + + body.write(b('--%s--\r\n' % (boundary))) + + content_type = str('multipart/form-data; boundary=%s' % boundary) + + return body.getvalue(), content_type diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py index 324c551..170e974 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py @@ -1,5 +1,5 @@ -from __future__ import absolute_import - -from . import ssl_match_hostname - -__all__ = ('ssl_match_hostname', ) +from __future__ import absolute_import + +from . import ssl_match_hostname + +__all__ = ('ssl_match_hostname', ) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py index 00dee0b..75b80dc 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py @@ -1,53 +1,53 @@ -# -*- coding: utf-8 -*- -""" -backports.makefile -~~~~~~~~~~~~~~~~~~ - -Backports the Python 3 ``socket.makefile`` method for use with anything that -wants to create a "fake" socket object. -""" -import io - -from socket import SocketIO - - -def backport_makefile(self, mode="r", buffering=None, encoding=None, - errors=None, newline=None): - """ - Backport of ``socket.makefile`` from Python 3.5. - """ - if not set(mode) <= set(["r", "w", "b"]): - raise ValueError( - "invalid mode %r (only r, w, b allowed)" % (mode,) - ) - writing = "w" in mode - reading = "r" in mode or not writing - assert reading or writing - binary = "b" in mode - rawmode = "" - if reading: - rawmode += "r" - if writing: - rawmode += "w" - raw = SocketIO(self, rawmode) - self._makefile_refs += 1 - if buffering is None: - buffering = -1 - if buffering < 0: - buffering = io.DEFAULT_BUFFER_SIZE - if buffering == 0: - if not binary: - raise ValueError("unbuffered streams must be binary") - return raw - if reading and writing: - buffer = io.BufferedRWPair(raw, raw, buffering) - elif reading: - buffer = io.BufferedReader(raw, buffering) - else: - assert writing - buffer = io.BufferedWriter(raw, buffering) - if binary: - return buffer - text = io.TextIOWrapper(buffer, encoding, errors, newline) - text.mode = mode - return text +# -*- coding: utf-8 -*- +""" +backports.makefile +~~~~~~~~~~~~~~~~~~ + +Backports the Python 3 ``socket.makefile`` method for use with anything that +wants to create a "fake" socket object. +""" +import io + +from socket import SocketIO + + +def backport_makefile(self, mode="r", buffering=None, encoding=None, + errors=None, newline=None): + """ + Backport of ``socket.makefile`` from Python 3.5. + """ + if not set(mode) <= set(["r", "w", "b"]): + raise ValueError( + "invalid mode %r (only r, w, b allowed)" % (mode,) + ) + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = SocketIO(self, rawmode) + self._makefile_refs += 1 + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode + return text diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py index 62dcb42..4479363 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py @@ -1,259 +1,259 @@ -# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy. -# Passes Python2.7's test suite and incorporates all the latest updates. -# Copyright 2009 Raymond Hettinger, released under the MIT License. -# http://code.activestate.com/recipes/576693/ -try: - from thread import get_ident as _get_ident -except ImportError: - from dummy_thread import get_ident as _get_ident - -try: - from _abcoll import KeysView, ValuesView, ItemsView -except ImportError: - pass - - -class OrderedDict(dict): - 'Dictionary that remembers insertion order' - # An inherited dict maps keys to values. - # The inherited dict provides __getitem__, __len__, __contains__, and get. - # The remaining methods are order-aware. - # Big-O running times for all methods are the same as for regular dictionaries. - - # The internal self.__map dictionary maps keys to links in a doubly linked list. - # The circular doubly linked list starts and ends with a sentinel element. - # The sentinel element never gets deleted (this simplifies the algorithm). - # Each link is stored as a list of length three: [PREV, NEXT, KEY]. - - def __init__(self, *args, **kwds): - '''Initialize an ordered dictionary. Signature is the same as for - regular dictionaries, but keyword arguments are not recommended - because their insertion order is arbitrary. - - ''' - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__root - except AttributeError: - self.__root = root = [] # sentinel node - root[:] = [root, root, None] - self.__map = {} - self.__update(*args, **kwds) - - def __setitem__(self, key, value, dict_setitem=dict.__setitem__): - 'od.__setitem__(i, y) <==> od[i]=y' - # Setting a new item creates a new link which goes at the end of the linked - # list, and the inherited dictionary is updated with the new key/value pair. - if key not in self: - root = self.__root - last = root[0] - last[1] = root[0] = self.__map[key] = [last, root, key] - dict_setitem(self, key, value) - - def __delitem__(self, key, dict_delitem=dict.__delitem__): - 'od.__delitem__(y) <==> del od[y]' - # Deleting an existing item uses self.__map to find the link which is - # then removed by updating the links in the predecessor and successor nodes. - dict_delitem(self, key) - link_prev, link_next, key = self.__map.pop(key) - link_prev[1] = link_next - link_next[0] = link_prev - - def __iter__(self): - 'od.__iter__() <==> iter(od)' - root = self.__root - curr = root[1] - while curr is not root: - yield curr[2] - curr = curr[1] - - def __reversed__(self): - 'od.__reversed__() <==> reversed(od)' - root = self.__root - curr = root[0] - while curr is not root: - yield curr[2] - curr = curr[0] - - def clear(self): - 'od.clear() -> None. Remove all items from od.' - try: - for node in self.__map.itervalues(): - del node[:] - root = self.__root - root[:] = [root, root, None] - self.__map.clear() - except AttributeError: - pass - dict.clear(self) - - def popitem(self, last=True): - '''od.popitem() -> (k, v), return and remove a (key, value) pair. - Pairs are returned in LIFO order if last is true or FIFO order if false. - - ''' - if not self: - raise KeyError('dictionary is empty') - root = self.__root - if last: - link = root[0] - link_prev = link[0] - link_prev[1] = root - root[0] = link_prev - else: - link = root[1] - link_next = link[1] - root[1] = link_next - link_next[0] = root - key = link[2] - del self.__map[key] - value = dict.pop(self, key) - return key, value - - # -- the following methods do not depend on the internal structure -- - - def keys(self): - 'od.keys() -> list of keys in od' - return list(self) - - def values(self): - 'od.values() -> list of values in od' - return [self[key] for key in self] - - def items(self): - 'od.items() -> list of (key, value) pairs in od' - return [(key, self[key]) for key in self] - - def iterkeys(self): - 'od.iterkeys() -> an iterator over the keys in od' - return iter(self) - - def itervalues(self): - 'od.itervalues -> an iterator over the values in od' - for k in self: - yield self[k] - - def iteritems(self): - 'od.iteritems -> an iterator over the (key, value) items in od' - for k in self: - yield (k, self[k]) - - def update(*args, **kwds): - '''od.update(E, **F) -> None. Update od from dict/iterable E and F. - - If E is a dict instance, does: for k in E: od[k] = E[k] - If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] - Or if E is an iterable of items, does: for k, v in E: od[k] = v - In either case, this is followed by: for k, v in F.items(): od[k] = v - - ''' - if len(args) > 2: - raise TypeError('update() takes at most 2 positional ' - 'arguments (%d given)' % (len(args),)) - elif not args: - raise TypeError('update() takes at least 1 argument (0 given)') - self = args[0] - # Make progressively weaker assumptions about "other" - other = () - if len(args) == 2: - other = args[1] - if isinstance(other, dict): - for key in other: - self[key] = other[key] - elif hasattr(other, 'keys'): - for key in other.keys(): - self[key] = other[key] - else: - for key, value in other: - self[key] = value - for key, value in kwds.items(): - self[key] = value - - __update = update # let subclasses override update without breaking __init__ - - __marker = object() - - def pop(self, key, default=__marker): - '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. - If key is not found, d is returned if given, otherwise KeyError is raised. - - ''' - if key in self: - result = self[key] - del self[key] - return result - if default is self.__marker: - raise KeyError(key) - return default - - def setdefault(self, key, default=None): - 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' - if key in self: - return self[key] - self[key] = default - return default - - def __repr__(self, _repr_running={}): - 'od.__repr__() <==> repr(od)' - call_key = id(self), _get_ident() - if call_key in _repr_running: - return '...' - _repr_running[call_key] = 1 - try: - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - finally: - del _repr_running[call_key] - - def __reduce__(self): - 'Return state information for pickling' - items = [[k, self[k]] for k in self] - inst_dict = vars(self).copy() - for k in vars(OrderedDict()): - inst_dict.pop(k, None) - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def copy(self): - 'od.copy() -> a shallow copy of od' - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S - and values equal to v (which defaults to None). - - ''' - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive - while comparison to a regular mapping is order-insensitive. - - ''' - if isinstance(other, OrderedDict): - return len(self)==len(other) and self.items() == other.items() - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other - - # -- the following methods are only used in Python 2.7 -- - - def viewkeys(self): - "od.viewkeys() -> a set-like object providing a view on od's keys" - return KeysView(self) - - def viewvalues(self): - "od.viewvalues() -> an object providing a view on od's values" - return ValuesView(self) - - def viewitems(self): - "od.viewitems() -> a set-like object providing a view on od's items" - return ItemsView(self) +# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy. +# Passes Python2.7's test suite and incorporates all the latest updates. +# Copyright 2009 Raymond Hettinger, released under the MIT License. +# http://code.activestate.com/recipes/576693/ +try: + from thread import get_ident as _get_ident +except ImportError: + from dummy_thread import get_ident as _get_ident + +try: + from _abcoll import KeysView, ValuesView, ItemsView +except ImportError: + pass + + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as for regular dictionaries. + + # The internal self.__map dictionary maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # Each link is stored as a list of length three: [PREV, NEXT, KEY]. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. Signature is the same as for + regular dictionaries, but keyword arguments are not recommended + because their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__root = root = [] # sentinel node + root[:] = [root, root, None] + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, dict_setitem=dict.__setitem__): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link which goes at the end of the linked + # list, and the inherited dictionary is updated with the new key/value pair. + if key not in self: + root = self.__root + last = root[0] + last[1] = root[0] = self.__map[key] = [last, root, key] + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which is + # then removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link_prev, link_next, key = self.__map.pop(key) + link_prev[1] = link_next + link_next[0] = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + root = self.__root + curr = root[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + root = self.__root + curr = root[0] + while curr is not root: + yield curr[2] + curr = curr[0] + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + try: + for node in self.__map.itervalues(): + del node[:] + root = self.__root + root[:] = [root, root, None] + self.__map.clear() + except AttributeError: + pass + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root[0] + link_prev = link[0] + link_prev[1] = root + root[0] = link_prev + else: + link = root[1] + link_next = link[1] + root[1] = link_next + link_next[0] = root + key = link[2] + del self.__map[key] + value = dict.pop(self, key) + return key, value + + # -- the following methods do not depend on the internal structure -- + + def keys(self): + 'od.keys() -> list of keys in od' + return list(self) + + def values(self): + 'od.values() -> list of values in od' + return [self[key] for key in self] + + def items(self): + 'od.items() -> list of (key, value) pairs in od' + return [(key, self[key]) for key in self] + + def iterkeys(self): + 'od.iterkeys() -> an iterator over the keys in od' + return iter(self) + + def itervalues(self): + 'od.itervalues -> an iterator over the values in od' + for k in self: + yield self[k] + + def iteritems(self): + 'od.iteritems -> an iterator over the (key, value) items in od' + for k in self: + yield (k, self[k]) + + def update(*args, **kwds): + '''od.update(E, **F) -> None. Update od from dict/iterable E and F. + + If E is a dict instance, does: for k in E: od[k] = E[k] + If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] + Or if E is an iterable of items, does: for k, v in E: od[k] = v + In either case, this is followed by: for k, v in F.items(): od[k] = v + + ''' + if len(args) > 2: + raise TypeError('update() takes at most 2 positional ' + 'arguments (%d given)' % (len(args),)) + elif not args: + raise TypeError('update() takes at least 1 argument (0 given)') + self = args[0] + # Make progressively weaker assumptions about "other" + other = () + if len(args) == 2: + other = args[1] + if isinstance(other, dict): + for key in other: + self[key] = other[key] + elif hasattr(other, 'keys'): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value + + __update = update # let subclasses override update without breaking __init__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + def __repr__(self, _repr_running={}): + 'od.__repr__() <==> repr(od)' + call_key = id(self), _get_ident() + if call_key in _repr_running: + return '...' + _repr_running[call_key] = 1 + try: + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + finally: + del _repr_running[call_key] + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S + and values equal to v (which defaults to None). + + ''' + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self)==len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other + + # -- the following methods are only used in Python 2.7 -- + + def viewkeys(self): + "od.viewkeys() -> a set-like object providing a view on od's keys" + return KeysView(self) + + def viewvalues(self): + "od.viewvalues() -> an object providing a view on od's values" + return ValuesView(self) + + def viewitems(self): + "od.viewitems() -> a set-like object providing a view on od's items" + return ItemsView(self) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py index 7bd9225..190c023 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py @@ -1,868 +1,868 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from __future__ import absolute_import - -import functools -import itertools -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.10.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) # Invokes __set__. - try: - # This is a bit ugly, but it avoids running this again by - # removing this descriptor. - delattr(obj.__class__, self.name) - except AttributeError: - pass - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - def __getattr__(self, attr): - _module = self._resolve() - value = getattr(_module, attr) - setattr(self, attr, value) - return value - - -class _LazyModule(types.ModuleType): - - def __init__(self, name): - super(_LazyModule, self).__init__(name) - self.__doc__ = self.__class__.__doc__ - - def __dir__(self): - attrs = ["__doc__", "__name__"] - attrs += [attr.name for attr in self._moved_attributes] - return attrs - - # Subclasses should override this - _moved_attributes = [] - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - -class _SixMetaPathImporter(object): - - """ - A meta path importer to import six.moves and its submodules. - - This class implements a PEP302 finder and loader. It should be compatible - with Python 2.5 and all existing versions of Python3 - """ - - def __init__(self, six_module_name): - self.name = six_module_name - self.known_modules = {} - - def _add_module(self, mod, *fullnames): - for fullname in fullnames: - self.known_modules[self.name + "." + fullname] = mod - - def _get_module(self, fullname): - return self.known_modules[self.name + "." + fullname] - - def find_module(self, fullname, path=None): - if fullname in self.known_modules: - return self - return None - - def __get_module(self, fullname): - try: - return self.known_modules[fullname] - except KeyError: - raise ImportError("This loader does not know module " + fullname) - - def load_module(self, fullname): - try: - # in case of a reload - return sys.modules[fullname] - except KeyError: - pass - mod = self.__get_module(fullname) - if isinstance(mod, MovedModule): - mod = mod._resolve() - else: - mod.__loader__ = self - sys.modules[fullname] = mod - return mod - - def is_package(self, fullname): - """ - Return true, if the named module is a package. - - We need this method to get correct spec objects with - Python 3.4 (see PEP451) - """ - return hasattr(self.__get_module(fullname), "__path__") - - def get_code(self, fullname): - """Return None - - Required, if is_package is implemented""" - self.__get_module(fullname) # eventually raises ImportError - return None - get_source = get_code # same as get_code - -_importer = _SixMetaPathImporter(__name__) - - -class _MovedItems(_LazyModule): - - """Lazy loading of moved objects""" - __path__ = [] # mark as package - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("intern", "__builtin__", "sys"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), - MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), - MovedAttribute("UserList", "UserList", "collections"), - MovedAttribute("UserString", "UserString", "collections"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("_thread", "thread", "_thread"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), -] -# Add windows specific modules. -if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] - -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) - if isinstance(attr, MovedModule): - _importer._add_module(attr, "moves." + attr.name) -del attr - -_MovedItems._moved_attributes = _moved_attributes - -moves = _MovedItems(__name__ + ".moves") -_importer._add_module(moves, "moves") - - -class Module_six_moves_urllib_parse(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_parse""" - - -_urllib_parse_moved_attributes = [ - MovedAttribute("ParseResult", "urlparse", "urllib.parse"), - MovedAttribute("SplitResult", "urlparse", "urllib.parse"), - MovedAttribute("parse_qs", "urlparse", "urllib.parse"), - MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), - MovedAttribute("urldefrag", "urlparse", "urllib.parse"), - MovedAttribute("urljoin", "urlparse", "urllib.parse"), - MovedAttribute("urlparse", "urlparse", "urllib.parse"), - MovedAttribute("urlsplit", "urlparse", "urllib.parse"), - MovedAttribute("urlunparse", "urlparse", "urllib.parse"), - MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), - MovedAttribute("quote", "urllib", "urllib.parse"), - MovedAttribute("quote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote", "urllib", "urllib.parse"), - MovedAttribute("unquote_plus", "urllib", "urllib.parse"), - MovedAttribute("urlencode", "urllib", "urllib.parse"), - MovedAttribute("splitquery", "urllib", "urllib.parse"), - MovedAttribute("splittag", "urllib", "urllib.parse"), - MovedAttribute("splituser", "urllib", "urllib.parse"), - MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), - MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), - MovedAttribute("uses_params", "urlparse", "urllib.parse"), - MovedAttribute("uses_query", "urlparse", "urllib.parse"), - MovedAttribute("uses_relative", "urlparse", "urllib.parse"), -] -for attr in _urllib_parse_moved_attributes: - setattr(Module_six_moves_urllib_parse, attr.name, attr) -del attr - -Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_error""" - - -_urllib_error_moved_attributes = [ - MovedAttribute("URLError", "urllib2", "urllib.error"), - MovedAttribute("HTTPError", "urllib2", "urllib.error"), - MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), -] -for attr in _urllib_error_moved_attributes: - setattr(Module_six_moves_urllib_error, attr.name, attr) -del attr - -Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_request""" - - -_urllib_request_moved_attributes = [ - MovedAttribute("urlopen", "urllib2", "urllib.request"), - MovedAttribute("install_opener", "urllib2", "urllib.request"), - MovedAttribute("build_opener", "urllib2", "urllib.request"), - MovedAttribute("pathname2url", "urllib", "urllib.request"), - MovedAttribute("url2pathname", "urllib", "urllib.request"), - MovedAttribute("getproxies", "urllib", "urllib.request"), - MovedAttribute("Request", "urllib2", "urllib.request"), - MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), - MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), - MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), - MovedAttribute("BaseHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), - MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), - MovedAttribute("FileHandler", "urllib2", "urllib.request"), - MovedAttribute("FTPHandler", "urllib2", "urllib.request"), - MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), - MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), - MovedAttribute("urlretrieve", "urllib", "urllib.request"), - MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), - MovedAttribute("proxy_bypass", "urllib", "urllib.request"), -] -for attr in _urllib_request_moved_attributes: - setattr(Module_six_moves_urllib_request, attr.name, attr) -del attr - -Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_response""" - - -_urllib_response_moved_attributes = [ - MovedAttribute("addbase", "urllib", "urllib.response"), - MovedAttribute("addclosehook", "urllib", "urllib.response"), - MovedAttribute("addinfo", "urllib", "urllib.response"), - MovedAttribute("addinfourl", "urllib", "urllib.response"), -] -for attr in _urllib_response_moved_attributes: - setattr(Module_six_moves_urllib_response, attr.name, attr) -del attr - -Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_robotparser""" - - -_urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), -] -for attr in _urllib_robotparser_moved_attributes: - setattr(Module_six_moves_urllib_robotparser, attr.name, attr) -del attr - -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - __path__ = [] # mark as package - parse = _importer._get_module("moves.urllib_parse") - error = _importer._get_module("moves.urllib_error") - request = _importer._get_module("moves.urllib_request") - response = _importer._get_module("moves.urllib_response") - robotparser = _importer._get_module("moves.urllib_robotparser") - - def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] - -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - def create_unbound_method(func, cls): - return func - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - def create_unbound_method(func, cls): - return types.MethodType(func, None, cls) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") -_add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") - - -if PY3: - def b(s): - return s.encode("latin-1") - - def u(s): - return s - unichr = chr - import struct - int2byte = struct.Struct(">B").pack - del struct - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" -else: - def b(s): - return s - # Workaround for standalone backslash - - def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - unichr = unichr - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) - - def indexbytes(buf, i): - return ord(buf[i]) - iterbytes = functools.partial(itertools.imap, ord) - import StringIO - StringIO = BytesIO = StringIO.StringIO - _assertCountEqual = "assertItemsEqual" - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -def assertCountEqual(self, *args, **kwargs): - return getattr(self, _assertCountEqual)(*args, **kwargs) - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -def assertRegex(self, *args, **kwargs): - return getattr(self, _assertRegex)(*args, **kwargs) - - -if PY3: - exec_ = getattr(moves.builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - if from_value is None: - raise value - raise value from from_value -""") -elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - raise value from from_value -""") -else: - def raise_from(value, from_value): - raise value - - -print_ = getattr(moves.builtins, "print", None) -if print_ is None: - def print_(*args, **kwargs): - """The new-style print function for Python 2.4 and 2.5.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - - def write(data): - if not isinstance(data, basestring): - data = str(data) - # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): - errors = getattr(fp, "errors", None) - if errors is None: - errors = "strict" - data = data.encode(fp.encoding, errors) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) -if sys.version_info[:2] < (3, 3): - _print = print_ - - def print_(*args, **kwargs): - fp = kwargs.get("file", sys.stdout) - flush = kwargs.pop("flush", False) - _print(*args, **kwargs) - if flush and fp is not None: - fp.flush() - -_add_doc(reraise, """Reraise an exception.""") - -if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper -else: - wraps = functools.wraps - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -# Complete the moves implementation. -# This code is at the end of this module to speed up module loading. -# Turn this module into a package. -__path__ = [] # required for PEP 302 and PEP 451 -__package__ = __name__ # see PEP 366 @ReservedAssignment -if globals().get("__spec__") is not None: - __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable -# Remove other six meta path importers, since they cause problems. This can -# happen if six is removed from sys.modules and then reloaded. (Setuptools does -# this for some reason.) -if sys.meta_path: - for i, importer in enumerate(sys.meta_path): - # Here's some real nastiness: Another "instance" of the six module might - # be floating around. Therefore, we can't use isinstance() to check for - # the six meta path importer, since the other six instance will have - # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): - del sys.meta_path[i] - break - del i, importer -# Finally, add the importer to the meta path import hook. -sys.meta_path.append(_importer) +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2015 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.10.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + if from_value is None: + raise value + raise value from from_value +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + raise value from from_value +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.p similarity index 97% rename from telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.py rename to telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.p index accb927..d6594eb 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.p @@ -1,19 +1,19 @@ -import sys - -try: - # Our match_hostname function is the same as 3.5's, so we only want to - # import the match_hostname function if it's at least that good. - if sys.version_info < (3, 5): - raise ImportError("Fallback to vendored code") - - from ssl import CertificateError, match_hostname -except ImportError: - try: - # Backport of the function from a pypi module - from backports.ssl_match_hostname import CertificateError, match_hostname - except ImportError: - # Our vendored copy - from ._implementation import CertificateError, match_hostname - -# Not needed, but documenting what we provide. -__all__ = ('CertificateError', 'match_hostname') +import sys + +try: + # Our match_hostname function is the same as 3.5's, so we only want to + # import the match_hostname function if it's at least that good. + if sys.version_info < (3, 5): + raise ImportError("Fallback to vendored code") + + from ssl import CertificateError, match_hostname +except ImportError: + try: + # Backport of the function from a pypi module + from backports.ssl_match_hostname import CertificateError, match_hostname + except ImportError: + # Our vendored copy + from ._implementation import CertificateError, match_hostname + +# Not needed, but documenting what we provide. +__all__ = ('CertificateError', 'match_hostname') diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implementation.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implement similarity index 97% rename from telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implementation.py rename to telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implement index 1bb3e3e..1fd42f3 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implementation.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implement @@ -1,157 +1,157 @@ -"""The match_hostname() function from Python 3.3.3, essential when using SSL.""" - -# Note: This file is under the PSF license as the code comes from the python -# stdlib. http://docs.python.org/3/license.html - -import re -import sys - -# ipaddress has been backported to 2.6+ in pypi. If it is installed on the -# system, use it to handle IPAddress ServerAltnames (this was added in -# python-3.5) otherwise only do DNS matching. This allows -# backports.ssl_match_hostname to continue to be used all the way back to -# python-2.4. -try: - import ipaddress -except ImportError: - ipaddress = None - -__version__ = '3.5.0.1' - - -class CertificateError(ValueError): - pass - - -def _dnsname_match(dn, hostname, max_wildcards=1): - """Matching according to RFC 6125, section 6.4.3 - - http://tools.ietf.org/html/rfc6125#section-6.4.3 - """ - pats = [] - if not dn: - return False - - # Ported from python3-syntax: - # leftmost, *remainder = dn.split(r'.') - parts = dn.split(r'.') - leftmost = parts[0] - remainder = parts[1:] - - wildcards = leftmost.count('*') - if wildcards > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survey of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise CertificateError( - "too many wildcards in certificate DNS name: " + repr(dn)) - - # speed up common case w/o wildcards - if not wildcards: - return dn.lower() == hostname.lower() - - # RFC 6125, section 6.4.3, subitem 1. - # The client SHOULD NOT attempt to match a presented identifier in which - # the wildcard character comprises a label other than the left-most label. - if leftmost == '*': - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append('[^.]+') - elif leftmost.startswith('xn--') or hostname.startswith('xn--'): - # RFC 6125, section 6.4.3, subitem 3. - # The client SHOULD NOT attempt to match a presented identifier - # where the wildcard character is embedded within an A-label or - # U-label of an internationalized domain name. - pats.append(re.escape(leftmost)) - else: - # Otherwise, '*' matches any dotless string, e.g. www* - pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) - - # add the remaining fragments, ignore any wildcards - for frag in remainder: - pats.append(re.escape(frag)) - - pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) - return pat.match(hostname) - - -def _to_unicode(obj): - if isinstance(obj, str) and sys.version_info < (3,): - obj = unicode(obj, encoding='ascii', errors='strict') - return obj - -def _ipaddress_match(ipname, host_ip): - """Exact matching of IP addresses. - - RFC 6125 explicitly doesn't define an algorithm for this - (section 1.7.2 - "Out of Scope"). - """ - # OpenSSL may add a trailing newline to a subjectAltName's IP address - # Divergence from upstream: ipaddress can't handle byte str - ip = ipaddress.ip_address(_to_unicode(ipname).rstrip()) - return ip == host_ip - - -def match_hostname(cert, hostname): - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 - rules are followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError("empty or no certificate, match_hostname needs a " - "SSL socket or SSL context with either " - "CERT_OPTIONAL or CERT_REQUIRED") - try: - # Divergence from upstream: ipaddress can't handle byte str - host_ip = ipaddress.ip_address(_to_unicode(hostname)) - except ValueError: - # Not an IP address (common case) - host_ip = None - except UnicodeError: - # Divergence from upstream: Have to deal with ipaddress not taking - # byte strings. addresses should be all ascii, so we consider it not - # an ipaddress in this case - host_ip = None - except AttributeError: - # Divergence from upstream: Make ipaddress library optional - if ipaddress is None: - host_ip = None - else: - raise - dnsnames = [] - san = cert.get('subjectAltName', ()) - for key, value in san: - if key == 'DNS': - if host_ip is None and _dnsname_match(value, hostname): - return - dnsnames.append(value) - elif key == 'IP Address': - if host_ip is not None and _ipaddress_match(value, host_ip): - return - dnsnames.append(value) - if not dnsnames: - # The subject is only checked when there is no dNSName entry - # in subjectAltName - for sub in cert.get('subject', ()): - for key, value in sub: - # XXX according to RFC 2818, the most specific Common Name - # must be used. - if key == 'commonName': - if _dnsname_match(value, hostname): - return - dnsnames.append(value) - if len(dnsnames) > 1: - raise CertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) - elif len(dnsnames) == 1: - raise CertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) - else: - raise CertificateError("no appropriate commonName or " - "subjectAltName fields were found") +"""The match_hostname() function from Python 3.3.3, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html + +import re +import sys + +# ipaddress has been backported to 2.6+ in pypi. If it is installed on the +# system, use it to handle IPAddress ServerAltnames (this was added in +# python-3.5) otherwise only do DNS matching. This allows +# backports.ssl_match_hostname to continue to be used all the way back to +# python-2.4. +try: + import ipaddress +except ImportError: + ipaddress = None + +__version__ = '3.5.0.1' + + +class CertificateError(ValueError): + pass + + +def _dnsname_match(dn, hostname, max_wildcards=1): + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r'.') + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count('*') + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn)) + + # speed up common case w/o wildcards + if not wildcards: + return dn.lower() == hostname.lower() + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == '*': + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append('[^.]+') + elif leftmost.startswith('xn--') or hostname.startswith('xn--'): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + return pat.match(hostname) + + +def _to_unicode(obj): + if isinstance(obj, str) and sys.version_info < (3,): + obj = unicode(obj, encoding='ascii', errors='strict') + return obj + +def _ipaddress_match(ipname, host_ip): + """Exact matching of IP addresses. + + RFC 6125 explicitly doesn't define an algorithm for this + (section 1.7.2 - "Out of Scope"). + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + # Divergence from upstream: ipaddress can't handle byte str + ip = ipaddress.ip_address(_to_unicode(ipname).rstrip()) + return ip == host_ip + + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED") + try: + # Divergence from upstream: ipaddress can't handle byte str + host_ip = ipaddress.ip_address(_to_unicode(hostname)) + except ValueError: + # Not an IP address (common case) + host_ip = None + except UnicodeError: + # Divergence from upstream: Have to deal with ipaddress not taking + # byte strings. addresses should be all ascii, so we consider it not + # an ipaddress in this case + host_ip = None + except AttributeError: + # Divergence from upstream: Make ipaddress library optional + if ipaddress is None: + host_ip = None + else: + raise + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == 'IP Address': + if host_ip is not None and _ipaddress_match(value, host_ip): + return + dnsnames.append(value) + if not dnsnames: + # The subject is only checked when there is no dNSName entry + # in subjectAltName + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py index c59935b..cc5a00e 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py @@ -1,363 +1,363 @@ -from __future__ import absolute_import -import collections -import functools -import logging - -from ._collections import RecentlyUsedContainer -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from .connectionpool import port_by_scheme -from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown -from .packages.six.moves.urllib.parse import urljoin -from .request import RequestMethods -from .util.url import parse_url -from .util.retry import Retry - - -__all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url'] - - -log = logging.getLogger(__name__) - -SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs', - 'ssl_version', 'ca_cert_dir', 'ssl_context') - -# The base fields to use when determining what pool to get a connection from; -# these do not rely on the ``connection_pool_kw`` and can be determined by the -# URL and potentially the ``urllib3.connection.port_by_scheme`` dictionary. -# -# All custom key schemes should include the fields in this key at a minimum. -BasePoolKey = collections.namedtuple('BasePoolKey', ('scheme', 'host', 'port')) - -# The fields to use when determining what pool to get a HTTP and HTTPS -# connection from. All additional fields must be present in the PoolManager's -# ``connection_pool_kw`` instance variable. -HTTPPoolKey = collections.namedtuple( - 'HTTPPoolKey', BasePoolKey._fields + ('timeout', 'retries', 'strict', - 'block', 'source_address') -) -HTTPSPoolKey = collections.namedtuple( - 'HTTPSPoolKey', HTTPPoolKey._fields + SSL_KEYWORDS -) - - -def _default_key_normalizer(key_class, request_context): - """ - Create a pool key of type ``key_class`` for a request. - - According to RFC 3986, both the scheme and host are case-insensitive. - Therefore, this function normalizes both before constructing the pool - key for an HTTPS request. If you wish to change this behaviour, provide - alternate callables to ``key_fn_by_scheme``. - - :param key_class: - The class to use when constructing the key. This should be a namedtuple - with the ``scheme`` and ``host`` keys at a minimum. - - :param request_context: - A dictionary-like object that contain the context for a request. - It should contain a key for each field in the :class:`HTTPPoolKey` - """ - context = {} - for key in key_class._fields: - context[key] = request_context.get(key) - context['scheme'] = context['scheme'].lower() - context['host'] = context['host'].lower() - return key_class(**context) - - -# A dictionary that maps a scheme to a callable that creates a pool key. -# This can be used to alter the way pool keys are constructed, if desired. -# Each PoolManager makes a copy of this dictionary so they can be configured -# globally here, or individually on the instance. -key_fn_by_scheme = { - 'http': functools.partial(_default_key_normalizer, HTTPPoolKey), - 'https': functools.partial(_default_key_normalizer, HTTPSPoolKey), -} - -pool_classes_by_scheme = { - 'http': HTTPConnectionPool, - 'https': HTTPSConnectionPool, -} - - -class PoolManager(RequestMethods): - """ - Allows for arbitrary requests while transparently keeping track of - necessary connection pools for you. - - :param num_pools: - Number of connection pools to cache before discarding the least - recently used pool. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param \\**connection_pool_kw: - Additional parameters are used to create fresh - :class:`urllib3.connectionpool.ConnectionPool` instances. - - Example:: - - >>> manager = PoolManager(num_pools=2) - >>> r = manager.request('GET', 'http://google.com/') - >>> r = manager.request('GET', 'http://google.com/mail') - >>> r = manager.request('GET', 'http://yahoo.com/') - >>> len(manager.pools) - 2 - - """ - - proxy = None - - def __init__(self, num_pools=10, headers=None, **connection_pool_kw): - RequestMethods.__init__(self, headers) - self.connection_pool_kw = connection_pool_kw - self.pools = RecentlyUsedContainer(num_pools, - dispose_func=lambda p: p.close()) - - # Locally set the pool classes and keys so other PoolManagers can - # override them. - self.pool_classes_by_scheme = pool_classes_by_scheme - self.key_fn_by_scheme = key_fn_by_scheme.copy() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.clear() - # Return False to re-raise any potential exceptions - return False - - def _new_pool(self, scheme, host, port): - """ - Create a new :class:`ConnectionPool` based on host, port and scheme. - - This method is used to actually create the connection pools handed out - by :meth:`connection_from_url` and companion methods. It is intended - to be overridden for customization. - """ - pool_cls = self.pool_classes_by_scheme[scheme] - kwargs = self.connection_pool_kw - if scheme == 'http': - kwargs = self.connection_pool_kw.copy() - for kw in SSL_KEYWORDS: - kwargs.pop(kw, None) - - return pool_cls(host, port, **kwargs) - - def clear(self): - """ - Empty our store of pools and direct them all to close. - - This will not affect in-flight connections, but they will not be - re-used after completion. - """ - self.pools.clear() - - def connection_from_host(self, host, port=None, scheme='http'): - """ - Get a :class:`ConnectionPool` based on the host, port, and scheme. - - If ``port`` isn't given, it will be derived from the ``scheme`` using - ``urllib3.connectionpool.port_by_scheme``. - """ - - if not host: - raise LocationValueError("No host specified.") - - request_context = self.connection_pool_kw.copy() - request_context['scheme'] = scheme or 'http' - if not port: - port = port_by_scheme.get(request_context['scheme'].lower(), 80) - request_context['port'] = port - request_context['host'] = host - - return self.connection_from_context(request_context) - - def connection_from_context(self, request_context): - """ - Get a :class:`ConnectionPool` based on the request context. - - ``request_context`` must at least contain the ``scheme`` key and its - value must be a key in ``key_fn_by_scheme`` instance variable. - """ - scheme = request_context['scheme'].lower() - pool_key_constructor = self.key_fn_by_scheme[scheme] - pool_key = pool_key_constructor(request_context) - - return self.connection_from_pool_key(pool_key) - - def connection_from_pool_key(self, pool_key): - """ - Get a :class:`ConnectionPool` based on the provided pool key. - - ``pool_key`` should be a namedtuple that only contains immutable - objects. At a minimum it must have the ``scheme``, ``host``, and - ``port`` fields. - """ - with self.pools.lock: - # If the scheme, host, or port doesn't match existing open - # connections, open a new ConnectionPool. - pool = self.pools.get(pool_key) - if pool: - return pool - - # Make a fresh ConnectionPool of the desired type - pool = self._new_pool(pool_key.scheme, pool_key.host, pool_key.port) - self.pools[pool_key] = pool - - return pool - - def connection_from_url(self, url): - """ - Similar to :func:`urllib3.connectionpool.connection_from_url` but - doesn't pass any additional parameters to the - :class:`urllib3.connectionpool.ConnectionPool` constructor. - - Additional parameters are taken from the :class:`.PoolManager` - constructor. - """ - u = parse_url(url) - return self.connection_from_host(u.host, port=u.port, scheme=u.scheme) - - def urlopen(self, method, url, redirect=True, **kw): - """ - Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen` - with custom cross-host redirect logic and only sends the request-uri - portion of the ``url``. - - The given ``url`` parameter must be absolute, such that an appropriate - :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. - """ - u = parse_url(url) - conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) - - kw['assert_same_host'] = False - kw['redirect'] = False - if 'headers' not in kw: - kw['headers'] = self.headers - - if self.proxy is not None and u.scheme == "http": - response = conn.urlopen(method, url, **kw) - else: - response = conn.urlopen(method, u.request_uri, **kw) - - redirect_location = redirect and response.get_redirect_location() - if not redirect_location: - return response - - # Support relative URLs for redirecting. - redirect_location = urljoin(url, redirect_location) - - # RFC 7231, Section 6.4.4 - if response.status == 303: - method = 'GET' - - retries = kw.get('retries') - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect) - - try: - retries = retries.increment(method, url, response=response, _pool=conn) - except MaxRetryError: - if retries.raise_on_redirect: - raise - return response - - kw['retries'] = retries - kw['redirect'] = redirect - - log.info("Redirecting %s -> %s", url, redirect_location) - return self.urlopen(method, redirect_location, **kw) - - -class ProxyManager(PoolManager): - """ - Behaves just like :class:`PoolManager`, but sends all requests through - the defined proxy, using the CONNECT method for HTTPS URLs. - - :param proxy_url: - The URL of the proxy to be used. - - :param proxy_headers: - A dictionary contaning headers that will be sent to the proxy. In case - of HTTP they are being sent with each request, while in the - HTTPS/CONNECT case they are sent only once. Could be used for proxy - authentication. - - Example: - >>> proxy = urllib3.ProxyManager('http://localhost:3128/') - >>> r1 = proxy.request('GET', 'http://google.com/') - >>> r2 = proxy.request('GET', 'http://httpbin.org/') - >>> len(proxy.pools) - 1 - >>> r3 = proxy.request('GET', 'https://httpbin.org/') - >>> r4 = proxy.request('GET', 'https://twitter.com/') - >>> len(proxy.pools) - 3 - - """ - - def __init__(self, proxy_url, num_pools=10, headers=None, - proxy_headers=None, **connection_pool_kw): - - if isinstance(proxy_url, HTTPConnectionPool): - proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host, - proxy_url.port) - proxy = parse_url(proxy_url) - if not proxy.port: - port = port_by_scheme.get(proxy.scheme, 80) - proxy = proxy._replace(port=port) - - if proxy.scheme not in ("http", "https"): - raise ProxySchemeUnknown(proxy.scheme) - - self.proxy = proxy - self.proxy_headers = proxy_headers or {} - - connection_pool_kw['_proxy'] = self.proxy - connection_pool_kw['_proxy_headers'] = self.proxy_headers - - super(ProxyManager, self).__init__( - num_pools, headers, **connection_pool_kw) - - def connection_from_host(self, host, port=None, scheme='http'): - if scheme == "https": - return super(ProxyManager, self).connection_from_host( - host, port, scheme) - - return super(ProxyManager, self).connection_from_host( - self.proxy.host, self.proxy.port, self.proxy.scheme) - - def _set_proxy_headers(self, url, headers=None): - """ - Sets headers needed by proxies: specifically, the Accept and Host - headers. Only sets headers not provided by the user. - """ - headers_ = {'Accept': '*/*'} - - netloc = parse_url(url).netloc - if netloc: - headers_['Host'] = netloc - - if headers: - headers_.update(headers) - return headers_ - - def urlopen(self, method, url, redirect=True, **kw): - "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." - u = parse_url(url) - - if u.scheme == "http": - # For proxied HTTPS requests, httplib sets the necessary headers - # on the CONNECT to the proxy. For HTTP, we'll definitely - # need to set 'Host' at the very least. - headers = kw.get('headers', self.headers) - kw['headers'] = self._set_proxy_headers(url, headers) - - return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw) - - -def proxy_from_url(url, **kw): - return ProxyManager(proxy_url=url, **kw) +from __future__ import absolute_import +import collections +import functools +import logging + +from ._collections import RecentlyUsedContainer +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from .connectionpool import port_by_scheme +from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown +from .packages.six.moves.urllib.parse import urljoin +from .request import RequestMethods +from .util.url import parse_url +from .util.retry import Retry + + +__all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url'] + + +log = logging.getLogger(__name__) + +SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs', + 'ssl_version', 'ca_cert_dir', 'ssl_context') + +# The base fields to use when determining what pool to get a connection from; +# these do not rely on the ``connection_pool_kw`` and can be determined by the +# URL and potentially the ``urllib3.connection.port_by_scheme`` dictionary. +# +# All custom key schemes should include the fields in this key at a minimum. +BasePoolKey = collections.namedtuple('BasePoolKey', ('scheme', 'host', 'port')) + +# The fields to use when determining what pool to get a HTTP and HTTPS +# connection from. All additional fields must be present in the PoolManager's +# ``connection_pool_kw`` instance variable. +HTTPPoolKey = collections.namedtuple( + 'HTTPPoolKey', BasePoolKey._fields + ('timeout', 'retries', 'strict', + 'block', 'source_address') +) +HTTPSPoolKey = collections.namedtuple( + 'HTTPSPoolKey', HTTPPoolKey._fields + SSL_KEYWORDS +) + + +def _default_key_normalizer(key_class, request_context): + """ + Create a pool key of type ``key_class`` for a request. + + According to RFC 3986, both the scheme and host are case-insensitive. + Therefore, this function normalizes both before constructing the pool + key for an HTTPS request. If you wish to change this behaviour, provide + alternate callables to ``key_fn_by_scheme``. + + :param key_class: + The class to use when constructing the key. This should be a namedtuple + with the ``scheme`` and ``host`` keys at a minimum. + + :param request_context: + A dictionary-like object that contain the context for a request. + It should contain a key for each field in the :class:`HTTPPoolKey` + """ + context = {} + for key in key_class._fields: + context[key] = request_context.get(key) + context['scheme'] = context['scheme'].lower() + context['host'] = context['host'].lower() + return key_class(**context) + + +# A dictionary that maps a scheme to a callable that creates a pool key. +# This can be used to alter the way pool keys are constructed, if desired. +# Each PoolManager makes a copy of this dictionary so they can be configured +# globally here, or individually on the instance. +key_fn_by_scheme = { + 'http': functools.partial(_default_key_normalizer, HTTPPoolKey), + 'https': functools.partial(_default_key_normalizer, HTTPSPoolKey), +} + +pool_classes_by_scheme = { + 'http': HTTPConnectionPool, + 'https': HTTPSConnectionPool, +} + + +class PoolManager(RequestMethods): + """ + Allows for arbitrary requests while transparently keeping track of + necessary connection pools for you. + + :param num_pools: + Number of connection pools to cache before discarding the least + recently used pool. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param \\**connection_pool_kw: + Additional parameters are used to create fresh + :class:`urllib3.connectionpool.ConnectionPool` instances. + + Example:: + + >>> manager = PoolManager(num_pools=2) + >>> r = manager.request('GET', 'http://google.com/') + >>> r = manager.request('GET', 'http://google.com/mail') + >>> r = manager.request('GET', 'http://yahoo.com/') + >>> len(manager.pools) + 2 + + """ + + proxy = None + + def __init__(self, num_pools=10, headers=None, **connection_pool_kw): + RequestMethods.__init__(self, headers) + self.connection_pool_kw = connection_pool_kw + self.pools = RecentlyUsedContainer(num_pools, + dispose_func=lambda p: p.close()) + + # Locally set the pool classes and keys so other PoolManagers can + # override them. + self.pool_classes_by_scheme = pool_classes_by_scheme + self.key_fn_by_scheme = key_fn_by_scheme.copy() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.clear() + # Return False to re-raise any potential exceptions + return False + + def _new_pool(self, scheme, host, port): + """ + Create a new :class:`ConnectionPool` based on host, port and scheme. + + This method is used to actually create the connection pools handed out + by :meth:`connection_from_url` and companion methods. It is intended + to be overridden for customization. + """ + pool_cls = self.pool_classes_by_scheme[scheme] + kwargs = self.connection_pool_kw + if scheme == 'http': + kwargs = self.connection_pool_kw.copy() + for kw in SSL_KEYWORDS: + kwargs.pop(kw, None) + + return pool_cls(host, port, **kwargs) + + def clear(self): + """ + Empty our store of pools and direct them all to close. + + This will not affect in-flight connections, but they will not be + re-used after completion. + """ + self.pools.clear() + + def connection_from_host(self, host, port=None, scheme='http'): + """ + Get a :class:`ConnectionPool` based on the host, port, and scheme. + + If ``port`` isn't given, it will be derived from the ``scheme`` using + ``urllib3.connectionpool.port_by_scheme``. + """ + + if not host: + raise LocationValueError("No host specified.") + + request_context = self.connection_pool_kw.copy() + request_context['scheme'] = scheme or 'http' + if not port: + port = port_by_scheme.get(request_context['scheme'].lower(), 80) + request_context['port'] = port + request_context['host'] = host + + return self.connection_from_context(request_context) + + def connection_from_context(self, request_context): + """ + Get a :class:`ConnectionPool` based on the request context. + + ``request_context`` must at least contain the ``scheme`` key and its + value must be a key in ``key_fn_by_scheme`` instance variable. + """ + scheme = request_context['scheme'].lower() + pool_key_constructor = self.key_fn_by_scheme[scheme] + pool_key = pool_key_constructor(request_context) + + return self.connection_from_pool_key(pool_key) + + def connection_from_pool_key(self, pool_key): + """ + Get a :class:`ConnectionPool` based on the provided pool key. + + ``pool_key`` should be a namedtuple that only contains immutable + objects. At a minimum it must have the ``scheme``, ``host``, and + ``port`` fields. + """ + with self.pools.lock: + # If the scheme, host, or port doesn't match existing open + # connections, open a new ConnectionPool. + pool = self.pools.get(pool_key) + if pool: + return pool + + # Make a fresh ConnectionPool of the desired type + pool = self._new_pool(pool_key.scheme, pool_key.host, pool_key.port) + self.pools[pool_key] = pool + + return pool + + def connection_from_url(self, url): + """ + Similar to :func:`urllib3.connectionpool.connection_from_url` but + doesn't pass any additional parameters to the + :class:`urllib3.connectionpool.ConnectionPool` constructor. + + Additional parameters are taken from the :class:`.PoolManager` + constructor. + """ + u = parse_url(url) + return self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + def urlopen(self, method, url, redirect=True, **kw): + """ + Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen` + with custom cross-host redirect logic and only sends the request-uri + portion of the ``url``. + + The given ``url`` parameter must be absolute, such that an appropriate + :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. + """ + u = parse_url(url) + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + kw['assert_same_host'] = False + kw['redirect'] = False + if 'headers' not in kw: + kw['headers'] = self.headers + + if self.proxy is not None and u.scheme == "http": + response = conn.urlopen(method, url, **kw) + else: + response = conn.urlopen(method, u.request_uri, **kw) + + redirect_location = redirect and response.get_redirect_location() + if not redirect_location: + return response + + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + + # RFC 7231, Section 6.4.4 + if response.status == 303: + method = 'GET' + + retries = kw.get('retries') + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + + try: + retries = retries.increment(method, url, response=response, _pool=conn) + except MaxRetryError: + if retries.raise_on_redirect: + raise + return response + + kw['retries'] = retries + kw['redirect'] = redirect + + log.info("Redirecting %s -> %s", url, redirect_location) + return self.urlopen(method, redirect_location, **kw) + + +class ProxyManager(PoolManager): + """ + Behaves just like :class:`PoolManager`, but sends all requests through + the defined proxy, using the CONNECT method for HTTPS URLs. + + :param proxy_url: + The URL of the proxy to be used. + + :param proxy_headers: + A dictionary contaning headers that will be sent to the proxy. In case + of HTTP they are being sent with each request, while in the + HTTPS/CONNECT case they are sent only once. Could be used for proxy + authentication. + + Example: + >>> proxy = urllib3.ProxyManager('http://localhost:3128/') + >>> r1 = proxy.request('GET', 'http://google.com/') + >>> r2 = proxy.request('GET', 'http://httpbin.org/') + >>> len(proxy.pools) + 1 + >>> r3 = proxy.request('GET', 'https://httpbin.org/') + >>> r4 = proxy.request('GET', 'https://twitter.com/') + >>> len(proxy.pools) + 3 + + """ + + def __init__(self, proxy_url, num_pools=10, headers=None, + proxy_headers=None, **connection_pool_kw): + + if isinstance(proxy_url, HTTPConnectionPool): + proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host, + proxy_url.port) + proxy = parse_url(proxy_url) + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + + if proxy.scheme not in ("http", "https"): + raise ProxySchemeUnknown(proxy.scheme) + + self.proxy = proxy + self.proxy_headers = proxy_headers or {} + + connection_pool_kw['_proxy'] = self.proxy + connection_pool_kw['_proxy_headers'] = self.proxy_headers + + super(ProxyManager, self).__init__( + num_pools, headers, **connection_pool_kw) + + def connection_from_host(self, host, port=None, scheme='http'): + if scheme == "https": + return super(ProxyManager, self).connection_from_host( + host, port, scheme) + + return super(ProxyManager, self).connection_from_host( + self.proxy.host, self.proxy.port, self.proxy.scheme) + + def _set_proxy_headers(self, url, headers=None): + """ + Sets headers needed by proxies: specifically, the Accept and Host + headers. Only sets headers not provided by the user. + """ + headers_ = {'Accept': '*/*'} + + netloc = parse_url(url).netloc + if netloc: + headers_['Host'] = netloc + + if headers: + headers_.update(headers) + return headers_ + + def urlopen(self, method, url, redirect=True, **kw): + "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." + u = parse_url(url) + + if u.scheme == "http": + # For proxied HTTPS requests, httplib sets the necessary headers + # on the CONNECT to the proxy. For HTTP, we'll definitely + # need to set 'Host' at the very least. + headers = kw.get('headers', self.headers) + kw['headers'] = self._set_proxy_headers(url, headers) + + return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw) + + +def proxy_from_url(url, **kw): + return ProxyManager(proxy_url=url, **kw) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py index 9d789d6..c0fddff 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py @@ -1,148 +1,148 @@ -from __future__ import absolute_import - -from .filepost import encode_multipart_formdata -from .packages.six.moves.urllib.parse import urlencode - - -__all__ = ['RequestMethods'] - - -class RequestMethods(object): - """ - Convenience mixin for classes who implement a :meth:`urlopen` method, such - as :class:`~urllib3.connectionpool.HTTPConnectionPool` and - :class:`~urllib3.poolmanager.PoolManager`. - - Provides behavior for making common types of HTTP request methods and - decides which type of request field encoding to use. - - Specifically, - - :meth:`.request_encode_url` is for sending requests whose fields are - encoded in the URL (such as GET, HEAD, DELETE). - - :meth:`.request_encode_body` is for sending requests whose fields are - encoded in the *body* of the request using multipart or www-form-urlencoded - (such as for POST, PUT, PATCH). - - :meth:`.request` is for making any kind of request, it will look up the - appropriate encoding format and use one of the above two methods to make - the request. - - Initializer parameters: - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - """ - - _encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS']) - - def __init__(self, headers=None): - self.headers = headers or {} - - def urlopen(self, method, url, body=None, headers=None, - encode_multipart=True, multipart_boundary=None, - **kw): # Abstract - raise NotImplemented("Classes extending RequestMethods must implement " - "their own ``urlopen`` method.") - - def request(self, method, url, fields=None, headers=None, **urlopen_kw): - """ - Make a request using :meth:`urlopen` with the appropriate encoding of - ``fields`` based on the ``method`` used. - - This is a convenience method that requires the least amount of manual - effort. It can be used in most situations, while still having the - option to drop down to more specific methods when necessary, such as - :meth:`request_encode_url`, :meth:`request_encode_body`, - or even the lowest level :meth:`urlopen`. - """ - method = method.upper() - - if method in self._encode_url_methods: - return self.request_encode_url(method, url, fields=fields, - headers=headers, - **urlopen_kw) - else: - return self.request_encode_body(method, url, fields=fields, - headers=headers, - **urlopen_kw) - - def request_encode_url(self, method, url, fields=None, headers=None, - **urlopen_kw): - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the url. This is useful for request methods like GET, HEAD, DELETE, etc. - """ - if headers is None: - headers = self.headers - - extra_kw = {'headers': headers} - extra_kw.update(urlopen_kw) - - if fields: - url += '?' + urlencode(fields) - - return self.urlopen(method, url, **extra_kw) - - def request_encode_body(self, method, url, fields=None, headers=None, - encode_multipart=True, multipart_boundary=None, - **urlopen_kw): - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the body. This is useful for request methods like POST, PUT, PATCH, etc. - - When ``encode_multipart=True`` (default), then - :meth:`urllib3.filepost.encode_multipart_formdata` is used to encode - the payload with the appropriate content type. Otherwise - :meth:`urllib.urlencode` is used with the - 'application/x-www-form-urlencoded' content type. - - Multipart encoding must be used when posting files, and it's reasonably - safe to use it in other times too. However, it may break request - signing, such as with OAuth. - - Supports an optional ``fields`` parameter of key/value strings AND - key/filetuple. A filetuple is a (filename, data, MIME type) tuple where - the MIME type is optional. For example:: - - fields = { - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), - 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - } - - When uploading a file, providing a filename (the first parameter of the - tuple) is optional but recommended to best mimick behavior of browsers. - - Note that if ``headers`` are supplied, the 'Content-Type' header will - be overwritten because it depends on the dynamic random boundary string - which is used to compose the body of the request. The random boundary - string can be explicitly set with the ``multipart_boundary`` parameter. - """ - if headers is None: - headers = self.headers - - extra_kw = {'headers': {}} - - if fields: - if 'body' in urlopen_kw: - raise TypeError( - "request got values for both 'fields' and 'body', can only specify one.") - - if encode_multipart: - body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary) - else: - body, content_type = urlencode(fields), 'application/x-www-form-urlencoded' - - extra_kw['body'] = body - extra_kw['headers'] = {'Content-Type': content_type} - - extra_kw['headers'].update(headers) - extra_kw.update(urlopen_kw) - - return self.urlopen(method, url, **extra_kw) +from __future__ import absolute_import + +from .filepost import encode_multipart_formdata +from .packages.six.moves.urllib.parse import urlencode + + +__all__ = ['RequestMethods'] + + +class RequestMethods(object): + """ + Convenience mixin for classes who implement a :meth:`urlopen` method, such + as :class:`~urllib3.connectionpool.HTTPConnectionPool` and + :class:`~urllib3.poolmanager.PoolManager`. + + Provides behavior for making common types of HTTP request methods and + decides which type of request field encoding to use. + + Specifically, + + :meth:`.request_encode_url` is for sending requests whose fields are + encoded in the URL (such as GET, HEAD, DELETE). + + :meth:`.request_encode_body` is for sending requests whose fields are + encoded in the *body* of the request using multipart or www-form-urlencoded + (such as for POST, PUT, PATCH). + + :meth:`.request` is for making any kind of request, it will look up the + appropriate encoding format and use one of the above two methods to make + the request. + + Initializer parameters: + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + """ + + _encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS']) + + def __init__(self, headers=None): + self.headers = headers or {} + + def urlopen(self, method, url, body=None, headers=None, + encode_multipart=True, multipart_boundary=None, + **kw): # Abstract + raise NotImplemented("Classes extending RequestMethods must implement " + "their own ``urlopen`` method.") + + def request(self, method, url, fields=None, headers=None, **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the appropriate encoding of + ``fields`` based on the ``method`` used. + + This is a convenience method that requires the least amount of manual + effort. It can be used in most situations, while still having the + option to drop down to more specific methods when necessary, such as + :meth:`request_encode_url`, :meth:`request_encode_body`, + or even the lowest level :meth:`urlopen`. + """ + method = method.upper() + + if method in self._encode_url_methods: + return self.request_encode_url(method, url, fields=fields, + headers=headers, + **urlopen_kw) + else: + return self.request_encode_body(method, url, fields=fields, + headers=headers, + **urlopen_kw) + + def request_encode_url(self, method, url, fields=None, headers=None, + **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the url. This is useful for request methods like GET, HEAD, DELETE, etc. + """ + if headers is None: + headers = self.headers + + extra_kw = {'headers': headers} + extra_kw.update(urlopen_kw) + + if fields: + url += '?' + urlencode(fields) + + return self.urlopen(method, url, **extra_kw) + + def request_encode_body(self, method, url, fields=None, headers=None, + encode_multipart=True, multipart_boundary=None, + **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the body. This is useful for request methods like POST, PUT, PATCH, etc. + + When ``encode_multipart=True`` (default), then + :meth:`urllib3.filepost.encode_multipart_formdata` is used to encode + the payload with the appropriate content type. Otherwise + :meth:`urllib.urlencode` is used with the + 'application/x-www-form-urlencoded' content type. + + Multipart encoding must be used when posting files, and it's reasonably + safe to use it in other times too. However, it may break request + signing, such as with OAuth. + + Supports an optional ``fields`` parameter of key/value strings AND + key/filetuple. A filetuple is a (filename, data, MIME type) tuple where + the MIME type is optional. For example:: + + fields = { + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), + 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + } + + When uploading a file, providing a filename (the first parameter of the + tuple) is optional but recommended to best mimick behavior of browsers. + + Note that if ``headers`` are supplied, the 'Content-Type' header will + be overwritten because it depends on the dynamic random boundary string + which is used to compose the body of the request. The random boundary + string can be explicitly set with the ``multipart_boundary`` parameter. + """ + if headers is None: + headers = self.headers + + extra_kw = {'headers': {}} + + if fields: + if 'body' in urlopen_kw: + raise TypeError( + "request got values for both 'fields' and 'body', can only specify one.") + + if encode_multipart: + body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary) + else: + body, content_type = urlencode(fields), 'application/x-www-form-urlencoded' + + extra_kw['body'] = body + extra_kw['headers'] = {'Content-Type': content_type} + + extra_kw['headers'].update(headers) + extra_kw.update(urlopen_kw) + + return self.urlopen(method, url, **extra_kw) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py index 55db29a..6f1b63c 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py @@ -1,618 +1,618 @@ -from __future__ import absolute_import -from contextlib import contextmanager -import zlib -import io -import logging -from socket import timeout as SocketTimeout -from socket import error as SocketError - -from ._collections import HTTPHeaderDict -from .exceptions import ( - BodyNotHttplibCompatible, ProtocolError, DecodeError, ReadTimeoutError, - ResponseNotChunked, IncompleteRead, InvalidHeader -) -from .packages.six import string_types as basestring, binary_type, PY3 -from .packages.six.moves import http_client as httplib -from .connection import HTTPException, BaseSSLError -from .util.response import is_fp_closed, is_response_to_head - -log = logging.getLogger(__name__) - - -class DeflateDecoder(object): - - def __init__(self): - self._first_try = True - self._data = binary_type() - self._obj = zlib.decompressobj() - - def __getattr__(self, name): - return getattr(self._obj, name) - - def decompress(self, data): - if not data: - return data - - if not self._first_try: - return self._obj.decompress(data) - - self._data += data - try: - return self._obj.decompress(data) - except zlib.error: - self._first_try = False - self._obj = zlib.decompressobj(-zlib.MAX_WBITS) - try: - return self.decompress(self._data) - finally: - self._data = None - - -class GzipDecoder(object): - - def __init__(self): - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - - def __getattr__(self, name): - return getattr(self._obj, name) - - def decompress(self, data): - if not data: - return data - return self._obj.decompress(data) - - -def _get_decoder(mode): - if mode == 'gzip': - return GzipDecoder() - - return DeflateDecoder() - - -class HTTPResponse(io.IOBase): - """ - HTTP Response container. - - Backwards-compatible to httplib's HTTPResponse but the response ``body`` is - loaded and decoded on-demand when the ``data`` property is accessed. This - class is also compatible with the Python standard library's :mod:`io` - module, and can hence be treated as a readable object in the context of that - framework. - - Extra parameters for behaviour not present in httplib.HTTPResponse: - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, attempts to decode specific content-encoding's based on headers - (like 'gzip' and 'deflate') will be skipped and raw data will be used - instead. - - :param original_response: - When this HTTPResponse wrapper is generated from an httplib.HTTPResponse - object, it's convenient to include the original for debug purposes. It's - otherwise unused. - - :param retries: - The retries contains the last :class:`~urllib3.util.retry.Retry` that - was used during the request. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - - CONTENT_DECODERS = ['gzip', 'deflate'] - REDIRECT_STATUSES = [301, 302, 303, 307, 308] - - def __init__(self, body='', headers=None, status=0, version=0, reason=None, - strict=0, preload_content=True, decode_content=True, - original_response=None, pool=None, connection=None, - retries=None, enforce_content_length=False, request_method=None): - - if isinstance(headers, HTTPHeaderDict): - self.headers = headers - else: - self.headers = HTTPHeaderDict(headers) - self.status = status - self.version = version - self.reason = reason - self.strict = strict - self.decode_content = decode_content - self.retries = retries - self.enforce_content_length = enforce_content_length - - self._decoder = None - self._body = None - self._fp = None - self._original_response = original_response - self._fp_bytes_read = 0 - - if body and isinstance(body, (basestring, binary_type)): - self._body = body - - self._pool = pool - self._connection = connection - - if hasattr(body, 'read'): - self._fp = body - - # Are we using the chunked-style of transfer encoding? - self.chunked = False - self.chunk_left = None - tr_enc = self.headers.get('transfer-encoding', '').lower() - # Don't incur the penalty of creating a list and then discarding it - encodings = (enc.strip() for enc in tr_enc.split(",")) - if "chunked" in encodings: - self.chunked = True - - # Determine length of response - self.length_remaining = self._init_length(request_method) - - # If requested, preload the body. - if preload_content and not self._body: - self._body = self.read(decode_content=decode_content) - - def get_redirect_location(self): - """ - Should we redirect and where to? - - :returns: Truthy redirect location string if we got a redirect status - code and valid location. ``None`` if redirect status and no - location. ``False`` if not a redirect status code. - """ - if self.status in self.REDIRECT_STATUSES: - return self.headers.get('location') - - return False - - def release_conn(self): - if not self._pool or not self._connection: - return - - self._pool._put_conn(self._connection) - self._connection = None - - @property - def data(self): - # For backwords-compat with earlier urllib3 0.4 and earlier. - if self._body: - return self._body - - if self._fp: - return self.read(cache_content=True) - - @property - def connection(self): - return self._connection - - def tell(self): - """ - Obtain the number of bytes pulled over the wire so far. May differ from - the amount of content returned by :meth:``HTTPResponse.read`` if bytes - are encoded on the wire (e.g, compressed). - """ - return self._fp_bytes_read - - def _init_length(self, request_method): - """ - Set initial length value for Response content if available. - """ - length = self.headers.get('content-length') - - if length is not None and self.chunked: - # This Response will fail with an IncompleteRead if it can't be - # received as chunked. This method falls back to attempt reading - # the response before raising an exception. - log.warning("Received response with both Content-Length and " - "Transfer-Encoding set. This is expressly forbidden " - "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " - "attempting to process response as Transfer-Encoding: " - "chunked.") - return None - - elif length is not None: - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = set([int(val) for val in length.split(',')]) - if len(lengths) > 1: - raise InvalidHeader("Content-Length contained multiple " - "unmatching values (%s)" % length) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - # Convert status to int for comparison - # In some cases, httplib returns a status of "_UNKNOWN" - try: - status = int(self.status) - except ValueError: - status = 0 - - # Check for responses that shouldn't include a body - if status in (204, 304) or 100 <= status < 200 or request_method == 'HEAD': - length = 0 - - return length - - def _init_decoder(self): - """ - Set-up the _decoder attribute if necessary. - """ - # Note: content-encoding value should be case-insensitive, per RFC 7230 - # Section 3.2 - content_encoding = self.headers.get('content-encoding', '').lower() - if self._decoder is None and content_encoding in self.CONTENT_DECODERS: - self._decoder = _get_decoder(content_encoding) - - def _decode(self, data, decode_content, flush_decoder): - """ - Decode the data passed in and potentially flush the decoder. - """ - try: - if decode_content and self._decoder: - data = self._decoder.decompress(data) - except (IOError, zlib.error) as e: - content_encoding = self.headers.get('content-encoding', '').lower() - raise DecodeError( - "Received response with content-encoding: %s, but " - "failed to decode it." % content_encoding, e) - - if flush_decoder and decode_content: - data += self._flush_decoder() - - return data - - def _flush_decoder(self): - """ - Flushes the decoder. Should only be called if the decoder is actually - being used. - """ - if self._decoder: - buf = self._decoder.decompress(b'') - return buf + self._decoder.flush() - - return b'' - - @contextmanager - def _error_catcher(self): - """ - Catch low-level python exceptions, instead re-raising urllib3 - variants, so that low-level exceptions are not leaked in the - high-level api. - - On exit, release the connection back to the pool. - """ - clean_exit = False - - try: - try: - yield - - except SocketTimeout: - # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but - # there is yet no clean way to get at it from this context. - raise ReadTimeoutError(self._pool, None, 'Read timed out.') - - except BaseSSLError as e: - # FIXME: Is there a better way to differentiate between SSLErrors? - if 'read operation timed out' not in str(e): # Defensive: - # This shouldn't happen but just in case we're missing an edge - # case, let's avoid swallowing SSL errors. - raise - - raise ReadTimeoutError(self._pool, None, 'Read timed out.') - - except (HTTPException, SocketError) as e: - # This includes IncompleteRead. - raise ProtocolError('Connection broken: %r' % e, e) - - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now to ensure that the connection is - # released back to the pool. - if self._original_response: - self._original_response.close() - - # Closing the response may not actually be sufficient to close - # everything, so if we have a hold of the connection close that - # too. - if self._connection: - self._connection.close() - - # If we hold the original response but it's closed now, we should - # return the connection back to the pool. - if self._original_response and self._original_response.isclosed(): - self.release_conn() - - def read(self, amt=None, decode_content=None, cache_content=False): - """ - Similar to :meth:`httplib.HTTPResponse.read`, but with two additional - parameters: ``decode_content`` and ``cache_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param cache_content: - If True, will save the returned data such that the same result is - returned despite of the state of the underlying file object. This - is useful if you want the ``.data`` property to continue working - after having ``.read()`` the file object. (Overridden if ``amt`` is - set.) - """ - self._init_decoder() - if decode_content is None: - decode_content = self.decode_content - - if self._fp is None: - return - - flush_decoder = False - data = None - - with self._error_catcher(): - if amt is None: - # cStringIO doesn't like amt=None - data = self._fp.read() - flush_decoder = True - else: - cache_content = False - data = self._fp.read(amt) - if amt != 0 and not data: # Platform-specific: Buggy versions of Python. - # Close the connection when no data is returned - # - # This is redundant to what httplib/http.client _should_ - # already do. However, versions of python released before - # December 15, 2012 (http://bugs.python.org/issue16298) do - # not properly close the connection in all cases. There is - # no harm in redundantly calling close. - self._fp.close() - flush_decoder = True - if self.enforce_content_length and self.length_remaining not in (0, None): - # This is an edge case that httplib failed to cover due - # to concerns of backward compatibility. We're - # addressing it here to make sure IncompleteRead is - # raised during streaming, so all calls with incorrect - # Content-Length are caught. - raise IncompleteRead(self._fp_bytes_read, self.length_remaining) - - if data: - self._fp_bytes_read += len(data) - if self.length_remaining is not None: - self.length_remaining -= len(data) - - data = self._decode(data, decode_content, flush_decoder) - - if cache_content: - self._body = data - - return data - - def stream(self, amt=2**16, decode_content=None): - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if self.chunked and self.supports_chunked_reads(): - for line in self.read_chunked(amt, decode_content=decode_content): - yield line - else: - while not is_fp_closed(self._fp): - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - - @classmethod - def from_httplib(ResponseCls, r, **response_kw): - """ - Given an :class:`httplib.HTTPResponse` instance ``r``, return a - corresponding :class:`urllib3.response.HTTPResponse` object. - - Remaining parameters are passed to the HTTPResponse constructor, along - with ``original_response=r``. - """ - headers = r.msg - - if not isinstance(headers, HTTPHeaderDict): - if PY3: # Python 3 - headers = HTTPHeaderDict(headers.items()) - else: # Python 2 - headers = HTTPHeaderDict.from_httplib(headers) - - # HTTPResponse objects in Python 3 don't have a .strict attribute - strict = getattr(r, 'strict', 0) - resp = ResponseCls(body=r, - headers=headers, - status=r.status, - version=r.version, - reason=r.reason, - strict=strict, - original_response=r, - **response_kw) - return resp - - # Backwards-compatibility methods for httplib.HTTPResponse - def getheaders(self): - return self.headers - - def getheader(self, name, default=None): - return self.headers.get(name, default) - - # Overrides from io.IOBase - def close(self): - if not self.closed: - self._fp.close() - - if self._connection: - self._connection.close() - - @property - def closed(self): - if self._fp is None: - return True - elif hasattr(self._fp, 'isclosed'): - return self._fp.isclosed() - elif hasattr(self._fp, 'closed'): - return self._fp.closed - else: - return True - - def fileno(self): - if self._fp is None: - raise IOError("HTTPResponse has no file to get a fileno from") - elif hasattr(self._fp, "fileno"): - return self._fp.fileno() - else: - raise IOError("The file-like object this HTTPResponse is wrapped " - "around has no file descriptor") - - def flush(self): - if self._fp is not None and hasattr(self._fp, 'flush'): - return self._fp.flush() - - def readable(self): - # This method is required for `io` module compatibility. - return True - - def readinto(self, b): - # This method is required for `io` module compatibility. - temp = self.read(len(b)) - if len(temp) == 0: - return 0 - else: - b[:len(temp)] = temp - return len(temp) - - def supports_chunked_reads(self): - """ - Checks if the underlying file-like object looks like a - httplib.HTTPResponse object. We do this by testing for the fp - attribute. If it is present we assume it returns raw chunks as - processed by read_chunked(). - """ - return hasattr(self._fp, 'fp') - - def _update_chunk_length(self): - # First, we'll figure out length of a chunk and then - # we'll try to read it from socket. - if self.chunk_left is not None: - return - line = self._fp.fp.readline() - line = line.split(b';', 1)[0] - try: - self.chunk_left = int(line, 16) - except ValueError: - # Invalid chunked protocol response, abort. - self.close() - raise httplib.IncompleteRead(line) - - def _handle_chunk(self, amt): - returned_chunk = None - if amt is None: - chunk = self._fp._safe_read(self.chunk_left) - returned_chunk = chunk - self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. - self.chunk_left = None - elif amt < self.chunk_left: - value = self._fp._safe_read(amt) - self.chunk_left = self.chunk_left - amt - returned_chunk = value - elif amt == self.chunk_left: - value = self._fp._safe_read(amt) - self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. - self.chunk_left = None - returned_chunk = value - else: # amt > self.chunk_left - returned_chunk = self._fp._safe_read(self.chunk_left) - self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. - self.chunk_left = None - return returned_chunk - - def read_chunked(self, amt=None, decode_content=None): - """ - Similar to :meth:`HTTPResponse.read`, but with an additional - parameter: ``decode_content``. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - self._init_decoder() - # FIXME: Rewrite this method and make it a class with a better structured logic. - if not self.chunked: - raise ResponseNotChunked( - "Response is not chunked. " - "Header 'transfer-encoding: chunked' is missing.") - if not self.supports_chunked_reads(): - raise BodyNotHttplibCompatible( - "Body should be httplib.HTTPResponse like. " - "It should have have an fp attribute which returns raw chunks.") - - # Don't bother reading the body of a HEAD request. - if self._original_response and is_response_to_head(self._original_response): - self._original_response.close() - return - - with self._error_catcher(): - while True: - self._update_chunk_length() - if self.chunk_left == 0: - break - chunk = self._handle_chunk(amt) - decoded = self._decode(chunk, decode_content=decode_content, - flush_decoder=False) - if decoded: - yield decoded - - if decode_content: - # On CPython and PyPy, we should never need to flush the - # decoder. However, on Jython we *might* need to, so - # lets defensively do it anyway. - decoded = self._flush_decoder() - if decoded: # Platform-specific: Jython. - yield decoded - - # Chunk content ends with \r\n: discard it. - while True: - line = self._fp.fp.readline() - if not line: - # Some sites may not end with '\r\n'. - break - if line == b'\r\n': - break - - # We read everything; close the "file". - if self._original_response: - self._original_response.close() +from __future__ import absolute_import +from contextlib import contextmanager +import zlib +import io +import logging +from socket import timeout as SocketTimeout +from socket import error as SocketError + +from ._collections import HTTPHeaderDict +from .exceptions import ( + BodyNotHttplibCompatible, ProtocolError, DecodeError, ReadTimeoutError, + ResponseNotChunked, IncompleteRead, InvalidHeader +) +from .packages.six import string_types as basestring, binary_type, PY3 +from .packages.six.moves import http_client as httplib +from .connection import HTTPException, BaseSSLError +from .util.response import is_fp_closed, is_response_to_head + +log = logging.getLogger(__name__) + + +class DeflateDecoder(object): + + def __init__(self): + self._first_try = True + self._data = binary_type() + self._obj = zlib.decompressobj() + + def __getattr__(self, name): + return getattr(self._obj, name) + + def decompress(self, data): + if not data: + return data + + if not self._first_try: + return self._obj.decompress(data) + + self._data += data + try: + return self._obj.decompress(data) + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress(self._data) + finally: + self._data = None + + +class GzipDecoder(object): + + def __init__(self): + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + + def __getattr__(self, name): + return getattr(self._obj, name) + + def decompress(self, data): + if not data: + return data + return self._obj.decompress(data) + + +def _get_decoder(mode): + if mode == 'gzip': + return GzipDecoder() + + return DeflateDecoder() + + +class HTTPResponse(io.IOBase): + """ + HTTP Response container. + + Backwards-compatible to httplib's HTTPResponse but the response ``body`` is + loaded and decoded on-demand when the ``data`` property is accessed. This + class is also compatible with the Python standard library's :mod:`io` + module, and can hence be treated as a readable object in the context of that + framework. + + Extra parameters for behaviour not present in httplib.HTTPResponse: + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, attempts to decode specific content-encoding's based on headers + (like 'gzip' and 'deflate') will be skipped and raw data will be used + instead. + + :param original_response: + When this HTTPResponse wrapper is generated from an httplib.HTTPResponse + object, it's convenient to include the original for debug purposes. It's + otherwise unused. + + :param retries: + The retries contains the last :class:`~urllib3.util.retry.Retry` that + was used during the request. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + + CONTENT_DECODERS = ['gzip', 'deflate'] + REDIRECT_STATUSES = [301, 302, 303, 307, 308] + + def __init__(self, body='', headers=None, status=0, version=0, reason=None, + strict=0, preload_content=True, decode_content=True, + original_response=None, pool=None, connection=None, + retries=None, enforce_content_length=False, request_method=None): + + if isinstance(headers, HTTPHeaderDict): + self.headers = headers + else: + self.headers = HTTPHeaderDict(headers) + self.status = status + self.version = version + self.reason = reason + self.strict = strict + self.decode_content = decode_content + self.retries = retries + self.enforce_content_length = enforce_content_length + + self._decoder = None + self._body = None + self._fp = None + self._original_response = original_response + self._fp_bytes_read = 0 + + if body and isinstance(body, (basestring, binary_type)): + self._body = body + + self._pool = pool + self._connection = connection + + if hasattr(body, 'read'): + self._fp = body + + # Are we using the chunked-style of transfer encoding? + self.chunked = False + self.chunk_left = None + tr_enc = self.headers.get('transfer-encoding', '').lower() + # Don't incur the penalty of creating a list and then discarding it + encodings = (enc.strip() for enc in tr_enc.split(",")) + if "chunked" in encodings: + self.chunked = True + + # Determine length of response + self.length_remaining = self._init_length(request_method) + + # If requested, preload the body. + if preload_content and not self._body: + self._body = self.read(decode_content=decode_content) + + def get_redirect_location(self): + """ + Should we redirect and where to? + + :returns: Truthy redirect location string if we got a redirect status + code and valid location. ``None`` if redirect status and no + location. ``False`` if not a redirect status code. + """ + if self.status in self.REDIRECT_STATUSES: + return self.headers.get('location') + + return False + + def release_conn(self): + if not self._pool or not self._connection: + return + + self._pool._put_conn(self._connection) + self._connection = None + + @property + def data(self): + # For backwords-compat with earlier urllib3 0.4 and earlier. + if self._body: + return self._body + + if self._fp: + return self.read(cache_content=True) + + @property + def connection(self): + return self._connection + + def tell(self): + """ + Obtain the number of bytes pulled over the wire so far. May differ from + the amount of content returned by :meth:``HTTPResponse.read`` if bytes + are encoded on the wire (e.g, compressed). + """ + return self._fp_bytes_read + + def _init_length(self, request_method): + """ + Set initial length value for Response content if available. + """ + length = self.headers.get('content-length') + + if length is not None and self.chunked: + # This Response will fail with an IncompleteRead if it can't be + # received as chunked. This method falls back to attempt reading + # the response before raising an exception. + log.warning("Received response with both Content-Length and " + "Transfer-Encoding set. This is expressly forbidden " + "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " + "attempting to process response as Transfer-Encoding: " + "chunked.") + return None + + elif length is not None: + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = set([int(val) for val in length.split(',')]) + if len(lengths) > 1: + raise InvalidHeader("Content-Length contained multiple " + "unmatching values (%s)" % length) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + # Convert status to int for comparison + # In some cases, httplib returns a status of "_UNKNOWN" + try: + status = int(self.status) + except ValueError: + status = 0 + + # Check for responses that shouldn't include a body + if status in (204, 304) or 100 <= status < 200 or request_method == 'HEAD': + length = 0 + + return length + + def _init_decoder(self): + """ + Set-up the _decoder attribute if necessary. + """ + # Note: content-encoding value should be case-insensitive, per RFC 7230 + # Section 3.2 + content_encoding = self.headers.get('content-encoding', '').lower() + if self._decoder is None and content_encoding in self.CONTENT_DECODERS: + self._decoder = _get_decoder(content_encoding) + + def _decode(self, data, decode_content, flush_decoder): + """ + Decode the data passed in and potentially flush the decoder. + """ + try: + if decode_content and self._decoder: + data = self._decoder.decompress(data) + except (IOError, zlib.error) as e: + content_encoding = self.headers.get('content-encoding', '').lower() + raise DecodeError( + "Received response with content-encoding: %s, but " + "failed to decode it." % content_encoding, e) + + if flush_decoder and decode_content: + data += self._flush_decoder() + + return data + + def _flush_decoder(self): + """ + Flushes the decoder. Should only be called if the decoder is actually + being used. + """ + if self._decoder: + buf = self._decoder.decompress(b'') + return buf + self._decoder.flush() + + return b'' + + @contextmanager + def _error_catcher(self): + """ + Catch low-level python exceptions, instead re-raising urllib3 + variants, so that low-level exceptions are not leaked in the + high-level api. + + On exit, release the connection back to the pool. + """ + clean_exit = False + + try: + try: + yield + + except SocketTimeout: + # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but + # there is yet no clean way to get at it from this context. + raise ReadTimeoutError(self._pool, None, 'Read timed out.') + + except BaseSSLError as e: + # FIXME: Is there a better way to differentiate between SSLErrors? + if 'read operation timed out' not in str(e): # Defensive: + # This shouldn't happen but just in case we're missing an edge + # case, let's avoid swallowing SSL errors. + raise + + raise ReadTimeoutError(self._pool, None, 'Read timed out.') + + except (HTTPException, SocketError) as e: + # This includes IncompleteRead. + raise ProtocolError('Connection broken: %r' % e, e) + + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now to ensure that the connection is + # released back to the pool. + if self._original_response: + self._original_response.close() + + # Closing the response may not actually be sufficient to close + # everything, so if we have a hold of the connection close that + # too. + if self._connection: + self._connection.close() + + # If we hold the original response but it's closed now, we should + # return the connection back to the pool. + if self._original_response and self._original_response.isclosed(): + self.release_conn() + + def read(self, amt=None, decode_content=None, cache_content=False): + """ + Similar to :meth:`httplib.HTTPResponse.read`, but with two additional + parameters: ``decode_content`` and ``cache_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param cache_content: + If True, will save the returned data such that the same result is + returned despite of the state of the underlying file object. This + is useful if you want the ``.data`` property to continue working + after having ``.read()`` the file object. (Overridden if ``amt`` is + set.) + """ + self._init_decoder() + if decode_content is None: + decode_content = self.decode_content + + if self._fp is None: + return + + flush_decoder = False + data = None + + with self._error_catcher(): + if amt is None: + # cStringIO doesn't like amt=None + data = self._fp.read() + flush_decoder = True + else: + cache_content = False + data = self._fp.read(amt) + if amt != 0 and not data: # Platform-specific: Buggy versions of Python. + # Close the connection when no data is returned + # + # This is redundant to what httplib/http.client _should_ + # already do. However, versions of python released before + # December 15, 2012 (http://bugs.python.org/issue16298) do + # not properly close the connection in all cases. There is + # no harm in redundantly calling close. + self._fp.close() + flush_decoder = True + if self.enforce_content_length and self.length_remaining not in (0, None): + # This is an edge case that httplib failed to cover due + # to concerns of backward compatibility. We're + # addressing it here to make sure IncompleteRead is + # raised during streaming, so all calls with incorrect + # Content-Length are caught. + raise IncompleteRead(self._fp_bytes_read, self.length_remaining) + + if data: + self._fp_bytes_read += len(data) + if self.length_remaining is not None: + self.length_remaining -= len(data) + + data = self._decode(data, decode_content, flush_decoder) + + if cache_content: + self._body = data + + return data + + def stream(self, amt=2**16, decode_content=None): + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if self.chunked and self.supports_chunked_reads(): + for line in self.read_chunked(amt, decode_content=decode_content): + yield line + else: + while not is_fp_closed(self._fp): + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + + @classmethod + def from_httplib(ResponseCls, r, **response_kw): + """ + Given an :class:`httplib.HTTPResponse` instance ``r``, return a + corresponding :class:`urllib3.response.HTTPResponse` object. + + Remaining parameters are passed to the HTTPResponse constructor, along + with ``original_response=r``. + """ + headers = r.msg + + if not isinstance(headers, HTTPHeaderDict): + if PY3: # Python 3 + headers = HTTPHeaderDict(headers.items()) + else: # Python 2 + headers = HTTPHeaderDict.from_httplib(headers) + + # HTTPResponse objects in Python 3 don't have a .strict attribute + strict = getattr(r, 'strict', 0) + resp = ResponseCls(body=r, + headers=headers, + status=r.status, + version=r.version, + reason=r.reason, + strict=strict, + original_response=r, + **response_kw) + return resp + + # Backwards-compatibility methods for httplib.HTTPResponse + def getheaders(self): + return self.headers + + def getheader(self, name, default=None): + return self.headers.get(name, default) + + # Overrides from io.IOBase + def close(self): + if not self.closed: + self._fp.close() + + if self._connection: + self._connection.close() + + @property + def closed(self): + if self._fp is None: + return True + elif hasattr(self._fp, 'isclosed'): + return self._fp.isclosed() + elif hasattr(self._fp, 'closed'): + return self._fp.closed + else: + return True + + def fileno(self): + if self._fp is None: + raise IOError("HTTPResponse has no file to get a fileno from") + elif hasattr(self._fp, "fileno"): + return self._fp.fileno() + else: + raise IOError("The file-like object this HTTPResponse is wrapped " + "around has no file descriptor") + + def flush(self): + if self._fp is not None and hasattr(self._fp, 'flush'): + return self._fp.flush() + + def readable(self): + # This method is required for `io` module compatibility. + return True + + def readinto(self, b): + # This method is required for `io` module compatibility. + temp = self.read(len(b)) + if len(temp) == 0: + return 0 + else: + b[:len(temp)] = temp + return len(temp) + + def supports_chunked_reads(self): + """ + Checks if the underlying file-like object looks like a + httplib.HTTPResponse object. We do this by testing for the fp + attribute. If it is present we assume it returns raw chunks as + processed by read_chunked(). + """ + return hasattr(self._fp, 'fp') + + def _update_chunk_length(self): + # First, we'll figure out length of a chunk and then + # we'll try to read it from socket. + if self.chunk_left is not None: + return + line = self._fp.fp.readline() + line = line.split(b';', 1)[0] + try: + self.chunk_left = int(line, 16) + except ValueError: + # Invalid chunked protocol response, abort. + self.close() + raise httplib.IncompleteRead(line) + + def _handle_chunk(self, amt): + returned_chunk = None + if amt is None: + chunk = self._fp._safe_read(self.chunk_left) + returned_chunk = chunk + self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. + self.chunk_left = None + elif amt < self.chunk_left: + value = self._fp._safe_read(amt) + self.chunk_left = self.chunk_left - amt + returned_chunk = value + elif amt == self.chunk_left: + value = self._fp._safe_read(amt) + self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. + self.chunk_left = None + returned_chunk = value + else: # amt > self.chunk_left + returned_chunk = self._fp._safe_read(self.chunk_left) + self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. + self.chunk_left = None + return returned_chunk + + def read_chunked(self, amt=None, decode_content=None): + """ + Similar to :meth:`HTTPResponse.read`, but with an additional + parameter: ``decode_content``. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + self._init_decoder() + # FIXME: Rewrite this method and make it a class with a better structured logic. + if not self.chunked: + raise ResponseNotChunked( + "Response is not chunked. " + "Header 'transfer-encoding: chunked' is missing.") + if not self.supports_chunked_reads(): + raise BodyNotHttplibCompatible( + "Body should be httplib.HTTPResponse like. " + "It should have have an fp attribute which returns raw chunks.") + + # Don't bother reading the body of a HEAD request. + if self._original_response and is_response_to_head(self._original_response): + self._original_response.close() + return + + with self._error_catcher(): + while True: + self._update_chunk_length() + if self.chunk_left == 0: + break + chunk = self._handle_chunk(amt) + decoded = self._decode(chunk, decode_content=decode_content, + flush_decoder=False) + if decoded: + yield decoded + + if decode_content: + # On CPython and PyPy, we should never need to flush the + # decoder. However, on Jython we *might* need to, so + # lets defensively do it anyway. + decoded = self._flush_decoder() + if decoded: # Platform-specific: Jython. + yield decoded + + # Chunk content ends with \r\n: discard it. + while True: + line = self._fp.fp.readline() + if not line: + # Some sites may not end with '\r\n'. + break + if line == b'\r\n': + break + + # We read everything; close the "file". + if self._original_response: + self._original_response.close() diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py index ab66169..5ced5a4 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py @@ -1,52 +1,52 @@ -from __future__ import absolute_import -# For backwards compatibility, provide imports that used to be here. -from .connection import is_connection_dropped -from .request import make_headers -from .response import is_fp_closed -from .ssl_ import ( - SSLContext, - HAS_SNI, - IS_PYOPENSSL, - assert_fingerprint, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .timeout import ( - current_time, - Timeout, -) - -from .retry import Retry -from .url import ( - get_host, - parse_url, - split_first, - Url, -) -from .wait import ( - wait_for_read, - wait_for_write -) - -__all__ = ( - 'HAS_SNI', - 'IS_PYOPENSSL', - 'SSLContext', - 'Retry', - 'Timeout', - 'Url', - 'assert_fingerprint', - 'current_time', - 'is_connection_dropped', - 'is_fp_closed', - 'get_host', - 'parse_url', - 'make_headers', - 'resolve_cert_reqs', - 'resolve_ssl_version', - 'split_first', - 'ssl_wrap_socket', - 'wait_for_read', - 'wait_for_write' -) +from __future__ import absolute_import +# For backwards compatibility, provide imports that used to be here. +from .connection import is_connection_dropped +from .request import make_headers +from .response import is_fp_closed +from .ssl_ import ( + SSLContext, + HAS_SNI, + IS_PYOPENSSL, + assert_fingerprint, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import ( + current_time, + Timeout, +) + +from .retry import Retry +from .url import ( + get_host, + parse_url, + split_first, + Url, +) +from .wait import ( + wait_for_read, + wait_for_write +) + +__all__ = ( + 'HAS_SNI', + 'IS_PYOPENSSL', + 'SSLContext', + 'Retry', + 'Timeout', + 'Url', + 'assert_fingerprint', + 'current_time', + 'is_connection_dropped', + 'is_fp_closed', + 'get_host', + 'parse_url', + 'make_headers', + 'resolve_cert_reqs', + 'resolve_ssl_version', + 'split_first', + 'ssl_wrap_socket', + 'wait_for_read', + 'wait_for_write' +) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py index 31ecd83..bf699cf 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py @@ -1,130 +1,130 @@ -from __future__ import absolute_import -import socket -from .wait import wait_for_read -from .selectors import HAS_SELECT, SelectorError - - -def is_connection_dropped(conn): # Platform-specific - """ - Returns True if the connection is dropped and should be closed. - - :param conn: - :class:`httplib.HTTPConnection` object. - - Note: For platforms like AppEngine, this will always return ``False`` to - let the platform handle connection recycling transparently for us. - """ - sock = getattr(conn, 'sock', False) - if sock is False: # Platform-specific: AppEngine - return False - if sock is None: # Connection already closed (such as by httplib). - return True - - if not HAS_SELECT: - return False - - try: - return bool(wait_for_read(sock, timeout=0.0)) - except SelectorError: - return True - - -# This function is copied from socket.py in the Python 2.7 standard -# library test suite. Added to its signature is only `socket_options`. -# One additional modification is that we avoid binding to IPv6 servers -# discovered in DNS if the system doesn't have IPv6 functionality. -def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, socket_options=None): - """Connect to *address* and return the socket object. - - Convenience function. Connect to *address* (a 2-tuple ``(host, - port)``) and return the socket object. Passing the optional - *timeout* parameter will set the timeout on the socket instance - before attempting to connect. If no *timeout* is supplied, the - global default timeout setting returned by :func:`getdefaulttimeout` - is used. If *source_address* is set it must be a tuple of (host, port) - for the socket to bind as a source address before making the connection. - An host of '' or port 0 tells the OS to use the default. - """ - - host, port = address - if host.startswith('['): - host = host.strip('[]') - err = None - - # Using the value from allowed_gai_family() in the context of getaddrinfo lets - # us select whether to work with IPv4 DNS records, IPv6 records, or both. - # The original create_connection function always returns all records. - family = allowed_gai_family() - - for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - sock = None - try: - sock = socket.socket(af, socktype, proto) - - # If provided, set socket level options before connecting. - _set_socket_options(sock, socket_options) - - if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: - sock.settimeout(timeout) - if source_address: - sock.bind(source_address) - sock.connect(sa) - return sock - - except socket.error as e: - err = e - if sock is not None: - sock.close() - sock = None - - if err is not None: - raise err - - raise socket.error("getaddrinfo returns an empty list") - - -def _set_socket_options(sock, options): - if options is None: - return - - for opt in options: - sock.setsockopt(*opt) - - -def allowed_gai_family(): - """This function is designed to work in the context of - getaddrinfo, where family=socket.AF_UNSPEC is the default and - will perform a DNS search for both IPv6 and IPv4 records.""" - - family = socket.AF_INET - if HAS_IPV6: - family = socket.AF_UNSPEC - return family - - -def _has_ipv6(host): - """ Returns True if the system can bind an IPv6 address. """ - sock = None - has_ipv6 = False - - if socket.has_ipv6: - # has_ipv6 returns true if cPython was compiled with IPv6 support. - # It does not tell us if the system has IPv6 support enabled. To - # determine that we must bind to an IPv6 address. - # https://github.com/shazow/urllib3/pull/611 - # https://bugs.python.org/issue658327 - try: - sock = socket.socket(socket.AF_INET6) - sock.bind((host, 0)) - has_ipv6 = True - except Exception: - pass - - if sock: - sock.close() - return has_ipv6 - - -HAS_IPV6 = _has_ipv6('::1') +from __future__ import absolute_import +import socket +from .wait import wait_for_read +from .selectors import HAS_SELECT, SelectorError + + +def is_connection_dropped(conn): # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + + :param conn: + :class:`httplib.HTTPConnection` object. + + Note: For platforms like AppEngine, this will always return ``False`` to + let the platform handle connection recycling transparently for us. + """ + sock = getattr(conn, 'sock', False) + if sock is False: # Platform-specific: AppEngine + return False + if sock is None: # Connection already closed (such as by httplib). + return True + + if not HAS_SELECT: + return False + + try: + return bool(wait_for_read(sock, timeout=0.0)) + except SelectorError: + return True + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +# One additional modification is that we avoid binding to IPv6 servers +# discovered in DNS if the system doesn't have IPv6 functionality. +def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, socket_options=None): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + if host.startswith('['): + host = host.strip('[]') + err = None + + # Using the value from allowed_gai_family() in the context of getaddrinfo lets + # us select whether to work with IPv4 DNS records, IPv6 records, or both. + # The original create_connection function always returns all records. + family = allowed_gai_family() + + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + _set_socket_options(sock, socket_options) + + if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + return sock + + except socket.error as e: + err = e + if sock is not None: + sock.close() + sock = None + + if err is not None: + raise err + + raise socket.error("getaddrinfo returns an empty list") + + +def _set_socket_options(sock, options): + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) + + +def allowed_gai_family(): + """This function is designed to work in the context of + getaddrinfo, where family=socket.AF_UNSPEC is the default and + will perform a DNS search for both IPv6 and IPv4 records.""" + + family = socket.AF_INET + if HAS_IPV6: + family = socket.AF_UNSPEC + return family + + +def _has_ipv6(host): + """ Returns True if the system can bind an IPv6 address. """ + sock = None + has_ipv6 = False + + if socket.has_ipv6: + # has_ipv6 returns true if cPython was compiled with IPv6 support. + # It does not tell us if the system has IPv6 support enabled. To + # determine that we must bind to an IPv6 address. + # https://github.com/shazow/urllib3/pull/611 + # https://bugs.python.org/issue658327 + try: + sock = socket.socket(socket.AF_INET6) + sock.bind((host, 0)) + has_ipv6 = True + except Exception: + pass + + if sock: + sock.close() + return has_ipv6 + + +HAS_IPV6 = _has_ipv6('::1') diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py index 31a3d44..974fc40 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py @@ -1,118 +1,118 @@ -from __future__ import absolute_import -from base64 import b64encode - -from ..packages.six import b, integer_types -from ..exceptions import UnrewindableBodyError - -ACCEPT_ENCODING = 'gzip,deflate' -_FAILEDTELL = object() - - -def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, - basic_auth=None, proxy_basic_auth=None, disable_cache=None): - """ - Shortcuts for generating request headers. - - :param keep_alive: - If ``True``, adds 'connection: keep-alive' header. - - :param accept_encoding: - Can be a boolean, list, or string. - ``True`` translates to 'gzip,deflate'. - List will get joined by comma. - String will be used as provided. - - :param user_agent: - String representing the user-agent you want, such as - "python-urllib3/0.6" - - :param basic_auth: - Colon-separated username:password string for 'authorization: basic ...' - auth header. - - :param proxy_basic_auth: - Colon-separated username:password string for 'proxy-authorization: basic ...' - auth header. - - :param disable_cache: - If ``True``, adds 'cache-control: no-cache' header. - - Example:: - - >>> make_headers(keep_alive=True, user_agent="Batman/1.0") - {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} - >>> make_headers(accept_encoding=True) - {'accept-encoding': 'gzip,deflate'} - """ - headers = {} - if accept_encoding: - if isinstance(accept_encoding, str): - pass - elif isinstance(accept_encoding, list): - accept_encoding = ','.join(accept_encoding) - else: - accept_encoding = ACCEPT_ENCODING - headers['accept-encoding'] = accept_encoding - - if user_agent: - headers['user-agent'] = user_agent - - if keep_alive: - headers['connection'] = 'keep-alive' - - if basic_auth: - headers['authorization'] = 'Basic ' + \ - b64encode(b(basic_auth)).decode('utf-8') - - if proxy_basic_auth: - headers['proxy-authorization'] = 'Basic ' + \ - b64encode(b(proxy_basic_auth)).decode('utf-8') - - if disable_cache: - headers['cache-control'] = 'no-cache' - - return headers - - -def set_file_position(body, pos): - """ - If a position is provided, move file to that point. - Otherwise, we'll attempt to record a position for future use. - """ - if pos is not None: - rewind_body(body, pos) - elif getattr(body, 'tell', None) is not None: - try: - pos = body.tell() - except (IOError, OSError): - # This differentiates from None, allowing us to catch - # a failed `tell()` later when trying to rewind the body. - pos = _FAILEDTELL - - return pos - - -def rewind_body(body, body_pos): - """ - Attempt to rewind body to a certain position. - Primarily used for request redirects and retries. - - :param body: - File-like object that supports seek. - - :param int pos: - Position to seek to in file. - """ - body_seek = getattr(body, 'seek', None) - if body_seek is not None and isinstance(body_pos, integer_types): - try: - body_seek(body_pos) - except (IOError, OSError): - raise UnrewindableBodyError("An error occured when rewinding request " - "body for redirect/retry.") - elif body_pos is _FAILEDTELL: - raise UnrewindableBodyError("Unable to record file position for rewinding " - "request body during a redirect/retry.") - else: - raise ValueError("body_pos must be of type integer, " - "instead it was %s." % type(body_pos)) +from __future__ import absolute_import +from base64 import b64encode + +from ..packages.six import b, integer_types +from ..exceptions import UnrewindableBodyError + +ACCEPT_ENCODING = 'gzip,deflate' +_FAILEDTELL = object() + + +def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, + basic_auth=None, proxy_basic_auth=None, disable_cache=None): + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + :param disable_cache: + If ``True``, adds 'cache-control: no-cache' header. + + Example:: + + >>> make_headers(keep_alive=True, user_agent="Batman/1.0") + {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + >>> make_headers(accept_encoding=True) + {'accept-encoding': 'gzip,deflate'} + """ + headers = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ','.join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers['accept-encoding'] = accept_encoding + + if user_agent: + headers['user-agent'] = user_agent + + if keep_alive: + headers['connection'] = 'keep-alive' + + if basic_auth: + headers['authorization'] = 'Basic ' + \ + b64encode(b(basic_auth)).decode('utf-8') + + if proxy_basic_auth: + headers['proxy-authorization'] = 'Basic ' + \ + b64encode(b(proxy_basic_auth)).decode('utf-8') + + if disable_cache: + headers['cache-control'] = 'no-cache' + + return headers + + +def set_file_position(body, pos): + """ + If a position is provided, move file to that point. + Otherwise, we'll attempt to record a position for future use. + """ + if pos is not None: + rewind_body(body, pos) + elif getattr(body, 'tell', None) is not None: + try: + pos = body.tell() + except (IOError, OSError): + # This differentiates from None, allowing us to catch + # a failed `tell()` later when trying to rewind the body. + pos = _FAILEDTELL + + return pos + + +def rewind_body(body, body_pos): + """ + Attempt to rewind body to a certain position. + Primarily used for request redirects and retries. + + :param body: + File-like object that supports seek. + + :param int pos: + Position to seek to in file. + """ + body_seek = getattr(body, 'seek', None) + if body_seek is not None and isinstance(body_pos, integer_types): + try: + body_seek(body_pos) + except (IOError, OSError): + raise UnrewindableBodyError("An error occured when rewinding request " + "body for redirect/retry.") + elif body_pos is _FAILEDTELL: + raise UnrewindableBodyError("Unable to record file position for rewinding " + "request body during a redirect/retry.") + else: + raise ValueError("body_pos must be of type integer, " + "instead it was %s." % type(body_pos)) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py index c2eb49c..67cf730 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py @@ -1,81 +1,81 @@ -from __future__ import absolute_import -from ..packages.six.moves import http_client as httplib - -from ..exceptions import HeaderParsingError - - -def is_fp_closed(obj): - """ - Checks whether a given file-like object is closed. - - :param obj: - The file-like object to check. - """ - - try: - # Check `isclosed()` first, in case Python3 doesn't set `closed`. - # GH Issue #928 - return obj.isclosed() - except AttributeError: - pass - - try: - # Check via the official file-like-object way. - return obj.closed - except AttributeError: - pass - - try: - # Check if the object is a container for another file-like object that - # gets released on exhaustion (e.g. HTTPResponse). - return obj.fp is None - except AttributeError: - pass - - raise ValueError("Unable to determine whether fp is closed.") - - -def assert_header_parsing(headers): - """ - Asserts whether all headers have been successfully parsed. - Extracts encountered errors from the result of parsing headers. - - Only works on Python 3. - - :param headers: Headers to verify. - :type headers: `httplib.HTTPMessage`. - - :raises urllib3.exceptions.HeaderParsingError: - If parsing errors are found. - """ - - # This will fail silently if we pass in the wrong kind of parameter. - # To make debugging easier add an explicit check. - if not isinstance(headers, httplib.HTTPMessage): - raise TypeError('expected httplib.Message, got {0}.'.format( - type(headers))) - - defects = getattr(headers, 'defects', None) - get_payload = getattr(headers, 'get_payload', None) - - unparsed_data = None - if get_payload: # Platform-specific: Python 3. - unparsed_data = get_payload() - - if defects or unparsed_data: - raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) - - -def is_response_to_head(response): - """ - Checks whether the request of a response has been a HEAD-request. - Handles the quirks of AppEngine. - - :param conn: - :type conn: :class:`httplib.HTTPResponse` - """ - # FIXME: Can we do this somehow without accessing private httplib _method? - method = response._method - if isinstance(method, int): # Platform-specific: Appengine - return method == 3 - return method.upper() == 'HEAD' +from __future__ import absolute_import +from ..packages.six.moves import http_client as httplib + +from ..exceptions import HeaderParsingError + + +def is_fp_closed(obj): + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + + try: + # Check `isclosed()` first, in case Python3 doesn't set `closed`. + # GH Issue #928 + return obj.isclosed() + except AttributeError: + pass + + try: + # Check via the official file-like-object way. + return obj.closed + except AttributeError: + pass + + try: + # Check if the object is a container for another file-like object that + # gets released on exhaustion (e.g. HTTPResponse). + return obj.fp is None + except AttributeError: + pass + + raise ValueError("Unable to determine whether fp is closed.") + + +def assert_header_parsing(headers): + """ + Asserts whether all headers have been successfully parsed. + Extracts encountered errors from the result of parsing headers. + + Only works on Python 3. + + :param headers: Headers to verify. + :type headers: `httplib.HTTPMessage`. + + :raises urllib3.exceptions.HeaderParsingError: + If parsing errors are found. + """ + + # This will fail silently if we pass in the wrong kind of parameter. + # To make debugging easier add an explicit check. + if not isinstance(headers, httplib.HTTPMessage): + raise TypeError('expected httplib.Message, got {0}.'.format( + type(headers))) + + defects = getattr(headers, 'defects', None) + get_payload = getattr(headers, 'get_payload', None) + + unparsed_data = None + if get_payload: # Platform-specific: Python 3. + unparsed_data = get_payload() + + if defects or unparsed_data: + raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) + + +def is_response_to_head(response): + """ + Checks whether the request of a response has been a HEAD-request. + Handles the quirks of AppEngine. + + :param conn: + :type conn: :class:`httplib.HTTPResponse` + """ + # FIXME: Can we do this somehow without accessing private httplib _method? + method = response._method + if isinstance(method, int): # Platform-specific: Appengine + return method == 3 + return method.upper() == 'HEAD' diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py index 9258eca..c9e7d28 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py @@ -1,389 +1,389 @@ -from __future__ import absolute_import -import time -import logging -from collections import namedtuple -from itertools import takewhile -import email -import re - -from ..exceptions import ( - ConnectTimeoutError, - MaxRetryError, - ProtocolError, - ReadTimeoutError, - ResponseError, - InvalidHeader, -) -from ..packages import six - - -log = logging.getLogger(__name__) - -# Data structure for representing the metadata of requests that result in a retry. -RequestHistory = namedtuple('RequestHistory', ["method", "url", "error", - "status", "redirect_location"]) - - -class Retry(object): - """ Retry configuration. - - Each retry attempt will create a new Retry object with updated values, so - they can be safely reused. - - Retries can be defined as a default for a pool:: - - retries = Retry(connect=5, read=2, redirect=5) - http = PoolManager(retries=retries) - response = http.request('GET', 'http://example.com/') - - Or per-request (which overrides the default for the pool):: - - response = http.request('GET', 'http://example.com/', retries=Retry(10)) - - Retries can be disabled by passing ``False``:: - - response = http.request('GET', 'http://example.com/', retries=False) - - Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless - retries are disabled, in which case the causing exception will be raised. - - :param int total: - Total number of retries to allow. Takes precedence over other counts. - - Set to ``None`` to remove this constraint and fall back on other - counts. It's a good idea to set this to some sensibly-high value to - account for unexpected edge cases and avoid infinite retry loops. - - Set to ``0`` to fail on the first retry. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int connect: - How many connection-related errors to retry on. - - These are errors raised before the request is sent to the remote server, - which we assume has not triggered the server to process the request. - - Set to ``0`` to fail on the first retry of this type. - - :param int read: - How many times to retry on read errors. - - These errors are raised after the request was sent to the server, so the - request may have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - :param int redirect: - How many redirects to perform. Limit this to avoid infinite redirect - loops. - - A redirect is a HTTP response with a status code 301, 302, 303, 307 or - 308. - - Set to ``0`` to fail on the first retry of this type. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param iterable method_whitelist: - Set of uppercased HTTP method verbs that we should retry on. - - By default, we only retry on methods which are considered to be - idempotent (multiple requests with the same parameters end with the - same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`. - - Set to a ``False`` value to retry on any verb. - - :param iterable status_forcelist: - A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``method_whitelist`` - and the response status code is in ``status_forcelist``. - - By default, this is disabled with ``None``. - - :param float backoff_factor: - A backoff factor to apply between attempts after the second try - (most errors are resolved immediately by a second try without a - delay). urllib3 will sleep for:: - - {backoff factor} * (2 ^ ({number of total retries} - 1)) - - seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep - for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer - than :attr:`Retry.BACKOFF_MAX`. - - By default, backoff is disabled (set to 0). - - :param bool raise_on_redirect: Whether, if the number of redirects is - exhausted, to raise a MaxRetryError, or to return a response with a - response code in the 3xx range. - - :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: - whether we should raise an exception, or return a response, - if status falls in ``status_forcelist`` range and retries have - been exhausted. - - :param tuple history: The history of the request encountered during - each call to :meth:`~Retry.increment`. The list is in the order - the requests occurred. Each list item is of class :class:`RequestHistory`. - - :param bool respect_retry_after_header: - Whether to respect Retry-After header on status codes defined as - :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. - - """ - - DEFAULT_METHOD_WHITELIST = frozenset([ - 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']) - - RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) - - #: Maximum backoff time. - BACKOFF_MAX = 120 - - def __init__(self, total=10, connect=None, read=None, redirect=None, - method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None, - backoff_factor=0, raise_on_redirect=True, raise_on_status=True, - history=None, respect_retry_after_header=True): - - self.total = total - self.connect = connect - self.read = read - - if redirect is False or total is False: - redirect = 0 - raise_on_redirect = False - - self.redirect = redirect - self.status_forcelist = status_forcelist or set() - self.method_whitelist = method_whitelist - self.backoff_factor = backoff_factor - self.raise_on_redirect = raise_on_redirect - self.raise_on_status = raise_on_status - self.history = history or tuple() - self.respect_retry_after_header = respect_retry_after_header - - def new(self, **kw): - params = dict( - total=self.total, - connect=self.connect, read=self.read, redirect=self.redirect, - method_whitelist=self.method_whitelist, - status_forcelist=self.status_forcelist, - backoff_factor=self.backoff_factor, - raise_on_redirect=self.raise_on_redirect, - raise_on_status=self.raise_on_status, - history=self.history, - ) - params.update(kw) - return type(self)(**params) - - @classmethod - def from_int(cls, retries, redirect=True, default=None): - """ Backwards-compatibility for the old retries format.""" - if retries is None: - retries = default if default is not None else cls.DEFAULT - - if isinstance(retries, Retry): - return retries - - redirect = bool(redirect) and None - new_retries = cls(retries, redirect=redirect) - log.debug("Converted retries value: %r -> %r", retries, new_retries) - return new_retries - - def get_backoff_time(self): - """ Formula for computing the current backoff - - :rtype: float - """ - # We want to consider only the last consecutive errors sequence (Ignore redirects). - consecutive_errors_len = len(list(takewhile(lambda x: x.redirect_location is None, - reversed(self.history)))) - if consecutive_errors_len <= 1: - return 0 - - backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) - return min(self.BACKOFF_MAX, backoff_value) - - def parse_retry_after(self, retry_after): - # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 - if re.match(r"^\s*[0-9]+\s*$", retry_after): - seconds = int(retry_after) - else: - retry_date_tuple = email.utils.parsedate(retry_after) - if retry_date_tuple is None: - raise InvalidHeader("Invalid Retry-After header: %s" % retry_after) - retry_date = time.mktime(retry_date_tuple) - seconds = retry_date - time.time() - - if seconds < 0: - seconds = 0 - - return seconds - - def get_retry_after(self, response): - """ Get the value of Retry-After in seconds. """ - - retry_after = response.getheader("Retry-After") - - if retry_after is None: - return None - - return self.parse_retry_after(retry_after) - - def sleep_for_retry(self, response=None): - retry_after = self.get_retry_after(response) - if retry_after: - time.sleep(retry_after) - return True - - return False - - def _sleep_backoff(self): - backoff = self.get_backoff_time() - if backoff <= 0: - return - time.sleep(backoff) - - def sleep(self, response=None): - """ Sleep between retry attempts. - - This method will respect a server's ``Retry-After`` response header - and sleep the duration of the time requested. If that is not present, it - will use an exponential backoff. By default, the backoff factor is 0 and - this method will return immediately. - """ - - if response: - slept = self.sleep_for_retry(response) - if slept: - return - - self._sleep_backoff() - - def _is_connection_error(self, err): - """ Errors when we're fairly sure that the server did not receive the - request, so it should be safe to retry. - """ - return isinstance(err, ConnectTimeoutError) - - def _is_read_error(self, err): - """ Errors that occur after the request has been started, so we should - assume that the server began processing it. - """ - return isinstance(err, (ReadTimeoutError, ProtocolError)) - - def _is_method_retryable(self, method): - """ Checks if a given HTTP method should be retried upon, depending if - it is included on the method whitelist. - """ - if self.method_whitelist and method.upper() not in self.method_whitelist: - return False - - return True - - def is_retry(self, method, status_code, has_retry_after=False): - """ Is this method/status code retryable? (Based on whitelists and control - variables such as the number of total retries to allow, whether to - respect the Retry-After header, whether this header is present, and - whether the returned status code is on the list of status codes to - be retried upon on the presence of the aforementioned header) - """ - if not self._is_method_retryable(method): - return False - - if self.status_forcelist and status_code in self.status_forcelist: - return True - - return (self.total and self.respect_retry_after_header and - has_retry_after and (status_code in self.RETRY_AFTER_STATUS_CODES)) - - def is_exhausted(self): - """ Are we out of retries? """ - retry_counts = (self.total, self.connect, self.read, self.redirect) - retry_counts = list(filter(None, retry_counts)) - if not retry_counts: - return False - - return min(retry_counts) < 0 - - def increment(self, method=None, url=None, response=None, error=None, - _pool=None, _stacktrace=None): - """ Return a new Retry object with incremented retry counters. - - :param response: A response object, or None, if the server did not - return a response. - :type response: :class:`~urllib3.response.HTTPResponse` - :param Exception error: An error encountered during the request, or - None if the response was received successfully. - - :return: A new ``Retry`` object. - """ - if self.total is False and error: - # Disabled, indicate to re-raise the error. - raise six.reraise(type(error), error, _stacktrace) - - total = self.total - if total is not None: - total -= 1 - - connect = self.connect - read = self.read - redirect = self.redirect - cause = 'unknown' - status = None - redirect_location = None - - if error and self._is_connection_error(error): - # Connect retry? - if connect is False: - raise six.reraise(type(error), error, _stacktrace) - elif connect is not None: - connect -= 1 - - elif error and self._is_read_error(error): - # Read retry? - if read is False or not self._is_method_retryable(method): - raise six.reraise(type(error), error, _stacktrace) - elif read is not None: - read -= 1 - - elif response and response.get_redirect_location(): - # Redirect retry? - if redirect is not None: - redirect -= 1 - cause = 'too many redirects' - redirect_location = response.get_redirect_location() - status = response.status - - else: - # Incrementing because of a server error like a 500 in - # status_forcelist and a the given method is in the whitelist - cause = ResponseError.GENERIC_ERROR - if response and response.status: - cause = ResponseError.SPECIFIC_ERROR.format( - status_code=response.status) - status = response.status - - history = self.history + (RequestHistory(method, url, error, status, redirect_location),) - - new_retry = self.new( - total=total, - connect=connect, read=read, redirect=redirect, - history=history) - - if new_retry.is_exhausted(): - raise MaxRetryError(_pool, url, error or ResponseError(cause)) - - log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) - - return new_retry - - def __repr__(self): - return ('{cls.__name__}(total={self.total}, connect={self.connect}, ' - 'read={self.read}, redirect={self.redirect})').format( - cls=type(self), self=self) - - -# For backwards compatibility (equivalent to pre-v1.9): -Retry.DEFAULT = Retry(3) +from __future__ import absolute_import +import time +import logging +from collections import namedtuple +from itertools import takewhile +import email +import re + +from ..exceptions import ( + ConnectTimeoutError, + MaxRetryError, + ProtocolError, + ReadTimeoutError, + ResponseError, + InvalidHeader, +) +from ..packages import six + + +log = logging.getLogger(__name__) + +# Data structure for representing the metadata of requests that result in a retry. +RequestHistory = namedtuple('RequestHistory', ["method", "url", "error", + "status", "redirect_location"]) + + +class Retry(object): + """ Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool:: + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request('GET', 'http://example.com/') + + Or per-request (which overrides the default for the pool):: + + response = http.request('GET', 'http://example.com/', retries=Retry(10)) + + Retries can be disabled by passing ``False``:: + + response = http.request('GET', 'http://example.com/', retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. It's a good idea to set this to some sensibly-high value to + account for unexpected edge cases and avoid infinite retry loops. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param iterable method_whitelist: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + idempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`. + + Set to a ``False`` value to retry on any verb. + + :param iterable status_forcelist: + A set of integer HTTP status codes that we should force a retry on. + A retry is initiated if the request method is in ``method_whitelist`` + and the response status code is in ``status_forcelist``. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts after the second try + (most errors are resolved immediately by a second try without a + delay). urllib3 will sleep for:: + + {backoff factor} * (2 ^ ({number of total retries} - 1)) + + seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep + for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer + than :attr:`Retry.BACKOFF_MAX`. + + By default, backoff is disabled (set to 0). + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + + :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: + whether we should raise an exception, or return a response, + if status falls in ``status_forcelist`` range and retries have + been exhausted. + + :param tuple history: The history of the request encountered during + each call to :meth:`~Retry.increment`. The list is in the order + the requests occurred. Each list item is of class :class:`RequestHistory`. + + :param bool respect_retry_after_header: + Whether to respect Retry-After header on status codes defined as + :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. + + """ + + DEFAULT_METHOD_WHITELIST = frozenset([ + 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']) + + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Maximum backoff time. + BACKOFF_MAX = 120 + + def __init__(self, total=10, connect=None, read=None, redirect=None, + method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None, + backoff_factor=0, raise_on_redirect=True, raise_on_status=True, + history=None, respect_retry_after_header=True): + + self.total = total + self.connect = connect + self.read = read + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.method_whitelist = method_whitelist + self.backoff_factor = backoff_factor + self.raise_on_redirect = raise_on_redirect + self.raise_on_status = raise_on_status + self.history = history or tuple() + self.respect_retry_after_header = respect_retry_after_header + + def new(self, **kw): + params = dict( + total=self.total, + connect=self.connect, read=self.read, redirect=self.redirect, + method_whitelist=self.method_whitelist, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + raise_on_redirect=self.raise_on_redirect, + raise_on_status=self.raise_on_status, + history=self.history, + ) + params.update(kw) + return type(self)(**params) + + @classmethod + def from_int(cls, retries, redirect=True, default=None): + """ Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r", retries, new_retries) + return new_retries + + def get_backoff_time(self): + """ Formula for computing the current backoff + + :rtype: float + """ + # We want to consider only the last consecutive errors sequence (Ignore redirects). + consecutive_errors_len = len(list(takewhile(lambda x: x.redirect_location is None, + reversed(self.history)))) + if consecutive_errors_len <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) + return min(self.BACKOFF_MAX, backoff_value) + + def parse_retry_after(self, retry_after): + # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = int(retry_after) + else: + retry_date_tuple = email.utils.parsedate(retry_after) + if retry_date_tuple is None: + raise InvalidHeader("Invalid Retry-After header: %s" % retry_after) + retry_date = time.mktime(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + def get_retry_after(self, response): + """ Get the value of Retry-After in seconds. """ + + retry_after = response.getheader("Retry-After") + + if retry_after is None: + return None + + return self.parse_retry_after(retry_after) + + def sleep_for_retry(self, response=None): + retry_after = self.get_retry_after(response) + if retry_after: + time.sleep(retry_after) + return True + + return False + + def _sleep_backoff(self): + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def sleep(self, response=None): + """ Sleep between retry attempts. + + This method will respect a server's ``Retry-After`` response header + and sleep the duration of the time requested. If that is not present, it + will use an exponential backoff. By default, the backoff factor is 0 and + this method will return immediately. + """ + + if response: + slept = self.sleep_for_retry(response) + if slept: + return + + self._sleep_backoff() + + def _is_connection_error(self, err): + """ Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err): + """ Errors that occur after the request has been started, so we should + assume that the server began processing it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def _is_method_retryable(self, method): + """ Checks if a given HTTP method should be retried upon, depending if + it is included on the method whitelist. + """ + if self.method_whitelist and method.upper() not in self.method_whitelist: + return False + + return True + + def is_retry(self, method, status_code, has_retry_after=False): + """ Is this method/status code retryable? (Based on whitelists and control + variables such as the number of total retries to allow, whether to + respect the Retry-After header, whether this header is present, and + whether the returned status code is on the list of status codes to + be retried upon on the presence of the aforementioned header) + """ + if not self._is_method_retryable(method): + return False + + if self.status_forcelist and status_code in self.status_forcelist: + return True + + return (self.total and self.respect_retry_after_header and + has_retry_after and (status_code in self.RETRY_AFTER_STATUS_CODES)) + + def is_exhausted(self): + """ Are we out of retries? """ + retry_counts = (self.total, self.connect, self.read, self.redirect) + retry_counts = list(filter(None, retry_counts)) + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment(self, method=None, url=None, response=None, error=None, + _pool=None, _stacktrace=None): + """ Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.HTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise six.reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + connect = self.connect + read = self.read + redirect = self.redirect + cause = 'unknown' + status = None + redirect_location = None + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise six.reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False or not self._is_method_retryable(method): + raise six.reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + cause = 'too many redirects' + redirect_location = response.get_redirect_location() + status = response.status + + else: + # Incrementing because of a server error like a 500 in + # status_forcelist and a the given method is in the whitelist + cause = ResponseError.GENERIC_ERROR + if response and response.status: + cause = ResponseError.SPECIFIC_ERROR.format( + status_code=response.status) + status = response.status + + history = self.history + (RequestHistory(method, url, error, status, redirect_location),) + + new_retry = self.new( + total=total, + connect=connect, read=read, redirect=redirect, + history=history) + + if new_retry.is_exhausted(): + raise MaxRetryError(_pool, url, error or ResponseError(cause)) + + log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) + + return new_retry + + def __repr__(self): + return ('{cls.__name__}(total={self.total}, connect={self.connect}, ' + 'read={self.read}, redirect={self.redirect})').format( + cls=type(self), self=self) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py index 0c41974..b381450 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py @@ -1,524 +1,529 @@ -# Backport of selectors.py from Python 3.5+ to support Python < 3.4 -# Also has the behavior specified in PEP 475 which is to retry syscalls -# in the case of an EINTR error. This module is required because selectors34 -# does not follow this behavior and instead returns that no dile descriptor -# events have occurred rather than retry the syscall. The decision to drop -# support for select.devpoll is made to maintain 100% test coverage. - -import errno -import math -import select -from collections import namedtuple, Mapping - -import time -try: - monotonic = time.monotonic -except (AttributeError, ImportError): # Python 3.3< - monotonic = time.time - -EVENT_READ = (1 << 0) -EVENT_WRITE = (1 << 1) - -HAS_SELECT = True # Variable that shows whether the platform has a selector. -_SYSCALL_SENTINEL = object() # Sentinel in case a system call returns None. - - -class SelectorError(Exception): - def __init__(self, errcode): - super(SelectorError, self).__init__() - self.errno = errcode - - def __repr__(self): - return "".format(self.errno) - - def __str__(self): - return self.__repr__() - - -def _fileobj_to_fd(fileobj): - """ Return a file descriptor from a file object. If - given an integer will simply return that integer back. """ - if isinstance(fileobj, int): - fd = fileobj - else: - try: - fd = int(fileobj.fileno()) - except (AttributeError, TypeError, ValueError): - raise ValueError("Invalid file object: {0!r}".format(fileobj)) - if fd < 0: - raise ValueError("Invalid file descriptor: {0}".format(fd)) - return fd - - -def _syscall_wrapper(func, recalc_timeout, *args, **kwargs): - """ Wrapper function for syscalls that could fail due to EINTR. - All functions should be retried if there is time left in the timeout - in accordance with PEP 475. """ - timeout = kwargs.get("timeout", None) - if timeout is None: - expires = None - recalc_timeout = False - else: - timeout = float(timeout) - if timeout < 0.0: # Timeout less than 0 treated as no timeout. - expires = None - else: - expires = monotonic() + timeout - - args = list(args) - if recalc_timeout and "timeout" not in kwargs: - raise ValueError( - "Timeout must be in args or kwargs to be recalculated") - - result = _SYSCALL_SENTINEL - while result is _SYSCALL_SENTINEL: - try: - result = func(*args, **kwargs) - # OSError is thrown by select.select - # IOError is thrown by select.epoll.poll - # select.error is thrown by select.poll.poll - # Aren't we thankful for Python 3.x rework for exceptions? - except (OSError, IOError, select.error) as e: - # select.error wasn't a subclass of OSError in the past. - errcode = None - if hasattr(e, "errno"): - errcode = e.errno - elif hasattr(e, "args"): - errcode = e.args[0] - - # Also test for the Windows equivalent of EINTR. - is_interrupt = (errcode == errno.EINTR or (hasattr(errno, "WSAEINTR") and - errcode == errno.WSAEINTR)) - - if is_interrupt: - if expires is not None: - current_time = monotonic() - if current_time > expires: - raise OSError(errno=errno.ETIMEDOUT) - if recalc_timeout: - if "timeout" in kwargs: - kwargs["timeout"] = expires - current_time - continue - if errcode: - raise SelectorError(errcode) - else: - raise - return result - - -SelectorKey = namedtuple('SelectorKey', ['fileobj', 'fd', 'events', 'data']) - - -class _SelectorMapping(Mapping): - """ Mapping of file objects to selector keys """ - - def __init__(self, selector): - self._selector = selector - - def __len__(self): - return len(self._selector._fd_to_key) - - def __getitem__(self, fileobj): - try: - fd = self._selector._fileobj_lookup(fileobj) - return self._selector._fd_to_key[fd] - except KeyError: - raise KeyError("{0!r} is not registered.".format(fileobj)) - - def __iter__(self): - return iter(self._selector._fd_to_key) - - -class BaseSelector(object): - """ Abstract Selector class - - A selector supports registering file objects to be monitored - for specific I/O events. - - A file object is a file descriptor or any object with a - `fileno()` method. An arbitrary object can be attached to the - file object which can be used for example to store context info, - a callback, etc. - - A selector can use various implementations (select(), poll(), epoll(), - and kqueue()) depending on the platform. The 'DefaultSelector' class uses - the most efficient implementation for the current platform. - """ - def __init__(self): - # Maps file descriptors to keys. - self._fd_to_key = {} - - # Read-only mapping returned by get_map() - self._map = _SelectorMapping(self) - - def _fileobj_lookup(self, fileobj): - """ Return a file descriptor from a file object. - This wraps _fileobj_to_fd() to do an exhaustive - search in case the object is invalid but we still - have it in our map. Used by unregister() so we can - unregister an object that was previously registered - even if it is closed. It is also used by _SelectorMapping - """ - try: - return _fileobj_to_fd(fileobj) - except ValueError: - - # Search through all our mapped keys. - for key in self._fd_to_key.values(): - if key.fileobj is fileobj: - return key.fd - - # Raise ValueError after all. - raise - - def register(self, fileobj, events, data=None): - """ Register a file object for a set of events to monitor. """ - if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)): - raise ValueError("Invalid events: {0!r}".format(events)) - - key = SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data) - - if key.fd in self._fd_to_key: - raise KeyError("{0!r} (FD {1}) is already registered" - .format(fileobj, key.fd)) - - self._fd_to_key[key.fd] = key - return key - - def unregister(self, fileobj): - """ Unregister a file object from being monitored. """ - try: - key = self._fd_to_key.pop(self._fileobj_lookup(fileobj)) - except KeyError: - raise KeyError("{0!r} is not registered".format(fileobj)) - return key - - def modify(self, fileobj, events, data=None): - """ Change a registered file object monitored events and data. """ - # NOTE: Some subclasses optimize this operation even further. - try: - key = self._fd_to_key[self._fileobj_lookup(fileobj)] - except KeyError: - raise KeyError("{0!r} is not registered".format(fileobj)) - - if events != key.events: - self.unregister(fileobj) - key = self.register(fileobj, events, data) - - elif data != key.data: - # Use a shortcut to update the data. - key = key._replace(data=data) - self._fd_to_key[key.fd] = key - - return key - - def select(self, timeout=None): - """ Perform the actual selection until some monitored file objects - are ready or the timeout expires. """ - raise NotImplementedError() - - def close(self): - """ Close the selector. This must be called to ensure that all - underlying resources are freed. """ - self._fd_to_key.clear() - self._map = None - - def get_key(self, fileobj): - """ Return the key associated with a registered file object. """ - mapping = self.get_map() - if mapping is None: - raise RuntimeError("Selector is closed") - try: - return mapping[fileobj] - except KeyError: - raise KeyError("{0!r} is not registered".format(fileobj)) - - def get_map(self): - """ Return a mapping of file objects to selector keys """ - return self._map - - def _key_from_fd(self, fd): - """ Return the key associated to a given file descriptor - Return None if it is not found. """ - try: - return self._fd_to_key[fd] - except KeyError: - return None - - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() - - -# Almost all platforms have select.select() -if hasattr(select, "select"): - class SelectSelector(BaseSelector): - """ Select-based selector. """ - def __init__(self): - super(SelectSelector, self).__init__() - self._readers = set() - self._writers = set() - - def register(self, fileobj, events, data=None): - key = super(SelectSelector, self).register(fileobj, events, data) - if events & EVENT_READ: - self._readers.add(key.fd) - if events & EVENT_WRITE: - self._writers.add(key.fd) - return key - - def unregister(self, fileobj): - key = super(SelectSelector, self).unregister(fileobj) - self._readers.discard(key.fd) - self._writers.discard(key.fd) - return key - - def _select(self, r, w, timeout=None): - """ Wrapper for select.select because timeout is a positional arg """ - return select.select(r, w, [], timeout) - - def select(self, timeout=None): - # Selecting on empty lists on Windows errors out. - if not len(self._readers) and not len(self._writers): - return [] - - timeout = None if timeout is None else max(timeout, 0.0) - ready = [] - r, w, _ = _syscall_wrapper(self._select, True, self._readers, - self._writers, timeout) - r = set(r) - w = set(w) - for fd in r | w: - events = 0 - if fd in r: - events |= EVENT_READ - if fd in w: - events |= EVENT_WRITE - - key = self._key_from_fd(fd) - if key: - ready.append((key, events & key.events)) - return ready - - -if hasattr(select, "poll"): - class PollSelector(BaseSelector): - """ Poll-based selector """ - def __init__(self): - super(PollSelector, self).__init__() - self._poll = select.poll() - - def register(self, fileobj, events, data=None): - key = super(PollSelector, self).register(fileobj, events, data) - event_mask = 0 - if events & EVENT_READ: - event_mask |= select.POLLIN - if events & EVENT_WRITE: - event_mask |= select.POLLOUT - self._poll.register(key.fd, event_mask) - return key - - def unregister(self, fileobj): - key = super(PollSelector, self).unregister(fileobj) - self._poll.unregister(key.fd) - return key - - def _wrap_poll(self, timeout=None): - """ Wrapper function for select.poll.poll() so that - _syscall_wrapper can work with only seconds. """ - if timeout is not None: - if timeout <= 0: - timeout = 0 - else: - # select.poll.poll() has a resolution of 1 millisecond, - # round away from zero to wait *at least* timeout seconds. - timeout = math.ceil(timeout * 1e3) - - result = self._poll.poll(timeout) - return result - - def select(self, timeout=None): - ready = [] - fd_events = _syscall_wrapper(self._wrap_poll, True, timeout=timeout) - for fd, event_mask in fd_events: - events = 0 - if event_mask & ~select.POLLIN: - events |= EVENT_WRITE - if event_mask & ~select.POLLOUT: - events |= EVENT_READ - - key = self._key_from_fd(fd) - if key: - ready.append((key, events & key.events)) - - return ready - - -if hasattr(select, "epoll"): - class EpollSelector(BaseSelector): - """ Epoll-based selector """ - def __init__(self): - super(EpollSelector, self).__init__() - self._epoll = select.epoll() - - def fileno(self): - return self._epoll.fileno() - - def register(self, fileobj, events, data=None): - key = super(EpollSelector, self).register(fileobj, events, data) - events_mask = 0 - if events & EVENT_READ: - events_mask |= select.EPOLLIN - if events & EVENT_WRITE: - events_mask |= select.EPOLLOUT - _syscall_wrapper(self._epoll.register, False, key.fd, events_mask) - return key - - def unregister(self, fileobj): - key = super(EpollSelector, self).unregister(fileobj) - try: - _syscall_wrapper(self._epoll.unregister, False, key.fd) - except SelectorError: - # This can occur when the fd was closed since registry. - pass - return key - - def select(self, timeout=None): - if timeout is not None: - if timeout <= 0: - timeout = 0.0 - else: - # select.epoll.poll() has a resolution of 1 millisecond - # but luckily takes seconds so we don't need a wrapper - # like PollSelector. Just for better rounding. - timeout = math.ceil(timeout * 1e3) * 1e-3 - timeout = float(timeout) - else: - timeout = -1.0 # epoll.poll() must have a float. - - # We always want at least 1 to ensure that select can be called - # with no file descriptors registered. Otherwise will fail. - max_events = max(len(self._fd_to_key), 1) - - ready = [] - fd_events = _syscall_wrapper(self._epoll.poll, True, - timeout=timeout, - maxevents=max_events) - for fd, event_mask in fd_events: - events = 0 - if event_mask & ~select.EPOLLIN: - events |= EVENT_WRITE - if event_mask & ~select.EPOLLOUT: - events |= EVENT_READ - - key = self._key_from_fd(fd) - if key: - ready.append((key, events & key.events)) - return ready - - def close(self): - self._epoll.close() - super(EpollSelector, self).close() - - -if hasattr(select, "kqueue"): - class KqueueSelector(BaseSelector): - """ Kqueue / Kevent-based selector """ - def __init__(self): - super(KqueueSelector, self).__init__() - self._kqueue = select.kqueue() - - def fileno(self): - return self._kqueue.fileno() - - def register(self, fileobj, events, data=None): - key = super(KqueueSelector, self).register(fileobj, events, data) - if events & EVENT_READ: - kevent = select.kevent(key.fd, - select.KQ_FILTER_READ, - select.KQ_EV_ADD) - - _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) - - if events & EVENT_WRITE: - kevent = select.kevent(key.fd, - select.KQ_FILTER_WRITE, - select.KQ_EV_ADD) - - _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) - - return key - - def unregister(self, fileobj): - key = super(KqueueSelector, self).unregister(fileobj) - if key.events & EVENT_READ: - kevent = select.kevent(key.fd, - select.KQ_FILTER_READ, - select.KQ_EV_DELETE) - try: - _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) - except SelectorError: - pass - if key.events & EVENT_WRITE: - kevent = select.kevent(key.fd, - select.KQ_FILTER_WRITE, - select.KQ_EV_DELETE) - try: - _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) - except SelectorError: - pass - - return key - - def select(self, timeout=None): - if timeout is not None: - timeout = max(timeout, 0) - - max_events = len(self._fd_to_key) * 2 - ready_fds = {} - - kevent_list = _syscall_wrapper(self._kqueue.control, True, - None, max_events, timeout) - - for kevent in kevent_list: - fd = kevent.ident - event_mask = kevent.filter - events = 0 - if event_mask == select.KQ_FILTER_READ: - events |= EVENT_READ - if event_mask == select.KQ_FILTER_WRITE: - events |= EVENT_WRITE - - key = self._key_from_fd(fd) - if key: - if key.fd not in ready_fds: - ready_fds[key.fd] = (key, events & key.events) - else: - old_events = ready_fds[key.fd][1] - ready_fds[key.fd] = (key, (events | old_events) & key.events) - - return list(ready_fds.values()) - - def close(self): - self._kqueue.close() - super(KqueueSelector, self).close() - - -# Choose the best implementation, roughly: -# kqueue == epoll > poll > select. Devpoll not supported. (See above) -# select() also can't accept a FD > FD_SETSIZE (usually around 1024) -if 'KqueueSelector' in globals(): # Platform-specific: Mac OS and BSD - DefaultSelector = KqueueSelector -elif 'EpollSelector' in globals(): # Platform-specific: Linux - DefaultSelector = EpollSelector -elif 'PollSelector' in globals(): # Platform-specific: Linux - DefaultSelector = PollSelector -elif 'SelectSelector' in globals(): # Platform-specific: Windows - DefaultSelector = SelectSelector -else: # Platform-specific: AppEngine - def no_selector(_): - raise ValueError("Platform does not have a selector") - DefaultSelector = no_selector - HAS_SELECT = False +# Backport of selectors.py from Python 3.5+ to support Python < 3.4 +# Also has the behavior specified in PEP 475 which is to retry syscalls +# in the case of an EINTR error. This module is required because selectors34 +# does not follow this behavior and instead returns that no dile descriptor +# events have occurred rather than retry the syscall. The decision to drop +# support for select.devpoll is made to maintain 100% test coverage. + +import errno +import math +import select +from collections import namedtuple + +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + +import time +try: + monotonic = time.monotonic +except (AttributeError, ImportError): # Python 3.3< + monotonic = time.time + +EVENT_READ = (1 << 0) +EVENT_WRITE = (1 << 1) + +HAS_SELECT = True # Variable that shows whether the platform has a selector. +_SYSCALL_SENTINEL = object() # Sentinel in case a system call returns None. + + +class SelectorError(Exception): + def __init__(self, errcode): + super(SelectorError, self).__init__() + self.errno = errcode + + def __repr__(self): + return "".format(self.errno) + + def __str__(self): + return self.__repr__() + + +def _fileobj_to_fd(fileobj): + """ Return a file descriptor from a file object. If + given an integer will simply return that integer back. """ + if isinstance(fileobj, int): + fd = fileobj + else: + try: + fd = int(fileobj.fileno()) + except (AttributeError, TypeError, ValueError): + raise ValueError("Invalid file object: {0!r}".format(fileobj)) + if fd < 0: + raise ValueError("Invalid file descriptor: {0}".format(fd)) + return fd + + +def _syscall_wrapper(func, recalc_timeout, *args, **kwargs): + """ Wrapper function for syscalls that could fail due to EINTR. + All functions should be retried if there is time left in the timeout + in accordance with PEP 475. """ + timeout = kwargs.get("timeout", None) + if timeout is None: + expires = None + recalc_timeout = False + else: + timeout = float(timeout) + if timeout < 0.0: # Timeout less than 0 treated as no timeout. + expires = None + else: + expires = monotonic() + timeout + + args = list(args) + if recalc_timeout and "timeout" not in kwargs: + raise ValueError( + "Timeout must be in args or kwargs to be recalculated") + + result = _SYSCALL_SENTINEL + while result is _SYSCALL_SENTINEL: + try: + result = func(*args, **kwargs) + # OSError is thrown by select.select + # IOError is thrown by select.epoll.poll + # select.error is thrown by select.poll.poll + # Aren't we thankful for Python 3.x rework for exceptions? + except (OSError, IOError, select.error) as e: + # select.error wasn't a subclass of OSError in the past. + errcode = None + if hasattr(e, "errno"): + errcode = e.errno + elif hasattr(e, "args"): + errcode = e.args[0] + + # Also test for the Windows equivalent of EINTR. + is_interrupt = (errcode == errno.EINTR or (hasattr(errno, "WSAEINTR") and + errcode == errno.WSAEINTR)) + + if is_interrupt: + if expires is not None: + current_time = monotonic() + if current_time > expires: + raise OSError(errno=errno.ETIMEDOUT) + if recalc_timeout: + if "timeout" in kwargs: + kwargs["timeout"] = expires - current_time + continue + if errcode: + raise SelectorError(errcode) + else: + raise + return result + + +SelectorKey = namedtuple('SelectorKey', ['fileobj', 'fd', 'events', 'data']) + + +class _SelectorMapping(Mapping): + """ Mapping of file objects to selector keys """ + + def __init__(self, selector): + self._selector = selector + + def __len__(self): + return len(self._selector._fd_to_key) + + def __getitem__(self, fileobj): + try: + fd = self._selector._fileobj_lookup(fileobj) + return self._selector._fd_to_key[fd] + except KeyError: + raise KeyError("{0!r} is not registered.".format(fileobj)) + + def __iter__(self): + return iter(self._selector._fd_to_key) + + +class BaseSelector(object): + """ Abstract Selector class + + A selector supports registering file objects to be monitored + for specific I/O events. + + A file object is a file descriptor or any object with a + `fileno()` method. An arbitrary object can be attached to the + file object which can be used for example to store context info, + a callback, etc. + + A selector can use various implementations (select(), poll(), epoll(), + and kqueue()) depending on the platform. The 'DefaultSelector' class uses + the most efficient implementation for the current platform. + """ + def __init__(self): + # Maps file descriptors to keys. + self._fd_to_key = {} + + # Read-only mapping returned by get_map() + self._map = _SelectorMapping(self) + + def _fileobj_lookup(self, fileobj): + """ Return a file descriptor from a file object. + This wraps _fileobj_to_fd() to do an exhaustive + search in case the object is invalid but we still + have it in our map. Used by unregister() so we can + unregister an object that was previously registered + even if it is closed. It is also used by _SelectorMapping + """ + try: + return _fileobj_to_fd(fileobj) + except ValueError: + + # Search through all our mapped keys. + for key in self._fd_to_key.values(): + if key.fileobj is fileobj: + return key.fd + + # Raise ValueError after all. + raise + + def register(self, fileobj, events, data=None): + """ Register a file object for a set of events to monitor. """ + if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)): + raise ValueError("Invalid events: {0!r}".format(events)) + + key = SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data) + + if key.fd in self._fd_to_key: + raise KeyError("{0!r} (FD {1}) is already registered" + .format(fileobj, key.fd)) + + self._fd_to_key[key.fd] = key + return key + + def unregister(self, fileobj): + """ Unregister a file object from being monitored. """ + try: + key = self._fd_to_key.pop(self._fileobj_lookup(fileobj)) + except KeyError: + raise KeyError("{0!r} is not registered".format(fileobj)) + return key + + def modify(self, fileobj, events, data=None): + """ Change a registered file object monitored events and data. """ + # NOTE: Some subclasses optimize this operation even further. + try: + key = self._fd_to_key[self._fileobj_lookup(fileobj)] + except KeyError: + raise KeyError("{0!r} is not registered".format(fileobj)) + + if events != key.events: + self.unregister(fileobj) + key = self.register(fileobj, events, data) + + elif data != key.data: + # Use a shortcut to update the data. + key = key._replace(data=data) + self._fd_to_key[key.fd] = key + + return key + + def select(self, timeout=None): + """ Perform the actual selection until some monitored file objects + are ready or the timeout expires. """ + raise NotImplementedError() + + def close(self): + """ Close the selector. This must be called to ensure that all + underlying resources are freed. """ + self._fd_to_key.clear() + self._map = None + + def get_key(self, fileobj): + """ Return the key associated with a registered file object. """ + mapping = self.get_map() + if mapping is None: + raise RuntimeError("Selector is closed") + try: + return mapping[fileobj] + except KeyError: + raise KeyError("{0!r} is not registered".format(fileobj)) + + def get_map(self): + """ Return a mapping of file objects to selector keys """ + return self._map + + def _key_from_fd(self, fd): + """ Return the key associated to a given file descriptor + Return None if it is not found. """ + try: + return self._fd_to_key[fd] + except KeyError: + return None + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +# Almost all platforms have select.select() +if hasattr(select, "select"): + class SelectSelector(BaseSelector): + """ Select-based selector. """ + def __init__(self): + super(SelectSelector, self).__init__() + self._readers = set() + self._writers = set() + + def register(self, fileobj, events, data=None): + key = super(SelectSelector, self).register(fileobj, events, data) + if events & EVENT_READ: + self._readers.add(key.fd) + if events & EVENT_WRITE: + self._writers.add(key.fd) + return key + + def unregister(self, fileobj): + key = super(SelectSelector, self).unregister(fileobj) + self._readers.discard(key.fd) + self._writers.discard(key.fd) + return key + + def _select(self, r, w, timeout=None): + """ Wrapper for select.select because timeout is a positional arg """ + return select.select(r, w, [], timeout) + + def select(self, timeout=None): + # Selecting on empty lists on Windows errors out. + if not len(self._readers) and not len(self._writers): + return [] + + timeout = None if timeout is None else max(timeout, 0.0) + ready = [] + r, w, _ = _syscall_wrapper(self._select, True, self._readers, + self._writers, timeout) + r = set(r) + w = set(w) + for fd in r | w: + events = 0 + if fd in r: + events |= EVENT_READ + if fd in w: + events |= EVENT_WRITE + + key = self._key_from_fd(fd) + if key: + ready.append((key, events & key.events)) + return ready + + +if hasattr(select, "poll"): + class PollSelector(BaseSelector): + """ Poll-based selector """ + def __init__(self): + super(PollSelector, self).__init__() + self._poll = select.poll() + + def register(self, fileobj, events, data=None): + key = super(PollSelector, self).register(fileobj, events, data) + event_mask = 0 + if events & EVENT_READ: + event_mask |= select.POLLIN + if events & EVENT_WRITE: + event_mask |= select.POLLOUT + self._poll.register(key.fd, event_mask) + return key + + def unregister(self, fileobj): + key = super(PollSelector, self).unregister(fileobj) + self._poll.unregister(key.fd) + return key + + def _wrap_poll(self, timeout=None): + """ Wrapper function for select.poll.poll() so that + _syscall_wrapper can work with only seconds. """ + if timeout is not None: + if timeout <= 0: + timeout = 0 + else: + # select.poll.poll() has a resolution of 1 millisecond, + # round away from zero to wait *at least* timeout seconds. + timeout = math.ceil(timeout * 1e3) + + result = self._poll.poll(timeout) + return result + + def select(self, timeout=None): + ready = [] + fd_events = _syscall_wrapper(self._wrap_poll, True, timeout=timeout) + for fd, event_mask in fd_events: + events = 0 + if event_mask & ~select.POLLIN: + events |= EVENT_WRITE + if event_mask & ~select.POLLOUT: + events |= EVENT_READ + + key = self._key_from_fd(fd) + if key: + ready.append((key, events & key.events)) + + return ready + + +if hasattr(select, "epoll"): + class EpollSelector(BaseSelector): + """ Epoll-based selector """ + def __init__(self): + super(EpollSelector, self).__init__() + self._epoll = select.epoll() + + def fileno(self): + return self._epoll.fileno() + + def register(self, fileobj, events, data=None): + key = super(EpollSelector, self).register(fileobj, events, data) + events_mask = 0 + if events & EVENT_READ: + events_mask |= select.EPOLLIN + if events & EVENT_WRITE: + events_mask |= select.EPOLLOUT + _syscall_wrapper(self._epoll.register, False, key.fd, events_mask) + return key + + def unregister(self, fileobj): + key = super(EpollSelector, self).unregister(fileobj) + try: + _syscall_wrapper(self._epoll.unregister, False, key.fd) + except SelectorError: + # This can occur when the fd was closed since registry. + pass + return key + + def select(self, timeout=None): + if timeout is not None: + if timeout <= 0: + timeout = 0.0 + else: + # select.epoll.poll() has a resolution of 1 millisecond + # but luckily takes seconds so we don't need a wrapper + # like PollSelector. Just for better rounding. + timeout = math.ceil(timeout * 1e3) * 1e-3 + timeout = float(timeout) + else: + timeout = -1.0 # epoll.poll() must have a float. + + # We always want at least 1 to ensure that select can be called + # with no file descriptors registered. Otherwise will fail. + max_events = max(len(self._fd_to_key), 1) + + ready = [] + fd_events = _syscall_wrapper(self._epoll.poll, True, + timeout=timeout, + maxevents=max_events) + for fd, event_mask in fd_events: + events = 0 + if event_mask & ~select.EPOLLIN: + events |= EVENT_WRITE + if event_mask & ~select.EPOLLOUT: + events |= EVENT_READ + + key = self._key_from_fd(fd) + if key: + ready.append((key, events & key.events)) + return ready + + def close(self): + self._epoll.close() + super(EpollSelector, self).close() + + +if hasattr(select, "kqueue"): + class KqueueSelector(BaseSelector): + """ Kqueue / Kevent-based selector """ + def __init__(self): + super(KqueueSelector, self).__init__() + self._kqueue = select.kqueue() + + def fileno(self): + return self._kqueue.fileno() + + def register(self, fileobj, events, data=None): + key = super(KqueueSelector, self).register(fileobj, events, data) + if events & EVENT_READ: + kevent = select.kevent(key.fd, + select.KQ_FILTER_READ, + select.KQ_EV_ADD) + + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + + if events & EVENT_WRITE: + kevent = select.kevent(key.fd, + select.KQ_FILTER_WRITE, + select.KQ_EV_ADD) + + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + + return key + + def unregister(self, fileobj): + key = super(KqueueSelector, self).unregister(fileobj) + if key.events & EVENT_READ: + kevent = select.kevent(key.fd, + select.KQ_FILTER_READ, + select.KQ_EV_DELETE) + try: + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + except SelectorError: + pass + if key.events & EVENT_WRITE: + kevent = select.kevent(key.fd, + select.KQ_FILTER_WRITE, + select.KQ_EV_DELETE) + try: + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + except SelectorError: + pass + + return key + + def select(self, timeout=None): + if timeout is not None: + timeout = max(timeout, 0) + + max_events = len(self._fd_to_key) * 2 + ready_fds = {} + + kevent_list = _syscall_wrapper(self._kqueue.control, True, + None, max_events, timeout) + + for kevent in kevent_list: + fd = kevent.ident + event_mask = kevent.filter + events = 0 + if event_mask == select.KQ_FILTER_READ: + events |= EVENT_READ + if event_mask == select.KQ_FILTER_WRITE: + events |= EVENT_WRITE + + key = self._key_from_fd(fd) + if key: + if key.fd not in ready_fds: + ready_fds[key.fd] = (key, events & key.events) + else: + old_events = ready_fds[key.fd][1] + ready_fds[key.fd] = (key, (events | old_events) & key.events) + + return list(ready_fds.values()) + + def close(self): + self._kqueue.close() + super(KqueueSelector, self).close() + + +# Choose the best implementation, roughly: +# kqueue == epoll > poll > select. Devpoll not supported. (See above) +# select() also can't accept a FD > FD_SETSIZE (usually around 1024) +if 'KqueueSelector' in globals(): # Platform-specific: Mac OS and BSD + DefaultSelector = KqueueSelector +elif 'EpollSelector' in globals(): # Platform-specific: Linux + DefaultSelector = EpollSelector +elif 'PollSelector' in globals(): # Platform-specific: Linux + DefaultSelector = PollSelector +elif 'SelectSelector' in globals(): # Platform-specific: Windows + DefaultSelector = SelectSelector +else: # Platform-specific: AppEngine + def no_selector(_): + raise ValueError("Platform does not have a selector") + DefaultSelector = no_selector + HAS_SELECT = False diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py index f85fede..c4c55df 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py @@ -1,336 +1,336 @@ -from __future__ import absolute_import -import errno -import warnings -import hmac - -from binascii import hexlify, unhexlify -from hashlib import md5, sha1, sha256 - -from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning - - -SSLContext = None -HAS_SNI = False -IS_PYOPENSSL = False - -# Maps the length of a digest to a possible hash function producing this digest -HASHFUNC_MAP = { - 32: md5, - 40: sha1, - 64: sha256, -} - - -def _const_compare_digest_backport(a, b): - """ - Compare two digests of equal length in constant time. - - The digests must be of type str/bytes. - Returns True if the digests match, and False otherwise. - """ - result = abs(len(a) - len(b)) - for l, r in zip(bytearray(a), bytearray(b)): - result |= l ^ r - return result == 0 - - -_const_compare_digest = getattr(hmac, 'compare_digest', - _const_compare_digest_backport) - - -try: # Test for SSL features - import ssl - from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 - from ssl import HAS_SNI # Has SNI? -except ImportError: - pass - - -try: - from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION -except ImportError: - OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000 - OP_NO_COMPRESSION = 0x20000 - -# A secure default. -# Sources for more information on TLS ciphers: -# -# - https://wiki.mozilla.org/Security/Server_Side_TLS -# - https://www.ssllabs.com/projects/best-practices/index.html -# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ -# -# The general intent is: -# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE), -# - prefer ECDHE over DHE for better performance, -# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and -# security, -# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common, -# - disable NULL authentication, MD5 MACs and DSS for security reasons. -DEFAULT_CIPHERS = ':'.join([ - 'ECDH+AESGCM', - 'ECDH+CHACHA20', - 'DH+AESGCM', - 'DH+CHACHA20', - 'ECDH+AES256', - 'DH+AES256', - 'ECDH+AES128', - 'DH+AES', - 'RSA+AESGCM', - 'RSA+AES', - '!aNULL', - '!eNULL', - '!MD5', -]) - -try: - from ssl import SSLContext # Modern SSL? -except ImportError: - import sys - - class SSLContext(object): # Platform-specific: Python 2 & 3.1 - supports_set_ciphers = ((2, 7) <= sys.version_info < (3,) or - (3, 2) <= sys.version_info) - - def __init__(self, protocol_version): - self.protocol = protocol_version - # Use default values from a real SSLContext - self.check_hostname = False - self.verify_mode = ssl.CERT_NONE - self.ca_certs = None - self.options = 0 - self.certfile = None - self.keyfile = None - self.ciphers = None - - def load_cert_chain(self, certfile, keyfile): - self.certfile = certfile - self.keyfile = keyfile - - def load_verify_locations(self, cafile=None, capath=None): - self.ca_certs = cafile - - if capath is not None: - raise SSLError("CA directories not supported in older Pythons") - - def set_ciphers(self, cipher_suite): - if not self.supports_set_ciphers: - raise TypeError( - 'Your version of Python does not support setting ' - 'a custom cipher suite. Please upgrade to Python ' - '2.7, 3.2, or later if you need this functionality.' - ) - self.ciphers = cipher_suite - - def wrap_socket(self, socket, server_hostname=None, server_side=False): - warnings.warn( - 'A true SSLContext object is not available. This prevents ' - 'urllib3 from configuring SSL appropriately and may cause ' - 'certain SSL connections to fail. You can upgrade to a newer ' - 'version of Python to solve this. For more information, see ' - 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' - '#ssl-warnings', - InsecurePlatformWarning - ) - kwargs = { - 'keyfile': self.keyfile, - 'certfile': self.certfile, - 'ca_certs': self.ca_certs, - 'cert_reqs': self.verify_mode, - 'ssl_version': self.protocol, - 'server_side': server_side, - } - if self.supports_set_ciphers: # Platform-specific: Python 2.7+ - return wrap_socket(socket, ciphers=self.ciphers, **kwargs) - else: # Platform-specific: Python 2.6 - return wrap_socket(socket, **kwargs) - - -def assert_fingerprint(cert, fingerprint): - """ - Checks if given fingerprint matches the supplied certificate. - - :param cert: - Certificate as bytes object. - :param fingerprint: - Fingerprint as string of hexdigits, can be interspersed by colons. - """ - - fingerprint = fingerprint.replace(':', '').lower() - digest_length = len(fingerprint) - hashfunc = HASHFUNC_MAP.get(digest_length) - if not hashfunc: - raise SSLError( - 'Fingerprint of invalid length: {0}'.format(fingerprint)) - - # We need encode() here for py32; works on py2 and p33. - fingerprint_bytes = unhexlify(fingerprint.encode()) - - cert_digest = hashfunc(cert).digest() - - if not _const_compare_digest(cert_digest, fingerprint_bytes): - raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".' - .format(fingerprint, hexlify(cert_digest))) - - -def resolve_cert_reqs(candidate): - """ - Resolves the argument to a numeric constant, which can be passed to - the wrap_socket function/method from the ssl module. - Defaults to :data:`ssl.CERT_NONE`. - If given a string it is assumed to be the name of the constant in the - :mod:`ssl` module or its abbrevation. - (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. - If it's neither `None` nor a string we assume it is already the numeric - constant which can directly be passed to wrap_socket. - """ - if candidate is None: - return CERT_NONE - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, 'CERT_' + candidate) - return res - - return candidate - - -def resolve_ssl_version(candidate): - """ - like resolve_cert_reqs - """ - if candidate is None: - return PROTOCOL_SSLv23 - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, 'PROTOCOL_' + candidate) - return res - - return candidate - - -def create_urllib3_context(ssl_version=None, cert_reqs=None, - options=None, ciphers=None): - """All arguments have the same meaning as ``ssl_wrap_socket``. - - By default, this function does a lot of the same work that - ``ssl.create_default_context`` does on Python 3.4+. It: - - - Disables SSLv2, SSLv3, and compression - - Sets a restricted set of server ciphers - - If you wish to enable SSLv3, you can do:: - - from urllib3.util import ssl_ - context = ssl_.create_urllib3_context() - context.options &= ~ssl_.OP_NO_SSLv3 - - You can do the same to enable compression (substituting ``COMPRESSION`` - for ``SSLv3`` in the last line above). - - :param ssl_version: - The desired protocol version to use. This will default to - PROTOCOL_SSLv23 which will negotiate the highest protocol that both - the server and your installation of OpenSSL support. - :param cert_reqs: - Whether to require the certificate verification. This defaults to - ``ssl.CERT_REQUIRED``. - :param options: - Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, - ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``. - :param ciphers: - Which cipher suites to allow the server to select. - :returns: - Constructed SSLContext object with specified options - :rtype: SSLContext - """ - context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23) - - # Setting the default here, as we may have no ssl module on import - cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs - - if options is None: - options = 0 - # SSLv2 is easily broken and is considered harmful and dangerous - options |= OP_NO_SSLv2 - # SSLv3 has several problems and is now dangerous - options |= OP_NO_SSLv3 - # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ - # (issue #309) - options |= OP_NO_COMPRESSION - - context.options |= options - - if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6 - context.set_ciphers(ciphers or DEFAULT_CIPHERS) - - context.verify_mode = cert_reqs - if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2 - # We do our own verification, including fingerprints and alternative - # hostnames. So disable it here - context.check_hostname = False - return context - - -def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, - ca_certs=None, server_hostname=None, - ssl_version=None, ciphers=None, ssl_context=None, - ca_cert_dir=None): - """ - All arguments except for server_hostname, ssl_context, and ca_cert_dir have - the same meaning as they do when using :func:`ssl.wrap_socket`. - - :param server_hostname: - When SNI is supported, the expected hostname of the certificate - :param ssl_context: - A pre-made :class:`SSLContext` object. If none is provided, one will - be created using :func:`create_urllib3_context`. - :param ciphers: - A string of ciphers we wish the client to support. This is not - supported on Python 2.6 as the ssl module does not support it. - :param ca_cert_dir: - A directory containing CA certificates in multiple separate files, as - supported by OpenSSL's -CApath flag or the capath argument to - SSLContext.load_verify_locations(). - """ - context = ssl_context - if context is None: - # Note: This branch of code and all the variables in it are no longer - # used by urllib3 itself. We should consider deprecating and removing - # this code. - context = create_urllib3_context(ssl_version, cert_reqs, - ciphers=ciphers) - - if ca_certs or ca_cert_dir: - try: - context.load_verify_locations(ca_certs, ca_cert_dir) - except IOError as e: # Platform-specific: Python 2.6, 2.7, 3.2 - raise SSLError(e) - # Py33 raises FileNotFoundError which subclasses OSError - # These are not equivalent unless we check the errno attribute - except OSError as e: # Platform-specific: Python 3.3 and beyond - if e.errno == errno.ENOENT: - raise SSLError(e) - raise - elif getattr(context, 'load_default_certs', None) is not None: - # try to load OS default certs; works well on Windows (require Python3.4+) - context.load_default_certs() - - if certfile: - context.load_cert_chain(certfile, keyfile) - if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI - return context.wrap_socket(sock, server_hostname=server_hostname) - - warnings.warn( - 'An HTTPS request has been made, but the SNI (Subject Name ' - 'Indication) extension to TLS is not available on this platform. ' - 'This may cause the server to present an incorrect TLS ' - 'certificate, which can cause validation failures. You can upgrade to ' - 'a newer version of Python to solve this. For more information, see ' - 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' - '#ssl-warnings', - SNIMissingWarning - ) - return context.wrap_socket(sock) +from __future__ import absolute_import +import errno +import warnings +import hmac + +from binascii import hexlify, unhexlify +from hashlib import md5, sha1, sha256 + +from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning + + +SSLContext = None +HAS_SNI = False +IS_PYOPENSSL = False + +# Maps the length of a digest to a possible hash function producing this digest +HASHFUNC_MAP = { + 32: md5, + 40: sha1, + 64: sha256, +} + + +def _const_compare_digest_backport(a, b): + """ + Compare two digests of equal length in constant time. + + The digests must be of type str/bytes. + Returns True if the digests match, and False otherwise. + """ + result = abs(len(a) - len(b)) + for l, r in zip(bytearray(a), bytearray(b)): + result |= l ^ r + return result == 0 + + +_const_compare_digest = getattr(hmac, 'compare_digest', + _const_compare_digest_backport) + + +try: # Test for SSL features + import ssl + from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 + from ssl import HAS_SNI # Has SNI? +except ImportError: + pass + + +try: + from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION +except ImportError: + OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000 + OP_NO_COMPRESSION = 0x20000 + +# A secure default. +# Sources for more information on TLS ciphers: +# +# - https://wiki.mozilla.org/Security/Server_Side_TLS +# - https://www.ssllabs.com/projects/best-practices/index.html +# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ +# +# The general intent is: +# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE), +# - prefer ECDHE over DHE for better performance, +# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and +# security, +# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common, +# - disable NULL authentication, MD5 MACs and DSS for security reasons. +DEFAULT_CIPHERS = ':'.join([ + 'ECDH+AESGCM', + 'ECDH+CHACHA20', + 'DH+AESGCM', + 'DH+CHACHA20', + 'ECDH+AES256', + 'DH+AES256', + 'ECDH+AES128', + 'DH+AES', + 'RSA+AESGCM', + 'RSA+AES', + '!aNULL', + '!eNULL', + '!MD5', +]) + +try: + from ssl import SSLContext # Modern SSL? +except ImportError: + import sys + + class SSLContext(object): # Platform-specific: Python 2 & 3.1 + supports_set_ciphers = ((2, 7) <= sys.version_info < (3,) or + (3, 2) <= sys.version_info) + + def __init__(self, protocol_version): + self.protocol = protocol_version + # Use default values from a real SSLContext + self.check_hostname = False + self.verify_mode = ssl.CERT_NONE + self.ca_certs = None + self.options = 0 + self.certfile = None + self.keyfile = None + self.ciphers = None + + def load_cert_chain(self, certfile, keyfile): + self.certfile = certfile + self.keyfile = keyfile + + def load_verify_locations(self, cafile=None, capath=None): + self.ca_certs = cafile + + if capath is not None: + raise SSLError("CA directories not supported in older Pythons") + + def set_ciphers(self, cipher_suite): + if not self.supports_set_ciphers: + raise TypeError( + 'Your version of Python does not support setting ' + 'a custom cipher suite. Please upgrade to Python ' + '2.7, 3.2, or later if you need this functionality.' + ) + self.ciphers = cipher_suite + + def wrap_socket(self, socket, server_hostname=None, server_side=False): + warnings.warn( + 'A true SSLContext object is not available. This prevents ' + 'urllib3 from configuring SSL appropriately and may cause ' + 'certain SSL connections to fail. You can upgrade to a newer ' + 'version of Python to solve this. For more information, see ' + 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' + '#ssl-warnings', + InsecurePlatformWarning + ) + kwargs = { + 'keyfile': self.keyfile, + 'certfile': self.certfile, + 'ca_certs': self.ca_certs, + 'cert_reqs': self.verify_mode, + 'ssl_version': self.protocol, + 'server_side': server_side, + } + if self.supports_set_ciphers: # Platform-specific: Python 2.7+ + return wrap_socket(socket, ciphers=self.ciphers, **kwargs) + else: # Platform-specific: Python 2.6 + return wrap_socket(socket, **kwargs) + + +def assert_fingerprint(cert, fingerprint): + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + fingerprint = fingerprint.replace(':', '').lower() + digest_length = len(fingerprint) + hashfunc = HASHFUNC_MAP.get(digest_length) + if not hashfunc: + raise SSLError( + 'Fingerprint of invalid length: {0}'.format(fingerprint)) + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + cert_digest = hashfunc(cert).digest() + + if not _const_compare_digest(cert_digest, fingerprint_bytes): + raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".' + .format(fingerprint, hexlify(cert_digest))) + + +def resolve_cert_reqs(candidate): + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_NONE`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbrevation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_NONE + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'CERT_' + candidate) + return res + + return candidate + + +def resolve_ssl_version(candidate): + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_SSLv23 + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'PROTOCOL_' + candidate) + return res + + return candidate + + +def create_urllib3_context(ssl_version=None, cert_reqs=None, + options=None, ciphers=None): + """All arguments have the same meaning as ``ssl_wrap_socket``. + + By default, this function does a lot of the same work that + ``ssl.create_default_context`` does on Python 3.4+. It: + + - Disables SSLv2, SSLv3, and compression + - Sets a restricted set of server ciphers + + If you wish to enable SSLv3, you can do:: + + from urllib3.util import ssl_ + context = ssl_.create_urllib3_context() + context.options &= ~ssl_.OP_NO_SSLv3 + + You can do the same to enable compression (substituting ``COMPRESSION`` + for ``SSLv3`` in the last line above). + + :param ssl_version: + The desired protocol version to use. This will default to + PROTOCOL_SSLv23 which will negotiate the highest protocol that both + the server and your installation of OpenSSL support. + :param cert_reqs: + Whether to require the certificate verification. This defaults to + ``ssl.CERT_REQUIRED``. + :param options: + Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``. + :param ciphers: + Which cipher suites to allow the server to select. + :returns: + Constructed SSLContext object with specified options + :rtype: SSLContext + """ + context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23) + + # Setting the default here, as we may have no ssl module on import + cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if options is None: + options = 0 + # SSLv2 is easily broken and is considered harmful and dangerous + options |= OP_NO_SSLv2 + # SSLv3 has several problems and is now dangerous + options |= OP_NO_SSLv3 + # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (issue #309) + options |= OP_NO_COMPRESSION + + context.options |= options + + if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6 + context.set_ciphers(ciphers or DEFAULT_CIPHERS) + + context.verify_mode = cert_reqs + if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2 + # We do our own verification, including fingerprints and alternative + # hostnames. So disable it here + context.check_hostname = False + return context + + +def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None, ciphers=None, ssl_context=None, + ca_cert_dir=None): + """ + All arguments except for server_hostname, ssl_context, and ca_cert_dir have + the same meaning as they do when using :func:`ssl.wrap_socket`. + + :param server_hostname: + When SNI is supported, the expected hostname of the certificate + :param ssl_context: + A pre-made :class:`SSLContext` object. If none is provided, one will + be created using :func:`create_urllib3_context`. + :param ciphers: + A string of ciphers we wish the client to support. This is not + supported on Python 2.6 as the ssl module does not support it. + :param ca_cert_dir: + A directory containing CA certificates in multiple separate files, as + supported by OpenSSL's -CApath flag or the capath argument to + SSLContext.load_verify_locations(). + """ + context = ssl_context + if context is None: + # Note: This branch of code and all the variables in it are no longer + # used by urllib3 itself. We should consider deprecating and removing + # this code. + context = create_urllib3_context(ssl_version, cert_reqs, + ciphers=ciphers) + + if ca_certs or ca_cert_dir: + try: + context.load_verify_locations(ca_certs, ca_cert_dir) + except IOError as e: # Platform-specific: Python 2.6, 2.7, 3.2 + raise SSLError(e) + # Py33 raises FileNotFoundError which subclasses OSError + # These are not equivalent unless we check the errno attribute + except OSError as e: # Platform-specific: Python 3.3 and beyond + if e.errno == errno.ENOENT: + raise SSLError(e) + raise + elif getattr(context, 'load_default_certs', None) is not None: + # try to load OS default certs; works well on Windows (require Python3.4+) + context.load_default_certs() + + if certfile: + context.load_cert_chain(certfile, keyfile) + if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI + return context.wrap_socket(sock, server_hostname=server_hostname) + + warnings.warn( + 'An HTTPS request has been made, but the SNI (Subject Name ' + 'Indication) extension to TLS is not available on this platform. ' + 'This may cause the server to present an incorrect TLS ' + 'certificate, which can cause validation failures. You can upgrade to ' + 'a newer version of Python to solve this. For more information, see ' + 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' + '#ssl-warnings', + SNIMissingWarning + ) + return context.wrap_socket(sock) diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py index 9c2e6ef..cec817e 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py @@ -1,242 +1,242 @@ -from __future__ import absolute_import -# The default socket timeout, used by httplib to indicate that no timeout was -# specified by the user -from socket import _GLOBAL_DEFAULT_TIMEOUT -import time - -from ..exceptions import TimeoutStateError - -# A sentinel value to indicate that no timeout was specified by the user in -# urllib3 -_Default = object() - - -# Use time.monotonic if available. -current_time = getattr(time, "monotonic", time.time) - - -class Timeout(object): - """ Timeout configuration. - - Timeouts can be defined as a default for a pool:: - - timeout = Timeout(connect=2.0, read=7.0) - http = PoolManager(timeout=timeout) - response = http.request('GET', 'http://example.com/') - - Or per-request (which overrides the default for the pool):: - - response = http.request('GET', 'http://example.com/', timeout=Timeout(10)) - - Timeouts can be disabled by setting all the parameters to ``None``:: - - no_timeout = Timeout(connect=None, read=None) - response = http.request('GET', 'http://example.com/, timeout=no_timeout) - - - :param total: - This combines the connect and read timeouts into one; the read timeout - will be set to the time leftover from the connect attempt. In the - event that both a connect timeout and a total are specified, or a read - timeout and a total are specified, the shorter timeout will be applied. - - Defaults to None. - - :type total: integer, float, or None - - :param connect: - The maximum amount of time to wait for a connection attempt to a server - to succeed. Omitting the parameter will default the connect timeout to - the system default, probably `the global default timeout in socket.py - `_. - None will set an infinite timeout for connection attempts. - - :type connect: integer, float, or None - - :param read: - The maximum amount of time to wait between consecutive - read operations for a response from the server. Omitting - the parameter will default the read timeout to the system - default, probably `the global default timeout in socket.py - `_. - None will set an infinite timeout. - - :type read: integer, float, or None - - .. note:: - - Many factors can affect the total amount of time for urllib3 to return - an HTTP response. - - For example, Python's DNS resolver does not obey the timeout specified - on the socket. Other factors that can affect total request time include - high CPU load, high swap, the program running at a low priority level, - or other behaviors. - - In addition, the read and total timeouts only measure the time between - read operations on the socket connecting the client and the server, - not the total amount of time for the request to return a complete - response. For most requests, the timeout is raised because the server - has not sent the first byte in the specified time. This is not always - the case; if a server streams one byte every fifteen seconds, a timeout - of 20 seconds will not trigger, even though the request will take - several minutes to complete. - - If your goal is to cut off any request after a set amount of wall clock - time, consider having a second "watcher" thread to cut off a slow - request. - """ - - #: A sentinel object representing the default timeout value - DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT - - def __init__(self, total=None, connect=_Default, read=_Default): - self._connect = self._validate_timeout(connect, 'connect') - self._read = self._validate_timeout(read, 'read') - self.total = self._validate_timeout(total, 'total') - self._start_connect = None - - def __str__(self): - return '%s(connect=%r, read=%r, total=%r)' % ( - type(self).__name__, self._connect, self._read, self.total) - - @classmethod - def _validate_timeout(cls, value, name): - """ Check that a timeout attribute is valid. - - :param value: The timeout value to validate - :param name: The name of the timeout attribute to validate. This is - used to specify in error messages. - :return: The validated and casted version of the given value. - :raises ValueError: If it is a numeric value less than or equal to - zero, or the type is not an integer, float, or None. - """ - if value is _Default: - return cls.DEFAULT_TIMEOUT - - if value is None or value is cls.DEFAULT_TIMEOUT: - return value - - if isinstance(value, bool): - raise ValueError("Timeout cannot be a boolean value. It must " - "be an int, float or None.") - try: - float(value) - except (TypeError, ValueError): - raise ValueError("Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value)) - - try: - if value <= 0: - raise ValueError("Attempted to set %s timeout to %s, but the " - "timeout cannot be set to a value less " - "than or equal to 0." % (name, value)) - except TypeError: # Python 3 - raise ValueError("Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value)) - - return value - - @classmethod - def from_float(cls, timeout): - """ Create a new Timeout from a legacy timeout value. - - The timeout value used by httplib.py sets the same timeout on the - connect(), and recv() socket requests. This creates a :class:`Timeout` - object that sets the individual timeouts to the ``timeout`` value - passed to this function. - - :param timeout: The legacy timeout value. - :type timeout: integer, float, sentinel default object, or None - :return: Timeout object - :rtype: :class:`Timeout` - """ - return Timeout(read=timeout, connect=timeout) - - def clone(self): - """ Create a copy of the timeout object - - Timeout properties are stored per-pool but each request needs a fresh - Timeout object to ensure each one has its own start/stop configured. - - :return: a copy of the timeout object - :rtype: :class:`Timeout` - """ - # We can't use copy.deepcopy because that will also create a new object - # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to - # detect the user default. - return Timeout(connect=self._connect, read=self._read, - total=self.total) - - def start_connect(self): - """ Start the timeout clock, used during a connect() attempt - - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to start a timer that has been started already. - """ - if self._start_connect is not None: - raise TimeoutStateError("Timeout timer has already been started.") - self._start_connect = current_time() - return self._start_connect - - def get_connect_duration(self): - """ Gets the time elapsed since the call to :meth:`start_connect`. - - :return: Elapsed time. - :rtype: float - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to get duration for a timer that hasn't been started. - """ - if self._start_connect is None: - raise TimeoutStateError("Can't get connect duration for timer " - "that has not started.") - return current_time() - self._start_connect - - @property - def connect_timeout(self): - """ Get the value to use when setting a connection timeout. - - This will be a positive float or integer, the value None - (never timeout), or the default system timeout. - - :return: Connect timeout. - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - """ - if self.total is None: - return self._connect - - if self._connect is None or self._connect is self.DEFAULT_TIMEOUT: - return self.total - - return min(self._connect, self.total) - - @property - def read_timeout(self): - """ Get the value for the read timeout. - - This assumes some time has elapsed in the connection timeout and - computes the read timeout appropriately. - - If self.total is set, the read timeout is dependent on the amount of - time taken by the connect timeout. If the connection time has not been - established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be - raised. - - :return: Value to use for the read timeout. - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` - has not yet been called on this object. - """ - if (self.total is not None and - self.total is not self.DEFAULT_TIMEOUT and - self._read is not None and - self._read is not self.DEFAULT_TIMEOUT): - # In case the connect timeout has not yet been established. - if self._start_connect is None: - return self._read - return max(0, min(self.total - self.get_connect_duration(), - self._read)) - elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT: - return max(0, self.total - self.get_connect_duration()) - else: - return self._read +from __future__ import absolute_import +# The default socket timeout, used by httplib to indicate that no timeout was +# specified by the user +from socket import _GLOBAL_DEFAULT_TIMEOUT +import time + +from ..exceptions import TimeoutStateError + +# A sentinel value to indicate that no timeout was specified by the user in +# urllib3 +_Default = object() + + +# Use time.monotonic if available. +current_time = getattr(time, "monotonic", time.time) + + +class Timeout(object): + """ Timeout configuration. + + Timeouts can be defined as a default for a pool:: + + timeout = Timeout(connect=2.0, read=7.0) + http = PoolManager(timeout=timeout) + response = http.request('GET', 'http://example.com/') + + Or per-request (which overrides the default for the pool):: + + response = http.request('GET', 'http://example.com/', timeout=Timeout(10)) + + Timeouts can be disabled by setting all the parameters to ``None``:: + + no_timeout = Timeout(connect=None, read=None) + response = http.request('GET', 'http://example.com/, timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: integer, float, or None + + :param connect: + The maximum amount of time to wait for a connection attempt to a server + to succeed. Omitting the parameter will default the connect timeout to + the system default, probably `the global default timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: integer, float, or None + + :param read: + The maximum amount of time to wait between consecutive + read operations for a response from the server. Omitting + the parameter will default the read timeout to the system + default, probably `the global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: integer, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not trigger, even though the request will take + several minutes to complete. + + If your goal is to cut off any request after a set amount of wall clock + time, consider having a second "watcher" thread to cut off a slow + request. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT + + def __init__(self, total=None, connect=_Default, read=_Default): + self._connect = self._validate_timeout(connect, 'connect') + self._read = self._validate_timeout(read, 'read') + self.total = self._validate_timeout(total, 'total') + self._start_connect = None + + def __str__(self): + return '%s(connect=%r, read=%r, total=%r)' % ( + type(self).__name__, self._connect, self._read, self.total) + + @classmethod + def _validate_timeout(cls, value, name): + """ Check that a timeout attribute is valid. + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is + used to specify in error messages. + :return: The validated and casted version of the given value. + :raises ValueError: If it is a numeric value less than or equal to + zero, or the type is not an integer, float, or None. + """ + if value is _Default: + return cls.DEFAULT_TIMEOUT + + if value is None or value is cls.DEFAULT_TIMEOUT: + return value + + if isinstance(value, bool): + raise ValueError("Timeout cannot be a boolean value. It must " + "be an int, float or None.") + try: + float(value) + except (TypeError, ValueError): + raise ValueError("Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value)) + + try: + if value <= 0: + raise ValueError("Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than or equal to 0." % (name, value)) + except TypeError: # Python 3 + raise ValueError("Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value)) + + return value + + @classmethod + def from_float(cls, timeout): + """ Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value + passed to this function. + + :param timeout: The legacy timeout value. + :type timeout: integer, float, sentinel default object, or None + :return: Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self): + """ Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, + total=self.total) + + def start_connect(self): + """ Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = current_time() + return self._start_connect + + def get_connect_duration(self): + """ Gets the time elapsed since the call to :meth:`start_connect`. + + :return: Elapsed time. + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError("Can't get connect duration for timer " + "that has not started.") + return current_time() - self._start_connect + + @property + def connect_timeout(self): + """ Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: Connect timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is self.DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) + + @property + def read_timeout(self): + """ Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: Value to use for the read timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if (self.total is not None and + self.total is not self.DEFAULT_TIMEOUT and + self._read is not None and + self._read is not self.DEFAULT_TIMEOUT): + # In case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), + self._read)) + elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self._read diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py index e51950b..61a326e 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py @@ -1,226 +1,226 @@ -from __future__ import absolute_import -from collections import namedtuple - -from ..exceptions import LocationParseError - - -url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'] - - -class Url(namedtuple('Url', url_attrs)): - """ - Datastructure for representing an HTTP URL. Used as a return value for - :func:`parse_url`. Both the scheme and host are normalized as they are - both case-insensitive according to RFC 3986. - """ - __slots__ = () - - def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, - query=None, fragment=None): - if path and not path.startswith('/'): - path = '/' + path - if scheme: - scheme = scheme.lower() - if host: - host = host.lower() - return super(Url, cls).__new__(cls, scheme, auth, host, port, path, - query, fragment) - - @property - def hostname(self): - """For backwards-compatibility with urlparse. We're nice like that.""" - return self.host - - @property - def request_uri(self): - """Absolute path including the query string.""" - uri = self.path or '/' - - if self.query is not None: - uri += '?' + self.query - - return uri - - @property - def netloc(self): - """Network location including host and port""" - if self.port: - return '%s:%d' % (self.host, self.port) - return self.host - - @property - def url(self): - """ - Convert self into a url - - This function should more or less round-trip with :func:`.parse_url`. The - returned url may not be exactly the same as the url inputted to - :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls - with a blank port will have : removed). - - Example: :: - - >>> U = parse_url('http://google.com/mail/') - >>> U.url - 'http://google.com/mail/' - >>> Url('http', 'username:password', 'host.com', 80, - ... '/path', 'query', 'fragment').url - 'http://username:password@host.com:80/path?query#fragment' - """ - scheme, auth, host, port, path, query, fragment = self - url = '' - - # We use "is not None" we want things to happen with empty strings (or 0 port) - if scheme is not None: - url += scheme + '://' - if auth is not None: - url += auth + '@' - if host is not None: - url += host - if port is not None: - url += ':' + str(port) - if path is not None: - url += path - if query is not None: - url += '?' + query - if fragment is not None: - url += '#' + fragment - - return url - - def __str__(self): - return self.url - - -def split_first(s, delims): - """ - Given a string and an iterable of delimiters, split on the first found - delimiter. Return two split parts and the matched delimiter. - - If not found, then the first part is the full input string. - - Example:: - - >>> split_first('foo/bar?baz', '?/=') - ('foo', 'bar?baz', '/') - >>> split_first('foo/bar?baz', '123') - ('foo/bar?baz', '', None) - - Scales linearly with number of delims. Not ideal for large number of delims. - """ - min_idx = None - min_delim = None - for d in delims: - idx = s.find(d) - if idx < 0: - continue - - if min_idx is None or idx < min_idx: - min_idx = idx - min_delim = d - - if min_idx is None or min_idx < 0: - return s, '', None - - return s[:min_idx], s[min_idx + 1:], min_delim - - -def parse_url(url): - """ - Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is - performed to parse incomplete urls. Fields not provided will be None. - - Partly backwards-compatible with :mod:`urlparse`. - - Example:: - - >>> parse_url('http://google.com/mail/') - Url(scheme='http', host='google.com', port=None, path='/mail/', ...) - >>> parse_url('google.com:80') - Url(scheme=None, host='google.com', port=80, path=None, ...) - >>> parse_url('/foo?bar') - Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) - """ - - # While this code has overlap with stdlib's urlparse, it is much - # simplified for our needs and less annoying. - # Additionally, this implementations does silly things to be optimal - # on CPython. - - if not url: - # Empty - return Url() - - scheme = None - auth = None - host = None - port = None - path = None - fragment = None - query = None - - # Scheme - if '://' in url: - scheme, url = url.split('://', 1) - - # Find the earliest Authority Terminator - # (http://tools.ietf.org/html/rfc3986#section-3.2) - url, path_, delim = split_first(url, ['/', '?', '#']) - - if delim: - # Reassemble the path - path = delim + path_ - - # Auth - if '@' in url: - # Last '@' denotes end of auth part - auth, url = url.rsplit('@', 1) - - # IPv6 - if url and url[0] == '[': - host, url = url.split(']', 1) - host += ']' - - # Port - if ':' in url: - _host, port = url.split(':', 1) - - if not host: - host = _host - - if port: - # If given, ports must be integers. No whitespace, no plus or - # minus prefixes, no non-integer digits such as ^2 (superscript). - if not port.isdigit(): - raise LocationParseError(url) - try: - port = int(port) - except ValueError: - raise LocationParseError(url) - else: - # Blank ports are cool, too. (rfc3986#section-3.2.3) - port = None - - elif not host and url: - host = url - - if not path: - return Url(scheme, auth, host, port, path, query, fragment) - - # Fragment - if '#' in path: - path, fragment = path.split('#', 1) - - # Query - if '?' in path: - path, query = path.split('?', 1) - - return Url(scheme, auth, host, port, path, query, fragment) - - -def get_host(url): - """ - Deprecated. Use :func:`parse_url` instead. - """ - p = parse_url(url) - return p.scheme or 'http', p.hostname, p.port +from __future__ import absolute_import +from collections import namedtuple + +from ..exceptions import LocationParseError + + +url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'] + + +class Url(namedtuple('Url', url_attrs)): + """ + Datastructure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. Both the scheme and host are normalized as they are + both case-insensitive according to RFC 3986. + """ + __slots__ = () + + def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, + query=None, fragment=None): + if path and not path.startswith('/'): + path = '/' + path + if scheme: + scheme = scheme.lower() + if host: + host = host.lower() + return super(Url, cls).__new__(cls, scheme, auth, host, port, path, + query, fragment) + + @property + def hostname(self): + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self): + """Absolute path including the query string.""" + uri = self.path or '/' + + if self.query is not None: + uri += '?' + self.query + + return uri + + @property + def netloc(self): + """Network location including host and port""" + if self.port: + return '%s:%d' % (self.host, self.port) + return self.host + + @property + def url(self): + """ + Convert self into a url + + This function should more or less round-trip with :func:`.parse_url`. The + returned url may not be exactly the same as the url inputted to + :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls + with a blank port will have : removed). + + Example: :: + + >>> U = parse_url('http://google.com/mail/') + >>> U.url + 'http://google.com/mail/' + >>> Url('http', 'username:password', 'host.com', 80, + ... '/path', 'query', 'fragment').url + 'http://username:password@host.com:80/path?query#fragment' + """ + scheme, auth, host, port, path, query, fragment = self + url = '' + + # We use "is not None" we want things to happen with empty strings (or 0 port) + if scheme is not None: + url += scheme + '://' + if auth is not None: + url += auth + '@' + if host is not None: + url += host + if port is not None: + url += ':' + str(port) + if path is not None: + url += path + if query is not None: + url += '?' + query + if fragment is not None: + url += '#' + fragment + + return url + + def __str__(self): + return self.url + + +def split_first(s, delims): + """ + Given a string and an iterable of delimiters, split on the first found + delimiter. Return two split parts and the matched delimiter. + + If not found, then the first part is the full input string. + + Example:: + + >>> split_first('foo/bar?baz', '?/=') + ('foo', 'bar?baz', '/') + >>> split_first('foo/bar?baz', '123') + ('foo/bar?baz', '', None) + + Scales linearly with number of delims. Not ideal for large number of delims. + """ + min_idx = None + min_delim = None + for d in delims: + idx = s.find(d) + if idx < 0: + continue + + if min_idx is None or idx < min_idx: + min_idx = idx + min_delim = d + + if min_idx is None or min_idx < 0: + return s, '', None + + return s[:min_idx], s[min_idx + 1:], min_delim + + +def parse_url(url): + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + + Partly backwards-compatible with :mod:`urlparse`. + + Example:: + + >>> parse_url('http://google.com/mail/') + Url(scheme='http', host='google.com', port=None, path='/mail/', ...) + >>> parse_url('google.com:80') + Url(scheme=None, host='google.com', port=80, path=None, ...) + >>> parse_url('/foo?bar') + Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + + # While this code has overlap with stdlib's urlparse, it is much + # simplified for our needs and less annoying. + # Additionally, this implementations does silly things to be optimal + # on CPython. + + if not url: + # Empty + return Url() + + scheme = None + auth = None + host = None + port = None + path = None + fragment = None + query = None + + # Scheme + if '://' in url: + scheme, url = url.split('://', 1) + + # Find the earliest Authority Terminator + # (http://tools.ietf.org/html/rfc3986#section-3.2) + url, path_, delim = split_first(url, ['/', '?', '#']) + + if delim: + # Reassemble the path + path = delim + path_ + + # Auth + if '@' in url: + # Last '@' denotes end of auth part + auth, url = url.rsplit('@', 1) + + # IPv6 + if url and url[0] == '[': + host, url = url.split(']', 1) + host += ']' + + # Port + if ':' in url: + _host, port = url.split(':', 1) + + if not host: + host = _host + + if port: + # If given, ports must be integers. No whitespace, no plus or + # minus prefixes, no non-integer digits such as ^2 (superscript). + if not port.isdigit(): + raise LocationParseError(url) + try: + port = int(port) + except ValueError: + raise LocationParseError(url) + else: + # Blank ports are cool, too. (rfc3986#section-3.2.3) + port = None + + elif not host and url: + host = url + + if not path: + return Url(scheme, auth, host, port, path, query, fragment) + + # Fragment + if '#' in path: + path, fragment = path.split('#', 1) + + # Query + if '?' in path: + path, query = path.split('?', 1) + + return Url(scheme, auth, host, port, path, query, fragment) + + +def get_host(url): + """ + Deprecated. Use :func:`parse_url` instead. + """ + p = parse_url(url) + return p.scheme or 'http', p.hostname, p.port diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py index 46392f2..cb396e5 100644 --- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py +++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py @@ -1,40 +1,40 @@ -from .selectors import ( - HAS_SELECT, - DefaultSelector, - EVENT_READ, - EVENT_WRITE -) - - -def _wait_for_io_events(socks, events, timeout=None): - """ Waits for IO events to be available from a list of sockets - or optionally a single socket if passed in. Returns a list of - sockets that can be interacted with immediately. """ - if not HAS_SELECT: - raise ValueError('Platform does not have a selector') - if not isinstance(socks, list): - # Probably just a single socket. - if hasattr(socks, "fileno"): - socks = [socks] - # Otherwise it might be a non-list iterable. - else: - socks = list(socks) - with DefaultSelector() as selector: - for sock in socks: - selector.register(sock, events) - return [key[0].fileobj for key in - selector.select(timeout) if key[1] & events] - - -def wait_for_read(socks, timeout=None): - """ Waits for reading to be available from a list of sockets - or optionally a single socket if passed in. Returns a list of - sockets that can be read from immediately. """ - return _wait_for_io_events(socks, EVENT_READ, timeout) - - -def wait_for_write(socks, timeout=None): - """ Waits for writing to be available from a list of sockets - or optionally a single socket if passed in. Returns a list of - sockets that can be written to immediately. """ - return _wait_for_io_events(socks, EVENT_WRITE, timeout) +from .selectors import ( + HAS_SELECT, + DefaultSelector, + EVENT_READ, + EVENT_WRITE +) + + +def _wait_for_io_events(socks, events, timeout=None): + """ Waits for IO events to be available from a list of sockets + or optionally a single socket if passed in. Returns a list of + sockets that can be interacted with immediately. """ + if not HAS_SELECT: + raise ValueError('Platform does not have a selector') + if not isinstance(socks, list): + # Probably just a single socket. + if hasattr(socks, "fileno"): + socks = [socks] + # Otherwise it might be a non-list iterable. + else: + socks = list(socks) + with DefaultSelector() as selector: + for sock in socks: + selector.register(sock, events) + return [key[0].fileobj for key in + selector.select(timeout) if key[1] & events] + + +def wait_for_read(socks, timeout=None): + """ Waits for reading to be available from a list of sockets + or optionally a single socket if passed in. Returns a list of + sockets that can be read from immediately. """ + return _wait_for_io_events(socks, EVENT_READ, timeout) + + +def wait_for_write(socks, timeout=None): + """ Waits for writing to be available from a list of sockets + or optionally a single socket if passed in. Returns a list of + sockets that can be written to immediately. """ + return _wait_for_io_events(socks, EVENT_WRITE, timeout) diff --git a/telegramer/include/telegram/version.py b/telegramer/include/telegram/version.py index eb67f6b..f831eee 100644 --- a/telegramer/include/telegram/version.py +++ b/telegramer/include/telegram/version.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -16,5 +16,9 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=C0114 -__version__ = '11.1.0' +from telegram import constants + +__version__ = '13.11' +bot_api_version = constants.BOT_API_VERSION # pylint: disable=C0103 diff --git a/telegramer/include/telegram/voicechat.py b/telegramer/include/telegram/voicechat.py new file mode 100644 index 0000000..1430f5d --- /dev/null +++ b/telegramer/include/telegram/voicechat.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains objects related to Telegram voice chats.""" + +import datetime as dtm +from typing import TYPE_CHECKING, Any, Optional, List + +from telegram import TelegramObject, User +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class VoiceChatStarted(TelegramObject): + """ + This object represents a service message about a voice + chat started in the chat. Currently holds no information. + + .. versionadded:: 13.4 + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): # skipcq: PTC-W0049 + pass + + +class VoiceChatEnded(TelegramObject): + """ + This object represents a service message about a + voice chat ended in the chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`duration` are equal. + + .. versionadded:: 13.4 + + Args: + duration (:obj:`int`): Voice chat duration in seconds. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + duration (:obj:`int`): Voice chat duration in seconds. + + """ + + __slots__ = ('duration', '_id_attrs') + + def __init__(self, duration: int, **_kwargs: Any) -> None: + self.duration = int(duration) if duration is not None else None + self._id_attrs = (self.duration,) + + +class VoiceChatParticipantsInvited(TelegramObject): + """ + This object represents a service message about + new members invited to a voice chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`users` are equal. + + .. versionadded:: 13.4 + + Args: + users (List[:class:`telegram.User`]): New members that + were invited to the voice chat. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + users (List[:class:`telegram.User`]): New members that + were invited to the voice chat. + + """ + + __slots__ = ('users', '_id_attrs') + + def __init__(self, users: List[User], **_kwargs: Any) -> None: + self.users = users + self._id_attrs = (self.users,) + + def __hash__(self) -> int: + return hash(tuple(self.users)) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: 'Bot' + ) -> Optional['VoiceChatParticipantsInvited']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['users'] = User.de_list(data.get('users', []), bot) + return cls(**data) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + data["users"] = [u.to_dict() for u in self.users] + return data + + +class VoiceChatScheduled(TelegramObject): + """This object represents a service message about a voice chat scheduled in the chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`start_date` are equal. + + Args: + start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the voice + chat is supposed to be started by a chat administrator + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the voice + chat is supposed to be started by a chat administrator + + """ + + __slots__ = ('start_date', '_id_attrs') + + def __init__(self, start_date: dtm.datetime, **_kwargs: Any) -> None: + self.start_date = start_date + + self._id_attrs = (self.start_date,) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['VoiceChatScheduled']: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data['start_date'] = from_timestamp(data['start_date']) + + return cls(**data, bot=bot) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + # Required + data['start_date'] = to_timestamp(self.start_date) + + return data diff --git a/telegramer/include/telegram/webhookinfo.py b/telegramer/include/telegram/webhookinfo.py index 57b9629..1611d6c 100644 --- a/telegramer/include/telegram/webhookinfo.py +++ b/telegramer/include/telegram/webhookinfo.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2018 +# Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify @@ -18,6 +18,8 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram WebhookInfo.""" +from typing import Any, List + from telegram import TelegramObject @@ -26,54 +28,83 @@ class WebhookInfo(TelegramObject): Contains information about the current status of a webhook. - Attributes: - url (:obj:`str`): Webhook URL. - has_custom_certificate (:obj:`bool`): If a custom certificate was provided for webhook. - pending_update_count (:obj:`int`): Number of updates awaiting delivery. - last_error_date (:obj:`int`): Optional. Unix time for the most recent error that happened. - last_error_message (:obj:`str`): Optional. Error message in human-readable format. - max_connections (:obj:`int`): Optional. Maximum allowed number of simultaneous HTTPS - connections. - allowed_updates (List[:obj:`str`]): Optional. A list of update types the bot is subscribed - to. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`url`, :attr:`has_custom_certificate`, + :attr:`pending_update_count`, :attr:`ip_address`, :attr:`last_error_date`, + :attr:`last_error_message`, :attr:`max_connections` and :attr:`allowed_updates` are equal. Args: url (:obj:`str`): Webhook URL, may be empty if webhook is not set up. - has_custom_certificate (:obj:`bool`): True, if a custom certificate was provided for + has_custom_certificate (:obj:`bool`): :obj:`True`, if a custom certificate was provided for webhook certificate checks. pending_update_count (:obj:`int`): Number of updates awaiting delivery. + ip_address (:obj:`str`, optional): Currently used webhook IP address. last_error_date (:obj:`int`, optional): Unix time for the most recent error that happened - when trying todeliver an update via webhook. + when trying to deliver an update via webhook. last_error_message (:obj:`str`, optional): Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook. max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery. allowed_updates (List[:obj:`str`], optional): A list of update types the bot is subscribed - to. Defaults to all update types. + to. Defaults to all update types, except :attr:`telegram.Update.chat_member`. + + Attributes: + url (:obj:`str`): Webhook URL. + has_custom_certificate (:obj:`bool`): If a custom certificate was provided for webhook. + pending_update_count (:obj:`int`): Number of updates awaiting delivery. + ip_address (:obj:`str`): Optional. Currently used webhook IP address. + last_error_date (:obj:`int`): Optional. Unix time for the most recent error that happened. + last_error_message (:obj:`str`): Optional. Error message in human-readable format. + max_connections (:obj:`int`): Optional. Maximum allowed number of simultaneous HTTPS + connections. + allowed_updates (List[:obj:`str`]): Optional. A list of update types the bot is subscribed + to. Defaults to all update types, except :attr:`telegram.Update.chat_member`. """ - def __init__(self, - url, - has_custom_certificate, - pending_update_count, - last_error_date=None, - last_error_message=None, - max_connections=None, - allowed_updates=None, - **kwargs): + __slots__ = ( + 'allowed_updates', + 'url', + 'max_connections', + 'last_error_date', + 'ip_address', + 'last_error_message', + 'pending_update_count', + 'has_custom_certificate', + '_id_attrs', + ) + + def __init__( + self, + url: str, + has_custom_certificate: bool, + pending_update_count: int, + last_error_date: int = None, + last_error_message: str = None, + max_connections: int = None, + allowed_updates: List[str] = None, + ip_address: str = None, + **_kwargs: Any, + ): # Required self.url = url self.has_custom_certificate = has_custom_certificate self.pending_update_count = pending_update_count + + # Optional + self.ip_address = ip_address self.last_error_date = last_error_date self.last_error_message = last_error_message self.max_connections = max_connections self.allowed_updates = allowed_updates - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) + self._id_attrs = ( + self.url, + self.has_custom_certificate, + self.pending_update_count, + self.ip_address, + self.last_error_date, + self.last_error_message, + self.max_connections, + self.allowed_updates, + ) diff --git a/telegramer/include/tornado/__init__.py b/telegramer/include/tornado/__init__.py new file mode 100644 index 0000000..a5f45e5 --- /dev/null +++ b/telegramer/include/tornado/__init__.py @@ -0,0 +1,26 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The Tornado web server and tools.""" + +# version is a human-readable version number. + +# version_info is a four-tuple for programmatic comparison. The first +# three numbers are the components of the version number. The fourth +# is zero for an official release, positive for a development branch, +# or negative for a release candidate or beta (after the base version +# number has been incremented) +version = "6.1" +version_info = (6, 1, 0, 0) diff --git a/telegramer/include/tornado/_locale_data.py b/telegramer/include/tornado/_locale_data.py new file mode 100644 index 0000000..c706230 --- /dev/null +++ b/telegramer/include/tornado/_locale_data.py @@ -0,0 +1,80 @@ +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Data used by the tornado.locale module.""" + +LOCALE_NAMES = { + "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"}, + "am_ET": {"name_en": u"Amharic", "name": u"አማርኛ"}, + "ar_AR": {"name_en": u"Arabic", "name": u"العربية"}, + "bg_BG": {"name_en": u"Bulgarian", "name": u"Български"}, + "bn_IN": {"name_en": u"Bengali", "name": u"বাংলা"}, + "bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"}, + "ca_ES": {"name_en": u"Catalan", "name": u"Català"}, + "cs_CZ": {"name_en": u"Czech", "name": u"Čeština"}, + "cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"}, + "da_DK": {"name_en": u"Danish", "name": u"Dansk"}, + "de_DE": {"name_en": u"German", "name": u"Deutsch"}, + "el_GR": {"name_en": u"Greek", "name": u"Ελληνικά"}, + "en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"}, + "en_US": {"name_en": u"English (US)", "name": u"English (US)"}, + "es_ES": {"name_en": u"Spanish (Spain)", "name": u"Español (España)"}, + "es_LA": {"name_en": u"Spanish", "name": u"Español"}, + "et_EE": {"name_en": u"Estonian", "name": u"Eesti"}, + "eu_ES": {"name_en": u"Basque", "name": u"Euskara"}, + "fa_IR": {"name_en": u"Persian", "name": u"فارسی"}, + "fi_FI": {"name_en": u"Finnish", "name": u"Suomi"}, + "fr_CA": {"name_en": u"French (Canada)", "name": u"Français (Canada)"}, + "fr_FR": {"name_en": u"French", "name": u"Français"}, + "ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"}, + "gl_ES": {"name_en": u"Galician", "name": u"Galego"}, + "he_IL": {"name_en": u"Hebrew", "name": u"עברית"}, + "hi_IN": {"name_en": u"Hindi", "name": u"हिन्दी"}, + "hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"}, + "hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"}, + "id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"}, + "is_IS": {"name_en": u"Icelandic", "name": u"Íslenska"}, + "it_IT": {"name_en": u"Italian", "name": u"Italiano"}, + "ja_JP": {"name_en": u"Japanese", "name": u"日本語"}, + "ko_KR": {"name_en": u"Korean", "name": u"한국어"}, + "lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvių"}, + "lv_LV": {"name_en": u"Latvian", "name": u"Latviešu"}, + "mk_MK": {"name_en": u"Macedonian", "name": u"Македонски"}, + "ml_IN": {"name_en": u"Malayalam", "name": u"മലയാളം"}, + "ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"}, + "nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokmål)"}, + "nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"}, + "nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"}, + "pa_IN": {"name_en": u"Punjabi", "name": u"ਪੰਜਾਬੀ"}, + "pl_PL": {"name_en": u"Polish", "name": u"Polski"}, + "pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Português (Brasil)"}, + "pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Português (Portugal)"}, + "ro_RO": {"name_en": u"Romanian", "name": u"Română"}, + "ru_RU": {"name_en": u"Russian", "name": u"Русский"}, + "sk_SK": {"name_en": u"Slovak", "name": u"Slovenčina"}, + "sl_SI": {"name_en": u"Slovenian", "name": u"Slovenščina"}, + "sq_AL": {"name_en": u"Albanian", "name": u"Shqip"}, + "sr_RS": {"name_en": u"Serbian", "name": u"Српски"}, + "sv_SE": {"name_en": u"Swedish", "name": u"Svenska"}, + "sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"}, + "ta_IN": {"name_en": u"Tamil", "name": u"தமிழ்"}, + "te_IN": {"name_en": u"Telugu", "name": u"తెలుగు"}, + "th_TH": {"name_en": u"Thai", "name": u"ภาษาไทย"}, + "tl_PH": {"name_en": u"Filipino", "name": u"Filipino"}, + "tr_TR": {"name_en": u"Turkish", "name": u"Türkçe"}, + "uk_UA": {"name_en": u"Ukraini ", "name": u"Українська"}, + "vi_VN": {"name_en": u"Vietnamese", "name": u"Tiếng Việt"}, + "zh_CN": {"name_en": u"Chinese (Simplified)", "name": u"中文(简体)"}, + "zh_TW": {"name_en": u"Chinese (Traditional)", "name": u"中文(繁體)"}, +} diff --git a/telegramer/include/tornado/auth.py b/telegramer/include/tornado/auth.py new file mode 100644 index 0000000..5f1068c --- /dev/null +++ b/telegramer/include/tornado/auth.py @@ -0,0 +1,1187 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""This module contains implementations of various third-party +authentication schemes. + +All the classes in this file are class mixins designed to be used with +the `tornado.web.RequestHandler` class. They are used in two ways: + +* On a login handler, use methods such as ``authenticate_redirect()``, + ``authorize_redirect()``, and ``get_authenticated_user()`` to + establish the user's identity and store authentication tokens to your + database and/or cookies. +* In non-login handlers, use methods such as ``facebook_request()`` + or ``twitter_request()`` to use the authentication tokens to make + requests to the respective services. + +They all take slightly different arguments due to the fact all these +services implement authentication and authorization slightly differently. +See the individual service classes below for complete documentation. + +Example usage for Google OAuth: + +.. testcode:: + + class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, + tornado.auth.GoogleOAuth2Mixin): + async def get(self): + if self.get_argument('code', False): + user = await self.get_authenticated_user( + redirect_uri='http://your.site.com/auth/google', + code=self.get_argument('code')) + # Save the user with e.g. set_secure_cookie + else: + self.authorize_redirect( + redirect_uri='http://your.site.com/auth/google', + client_id=self.settings['google_oauth']['key'], + scope=['profile', 'email'], + response_type='code', + extra_params={'approval_prompt': 'auto'}) + +.. testoutput:: + :hide: + +""" + +import base64 +import binascii +import hashlib +import hmac +import time +import urllib.parse +import uuid + +from tornado import httpclient +from tornado import escape +from tornado.httputil import url_concat +from tornado.util import unicode_type +from tornado.web import RequestHandler + +from typing import List, Any, Dict, cast, Iterable, Union, Optional + + +class AuthError(Exception): + pass + + +class OpenIdMixin(object): + """Abstract implementation of OpenID and Attribute Exchange. + + Class attributes: + + * ``_OPENID_ENDPOINT``: the identity provider's URI. + """ + + def authenticate_redirect( + self, + callback_uri: Optional[str] = None, + ax_attrs: List[str] = ["name", "email", "language", "username"], + ) -> None: + """Redirects to the authentication URL for this service. + + After authentication, the service will redirect back to the given + callback URI with additional parameters including ``openid.mode``. + + We request the given attributes for the authenticated user by + default (name, email, language, and username). If you don't need + all those attributes for your app, you can request fewer with + the ax_attrs keyword argument. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed and this method no + longer returns an awaitable object. It is now an ordinary + synchronous function. + """ + handler = cast(RequestHandler, self) + callback_uri = callback_uri or handler.request.uri + assert callback_uri is not None + args = self._openid_args(callback_uri, ax_attrs=ax_attrs) + endpoint = self._OPENID_ENDPOINT # type: ignore + handler.redirect(endpoint + "?" + urllib.parse.urlencode(args)) + + async def get_authenticated_user( + self, http_client: Optional[httpclient.AsyncHTTPClient] = None + ) -> Dict[str, Any]: + """Fetches the authenticated user data upon redirect. + + This method should be called by the handler that receives the + redirect from the `authenticate_redirect()` method (which is + often the same as the one that calls it; in that case you would + call `get_authenticated_user` if the ``openid.mode`` parameter + is present and `authenticate_redirect` if it is not). + + The result of this method will generally be used to set a cookie. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + awaitable object instead. + """ + handler = cast(RequestHandler, self) + # Verify the OpenID response via direct request to the OP + args = dict( + (k, v[-1]) for k, v in handler.request.arguments.items() + ) # type: Dict[str, Union[str, bytes]] + args["openid.mode"] = u"check_authentication" + url = self._OPENID_ENDPOINT # type: ignore + if http_client is None: + http_client = self.get_auth_http_client() + resp = await http_client.fetch( + url, method="POST", body=urllib.parse.urlencode(args) + ) + return self._on_authentication_verified(resp) + + def _openid_args( + self, + callback_uri: str, + ax_attrs: Iterable[str] = [], + oauth_scope: Optional[str] = None, + ) -> Dict[str, str]: + handler = cast(RequestHandler, self) + url = urllib.parse.urljoin(handler.request.full_url(), callback_uri) + args = { + "openid.ns": "http://specs.openid.net/auth/2.0", + "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select", + "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select", + "openid.return_to": url, + "openid.realm": urllib.parse.urljoin(url, "/"), + "openid.mode": "checkid_setup", + } + if ax_attrs: + args.update( + { + "openid.ns.ax": "http://openid.net/srv/ax/1.0", + "openid.ax.mode": "fetch_request", + } + ) + ax_attrs = set(ax_attrs) + required = [] # type: List[str] + if "name" in ax_attrs: + ax_attrs -= set(["name", "firstname", "fullname", "lastname"]) + required += ["firstname", "fullname", "lastname"] + args.update( + { + "openid.ax.type.firstname": "http://axschema.org/namePerson/first", + "openid.ax.type.fullname": "http://axschema.org/namePerson", + "openid.ax.type.lastname": "http://axschema.org/namePerson/last", + } + ) + known_attrs = { + "email": "http://axschema.org/contact/email", + "language": "http://axschema.org/pref/language", + "username": "http://axschema.org/namePerson/friendly", + } + for name in ax_attrs: + args["openid.ax.type." + name] = known_attrs[name] + required.append(name) + args["openid.ax.required"] = ",".join(required) + if oauth_scope: + args.update( + { + "openid.ns.oauth": "http://specs.openid.net/extensions/oauth/1.0", + "openid.oauth.consumer": handler.request.host.split(":")[0], + "openid.oauth.scope": oauth_scope, + } + ) + return args + + def _on_authentication_verified( + self, response: httpclient.HTTPResponse + ) -> Dict[str, Any]: + handler = cast(RequestHandler, self) + if b"is_valid:true" not in response.body: + raise AuthError("Invalid OpenID response: %r" % response.body) + + # Make sure we got back at least an email from attribute exchange + ax_ns = None + for key in handler.request.arguments: + if ( + key.startswith("openid.ns.") + and handler.get_argument(key) == u"http://openid.net/srv/ax/1.0" + ): + ax_ns = key[10:] + break + + def get_ax_arg(uri: str) -> str: + if not ax_ns: + return u"" + prefix = "openid." + ax_ns + ".type." + ax_name = None + for name in handler.request.arguments.keys(): + if handler.get_argument(name) == uri and name.startswith(prefix): + part = name[len(prefix) :] + ax_name = "openid." + ax_ns + ".value." + part + break + if not ax_name: + return u"" + return handler.get_argument(ax_name, u"") + + email = get_ax_arg("http://axschema.org/contact/email") + name = get_ax_arg("http://axschema.org/namePerson") + first_name = get_ax_arg("http://axschema.org/namePerson/first") + last_name = get_ax_arg("http://axschema.org/namePerson/last") + username = get_ax_arg("http://axschema.org/namePerson/friendly") + locale = get_ax_arg("http://axschema.org/pref/language").lower() + user = dict() + name_parts = [] + if first_name: + user["first_name"] = first_name + name_parts.append(first_name) + if last_name: + user["last_name"] = last_name + name_parts.append(last_name) + if name: + user["name"] = name + elif name_parts: + user["name"] = u" ".join(name_parts) + elif email: + user["name"] = email.split("@")[0] + if email: + user["email"] = email + if locale: + user["locale"] = locale + if username: + user["username"] = username + claimed_id = handler.get_argument("openid.claimed_id", None) + if claimed_id: + user["claimed_id"] = claimed_id + return user + + def get_auth_http_client(self) -> httpclient.AsyncHTTPClient: + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. + + May be overridden by subclasses to use an HTTP client other than + the default. + """ + return httpclient.AsyncHTTPClient() + + +class OAuthMixin(object): + """Abstract implementation of OAuth 1.0 and 1.0a. + + See `TwitterMixin` below for an example implementation. + + Class attributes: + + * ``_OAUTH_AUTHORIZE_URL``: The service's OAuth authorization url. + * ``_OAUTH_ACCESS_TOKEN_URL``: The service's OAuth access token url. + * ``_OAUTH_VERSION``: May be either "1.0" or "1.0a". + * ``_OAUTH_NO_CALLBACKS``: Set this to True if the service requires + advance registration of callbacks. + + Subclasses must also override the `_oauth_get_user_future` and + `_oauth_consumer_token` methods. + """ + + async def authorize_redirect( + self, + callback_uri: Optional[str] = None, + extra_params: Optional[Dict[str, Any]] = None, + http_client: Optional[httpclient.AsyncHTTPClient] = None, + ) -> None: + """Redirects the user to obtain OAuth authorization for this service. + + The ``callback_uri`` may be omitted if you have previously + registered a callback URI with the third-party service. For + some services, you must use a previously-registered callback + URI and cannot specify a callback via this method. + + This method sets a cookie called ``_oauth_request_token`` which is + subsequently used (and cleared) in `get_authenticated_user` for + security purposes. + + This method is asynchronous and must be called with ``await`` + or ``yield`` (This is different from other ``auth*_redirect`` + methods defined in this module). It calls + `.RequestHandler.finish` for you so you should not write any + other response after it returns. + + .. versionchanged:: 3.1 + Now returns a `.Future` and takes an optional callback, for + compatibility with `.gen.coroutine`. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + awaitable object instead. + + """ + if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False): + raise Exception("This service does not support oauth_callback") + if http_client is None: + http_client = self.get_auth_http_client() + assert http_client is not None + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + response = await http_client.fetch( + self._oauth_request_token_url( + callback_uri=callback_uri, extra_params=extra_params + ) + ) + else: + response = await http_client.fetch(self._oauth_request_token_url()) + url = self._OAUTH_AUTHORIZE_URL # type: ignore + self._on_request_token(url, callback_uri, response) + + async def get_authenticated_user( + self, http_client: Optional[httpclient.AsyncHTTPClient] = None + ) -> Dict[str, Any]: + """Gets the OAuth authorized user and access token. + + This method should be called from the handler for your + OAuth callback URL to complete the registration process. We run the + callback with the authenticated user dictionary. This dictionary + will contain an ``access_key`` which can be used to make authorized + requests to this service on behalf of the user. The dictionary will + also contain other fields such as ``name``, depending on the service + used. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + awaitable object instead. + """ + handler = cast(RequestHandler, self) + request_key = escape.utf8(handler.get_argument("oauth_token")) + oauth_verifier = handler.get_argument("oauth_verifier", None) + request_cookie = handler.get_cookie("_oauth_request_token") + if not request_cookie: + raise AuthError("Missing OAuth request token cookie") + handler.clear_cookie("_oauth_request_token") + cookie_key, cookie_secret = [ + base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|") + ] + if cookie_key != request_key: + raise AuthError("Request token does not match cookie") + token = dict( + key=cookie_key, secret=cookie_secret + ) # type: Dict[str, Union[str, bytes]] + if oauth_verifier: + token["verifier"] = oauth_verifier + if http_client is None: + http_client = self.get_auth_http_client() + assert http_client is not None + response = await http_client.fetch(self._oauth_access_token_url(token)) + access_token = _oauth_parse_response(response.body) + user = await self._oauth_get_user_future(access_token) + if not user: + raise AuthError("Error getting user") + user["access_token"] = access_token + return user + + def _oauth_request_token_url( + self, + callback_uri: Optional[str] = None, + extra_params: Optional[Dict[str, Any]] = None, + ) -> str: + handler = cast(RequestHandler, self) + consumer_token = self._oauth_consumer_token() + url = self._OAUTH_REQUEST_TOKEN_URL # type: ignore + args = dict( + oauth_consumer_key=escape.to_basestring(consumer_token["key"]), + oauth_signature_method="HMAC-SHA1", + oauth_timestamp=str(int(time.time())), + oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), + oauth_version="1.0", + ) + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + if callback_uri == "oob": + args["oauth_callback"] = "oob" + elif callback_uri: + args["oauth_callback"] = urllib.parse.urljoin( + handler.request.full_url(), callback_uri + ) + if extra_params: + args.update(extra_params) + signature = _oauth10a_signature(consumer_token, "GET", url, args) + else: + signature = _oauth_signature(consumer_token, "GET", url, args) + + args["oauth_signature"] = signature + return url + "?" + urllib.parse.urlencode(args) + + def _on_request_token( + self, + authorize_url: str, + callback_uri: Optional[str], + response: httpclient.HTTPResponse, + ) -> None: + handler = cast(RequestHandler, self) + request_token = _oauth_parse_response(response.body) + data = ( + base64.b64encode(escape.utf8(request_token["key"])) + + b"|" + + base64.b64encode(escape.utf8(request_token["secret"])) + ) + handler.set_cookie("_oauth_request_token", data) + args = dict(oauth_token=request_token["key"]) + if callback_uri == "oob": + handler.finish(authorize_url + "?" + urllib.parse.urlencode(args)) + return + elif callback_uri: + args["oauth_callback"] = urllib.parse.urljoin( + handler.request.full_url(), callback_uri + ) + handler.redirect(authorize_url + "?" + urllib.parse.urlencode(args)) + + def _oauth_access_token_url(self, request_token: Dict[str, Any]) -> str: + consumer_token = self._oauth_consumer_token() + url = self._OAUTH_ACCESS_TOKEN_URL # type: ignore + args = dict( + oauth_consumer_key=escape.to_basestring(consumer_token["key"]), + oauth_token=escape.to_basestring(request_token["key"]), + oauth_signature_method="HMAC-SHA1", + oauth_timestamp=str(int(time.time())), + oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), + oauth_version="1.0", + ) + if "verifier" in request_token: + args["oauth_verifier"] = request_token["verifier"] + + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + signature = _oauth10a_signature( + consumer_token, "GET", url, args, request_token + ) + else: + signature = _oauth_signature( + consumer_token, "GET", url, args, request_token + ) + + args["oauth_signature"] = signature + return url + "?" + urllib.parse.urlencode(args) + + def _oauth_consumer_token(self) -> Dict[str, Any]: + """Subclasses must override this to return their OAuth consumer keys. + + The return value should be a `dict` with keys ``key`` and ``secret``. + """ + raise NotImplementedError() + + async def _oauth_get_user_future( + self, access_token: Dict[str, Any] + ) -> Dict[str, Any]: + """Subclasses must override this to get basic information about the + user. + + Should be a coroutine whose result is a dictionary + containing information about the user, which may have been + retrieved by using ``access_token`` to make a request to the + service. + + The access token will be added to the returned dictionary to make + the result of `get_authenticated_user`. + + .. versionchanged:: 5.1 + + Subclasses may also define this method with ``async def``. + + .. versionchanged:: 6.0 + + A synchronous fallback to ``_oauth_get_user`` was removed. + """ + raise NotImplementedError() + + def _oauth_request_parameters( + self, + url: str, + access_token: Dict[str, Any], + parameters: Dict[str, Any] = {}, + method: str = "GET", + ) -> Dict[str, Any]: + """Returns the OAuth parameters as a dict for the given request. + + parameters should include all POST arguments and query string arguments + that will be sent with the request. + """ + consumer_token = self._oauth_consumer_token() + base_args = dict( + oauth_consumer_key=escape.to_basestring(consumer_token["key"]), + oauth_token=escape.to_basestring(access_token["key"]), + oauth_signature_method="HMAC-SHA1", + oauth_timestamp=str(int(time.time())), + oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), + oauth_version="1.0", + ) + args = {} + args.update(base_args) + args.update(parameters) + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + signature = _oauth10a_signature( + consumer_token, method, url, args, access_token + ) + else: + signature = _oauth_signature( + consumer_token, method, url, args, access_token + ) + base_args["oauth_signature"] = escape.to_basestring(signature) + return base_args + + def get_auth_http_client(self) -> httpclient.AsyncHTTPClient: + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. + + May be overridden by subclasses to use an HTTP client other than + the default. + """ + return httpclient.AsyncHTTPClient() + + +class OAuth2Mixin(object): + """Abstract implementation of OAuth 2.0. + + See `FacebookGraphMixin` or `GoogleOAuth2Mixin` below for example + implementations. + + Class attributes: + + * ``_OAUTH_AUTHORIZE_URL``: The service's authorization url. + * ``_OAUTH_ACCESS_TOKEN_URL``: The service's access token url. + """ + + def authorize_redirect( + self, + redirect_uri: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + extra_params: Optional[Dict[str, Any]] = None, + scope: Optional[List[str]] = None, + response_type: str = "code", + ) -> None: + """Redirects the user to obtain OAuth authorization for this service. + + Some providers require that you register a redirect URL with + your application instead of passing one via this method. You + should call this method to log the user in, and then call + ``get_authenticated_user`` in the handler for your + redirect URL to complete the authorization process. + + .. versionchanged:: 6.0 + + The ``callback`` argument and returned awaitable were removed; + this is now an ordinary synchronous function. + """ + handler = cast(RequestHandler, self) + args = {"response_type": response_type} + if redirect_uri is not None: + args["redirect_uri"] = redirect_uri + if client_id is not None: + args["client_id"] = client_id + if extra_params: + args.update(extra_params) + if scope: + args["scope"] = " ".join(scope) + url = self._OAUTH_AUTHORIZE_URL # type: ignore + handler.redirect(url_concat(url, args)) + + def _oauth_request_token_url( + self, + redirect_uri: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + code: Optional[str] = None, + extra_params: Optional[Dict[str, Any]] = None, + ) -> str: + url = self._OAUTH_ACCESS_TOKEN_URL # type: ignore + args = {} # type: Dict[str, str] + if redirect_uri is not None: + args["redirect_uri"] = redirect_uri + if code is not None: + args["code"] = code + if client_id is not None: + args["client_id"] = client_id + if client_secret is not None: + args["client_secret"] = client_secret + if extra_params: + args.update(extra_params) + return url_concat(url, args) + + async def oauth2_request( + self, + url: str, + access_token: Optional[str] = None, + post_args: Optional[Dict[str, Any]] = None, + **args: Any + ) -> Any: + """Fetches the given URL auth an OAuth2 access token. + + If the request is a POST, ``post_args`` should be provided. Query + string arguments should be given as keyword arguments. + + Example usage: + + ..testcode:: + + class MainHandler(tornado.web.RequestHandler, + tornado.auth.FacebookGraphMixin): + @tornado.web.authenticated + async def get(self): + new_entry = await self.oauth2_request( + "https://graph.facebook.com/me/feed", + post_args={"message": "I am posting from my Tornado application!"}, + access_token=self.current_user["access_token"]) + + if not new_entry: + # Call failed; perhaps missing permission? + self.authorize_redirect() + return + self.finish("Posted a message!") + + .. testoutput:: + :hide: + + .. versionadded:: 4.3 + + .. versionchanged::: 6.0 + + The ``callback`` argument was removed. Use the returned awaitable object instead. + """ + all_args = {} + if access_token: + all_args["access_token"] = access_token + all_args.update(args) + + if all_args: + url += "?" + urllib.parse.urlencode(all_args) + http = self.get_auth_http_client() + if post_args is not None: + response = await http.fetch( + url, method="POST", body=urllib.parse.urlencode(post_args) + ) + else: + response = await http.fetch(url) + return escape.json_decode(response.body) + + def get_auth_http_client(self) -> httpclient.AsyncHTTPClient: + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. + + May be overridden by subclasses to use an HTTP client other than + the default. + + .. versionadded:: 4.3 + """ + return httpclient.AsyncHTTPClient() + + +class TwitterMixin(OAuthMixin): + """Twitter OAuth authentication. + + To authenticate with Twitter, register your application with + Twitter at http://twitter.com/apps. Then copy your Consumer Key + and Consumer Secret to the application + `~tornado.web.Application.settings` ``twitter_consumer_key`` and + ``twitter_consumer_secret``. Use this mixin on the handler for the + URL you registered as your application's callback URL. + + When your application is set up, you can use this mixin like this + to authenticate the user with Twitter and get access to their stream: + + .. testcode:: + + class TwitterLoginHandler(tornado.web.RequestHandler, + tornado.auth.TwitterMixin): + async def get(self): + if self.get_argument("oauth_token", None): + user = await self.get_authenticated_user() + # Save the user using e.g. set_secure_cookie() + else: + await self.authorize_redirect() + + .. testoutput:: + :hide: + + The user object returned by `~OAuthMixin.get_authenticated_user` + includes the attributes ``username``, ``name``, ``access_token``, + and all of the custom Twitter user attributes described at + https://dev.twitter.com/docs/api/1.1/get/users/show + """ + + _OAUTH_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token" + _OAUTH_ACCESS_TOKEN_URL = "https://api.twitter.com/oauth/access_token" + _OAUTH_AUTHORIZE_URL = "https://api.twitter.com/oauth/authorize" + _OAUTH_AUTHENTICATE_URL = "https://api.twitter.com/oauth/authenticate" + _OAUTH_NO_CALLBACKS = False + _TWITTER_BASE_URL = "https://api.twitter.com/1.1" + + async def authenticate_redirect(self, callback_uri: Optional[str] = None) -> None: + """Just like `~OAuthMixin.authorize_redirect`, but + auto-redirects if authorized. + + This is generally the right interface to use if you are using + Twitter for single-sign on. + + .. versionchanged:: 3.1 + Now returns a `.Future` and takes an optional callback, for + compatibility with `.gen.coroutine`. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + awaitable object instead. + """ + http = self.get_auth_http_client() + response = await http.fetch( + self._oauth_request_token_url(callback_uri=callback_uri) + ) + self._on_request_token(self._OAUTH_AUTHENTICATE_URL, None, response) + + async def twitter_request( + self, + path: str, + access_token: Dict[str, Any], + post_args: Optional[Dict[str, Any]] = None, + **args: Any + ) -> Any: + """Fetches the given API path, e.g., ``statuses/user_timeline/btaylor`` + + The path should not include the format or API version number. + (we automatically use JSON format and API version 1). + + If the request is a POST, ``post_args`` should be provided. Query + string arguments should be given as keyword arguments. + + All the Twitter methods are documented at http://dev.twitter.com/ + + Many methods require an OAuth access token which you can + obtain through `~OAuthMixin.authorize_redirect` and + `~OAuthMixin.get_authenticated_user`. The user returned through that + process includes an 'access_token' attribute that can be used + to make authenticated requests via this method. Example + usage: + + .. testcode:: + + class MainHandler(tornado.web.RequestHandler, + tornado.auth.TwitterMixin): + @tornado.web.authenticated + async def get(self): + new_entry = await self.twitter_request( + "/statuses/update", + post_args={"status": "Testing Tornado Web Server"}, + access_token=self.current_user["access_token"]) + if not new_entry: + # Call failed; perhaps missing permission? + await self.authorize_redirect() + return + self.finish("Posted a message!") + + .. testoutput:: + :hide: + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + awaitable object instead. + """ + if path.startswith("http:") or path.startswith("https:"): + # Raw urls are useful for e.g. search which doesn't follow the + # usual pattern: http://search.twitter.com/search.json + url = path + else: + url = self._TWITTER_BASE_URL + path + ".json" + # Add the OAuth resource request signature if we have credentials + if access_token: + all_args = {} + all_args.update(args) + all_args.update(post_args or {}) + method = "POST" if post_args is not None else "GET" + oauth = self._oauth_request_parameters( + url, access_token, all_args, method=method + ) + args.update(oauth) + if args: + url += "?" + urllib.parse.urlencode(args) + http = self.get_auth_http_client() + if post_args is not None: + response = await http.fetch( + url, method="POST", body=urllib.parse.urlencode(post_args) + ) + else: + response = await http.fetch(url) + return escape.json_decode(response.body) + + def _oauth_consumer_token(self) -> Dict[str, Any]: + handler = cast(RequestHandler, self) + handler.require_setting("twitter_consumer_key", "Twitter OAuth") + handler.require_setting("twitter_consumer_secret", "Twitter OAuth") + return dict( + key=handler.settings["twitter_consumer_key"], + secret=handler.settings["twitter_consumer_secret"], + ) + + async def _oauth_get_user_future( + self, access_token: Dict[str, Any] + ) -> Dict[str, Any]: + user = await self.twitter_request( + "/account/verify_credentials", access_token=access_token + ) + if user: + user["username"] = user["screen_name"] + return user + + +class GoogleOAuth2Mixin(OAuth2Mixin): + """Google authentication using OAuth2. + + In order to use, register your application with Google and copy the + relevant parameters to your application settings. + + * Go to the Google Dev Console at http://console.developers.google.com + * Select a project, or create a new one. + * In the sidebar on the left, select APIs & Auth. + * In the list of APIs, find the Google+ API service and set it to ON. + * In the sidebar on the left, select Credentials. + * In the OAuth section of the page, select Create New Client ID. + * Set the Redirect URI to point to your auth handler + * Copy the "Client secret" and "Client ID" to the application settings as + ``{"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}}`` + + .. versionadded:: 3.2 + """ + + _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth" + _OAUTH_ACCESS_TOKEN_URL = "https://www.googleapis.com/oauth2/v4/token" + _OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo" + _OAUTH_NO_CALLBACKS = False + _OAUTH_SETTINGS_KEY = "google_oauth" + + async def get_authenticated_user( + self, redirect_uri: str, code: str + ) -> Dict[str, Any]: + """Handles the login for the Google user, returning an access token. + + The result is a dictionary containing an ``access_token`` field + ([among others](https://developers.google.com/identity/protocols/OAuth2WebServer#handlingtheresponse)). + Unlike other ``get_authenticated_user`` methods in this package, + this method does not return any additional information about the user. + The returned access token can be used with `OAuth2Mixin.oauth2_request` + to request additional information (perhaps from + ``https://www.googleapis.com/oauth2/v2/userinfo``) + + Example usage: + + .. testcode:: + + class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, + tornado.auth.GoogleOAuth2Mixin): + async def get(self): + if self.get_argument('code', False): + access = await self.get_authenticated_user( + redirect_uri='http://your.site.com/auth/google', + code=self.get_argument('code')) + user = await self.oauth2_request( + "https://www.googleapis.com/oauth2/v1/userinfo", + access_token=access["access_token"]) + # Save the user and access token with + # e.g. set_secure_cookie. + else: + self.authorize_redirect( + redirect_uri='http://your.site.com/auth/google', + client_id=self.settings['google_oauth']['key'], + scope=['profile', 'email'], + response_type='code', + extra_params={'approval_prompt': 'auto'}) + + .. testoutput:: + :hide: + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned awaitable object instead. + """ # noqa: E501 + handler = cast(RequestHandler, self) + http = self.get_auth_http_client() + body = urllib.parse.urlencode( + { + "redirect_uri": redirect_uri, + "code": code, + "client_id": handler.settings[self._OAUTH_SETTINGS_KEY]["key"], + "client_secret": handler.settings[self._OAUTH_SETTINGS_KEY]["secret"], + "grant_type": "authorization_code", + } + ) + + response = await http.fetch( + self._OAUTH_ACCESS_TOKEN_URL, + method="POST", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + body=body, + ) + return escape.json_decode(response.body) + + +class FacebookGraphMixin(OAuth2Mixin): + """Facebook authentication using the new Graph API and OAuth2.""" + + _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?" + _OAUTH_AUTHORIZE_URL = "https://www.facebook.com/dialog/oauth?" + _OAUTH_NO_CALLBACKS = False + _FACEBOOK_BASE_URL = "https://graph.facebook.com" + + async def get_authenticated_user( + self, + redirect_uri: str, + client_id: str, + client_secret: str, + code: str, + extra_fields: Optional[Dict[str, Any]] = None, + ) -> Optional[Dict[str, Any]]: + """Handles the login for the Facebook user, returning a user object. + + Example usage: + + .. testcode:: + + class FacebookGraphLoginHandler(tornado.web.RequestHandler, + tornado.auth.FacebookGraphMixin): + async def get(self): + if self.get_argument("code", False): + user = await self.get_authenticated_user( + redirect_uri='/auth/facebookgraph/', + client_id=self.settings["facebook_api_key"], + client_secret=self.settings["facebook_secret"], + code=self.get_argument("code")) + # Save the user with e.g. set_secure_cookie + else: + self.authorize_redirect( + redirect_uri='/auth/facebookgraph/', + client_id=self.settings["facebook_api_key"], + extra_params={"scope": "read_stream,offline_access"}) + + .. testoutput:: + :hide: + + This method returns a dictionary which may contain the following fields: + + * ``access_token``, a string which may be passed to `facebook_request` + * ``session_expires``, an integer encoded as a string representing + the time until the access token expires in seconds. This field should + be used like ``int(user['session_expires'])``; in a future version of + Tornado it will change from a string to an integer. + * ``id``, ``name``, ``first_name``, ``last_name``, ``locale``, ``picture``, + ``link``, plus any fields named in the ``extra_fields`` argument. These + fields are copied from the Facebook graph API + `user object `_ + + .. versionchanged:: 4.5 + The ``session_expires`` field was updated to support changes made to the + Facebook API in March 2017. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned awaitable object instead. + """ + http = self.get_auth_http_client() + args = { + "redirect_uri": redirect_uri, + "code": code, + "client_id": client_id, + "client_secret": client_secret, + } + + fields = set( + ["id", "name", "first_name", "last_name", "locale", "picture", "link"] + ) + if extra_fields: + fields.update(extra_fields) + + response = await http.fetch( + self._oauth_request_token_url(**args) # type: ignore + ) + args = escape.json_decode(response.body) + session = { + "access_token": args.get("access_token"), + "expires_in": args.get("expires_in"), + } + assert session["access_token"] is not None + + user = await self.facebook_request( + path="/me", + access_token=session["access_token"], + appsecret_proof=hmac.new( + key=client_secret.encode("utf8"), + msg=session["access_token"].encode("utf8"), + digestmod=hashlib.sha256, + ).hexdigest(), + fields=",".join(fields), + ) + + if user is None: + return None + + fieldmap = {} + for field in fields: + fieldmap[field] = user.get(field) + + # session_expires is converted to str for compatibility with + # older versions in which the server used url-encoding and + # this code simply returned the string verbatim. + # This should change in Tornado 5.0. + fieldmap.update( + { + "access_token": session["access_token"], + "session_expires": str(session.get("expires_in")), + } + ) + return fieldmap + + async def facebook_request( + self, + path: str, + access_token: Optional[str] = None, + post_args: Optional[Dict[str, Any]] = None, + **args: Any + ) -> Any: + """Fetches the given relative API path, e.g., "/btaylor/picture" + + If the request is a POST, ``post_args`` should be provided. Query + string arguments should be given as keyword arguments. + + An introduction to the Facebook Graph API can be found at + http://developers.facebook.com/docs/api + + Many methods require an OAuth access token which you can + obtain through `~OAuth2Mixin.authorize_redirect` and + `get_authenticated_user`. The user returned through that + process includes an ``access_token`` attribute that can be + used to make authenticated requests via this method. + + Example usage: + + .. testcode:: + + class MainHandler(tornado.web.RequestHandler, + tornado.auth.FacebookGraphMixin): + @tornado.web.authenticated + async def get(self): + new_entry = await self.facebook_request( + "/me/feed", + post_args={"message": "I am posting from my Tornado application!"}, + access_token=self.current_user["access_token"]) + + if not new_entry: + # Call failed; perhaps missing permission? + self.authorize_redirect() + return + self.finish("Posted a message!") + + .. testoutput:: + :hide: + + The given path is relative to ``self._FACEBOOK_BASE_URL``, + by default "https://graph.facebook.com". + + This method is a wrapper around `OAuth2Mixin.oauth2_request`; + the only difference is that this method takes a relative path, + while ``oauth2_request`` takes a complete url. + + .. versionchanged:: 3.1 + Added the ability to override ``self._FACEBOOK_BASE_URL``. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned awaitable object instead. + """ + url = self._FACEBOOK_BASE_URL + path + return await self.oauth2_request( + url, access_token=access_token, post_args=post_args, **args + ) + + +def _oauth_signature( + consumer_token: Dict[str, Any], + method: str, + url: str, + parameters: Dict[str, Any] = {}, + token: Optional[Dict[str, Any]] = None, +) -> bytes: + """Calculates the HMAC-SHA1 OAuth signature for the given request. + + See http://oauth.net/core/1.0/#signing_process + """ + parts = urllib.parse.urlparse(url) + scheme, netloc, path = parts[:3] + normalized_url = scheme.lower() + "://" + netloc.lower() + path + + base_elems = [] + base_elems.append(method.upper()) + base_elems.append(normalized_url) + base_elems.append( + "&".join( + "%s=%s" % (k, _oauth_escape(str(v))) for k, v in sorted(parameters.items()) + ) + ) + base_string = "&".join(_oauth_escape(e) for e in base_elems) + + key_elems = [escape.utf8(consumer_token["secret"])] + key_elems.append(escape.utf8(token["secret"] if token else "")) + key = b"&".join(key_elems) + + hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1) + return binascii.b2a_base64(hash.digest())[:-1] + + +def _oauth10a_signature( + consumer_token: Dict[str, Any], + method: str, + url: str, + parameters: Dict[str, Any] = {}, + token: Optional[Dict[str, Any]] = None, +) -> bytes: + """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request. + + See http://oauth.net/core/1.0a/#signing_process + """ + parts = urllib.parse.urlparse(url) + scheme, netloc, path = parts[:3] + normalized_url = scheme.lower() + "://" + netloc.lower() + path + + base_elems = [] + base_elems.append(method.upper()) + base_elems.append(normalized_url) + base_elems.append( + "&".join( + "%s=%s" % (k, _oauth_escape(str(v))) for k, v in sorted(parameters.items()) + ) + ) + + base_string = "&".join(_oauth_escape(e) for e in base_elems) + key_elems = [escape.utf8(urllib.parse.quote(consumer_token["secret"], safe="~"))] + key_elems.append( + escape.utf8(urllib.parse.quote(token["secret"], safe="~") if token else "") + ) + key = b"&".join(key_elems) + + hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1) + return binascii.b2a_base64(hash.digest())[:-1] + + +def _oauth_escape(val: Union[str, bytes]) -> str: + if isinstance(val, unicode_type): + val = val.encode("utf-8") + return urllib.parse.quote(val, safe="~") + + +def _oauth_parse_response(body: bytes) -> Dict[str, Any]: + # I can't find an officially-defined encoding for oauth responses and + # have never seen anyone use non-ascii. Leave the response in a byte + # string for python 2, and use utf8 on python 3. + body_str = escape.native_str(body) + p = urllib.parse.parse_qs(body_str, keep_blank_values=False) + token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0]) + + # Add the extra parameters the Provider included to the token + special = ("oauth_token", "oauth_token_secret") + token.update((k, p[k][0]) for k in p if k not in special) + return token diff --git a/telegramer/include/tornado/autoreload.py b/telegramer/include/tornado/autoreload.py new file mode 100644 index 0000000..3299a3b --- /dev/null +++ b/telegramer/include/tornado/autoreload.py @@ -0,0 +1,363 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Automatically restart the server when a source file is modified. + +Most applications should not access this module directly. Instead, +pass the keyword argument ``autoreload=True`` to the +`tornado.web.Application` constructor (or ``debug=True``, which +enables this setting and several others). This will enable autoreload +mode as well as checking for changes to templates and static +resources. Note that restarting is a destructive operation and any +requests in progress will be aborted when the process restarts. (If +you want to disable autoreload while using other debug-mode features, +pass both ``debug=True`` and ``autoreload=False``). + +This module can also be used as a command-line wrapper around scripts +such as unit test runners. See the `main` method for details. + +The command-line wrapper and Application debug modes can be used together. +This combination is encouraged as the wrapper catches syntax errors and +other import-time failures, while debug mode catches changes once +the server has started. + +This module will not work correctly when `.HTTPServer`'s multi-process +mode is used. + +Reloading loses any Python interpreter command-line arguments (e.g. ``-u``) +because it re-executes Python using ``sys.executable`` and ``sys.argv``. +Additionally, modifying these variables will cause reloading to behave +incorrectly. + +""" + +import os +import sys + +# sys.path handling +# ----------------- +# +# If a module is run with "python -m", the current directory (i.e. "") +# is automatically prepended to sys.path, but not if it is run as +# "path/to/file.py". The processing for "-m" rewrites the former to +# the latter, so subsequent executions won't have the same path as the +# original. +# +# Conversely, when run as path/to/file.py, the directory containing +# file.py gets added to the path, which can cause confusion as imports +# may become relative in spite of the future import. +# +# We address the former problem by reconstructing the original command +# line (Python >= 3.4) or by setting the $PYTHONPATH environment +# variable (Python < 3.4) before re-execution so the new process will +# see the correct path. We attempt to address the latter problem when +# tornado.autoreload is run as __main__. + +if __name__ == "__main__": + # This sys.path manipulation must come before our imports (as much + # as possible - if we introduced a tornado.sys or tornado.os + # module we'd be in trouble), or else our imports would become + # relative again despite the future import. + # + # There is a separate __main__ block at the end of the file to call main(). + if sys.path[0] == os.path.dirname(__file__): + del sys.path[0] + +import functools +import logging +import os +import pkgutil # type: ignore +import sys +import traceback +import types +import subprocess +import weakref + +from tornado import ioloop +from tornado.log import gen_log +from tornado import process +from tornado.util import exec_in + +try: + import signal +except ImportError: + signal = None # type: ignore + +import typing +from typing import Callable, Dict + +if typing.TYPE_CHECKING: + from typing import List, Optional, Union # noqa: F401 + +# os.execv is broken on Windows and can't properly parse command line +# arguments and executable name if they contain whitespaces. subprocess +# fixes that behavior. +_has_execv = sys.platform != "win32" + +_watched_files = set() +_reload_hooks = [] +_reload_attempted = False +_io_loops = weakref.WeakKeyDictionary() # type: ignore +_autoreload_is_main = False +_original_argv = None # type: Optional[List[str]] +_original_spec = None + + +def start(check_time: int = 500) -> None: + """Begins watching source files for changes. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + """ + io_loop = ioloop.IOLoop.current() + if io_loop in _io_loops: + return + _io_loops[io_loop] = True + if len(_io_loops) > 1: + gen_log.warning("tornado.autoreload started more than once in the same process") + modify_times = {} # type: Dict[str, float] + callback = functools.partial(_reload_on_update, modify_times) + scheduler = ioloop.PeriodicCallback(callback, check_time) + scheduler.start() + + +def wait() -> None: + """Wait for a watched file to change, then restart the process. + + Intended to be used at the end of scripts like unit test runners, + to run the tests again after any source file changes (but see also + the command-line interface in `main`) + """ + io_loop = ioloop.IOLoop() + io_loop.add_callback(start) + io_loop.start() + + +def watch(filename: str) -> None: + """Add a file to the watch list. + + All imported modules are watched by default. + """ + _watched_files.add(filename) + + +def add_reload_hook(fn: Callable[[], None]) -> None: + """Add a function to be called before reloading the process. + + Note that for open file and socket handles it is generally + preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or + `os.set_inheritable`) instead of using a reload hook to close them. + """ + _reload_hooks.append(fn) + + +def _reload_on_update(modify_times: Dict[str, float]) -> None: + if _reload_attempted: + # We already tried to reload and it didn't work, so don't try again. + return + if process.task_id() is not None: + # We're in a child process created by fork_processes. If child + # processes restarted themselves, they'd all restart and then + # all call fork_processes again. + return + for module in list(sys.modules.values()): + # Some modules play games with sys.modules (e.g. email/__init__.py + # in the standard library), and occasionally this can cause strange + # failures in getattr. Just ignore anything that's not an ordinary + # module. + if not isinstance(module, types.ModuleType): + continue + path = getattr(module, "__file__", None) + if not path: + continue + if path.endswith(".pyc") or path.endswith(".pyo"): + path = path[:-1] + _check_file(modify_times, path) + for path in _watched_files: + _check_file(modify_times, path) + + +def _check_file(modify_times: Dict[str, float], path: str) -> None: + try: + modified = os.stat(path).st_mtime + except Exception: + return + if path not in modify_times: + modify_times[path] = modified + return + if modify_times[path] != modified: + gen_log.info("%s modified; restarting server", path) + _reload() + + +def _reload() -> None: + global _reload_attempted + _reload_attempted = True + for fn in _reload_hooks: + fn() + if hasattr(signal, "setitimer"): + # Clear the alarm signal set by + # ioloop.set_blocking_log_threshold so it doesn't fire + # after the exec. + signal.setitimer(signal.ITIMER_REAL, 0, 0) + # sys.path fixes: see comments at top of file. If __main__.__spec__ + # exists, we were invoked with -m and the effective path is about to + # change on re-exec. Reconstruct the original command line to + # ensure that the new process sees the same path we did. If + # __spec__ is not available (Python < 3.4), check instead if + # sys.path[0] is an empty string and add the current directory to + # $PYTHONPATH. + if _autoreload_is_main: + assert _original_argv is not None + spec = _original_spec + argv = _original_argv + else: + spec = getattr(sys.modules["__main__"], "__spec__", None) + argv = sys.argv + if spec: + argv = ["-m", spec.name] + argv[1:] + else: + path_prefix = "." + os.pathsep + if sys.path[0] == "" and not os.environ.get("PYTHONPATH", "").startswith( + path_prefix + ): + os.environ["PYTHONPATH"] = path_prefix + os.environ.get("PYTHONPATH", "") + if not _has_execv: + subprocess.Popen([sys.executable] + argv) + os._exit(0) + else: + try: + os.execv(sys.executable, [sys.executable] + argv) + except OSError: + # Mac OS X versions prior to 10.6 do not support execv in + # a process that contains multiple threads. Instead of + # re-executing in the current process, start a new one + # and cause the current process to exit. This isn't + # ideal since the new process is detached from the parent + # terminal and thus cannot easily be killed with ctrl-C, + # but it's better than not being able to autoreload at + # all. + # Unfortunately the errno returned in this case does not + # appear to be consistent, so we can't easily check for + # this error specifically. + os.spawnv( + os.P_NOWAIT, sys.executable, [sys.executable] + argv # type: ignore + ) + # At this point the IOLoop has been closed and finally + # blocks will experience errors if we allow the stack to + # unwind, so just exit uncleanly. + os._exit(0) + + +_USAGE = """\ +Usage: + python -m tornado.autoreload -m module.to.run [args...] + python -m tornado.autoreload path/to/script.py [args...] +""" + + +def main() -> None: + """Command-line wrapper to re-run a script whenever its source changes. + + Scripts may be specified by filename or module name:: + + python -m tornado.autoreload -m tornado.test.runtests + python -m tornado.autoreload tornado/test/runtests.py + + Running a script with this wrapper is similar to calling + `tornado.autoreload.wait` at the end of the script, but this wrapper + can catch import-time problems like syntax errors that would otherwise + prevent the script from reaching its call to `wait`. + """ + # Remember that we were launched with autoreload as main. + # The main module can be tricky; set the variables both in our globals + # (which may be __main__) and the real importable version. + import tornado.autoreload + + global _autoreload_is_main + global _original_argv, _original_spec + tornado.autoreload._autoreload_is_main = _autoreload_is_main = True + original_argv = sys.argv + tornado.autoreload._original_argv = _original_argv = original_argv + original_spec = getattr(sys.modules["__main__"], "__spec__", None) + tornado.autoreload._original_spec = _original_spec = original_spec + sys.argv = sys.argv[:] + if len(sys.argv) >= 3 and sys.argv[1] == "-m": + mode = "module" + module = sys.argv[2] + del sys.argv[1:3] + elif len(sys.argv) >= 2: + mode = "script" + script = sys.argv[1] + sys.argv = sys.argv[1:] + else: + print(_USAGE, file=sys.stderr) + sys.exit(1) + + try: + if mode == "module": + import runpy + + runpy.run_module(module, run_name="__main__", alter_sys=True) + elif mode == "script": + with open(script) as f: + # Execute the script in our namespace instead of creating + # a new one so that something that tries to import __main__ + # (e.g. the unittest module) will see names defined in the + # script instead of just those defined in this module. + global __file__ + __file__ = script + # If __package__ is defined, imports may be incorrectly + # interpreted as relative to this module. + global __package__ + del __package__ + exec_in(f.read(), globals(), globals()) + except SystemExit as e: + logging.basicConfig() + gen_log.info("Script exited with status %s", e.code) + except Exception as e: + logging.basicConfig() + gen_log.warning("Script exited with uncaught exception", exc_info=True) + # If an exception occurred at import time, the file with the error + # never made it into sys.modules and so we won't know to watch it. + # Just to make sure we've covered everything, walk the stack trace + # from the exception and watch every file. + for (filename, lineno, name, line) in traceback.extract_tb(sys.exc_info()[2]): + watch(filename) + if isinstance(e, SyntaxError): + # SyntaxErrors are special: their innermost stack frame is fake + # so extract_tb won't see it and we have to get the filename + # from the exception object. + watch(e.filename) + else: + logging.basicConfig() + gen_log.info("Script exited normally") + # restore sys.argv so subsequent executions will include autoreload + sys.argv = original_argv + + if mode == "module": + # runpy did a fake import of the module as __main__, but now it's + # no longer in sys.modules. Figure out where it is and watch it. + loader = pkgutil.get_loader(module) + if loader is not None: + watch(loader.get_filename()) # type: ignore + + wait() + + +if __name__ == "__main__": + # See also the other __main__ block at the top of the file, which modifies + # sys.path before our imports + main() diff --git a/telegramer/include/tornado/concurrent.py b/telegramer/include/tornado/concurrent.py new file mode 100644 index 0000000..7638fcf --- /dev/null +++ b/telegramer/include/tornado/concurrent.py @@ -0,0 +1,263 @@ +# +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Utilities for working with ``Future`` objects. + +Tornado previously provided its own ``Future`` class, but now uses +`asyncio.Future`. This module contains utility functions for working +with `asyncio.Future` in a way that is backwards-compatible with +Tornado's old ``Future`` implementation. + +While this module is an important part of Tornado's internal +implementation, applications rarely need to interact with it +directly. + +""" + +import asyncio +from concurrent import futures +import functools +import sys +import types + +from tornado.log import app_log + +import typing +from typing import Any, Callable, Optional, Tuple, Union + +_T = typing.TypeVar("_T") + + +class ReturnValueIgnoredError(Exception): + # No longer used; was previously used by @return_future + pass + + +Future = asyncio.Future + +FUTURES = (futures.Future, Future) + + +def is_future(x: Any) -> bool: + return isinstance(x, FUTURES) + + +class DummyExecutor(futures.Executor): + def submit( + self, fn: Callable[..., _T], *args: Any, **kwargs: Any + ) -> "futures.Future[_T]": + future = futures.Future() # type: futures.Future[_T] + try: + future_set_result_unless_cancelled(future, fn(*args, **kwargs)) + except Exception: + future_set_exc_info(future, sys.exc_info()) + return future + + def shutdown(self, wait: bool = True) -> None: + pass + + +dummy_executor = DummyExecutor() + + +def run_on_executor(*args: Any, **kwargs: Any) -> Callable: + """Decorator to run a synchronous method asynchronously on an executor. + + Returns a future. + + The executor to be used is determined by the ``executor`` + attributes of ``self``. To use a different attribute name, pass a + keyword argument to the decorator:: + + @run_on_executor(executor='_thread_pool') + def foo(self): + pass + + This decorator should not be confused with the similarly-named + `.IOLoop.run_in_executor`. In general, using ``run_in_executor`` + when *calling* a blocking method is recommended instead of using + this decorator when *defining* a method. If compatibility with older + versions of Tornado is required, consider defining an executor + and using ``executor.submit()`` at the call site. + + .. versionchanged:: 4.2 + Added keyword arguments to use alternative attributes. + + .. versionchanged:: 5.0 + Always uses the current IOLoop instead of ``self.io_loop``. + + .. versionchanged:: 5.1 + Returns a `.Future` compatible with ``await`` instead of a + `concurrent.futures.Future`. + + .. deprecated:: 5.1 + + The ``callback`` argument is deprecated and will be removed in + 6.0. The decorator itself is discouraged in new code but will + not be removed in 6.0. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. + """ + # Fully type-checking decorators is tricky, and this one is + # discouraged anyway so it doesn't have all the generic magic. + def run_on_executor_decorator(fn: Callable) -> Callable[..., Future]: + executor = kwargs.get("executor", "executor") + + @functools.wraps(fn) + def wrapper(self: Any, *args: Any, **kwargs: Any) -> Future: + async_future = Future() # type: Future + conc_future = getattr(self, executor).submit(fn, self, *args, **kwargs) + chain_future(conc_future, async_future) + return async_future + + return wrapper + + if args and kwargs: + raise ValueError("cannot combine positional and keyword args") + if len(args) == 1: + return run_on_executor_decorator(args[0]) + elif len(args) != 0: + raise ValueError("expected 1 argument, got %d", len(args)) + return run_on_executor_decorator + + +_NO_RESULT = object() + + +def chain_future(a: "Future[_T]", b: "Future[_T]") -> None: + """Chain two futures together so that when one completes, so does the other. + + The result (success or failure) of ``a`` will be copied to ``b``, unless + ``b`` has already been completed or cancelled by the time ``a`` finishes. + + .. versionchanged:: 5.0 + + Now accepts both Tornado/asyncio `Future` objects and + `concurrent.futures.Future`. + + """ + + def copy(future: "Future[_T]") -> None: + assert future is a + if b.done(): + return + if hasattr(a, "exc_info") and a.exc_info() is not None: # type: ignore + future_set_exc_info(b, a.exc_info()) # type: ignore + elif a.exception() is not None: + b.set_exception(a.exception()) + else: + b.set_result(a.result()) + + if isinstance(a, Future): + future_add_done_callback(a, copy) + else: + # concurrent.futures.Future + from tornado.ioloop import IOLoop + + IOLoop.current().add_future(a, copy) + + +def future_set_result_unless_cancelled( + future: "Union[futures.Future[_T], Future[_T]]", value: _T +) -> None: + """Set the given ``value`` as the `Future`'s result, if not cancelled. + + Avoids ``asyncio.InvalidStateError`` when calling ``set_result()`` on + a cancelled `asyncio.Future`. + + .. versionadded:: 5.0 + """ + if not future.cancelled(): + future.set_result(value) + + +def future_set_exception_unless_cancelled( + future: "Union[futures.Future[_T], Future[_T]]", exc: BaseException +) -> None: + """Set the given ``exc`` as the `Future`'s exception. + + If the Future is already canceled, logs the exception instead. If + this logging is not desired, the caller should explicitly check + the state of the Future and call ``Future.set_exception`` instead of + this wrapper. + + Avoids ``asyncio.InvalidStateError`` when calling ``set_exception()`` on + a cancelled `asyncio.Future`. + + .. versionadded:: 6.0 + + """ + if not future.cancelled(): + future.set_exception(exc) + else: + app_log.error("Exception after Future was cancelled", exc_info=exc) + + +def future_set_exc_info( + future: "Union[futures.Future[_T], Future[_T]]", + exc_info: Tuple[ + Optional[type], Optional[BaseException], Optional[types.TracebackType] + ], +) -> None: + """Set the given ``exc_info`` as the `Future`'s exception. + + Understands both `asyncio.Future` and the extensions in older + versions of Tornado to enable better tracebacks on Python 2. + + .. versionadded:: 5.0 + + .. versionchanged:: 6.0 + + If the future is already cancelled, this function is a no-op. + (previously ``asyncio.InvalidStateError`` would be raised) + + """ + if exc_info[1] is None: + raise Exception("future_set_exc_info called with no exception") + future_set_exception_unless_cancelled(future, exc_info[1]) + + +@typing.overload +def future_add_done_callback( + future: "futures.Future[_T]", callback: Callable[["futures.Future[_T]"], None] +) -> None: + pass + + +@typing.overload # noqa: F811 +def future_add_done_callback( + future: "Future[_T]", callback: Callable[["Future[_T]"], None] +) -> None: + pass + + +def future_add_done_callback( # noqa: F811 + future: "Union[futures.Future[_T], Future[_T]]", callback: Callable[..., None] +) -> None: + """Arrange to call ``callback`` when ``future`` is complete. + + ``callback`` is invoked with one argument, the ``future``. + + If ``future`` is already done, ``callback`` is invoked immediately. + This may differ from the behavior of ``Future.add_done_callback``, + which makes no such guarantee. + + .. versionadded:: 5.0 + """ + if future.done(): + callback(future) + else: + future.add_done_callback(callback) diff --git a/telegramer/include/tornado/curl_httpclient.py b/telegramer/include/tornado/curl_httpclient.py new file mode 100644 index 0000000..6553999 --- /dev/null +++ b/telegramer/include/tornado/curl_httpclient.py @@ -0,0 +1,583 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Non-blocking HTTP client implementation using pycurl.""" + +import collections +import functools +import logging +import pycurl +import threading +import time +from io import BytesIO + +from tornado import httputil +from tornado import ioloop + +from tornado.escape import utf8, native_str +from tornado.httpclient import ( + HTTPRequest, + HTTPResponse, + HTTPError, + AsyncHTTPClient, + main, +) +from tornado.log import app_log + +from typing import Dict, Any, Callable, Union, Tuple, Optional +import typing + +if typing.TYPE_CHECKING: + from typing import Deque # noqa: F401 + +curl_log = logging.getLogger("tornado.curl_httpclient") + + +class CurlAsyncHTTPClient(AsyncHTTPClient): + def initialize( # type: ignore + self, max_clients: int = 10, defaults: Optional[Dict[str, Any]] = None + ) -> None: + super().initialize(defaults=defaults) + # Typeshed is incomplete for CurlMulti, so just use Any for now. + self._multi = pycurl.CurlMulti() # type: Any + self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout) + self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket) + self._curls = [self._curl_create() for i in range(max_clients)] + self._free_list = self._curls[:] + self._requests = ( + collections.deque() + ) # type: Deque[Tuple[HTTPRequest, Callable[[HTTPResponse], None], float]] + self._fds = {} # type: Dict[int, int] + self._timeout = None # type: Optional[object] + + # libcurl has bugs that sometimes cause it to not report all + # relevant file descriptors and timeouts to TIMERFUNCTION/ + # SOCKETFUNCTION. Mitigate the effects of such bugs by + # forcing a periodic scan of all active requests. + self._force_timeout_callback = ioloop.PeriodicCallback( + self._handle_force_timeout, 1000 + ) + self._force_timeout_callback.start() + + # Work around a bug in libcurl 7.29.0: Some fields in the curl + # multi object are initialized lazily, and its destructor will + # segfault if it is destroyed without having been used. Add + # and remove a dummy handle to make sure everything is + # initialized. + dummy_curl_handle = pycurl.Curl() + self._multi.add_handle(dummy_curl_handle) + self._multi.remove_handle(dummy_curl_handle) + + def close(self) -> None: + self._force_timeout_callback.stop() + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + for curl in self._curls: + curl.close() + self._multi.close() + super().close() + + # Set below properties to None to reduce the reference count of current + # instance, because those properties hold some methods of current + # instance that will case circular reference. + self._force_timeout_callback = None # type: ignore + self._multi = None + + def fetch_impl( + self, request: HTTPRequest, callback: Callable[[HTTPResponse], None] + ) -> None: + self._requests.append((request, callback, self.io_loop.time())) + self._process_queue() + self._set_timeout(0) + + def _handle_socket(self, event: int, fd: int, multi: Any, data: bytes) -> None: + """Called by libcurl when it wants to change the file descriptors + it cares about. + """ + event_map = { + pycurl.POLL_NONE: ioloop.IOLoop.NONE, + pycurl.POLL_IN: ioloop.IOLoop.READ, + pycurl.POLL_OUT: ioloop.IOLoop.WRITE, + pycurl.POLL_INOUT: ioloop.IOLoop.READ | ioloop.IOLoop.WRITE, + } + if event == pycurl.POLL_REMOVE: + if fd in self._fds: + self.io_loop.remove_handler(fd) + del self._fds[fd] + else: + ioloop_event = event_map[event] + # libcurl sometimes closes a socket and then opens a new + # one using the same FD without giving us a POLL_NONE in + # between. This is a problem with the epoll IOLoop, + # because the kernel can tell when a socket is closed and + # removes it from the epoll automatically, causing future + # update_handler calls to fail. Since we can't tell when + # this has happened, always use remove and re-add + # instead of update. + if fd in self._fds: + self.io_loop.remove_handler(fd) + self.io_loop.add_handler(fd, self._handle_events, ioloop_event) + self._fds[fd] = ioloop_event + + def _set_timeout(self, msecs: int) -> None: + """Called by libcurl to schedule a timeout.""" + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + self._timeout = self.io_loop.add_timeout( + self.io_loop.time() + msecs / 1000.0, self._handle_timeout + ) + + def _handle_events(self, fd: int, events: int) -> None: + """Called by IOLoop when there is activity on one of our + file descriptors. + """ + action = 0 + if events & ioloop.IOLoop.READ: + action |= pycurl.CSELECT_IN + if events & ioloop.IOLoop.WRITE: + action |= pycurl.CSELECT_OUT + while True: + try: + ret, num_handles = self._multi.socket_action(fd, action) + except pycurl.error as e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + def _handle_timeout(self) -> None: + """Called by IOLoop when the requested timeout has passed.""" + self._timeout = None + while True: + try: + ret, num_handles = self._multi.socket_action(pycurl.SOCKET_TIMEOUT, 0) + except pycurl.error as e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + # In theory, we shouldn't have to do this because curl will + # call _set_timeout whenever the timeout changes. However, + # sometimes after _handle_timeout we will need to reschedule + # immediately even though nothing has changed from curl's + # perspective. This is because when socket_action is + # called with SOCKET_TIMEOUT, libcurl decides internally which + # timeouts need to be processed by using a monotonic clock + # (where available) while tornado uses python's time.time() + # to decide when timeouts have occurred. When those clocks + # disagree on elapsed time (as they will whenever there is an + # NTP adjustment), tornado might call _handle_timeout before + # libcurl is ready. After each timeout, resync the scheduled + # timeout with libcurl's current state. + new_timeout = self._multi.timeout() + if new_timeout >= 0: + self._set_timeout(new_timeout) + + def _handle_force_timeout(self) -> None: + """Called by IOLoop periodically to ask libcurl to process any + events it may have forgotten about. + """ + while True: + try: + ret, num_handles = self._multi.socket_all() + except pycurl.error as e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + def _finish_pending_requests(self) -> None: + """Process any requests that were completed by the last + call to multi.socket_action. + """ + while True: + num_q, ok_list, err_list = self._multi.info_read() + for curl in ok_list: + self._finish(curl) + for curl, errnum, errmsg in err_list: + self._finish(curl, errnum, errmsg) + if num_q == 0: + break + self._process_queue() + + def _process_queue(self) -> None: + while True: + started = 0 + while self._free_list and self._requests: + started += 1 + curl = self._free_list.pop() + (request, callback, queue_start_time) = self._requests.popleft() + # TODO: Don't smuggle extra data on an attribute of the Curl object. + curl.info = { # type: ignore + "headers": httputil.HTTPHeaders(), + "buffer": BytesIO(), + "request": request, + "callback": callback, + "queue_start_time": queue_start_time, + "curl_start_time": time.time(), + "curl_start_ioloop_time": self.io_loop.current().time(), + } + try: + self._curl_setup_request( + curl, + request, + curl.info["buffer"], # type: ignore + curl.info["headers"], # type: ignore + ) + except Exception as e: + # If there was an error in setup, pass it on + # to the callback. Note that allowing the + # error to escape here will appear to work + # most of the time since we are still in the + # caller's original stack frame, but when + # _process_queue() is called from + # _finish_pending_requests the exceptions have + # nowhere to go. + self._free_list.append(curl) + callback(HTTPResponse(request=request, code=599, error=e)) + else: + self._multi.add_handle(curl) + + if not started: + break + + def _finish( + self, + curl: pycurl.Curl, + curl_error: Optional[int] = None, + curl_message: Optional[str] = None, + ) -> None: + info = curl.info # type: ignore + curl.info = None # type: ignore + self._multi.remove_handle(curl) + self._free_list.append(curl) + buffer = info["buffer"] + if curl_error: + assert curl_message is not None + error = CurlError(curl_error, curl_message) # type: Optional[CurlError] + assert error is not None + code = error.code + effective_url = None + buffer.close() + buffer = None + else: + error = None + code = curl.getinfo(pycurl.HTTP_CODE) + effective_url = curl.getinfo(pycurl.EFFECTIVE_URL) + buffer.seek(0) + # the various curl timings are documented at + # http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html + time_info = dict( + queue=info["curl_start_ioloop_time"] - info["queue_start_time"], + namelookup=curl.getinfo(pycurl.NAMELOOKUP_TIME), + connect=curl.getinfo(pycurl.CONNECT_TIME), + appconnect=curl.getinfo(pycurl.APPCONNECT_TIME), + pretransfer=curl.getinfo(pycurl.PRETRANSFER_TIME), + starttransfer=curl.getinfo(pycurl.STARTTRANSFER_TIME), + total=curl.getinfo(pycurl.TOTAL_TIME), + redirect=curl.getinfo(pycurl.REDIRECT_TIME), + ) + try: + info["callback"]( + HTTPResponse( + request=info["request"], + code=code, + headers=info["headers"], + buffer=buffer, + effective_url=effective_url, + error=error, + reason=info["headers"].get("X-Http-Reason", None), + request_time=self.io_loop.time() - info["curl_start_ioloop_time"], + start_time=info["curl_start_time"], + time_info=time_info, + ) + ) + except Exception: + self.handle_callback_exception(info["callback"]) + + def handle_callback_exception(self, callback: Any) -> None: + app_log.error("Exception in callback %r", callback, exc_info=True) + + def _curl_create(self) -> pycurl.Curl: + curl = pycurl.Curl() + if curl_log.isEnabledFor(logging.DEBUG): + curl.setopt(pycurl.VERBOSE, 1) + curl.setopt(pycurl.DEBUGFUNCTION, self._curl_debug) + if hasattr( + pycurl, "PROTOCOLS" + ): # PROTOCOLS first appeared in pycurl 7.19.5 (2014-07-12) + curl.setopt(pycurl.PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS) + curl.setopt(pycurl.REDIR_PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS) + return curl + + def _curl_setup_request( + self, + curl: pycurl.Curl, + request: HTTPRequest, + buffer: BytesIO, + headers: httputil.HTTPHeaders, + ) -> None: + curl.setopt(pycurl.URL, native_str(request.url)) + + # libcurl's magic "Expect: 100-continue" behavior causes delays + # with servers that don't support it (which include, among others, + # Google's OpenID endpoint). Additionally, this behavior has + # a bug in conjunction with the curl_multi_socket_action API + # (https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3039744&group_id=976), + # which increases the delays. It's more trouble than it's worth, + # so just turn off the feature (yes, setting Expect: to an empty + # value is the official way to disable this) + if "Expect" not in request.headers: + request.headers["Expect"] = "" + + # libcurl adds Pragma: no-cache by default; disable that too + if "Pragma" not in request.headers: + request.headers["Pragma"] = "" + + curl.setopt( + pycurl.HTTPHEADER, + [ + "%s: %s" % (native_str(k), native_str(v)) + for k, v in request.headers.get_all() + ], + ) + + curl.setopt( + pycurl.HEADERFUNCTION, + functools.partial( + self._curl_header_callback, headers, request.header_callback + ), + ) + if request.streaming_callback: + + def write_function(b: Union[bytes, bytearray]) -> int: + assert request.streaming_callback is not None + self.io_loop.add_callback(request.streaming_callback, b) + return len(b) + + else: + write_function = buffer.write + curl.setopt(pycurl.WRITEFUNCTION, write_function) + curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects) + curl.setopt(pycurl.MAXREDIRS, request.max_redirects) + assert request.connect_timeout is not None + curl.setopt(pycurl.CONNECTTIMEOUT_MS, int(1000 * request.connect_timeout)) + assert request.request_timeout is not None + curl.setopt(pycurl.TIMEOUT_MS, int(1000 * request.request_timeout)) + if request.user_agent: + curl.setopt(pycurl.USERAGENT, native_str(request.user_agent)) + else: + curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)") + if request.network_interface: + curl.setopt(pycurl.INTERFACE, request.network_interface) + if request.decompress_response: + curl.setopt(pycurl.ENCODING, "gzip,deflate") + else: + curl.setopt(pycurl.ENCODING, None) + if request.proxy_host and request.proxy_port: + curl.setopt(pycurl.PROXY, request.proxy_host) + curl.setopt(pycurl.PROXYPORT, request.proxy_port) + if request.proxy_username: + assert request.proxy_password is not None + credentials = httputil.encode_username_password( + request.proxy_username, request.proxy_password + ) + curl.setopt(pycurl.PROXYUSERPWD, credentials) + + if request.proxy_auth_mode is None or request.proxy_auth_mode == "basic": + curl.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_BASIC) + elif request.proxy_auth_mode == "digest": + curl.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_DIGEST) + else: + raise ValueError( + "Unsupported proxy_auth_mode %s" % request.proxy_auth_mode + ) + else: + try: + curl.unsetopt(pycurl.PROXY) + except TypeError: # not supported, disable proxy + curl.setopt(pycurl.PROXY, "") + curl.unsetopt(pycurl.PROXYUSERPWD) + if request.validate_cert: + curl.setopt(pycurl.SSL_VERIFYPEER, 1) + curl.setopt(pycurl.SSL_VERIFYHOST, 2) + else: + curl.setopt(pycurl.SSL_VERIFYPEER, 0) + curl.setopt(pycurl.SSL_VERIFYHOST, 0) + if request.ca_certs is not None: + curl.setopt(pycurl.CAINFO, request.ca_certs) + else: + # There is no way to restore pycurl.CAINFO to its default value + # (Using unsetopt makes it reject all certificates). + # I don't see any way to read the default value from python so it + # can be restored later. We'll have to just leave CAINFO untouched + # if no ca_certs file was specified, and require that if any + # request uses a custom ca_certs file, they all must. + pass + + if request.allow_ipv6 is False: + # Curl behaves reasonably when DNS resolution gives an ipv6 address + # that we can't reach, so allow ipv6 unless the user asks to disable. + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4) + else: + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER) + + # Set the request method through curl's irritating interface which makes + # up names for almost every single method + curl_options = { + "GET": pycurl.HTTPGET, + "POST": pycurl.POST, + "PUT": pycurl.UPLOAD, + "HEAD": pycurl.NOBODY, + } + custom_methods = set(["DELETE", "OPTIONS", "PATCH"]) + for o in curl_options.values(): + curl.setopt(o, False) + if request.method in curl_options: + curl.unsetopt(pycurl.CUSTOMREQUEST) + curl.setopt(curl_options[request.method], True) + elif request.allow_nonstandard_methods or request.method in custom_methods: + curl.setopt(pycurl.CUSTOMREQUEST, request.method) + else: + raise KeyError("unknown method " + request.method) + + body_expected = request.method in ("POST", "PATCH", "PUT") + body_present = request.body is not None + if not request.allow_nonstandard_methods: + # Some HTTP methods nearly always have bodies while others + # almost never do. Fail in this case unless the user has + # opted out of sanity checks with allow_nonstandard_methods. + if (body_expected and not body_present) or ( + body_present and not body_expected + ): + raise ValueError( + "Body must %sbe None for method %s (unless " + "allow_nonstandard_methods is true)" + % ("not " if body_expected else "", request.method) + ) + + if body_expected or body_present: + if request.method == "GET": + # Even with `allow_nonstandard_methods` we disallow + # GET with a body (because libcurl doesn't allow it + # unless we use CUSTOMREQUEST). While the spec doesn't + # forbid clients from sending a body, it arguably + # disallows the server from doing anything with them. + raise ValueError("Body must be None for GET request") + request_buffer = BytesIO(utf8(request.body or "")) + + def ioctl(cmd: int) -> None: + if cmd == curl.IOCMD_RESTARTREAD: # type: ignore + request_buffer.seek(0) + + curl.setopt(pycurl.READFUNCTION, request_buffer.read) + curl.setopt(pycurl.IOCTLFUNCTION, ioctl) + if request.method == "POST": + curl.setopt(pycurl.POSTFIELDSIZE, len(request.body or "")) + else: + curl.setopt(pycurl.UPLOAD, True) + curl.setopt(pycurl.INFILESIZE, len(request.body or "")) + + if request.auth_username is not None: + assert request.auth_password is not None + if request.auth_mode is None or request.auth_mode == "basic": + curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) + elif request.auth_mode == "digest": + curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST) + else: + raise ValueError("Unsupported auth_mode %s" % request.auth_mode) + + userpwd = httputil.encode_username_password( + request.auth_username, request.auth_password + ) + curl.setopt(pycurl.USERPWD, userpwd) + curl_log.debug( + "%s %s (username: %r)", + request.method, + request.url, + request.auth_username, + ) + else: + curl.unsetopt(pycurl.USERPWD) + curl_log.debug("%s %s", request.method, request.url) + + if request.client_cert is not None: + curl.setopt(pycurl.SSLCERT, request.client_cert) + + if request.client_key is not None: + curl.setopt(pycurl.SSLKEY, request.client_key) + + if request.ssl_options is not None: + raise ValueError("ssl_options not supported in curl_httpclient") + + if threading.active_count() > 1: + # libcurl/pycurl is not thread-safe by default. When multiple threads + # are used, signals should be disabled. This has the side effect + # of disabling DNS timeouts in some environments (when libcurl is + # not linked against ares), so we don't do it when there is only one + # thread. Applications that use many short-lived threads may need + # to set NOSIGNAL manually in a prepare_curl_callback since + # there may not be any other threads running at the time we call + # threading.activeCount. + curl.setopt(pycurl.NOSIGNAL, 1) + if request.prepare_curl_callback is not None: + request.prepare_curl_callback(curl) + + def _curl_header_callback( + self, + headers: httputil.HTTPHeaders, + header_callback: Callable[[str], None], + header_line_bytes: bytes, + ) -> None: + header_line = native_str(header_line_bytes.decode("latin1")) + if header_callback is not None: + self.io_loop.add_callback(header_callback, header_line) + # header_line as returned by curl includes the end-of-line characters. + # whitespace at the start should be preserved to allow multi-line headers + header_line = header_line.rstrip() + if header_line.startswith("HTTP/"): + headers.clear() + try: + (__, __, reason) = httputil.parse_response_start_line(header_line) + header_line = "X-Http-Reason: %s" % reason + except httputil.HTTPInputError: + return + if not header_line: + return + headers.parse_line(header_line) + + def _curl_debug(self, debug_type: int, debug_msg: str) -> None: + debug_types = ("I", "<", ">", "<", ">") + if debug_type == 0: + debug_msg = native_str(debug_msg) + curl_log.debug("%s", debug_msg.strip()) + elif debug_type in (1, 2): + debug_msg = native_str(debug_msg) + for line in debug_msg.splitlines(): + curl_log.debug("%s %s", debug_types[debug_type], line) + elif debug_type == 4: + curl_log.debug("%s %r", debug_types[debug_type], debug_msg) + + +class CurlError(HTTPError): + def __init__(self, errno: int, message: str) -> None: + HTTPError.__init__(self, 599, message) + self.errno = errno + + +if __name__ == "__main__": + AsyncHTTPClient.configure(CurlAsyncHTTPClient) + main() diff --git a/telegramer/include/tornado/escape.py b/telegramer/include/tornado/escape.py new file mode 100644 index 0000000..3cf7ff2 --- /dev/null +++ b/telegramer/include/tornado/escape.py @@ -0,0 +1,402 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Escaping/unescaping methods for HTML, JSON, URLs, and others. + +Also includes a few other miscellaneous string manipulation functions that +have crept in over time. +""" + +import html.entities +import json +import re +import urllib.parse + +from tornado.util import unicode_type + +import typing +from typing import Union, Any, Optional, Dict, List, Callable + + +_XHTML_ESCAPE_RE = re.compile("[&<>\"']") +_XHTML_ESCAPE_DICT = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", +} + + +def xhtml_escape(value: Union[str, bytes]) -> str: + """Escapes a string so it is valid within HTML or XML. + + Escapes the characters ``<``, ``>``, ``"``, ``'``, and ``&``. + When used in attribute values the escaped strings must be enclosed + in quotes. + + .. versionchanged:: 3.2 + + Added the single quote to the list of escaped characters. + """ + return _XHTML_ESCAPE_RE.sub( + lambda match: _XHTML_ESCAPE_DICT[match.group(0)], to_basestring(value) + ) + + +def xhtml_unescape(value: Union[str, bytes]) -> str: + """Un-escapes an XML-escaped string.""" + return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value)) + + +# The fact that json_encode wraps json.dumps is an implementation detail. +# Please see https://github.com/tornadoweb/tornado/pull/706 +# before sending a pull request that adds **kwargs to this function. +def json_encode(value: Any) -> str: + """JSON-encodes the given Python object.""" + # JSON permits but does not require forward slashes to be escaped. + # This is useful when json data is emitted in a tags from prematurely terminating + # the JavaScript. Some json libraries do this escaping by default, + # although python's standard library does not, so we do it here. + # http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped + return json.dumps(value).replace(" Any: + """Returns Python objects for the given JSON string. + + Supports both `str` and `bytes` inputs. + """ + return json.loads(to_basestring(value)) + + +def squeeze(value: str) -> str: + """Replace all sequences of whitespace chars with a single space.""" + return re.sub(r"[\x00-\x20]+", " ", value).strip() + + +def url_escape(value: Union[str, bytes], plus: bool = True) -> str: + """Returns a URL-encoded version of the given value. + + If ``plus`` is true (the default), spaces will be represented + as "+" instead of "%20". This is appropriate for query strings + but not for the path component of a URL. Note that this default + is the reverse of Python's urllib module. + + .. versionadded:: 3.1 + The ``plus`` argument + """ + quote = urllib.parse.quote_plus if plus else urllib.parse.quote + return quote(utf8(value)) + + +@typing.overload +def url_unescape(value: Union[str, bytes], encoding: None, plus: bool = True) -> bytes: + pass + + +@typing.overload # noqa: F811 +def url_unescape( + value: Union[str, bytes], encoding: str = "utf-8", plus: bool = True +) -> str: + pass + + +def url_unescape( # noqa: F811 + value: Union[str, bytes], encoding: Optional[str] = "utf-8", plus: bool = True +) -> Union[str, bytes]: + """Decodes the given value from a URL. + + The argument may be either a byte or unicode string. + + If encoding is None, the result will be a byte string. Otherwise, + the result is a unicode string in the specified encoding. + + If ``plus`` is true (the default), plus signs will be interpreted + as spaces (literal plus signs must be represented as "%2B"). This + is appropriate for query strings and form-encoded values but not + for the path component of a URL. Note that this default is the + reverse of Python's urllib module. + + .. versionadded:: 3.1 + The ``plus`` argument + """ + if encoding is None: + if plus: + # unquote_to_bytes doesn't have a _plus variant + value = to_basestring(value).replace("+", " ") + return urllib.parse.unquote_to_bytes(value) + else: + unquote = urllib.parse.unquote_plus if plus else urllib.parse.unquote + return unquote(to_basestring(value), encoding=encoding) + + +def parse_qs_bytes( + qs: Union[str, bytes], keep_blank_values: bool = False, strict_parsing: bool = False +) -> Dict[str, List[bytes]]: + """Parses a query string like urlparse.parse_qs, + but takes bytes and returns the values as byte strings. + + Keys still become type str (interpreted as latin1 in python3!) + because it's too painful to keep them as byte strings in + python3 and in practice they're nearly always ascii anyway. + """ + # This is gross, but python3 doesn't give us another way. + # Latin1 is the universal donor of character encodings. + if isinstance(qs, bytes): + qs = qs.decode("latin1") + result = urllib.parse.parse_qs( + qs, keep_blank_values, strict_parsing, encoding="latin1", errors="strict" + ) + encoded = {} + for k, v in result.items(): + encoded[k] = [i.encode("latin1") for i in v] + return encoded + + +_UTF8_TYPES = (bytes, type(None)) + + +@typing.overload +def utf8(value: bytes) -> bytes: + pass + + +@typing.overload # noqa: F811 +def utf8(value: str) -> bytes: + pass + + +@typing.overload # noqa: F811 +def utf8(value: None) -> None: + pass + + +def utf8(value: Union[None, str, bytes]) -> Optional[bytes]: # noqa: F811 + """Converts a string argument to a byte string. + + If the argument is already a byte string or None, it is returned unchanged. + Otherwise it must be a unicode string and is encoded as utf8. + """ + if isinstance(value, _UTF8_TYPES): + return value + if not isinstance(value, unicode_type): + raise TypeError("Expected bytes, unicode, or None; got %r" % type(value)) + return value.encode("utf-8") + + +_TO_UNICODE_TYPES = (unicode_type, type(None)) + + +@typing.overload +def to_unicode(value: str) -> str: + pass + + +@typing.overload # noqa: F811 +def to_unicode(value: bytes) -> str: + pass + + +@typing.overload # noqa: F811 +def to_unicode(value: None) -> None: + pass + + +def to_unicode(value: Union[None, str, bytes]) -> Optional[str]: # noqa: F811 + """Converts a string argument to a unicode string. + + If the argument is already a unicode string or None, it is returned + unchanged. Otherwise it must be a byte string and is decoded as utf8. + """ + if isinstance(value, _TO_UNICODE_TYPES): + return value + if not isinstance(value, bytes): + raise TypeError("Expected bytes, unicode, or None; got %r" % type(value)) + return value.decode("utf-8") + + +# to_unicode was previously named _unicode not because it was private, +# but to avoid conflicts with the built-in unicode() function/type +_unicode = to_unicode + +# When dealing with the standard library across python 2 and 3 it is +# sometimes useful to have a direct conversion to the native string type +native_str = to_unicode +to_basestring = to_unicode + + +def recursive_unicode(obj: Any) -> Any: + """Walks a simple data structure, converting byte strings to unicode. + + Supports lists, tuples, and dictionaries. + """ + if isinstance(obj, dict): + return dict( + (recursive_unicode(k), recursive_unicode(v)) for (k, v) in obj.items() + ) + elif isinstance(obj, list): + return list(recursive_unicode(i) for i in obj) + elif isinstance(obj, tuple): + return tuple(recursive_unicode(i) for i in obj) + elif isinstance(obj, bytes): + return to_unicode(obj) + else: + return obj + + +# I originally used the regex from +# http://daringfireball.net/2010/07/improved_regex_for_matching_urls +# but it gets all exponential on certain patterns (such as too many trailing +# dots), causing the regex matcher to never return. +# This regex should avoid those problems. +# Use to_unicode instead of tornado.util.u - we don't want backslashes getting +# processed as escapes. +_URL_RE = re.compile( + to_unicode( + r"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&|")*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&|")*\)))+)""" # noqa: E501 + ) +) + + +def linkify( + text: Union[str, bytes], + shorten: bool = False, + extra_params: Union[str, Callable[[str], str]] = "", + require_protocol: bool = False, + permitted_protocols: List[str] = ["http", "https"], +) -> str: + """Converts plain text into HTML with links. + + For example: ``linkify("Hello http://tornadoweb.org!")`` would return + ``Hello http://tornadoweb.org!`` + + Parameters: + + * ``shorten``: Long urls will be shortened for display. + + * ``extra_params``: Extra text to include in the link tag, or a callable + taking the link as an argument and returning the extra text + e.g. ``linkify(text, extra_params='rel="nofollow" class="external"')``, + or:: + + def extra_params_cb(url): + if url.startswith("http://example.com"): + return 'class="internal"' + else: + return 'class="external" rel="nofollow"' + linkify(text, extra_params=extra_params_cb) + + * ``require_protocol``: Only linkify urls which include a protocol. If + this is False, urls such as www.facebook.com will also be linkified. + + * ``permitted_protocols``: List (or set) of protocols which should be + linkified, e.g. ``linkify(text, permitted_protocols=["http", "ftp", + "mailto"])``. It is very unsafe to include protocols such as + ``javascript``. + """ + if extra_params and not callable(extra_params): + extra_params = " " + extra_params.strip() + + def make_link(m: typing.Match) -> str: + url = m.group(1) + proto = m.group(2) + if require_protocol and not proto: + return url # not protocol, no linkify + + if proto and proto not in permitted_protocols: + return url # bad protocol, no linkify + + href = m.group(1) + if not proto: + href = "http://" + href # no proto specified, use http + + if callable(extra_params): + params = " " + extra_params(href).strip() + else: + params = extra_params + + # clip long urls. max_len is just an approximation + max_len = 30 + if shorten and len(url) > max_len: + before_clip = url + if proto: + proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for : + else: + proto_len = 0 + + parts = url[proto_len:].split("/") + if len(parts) > 1: + # Grab the whole host part plus the first bit of the path + # The path is usually not that interesting once shortened + # (no more slug, etc), so it really just provides a little + # extra indication of shortening. + url = ( + url[:proto_len] + + parts[0] + + "/" + + parts[1][:8].split("?")[0].split(".")[0] + ) + + if len(url) > max_len * 1.5: # still too long + url = url[:max_len] + + if url != before_clip: + amp = url.rfind("&") + # avoid splitting html char entities + if amp > max_len - 5: + url = url[:amp] + url += "..." + + if len(url) >= len(before_clip): + url = before_clip + else: + # full url is visible on mouse-over (for those who don't + # have a status bar, such as Safari by default) + params += ' title="%s"' % href + + return u'%s' % (href, params, url) + + # First HTML-escape so that our strings are all safe. + # The regex is modified to avoid character entites other than & so + # that we won't pick up ", etc. + text = _unicode(xhtml_escape(text)) + return _URL_RE.sub(make_link, text) + + +def _convert_entity(m: typing.Match) -> str: + if m.group(1) == "#": + try: + if m.group(2)[:1].lower() == "x": + return chr(int(m.group(2)[1:], 16)) + else: + return chr(int(m.group(2))) + except ValueError: + return "&#%s;" % m.group(2) + try: + return _HTML_UNICODE_MAP[m.group(2)] + except KeyError: + return "&%s;" % m.group(2) + + +def _build_unicode_map() -> Dict[str, str]: + unicode_map = {} + for name, value in html.entities.name2codepoint.items(): + unicode_map[name] = chr(value) + return unicode_map + + +_HTML_UNICODE_MAP = _build_unicode_map() diff --git a/telegramer/include/tornado/gen.py b/telegramer/include/tornado/gen.py new file mode 100644 index 0000000..cab9689 --- /dev/null +++ b/telegramer/include/tornado/gen.py @@ -0,0 +1,872 @@ +"""``tornado.gen`` implements generator-based coroutines. + +.. note:: + + The "decorator and generator" approach in this module is a + precursor to native coroutines (using ``async def`` and ``await``) + which were introduced in Python 3.5. Applications that do not + require compatibility with older versions of Python should use + native coroutines instead. Some parts of this module are still + useful with native coroutines, notably `multi`, `sleep`, + `WaitIterator`, and `with_timeout`. Some of these functions have + counterparts in the `asyncio` module which may be used as well, + although the two may not necessarily be 100% compatible. + +Coroutines provide an easier way to work in an asynchronous +environment than chaining callbacks. Code using coroutines is +technically asynchronous, but it is written as a single generator +instead of a collection of separate functions. + +For example, here's a coroutine-based handler: + +.. testcode:: + + class GenAsyncHandler(RequestHandler): + @gen.coroutine + def get(self): + http_client = AsyncHTTPClient() + response = yield http_client.fetch("http://example.com") + do_something_with_response(response) + self.render("template.html") + +.. testoutput:: + :hide: + +Asynchronous functions in Tornado return an ``Awaitable`` or `.Future`; +yielding this object returns its result. + +You can also yield a list or dict of other yieldable objects, which +will be started at the same time and run in parallel; a list or dict +of results will be returned when they are all finished: + +.. testcode:: + + @gen.coroutine + def get(self): + http_client = AsyncHTTPClient() + response1, response2 = yield [http_client.fetch(url1), + http_client.fetch(url2)] + response_dict = yield dict(response3=http_client.fetch(url3), + response4=http_client.fetch(url4)) + response3 = response_dict['response3'] + response4 = response_dict['response4'] + +.. testoutput:: + :hide: + +If ``tornado.platform.twisted`` is imported, it is also possible to +yield Twisted's ``Deferred`` objects. See the `convert_yielded` +function to extend this mechanism. + +.. versionchanged:: 3.2 + Dict support added. + +.. versionchanged:: 4.1 + Support added for yielding ``asyncio`` Futures and Twisted Deferreds + via ``singledispatch``. + +""" +import asyncio +import builtins +import collections +from collections.abc import Generator +import concurrent.futures +import datetime +import functools +from functools import singledispatch +from inspect import isawaitable +import sys +import types + +from tornado.concurrent import ( + Future, + is_future, + chain_future, + future_set_exc_info, + future_add_done_callback, + future_set_result_unless_cancelled, +) +from tornado.ioloop import IOLoop +from tornado.log import app_log +from tornado.util import TimeoutError + +try: + import contextvars +except ImportError: + contextvars = None # type: ignore + +import typing +from typing import Union, Any, Callable, List, Type, Tuple, Awaitable, Dict, overload + +if typing.TYPE_CHECKING: + from typing import Sequence, Deque, Optional, Set, Iterable # noqa: F401 + +_T = typing.TypeVar("_T") + +_Yieldable = Union[ + None, Awaitable, List[Awaitable], Dict[Any, Awaitable], concurrent.futures.Future +] + + +class KeyReuseError(Exception): + pass + + +class UnknownKeyError(Exception): + pass + + +class LeakedCallbackError(Exception): + pass + + +class BadYieldError(Exception): + pass + + +class ReturnValueIgnoredError(Exception): + pass + + +def _value_from_stopiteration(e: Union[StopIteration, "Return"]) -> Any: + try: + # StopIteration has a value attribute beginning in py33. + # So does our Return class. + return e.value + except AttributeError: + pass + try: + # Cython backports coroutine functionality by putting the value in + # e.args[0]. + return e.args[0] + except (AttributeError, IndexError): + return None + + +def _create_future() -> Future: + future = Future() # type: Future + # Fixup asyncio debug info by removing extraneous stack entries + source_traceback = getattr(future, "_source_traceback", ()) + while source_traceback: + # Each traceback entry is equivalent to a + # (filename, self.lineno, self.name, self.line) tuple + filename = source_traceback[-1][0] + if filename == __file__: + del source_traceback[-1] + else: + break + return future + + +def _fake_ctx_run(f: Callable[..., _T], *args: Any, **kw: Any) -> _T: + return f(*args, **kw) + + +@overload +def coroutine( + func: Callable[..., "Generator[Any, Any, _T]"] +) -> Callable[..., "Future[_T]"]: + ... + + +@overload +def coroutine(func: Callable[..., _T]) -> Callable[..., "Future[_T]"]: + ... + + +def coroutine( + func: Union[Callable[..., "Generator[Any, Any, _T]"], Callable[..., _T]] +) -> Callable[..., "Future[_T]"]: + """Decorator for asynchronous generators. + + For compatibility with older versions of Python, coroutines may + also "return" by raising the special exception `Return(value) + `. + + Functions with this decorator return a `.Future`. + + .. warning:: + + When exceptions occur inside a coroutine, the exception + information will be stored in the `.Future` object. You must + examine the result of the `.Future` object, or the exception + may go unnoticed by your code. This means yielding the function + if called from another coroutine, using something like + `.IOLoop.run_sync` for top-level calls, or passing the `.Future` + to `.IOLoop.add_future`. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + awaitable object instead. + + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Future[_T] + # This function is type-annotated with a comment to work around + # https://bitbucket.org/pypy/pypy/issues/2868/segfault-with-args-type-annotation-in + future = _create_future() + if contextvars is not None: + ctx_run = contextvars.copy_context().run # type: Callable + else: + ctx_run = _fake_ctx_run + try: + result = ctx_run(func, *args, **kwargs) + except (Return, StopIteration) as e: + result = _value_from_stopiteration(e) + except Exception: + future_set_exc_info(future, sys.exc_info()) + try: + return future + finally: + # Avoid circular references + future = None # type: ignore + else: + if isinstance(result, Generator): + # Inline the first iteration of Runner.run. This lets us + # avoid the cost of creating a Runner when the coroutine + # never actually yields, which in turn allows us to + # use "optional" coroutines in critical path code without + # performance penalty for the synchronous case. + try: + yielded = ctx_run(next, result) + except (StopIteration, Return) as e: + future_set_result_unless_cancelled( + future, _value_from_stopiteration(e) + ) + except Exception: + future_set_exc_info(future, sys.exc_info()) + else: + # Provide strong references to Runner objects as long + # as their result future objects also have strong + # references (typically from the parent coroutine's + # Runner). This keeps the coroutine's Runner alive. + # We do this by exploiting the public API + # add_done_callback() instead of putting a private + # attribute on the Future. + # (GitHub issues #1769, #2229). + runner = Runner(ctx_run, result, future, yielded) + future.add_done_callback(lambda _: runner) + yielded = None + try: + return future + finally: + # Subtle memory optimization: if next() raised an exception, + # the future's exc_info contains a traceback which + # includes this stack frame. This creates a cycle, + # which will be collected at the next full GC but has + # been shown to greatly increase memory usage of + # benchmarks (relative to the refcount-based scheme + # used in the absence of cycles). We can avoid the + # cycle by clearing the local variable after we return it. + future = None # type: ignore + future_set_result_unless_cancelled(future, result) + return future + + wrapper.__wrapped__ = func # type: ignore + wrapper.__tornado_coroutine__ = True # type: ignore + return wrapper + + +def is_coroutine_function(func: Any) -> bool: + """Return whether *func* is a coroutine function, i.e. a function + wrapped with `~.gen.coroutine`. + + .. versionadded:: 4.5 + """ + return getattr(func, "__tornado_coroutine__", False) + + +class Return(Exception): + """Special exception to return a value from a `coroutine`. + + If this exception is raised, its value argument is used as the + result of the coroutine:: + + @gen.coroutine + def fetch_json(url): + response = yield AsyncHTTPClient().fetch(url) + raise gen.Return(json_decode(response.body)) + + In Python 3.3, this exception is no longer necessary: the ``return`` + statement can be used directly to return a value (previously + ``yield`` and ``return`` with a value could not be combined in the + same function). + + By analogy with the return statement, the value argument is optional, + but it is never necessary to ``raise gen.Return()``. The ``return`` + statement can be used with no arguments instead. + """ + + def __init__(self, value: Any = None) -> None: + super().__init__() + self.value = value + # Cython recognizes subclasses of StopIteration with a .args tuple. + self.args = (value,) + + +class WaitIterator(object): + """Provides an iterator to yield the results of awaitables as they finish. + + Yielding a set of awaitables like this: + + ``results = yield [awaitable1, awaitable2]`` + + pauses the coroutine until both ``awaitable1`` and ``awaitable2`` + return, and then restarts the coroutine with the results of both + awaitables. If either awaitable raises an exception, the + expression will raise that exception and all the results will be + lost. + + If you need to get the result of each awaitable as soon as possible, + or if you need the result of some awaitables even if others produce + errors, you can use ``WaitIterator``:: + + wait_iterator = gen.WaitIterator(awaitable1, awaitable2) + while not wait_iterator.done(): + try: + result = yield wait_iterator.next() + except Exception as e: + print("Error {} from {}".format(e, wait_iterator.current_future)) + else: + print("Result {} received from {} at {}".format( + result, wait_iterator.current_future, + wait_iterator.current_index)) + + Because results are returned as soon as they are available the + output from the iterator *will not be in the same order as the + input arguments*. If you need to know which future produced the + current result, you can use the attributes + ``WaitIterator.current_future``, or ``WaitIterator.current_index`` + to get the index of the awaitable from the input list. (if keyword + arguments were used in the construction of the `WaitIterator`, + ``current_index`` will use the corresponding keyword). + + On Python 3.5, `WaitIterator` implements the async iterator + protocol, so it can be used with the ``async for`` statement (note + that in this version the entire iteration is aborted if any value + raises an exception, while the previous example can continue past + individual errors):: + + async for result in gen.WaitIterator(future1, future2): + print("Result {} received from {} at {}".format( + result, wait_iterator.current_future, + wait_iterator.current_index)) + + .. versionadded:: 4.1 + + .. versionchanged:: 4.3 + Added ``async for`` support in Python 3.5. + + """ + + _unfinished = {} # type: Dict[Future, Union[int, str]] + + def __init__(self, *args: Future, **kwargs: Future) -> None: + if args and kwargs: + raise ValueError("You must provide args or kwargs, not both") + + if kwargs: + self._unfinished = dict((f, k) for (k, f) in kwargs.items()) + futures = list(kwargs.values()) # type: Sequence[Future] + else: + self._unfinished = dict((f, i) for (i, f) in enumerate(args)) + futures = args + + self._finished = collections.deque() # type: Deque[Future] + self.current_index = None # type: Optional[Union[str, int]] + self.current_future = None # type: Optional[Future] + self._running_future = None # type: Optional[Future] + + for future in futures: + future_add_done_callback(future, self._done_callback) + + def done(self) -> bool: + """Returns True if this iterator has no more results.""" + if self._finished or self._unfinished: + return False + # Clear the 'current' values when iteration is done. + self.current_index = self.current_future = None + return True + + def next(self) -> Future: + """Returns a `.Future` that will yield the next available result. + + Note that this `.Future` will not be the same object as any of + the inputs. + """ + self._running_future = Future() + + if self._finished: + self._return_result(self._finished.popleft()) + + return self._running_future + + def _done_callback(self, done: Future) -> None: + if self._running_future and not self._running_future.done(): + self._return_result(done) + else: + self._finished.append(done) + + def _return_result(self, done: Future) -> None: + """Called set the returned future's state that of the future + we yielded, and set the current future for the iterator. + """ + if self._running_future is None: + raise Exception("no future is running") + chain_future(done, self._running_future) + + self.current_future = done + self.current_index = self._unfinished.pop(done) + + def __aiter__(self) -> typing.AsyncIterator: + return self + + def __anext__(self) -> Future: + if self.done(): + # Lookup by name to silence pyflakes on older versions. + raise getattr(builtins, "StopAsyncIteration")() + return self.next() + + +def multi( + children: Union[List[_Yieldable], Dict[Any, _Yieldable]], + quiet_exceptions: "Union[Type[Exception], Tuple[Type[Exception], ...]]" = (), +) -> "Union[Future[List], Future[Dict]]": + """Runs multiple asynchronous operations in parallel. + + ``children`` may either be a list or a dict whose values are + yieldable objects. ``multi()`` returns a new yieldable + object that resolves to a parallel structure containing their + results. If ``children`` is a list, the result is a list of + results in the same order; if it is a dict, the result is a dict + with the same keys. + + That is, ``results = yield multi(list_of_futures)`` is equivalent + to:: + + results = [] + for future in list_of_futures: + results.append(yield future) + + If any children raise exceptions, ``multi()`` will raise the first + one. All others will be logged, unless they are of types + contained in the ``quiet_exceptions`` argument. + + In a ``yield``-based coroutine, it is not normally necessary to + call this function directly, since the coroutine runner will + do it automatically when a list or dict is yielded. However, + it is necessary in ``await``-based coroutines, or to pass + the ``quiet_exceptions`` argument. + + This function is available under the names ``multi()`` and ``Multi()`` + for historical reasons. + + Cancelling a `.Future` returned by ``multi()`` does not cancel its + children. `asyncio.gather` is similar to ``multi()``, but it does + cancel its children. + + .. versionchanged:: 4.2 + If multiple yieldables fail, any exceptions after the first + (which is raised) will be logged. Added the ``quiet_exceptions`` + argument to suppress this logging for selected exception types. + + .. versionchanged:: 4.3 + Replaced the class ``Multi`` and the function ``multi_future`` + with a unified function ``multi``. Added support for yieldables + other than ``YieldPoint`` and `.Future`. + + """ + return multi_future(children, quiet_exceptions=quiet_exceptions) + + +Multi = multi + + +def multi_future( + children: Union[List[_Yieldable], Dict[Any, _Yieldable]], + quiet_exceptions: "Union[Type[Exception], Tuple[Type[Exception], ...]]" = (), +) -> "Union[Future[List], Future[Dict]]": + """Wait for multiple asynchronous futures in parallel. + + Since Tornado 6.0, this function is exactly the same as `multi`. + + .. versionadded:: 4.0 + + .. versionchanged:: 4.2 + If multiple ``Futures`` fail, any exceptions after the first (which is + raised) will be logged. Added the ``quiet_exceptions`` + argument to suppress this logging for selected exception types. + + .. deprecated:: 4.3 + Use `multi` instead. + """ + if isinstance(children, dict): + keys = list(children.keys()) # type: Optional[List] + children_seq = children.values() # type: Iterable + else: + keys = None + children_seq = children + children_futs = list(map(convert_yielded, children_seq)) + assert all(is_future(i) or isinstance(i, _NullFuture) for i in children_futs) + unfinished_children = set(children_futs) + + future = _create_future() + if not children_futs: + future_set_result_unless_cancelled(future, {} if keys is not None else []) + + def callback(fut: Future) -> None: + unfinished_children.remove(fut) + if not unfinished_children: + result_list = [] + for f in children_futs: + try: + result_list.append(f.result()) + except Exception as e: + if future.done(): + if not isinstance(e, quiet_exceptions): + app_log.error( + "Multiple exceptions in yield list", exc_info=True + ) + else: + future_set_exc_info(future, sys.exc_info()) + if not future.done(): + if keys is not None: + future_set_result_unless_cancelled( + future, dict(zip(keys, result_list)) + ) + else: + future_set_result_unless_cancelled(future, result_list) + + listening = set() # type: Set[Future] + for f in children_futs: + if f not in listening: + listening.add(f) + future_add_done_callback(f, callback) + return future + + +def maybe_future(x: Any) -> Future: + """Converts ``x`` into a `.Future`. + + If ``x`` is already a `.Future`, it is simply returned; otherwise + it is wrapped in a new `.Future`. This is suitable for use as + ``result = yield gen.maybe_future(f())`` when you don't know whether + ``f()`` returns a `.Future` or not. + + .. deprecated:: 4.3 + This function only handles ``Futures``, not other yieldable objects. + Instead of `maybe_future`, check for the non-future result types + you expect (often just ``None``), and ``yield`` anything unknown. + """ + if is_future(x): + return x + else: + fut = _create_future() + fut.set_result(x) + return fut + + +def with_timeout( + timeout: Union[float, datetime.timedelta], + future: _Yieldable, + quiet_exceptions: "Union[Type[Exception], Tuple[Type[Exception], ...]]" = (), +) -> Future: + """Wraps a `.Future` (or other yieldable object) in a timeout. + + Raises `tornado.util.TimeoutError` if the input future does not + complete before ``timeout``, which may be specified in any form + allowed by `.IOLoop.add_timeout` (i.e. a `datetime.timedelta` or + an absolute time relative to `.IOLoop.time`) + + If the wrapped `.Future` fails after it has timed out, the exception + will be logged unless it is either of a type contained in + ``quiet_exceptions`` (which may be an exception type or a sequence of + types), or an ``asyncio.CancelledError``. + + The wrapped `.Future` is not canceled when the timeout expires, + permitting it to be reused. `asyncio.wait_for` is similar to this + function but it does cancel the wrapped `.Future` on timeout. + + .. versionadded:: 4.0 + + .. versionchanged:: 4.1 + Added the ``quiet_exceptions`` argument and the logging of unhandled + exceptions. + + .. versionchanged:: 4.4 + Added support for yieldable objects other than `.Future`. + + .. versionchanged:: 6.0.3 + ``asyncio.CancelledError`` is now always considered "quiet". + + """ + # It's tempting to optimize this by cancelling the input future on timeout + # instead of creating a new one, but A) we can't know if we are the only + # one waiting on the input future, so cancelling it might disrupt other + # callers and B) concurrent futures can only be cancelled while they are + # in the queue, so cancellation cannot reliably bound our waiting time. + future_converted = convert_yielded(future) + result = _create_future() + chain_future(future_converted, result) + io_loop = IOLoop.current() + + def error_callback(future: Future) -> None: + try: + future.result() + except asyncio.CancelledError: + pass + except Exception as e: + if not isinstance(e, quiet_exceptions): + app_log.error( + "Exception in Future %r after timeout", future, exc_info=True + ) + + def timeout_callback() -> None: + if not result.done(): + result.set_exception(TimeoutError("Timeout")) + # In case the wrapped future goes on to fail, log it. + future_add_done_callback(future_converted, error_callback) + + timeout_handle = io_loop.add_timeout(timeout, timeout_callback) + if isinstance(future_converted, Future): + # We know this future will resolve on the IOLoop, so we don't + # need the extra thread-safety of IOLoop.add_future (and we also + # don't care about StackContext here. + future_add_done_callback( + future_converted, lambda future: io_loop.remove_timeout(timeout_handle) + ) + else: + # concurrent.futures.Futures may resolve on any thread, so we + # need to route them back to the IOLoop. + io_loop.add_future( + future_converted, lambda future: io_loop.remove_timeout(timeout_handle) + ) + return result + + +def sleep(duration: float) -> "Future[None]": + """Return a `.Future` that resolves after the given number of seconds. + + When used with ``yield`` in a coroutine, this is a non-blocking + analogue to `time.sleep` (which should not be used in coroutines + because it is blocking):: + + yield gen.sleep(0.5) + + Note that calling this function on its own does nothing; you must + wait on the `.Future` it returns (usually by yielding it). + + .. versionadded:: 4.1 + """ + f = _create_future() + IOLoop.current().call_later( + duration, lambda: future_set_result_unless_cancelled(f, None) + ) + return f + + +class _NullFuture(object): + """_NullFuture resembles a Future that finished with a result of None. + + It's not actually a `Future` to avoid depending on a particular event loop. + Handled as a special case in the coroutine runner. + + We lie and tell the type checker that a _NullFuture is a Future so + we don't have to leak _NullFuture into lots of public APIs. But + this means that the type checker can't warn us when we're passing + a _NullFuture into a code path that doesn't understand what to do + with it. + """ + + def result(self) -> None: + return None + + def done(self) -> bool: + return True + + +# _null_future is used as a dummy value in the coroutine runner. It differs +# from moment in that moment always adds a delay of one IOLoop iteration +# while _null_future is processed as soon as possible. +_null_future = typing.cast(Future, _NullFuture()) + +moment = typing.cast(Future, _NullFuture()) +moment.__doc__ = """A special object which may be yielded to allow the IOLoop to run for +one iteration. + +This is not needed in normal use but it can be helpful in long-running +coroutines that are likely to yield Futures that are ready instantly. + +Usage: ``yield gen.moment`` + +In native coroutines, the equivalent of ``yield gen.moment`` is +``await asyncio.sleep(0)``. + +.. versionadded:: 4.0 + +.. deprecated:: 4.5 + ``yield None`` (or ``yield`` with no argument) is now equivalent to + ``yield gen.moment``. +""" + + +class Runner(object): + """Internal implementation of `tornado.gen.coroutine`. + + Maintains information about pending callbacks and their results. + + The results of the generator are stored in ``result_future`` (a + `.Future`) + """ + + def __init__( + self, + ctx_run: Callable, + gen: "Generator[_Yieldable, Any, _T]", + result_future: "Future[_T]", + first_yielded: _Yieldable, + ) -> None: + self.ctx_run = ctx_run + self.gen = gen + self.result_future = result_future + self.future = _null_future # type: Union[None, Future] + self.running = False + self.finished = False + self.io_loop = IOLoop.current() + if self.handle_yield(first_yielded): + gen = result_future = first_yielded = None # type: ignore + self.ctx_run(self.run) + + def run(self) -> None: + """Starts or resumes the generator, running until it reaches a + yield point that is not ready. + """ + if self.running or self.finished: + return + try: + self.running = True + while True: + future = self.future + if future is None: + raise Exception("No pending future") + if not future.done(): + return + self.future = None + try: + exc_info = None + + try: + value = future.result() + except Exception: + exc_info = sys.exc_info() + future = None + + if exc_info is not None: + try: + yielded = self.gen.throw(*exc_info) # type: ignore + finally: + # Break up a reference to itself + # for faster GC on CPython. + exc_info = None + else: + yielded = self.gen.send(value) + + except (StopIteration, Return) as e: + self.finished = True + self.future = _null_future + future_set_result_unless_cancelled( + self.result_future, _value_from_stopiteration(e) + ) + self.result_future = None # type: ignore + return + except Exception: + self.finished = True + self.future = _null_future + future_set_exc_info(self.result_future, sys.exc_info()) + self.result_future = None # type: ignore + return + if not self.handle_yield(yielded): + return + yielded = None + finally: + self.running = False + + def handle_yield(self, yielded: _Yieldable) -> bool: + try: + self.future = convert_yielded(yielded) + except BadYieldError: + self.future = Future() + future_set_exc_info(self.future, sys.exc_info()) + + if self.future is moment: + self.io_loop.add_callback(self.ctx_run, self.run) + return False + elif self.future is None: + raise Exception("no pending future") + elif not self.future.done(): + + def inner(f: Any) -> None: + # Break a reference cycle to speed GC. + f = None # noqa: F841 + self.ctx_run(self.run) + + self.io_loop.add_future(self.future, inner) + return False + return True + + def handle_exception( + self, typ: Type[Exception], value: Exception, tb: types.TracebackType + ) -> bool: + if not self.running and not self.finished: + self.future = Future() + future_set_exc_info(self.future, (typ, value, tb)) + self.ctx_run(self.run) + return True + else: + return False + + +# Convert Awaitables into Futures. +try: + _wrap_awaitable = asyncio.ensure_future +except AttributeError: + # asyncio.ensure_future was introduced in Python 3.4.4, but + # Debian jessie still ships with 3.4.2 so try the old name. + _wrap_awaitable = getattr(asyncio, "async") + + +def convert_yielded(yielded: _Yieldable) -> Future: + """Convert a yielded object into a `.Future`. + + The default implementation accepts lists, dictionaries, and + Futures. This has the side effect of starting any coroutines that + did not start themselves, similar to `asyncio.ensure_future`. + + If the `~functools.singledispatch` library is available, this function + may be extended to support additional types. For example:: + + @convert_yielded.register(asyncio.Future) + def _(asyncio_future): + return tornado.platform.asyncio.to_tornado_future(asyncio_future) + + .. versionadded:: 4.1 + + """ + if yielded is None or yielded is moment: + return moment + elif yielded is _null_future: + return _null_future + elif isinstance(yielded, (list, dict)): + return multi(yielded) # type: ignore + elif is_future(yielded): + return typing.cast(Future, yielded) + elif isawaitable(yielded): + return _wrap_awaitable(yielded) # type: ignore + else: + raise BadYieldError("yielded unknown object %r" % (yielded,)) + + +convert_yielded = singledispatch(convert_yielded) diff --git a/telegramer/include/tornado/http1connection.py b/telegramer/include/tornado/http1connection.py new file mode 100644 index 0000000..835027b --- /dev/null +++ b/telegramer/include/tornado/http1connection.py @@ -0,0 +1,842 @@ +# +# Copyright 2014 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Client and server implementations of HTTP/1.x. + +.. versionadded:: 4.0 +""" + +import asyncio +import logging +import re +import types + +from tornado.concurrent import ( + Future, + future_add_done_callback, + future_set_result_unless_cancelled, +) +from tornado.escape import native_str, utf8 +from tornado import gen +from tornado import httputil +from tornado import iostream +from tornado.log import gen_log, app_log +from tornado.util import GzipDecompressor + + +from typing import cast, Optional, Type, Awaitable, Callable, Union, Tuple + + +class _QuietException(Exception): + def __init__(self) -> None: + pass + + +class _ExceptionLoggingContext(object): + """Used with the ``with`` statement when calling delegate methods to + log any exceptions with the given logger. Any exceptions caught are + converted to _QuietException + """ + + def __init__(self, logger: logging.Logger) -> None: + self.logger = logger + + def __enter__(self) -> None: + pass + + def __exit__( + self, + typ: "Optional[Type[BaseException]]", + value: Optional[BaseException], + tb: types.TracebackType, + ) -> None: + if value is not None: + assert typ is not None + self.logger.error("Uncaught exception", exc_info=(typ, value, tb)) + raise _QuietException + + +class HTTP1ConnectionParameters(object): + """Parameters for `.HTTP1Connection` and `.HTTP1ServerConnection`. + """ + + def __init__( + self, + no_keep_alive: bool = False, + chunk_size: Optional[int] = None, + max_header_size: Optional[int] = None, + header_timeout: Optional[float] = None, + max_body_size: Optional[int] = None, + body_timeout: Optional[float] = None, + decompress: bool = False, + ) -> None: + """ + :arg bool no_keep_alive: If true, always close the connection after + one request. + :arg int chunk_size: how much data to read into memory at once + :arg int max_header_size: maximum amount of data for HTTP headers + :arg float header_timeout: how long to wait for all headers (seconds) + :arg int max_body_size: maximum amount of data for body + :arg float body_timeout: how long to wait while reading body (seconds) + :arg bool decompress: if true, decode incoming + ``Content-Encoding: gzip`` + """ + self.no_keep_alive = no_keep_alive + self.chunk_size = chunk_size or 65536 + self.max_header_size = max_header_size or 65536 + self.header_timeout = header_timeout + self.max_body_size = max_body_size + self.body_timeout = body_timeout + self.decompress = decompress + + +class HTTP1Connection(httputil.HTTPConnection): + """Implements the HTTP/1.x protocol. + + This class can be on its own for clients, or via `HTTP1ServerConnection` + for servers. + """ + + def __init__( + self, + stream: iostream.IOStream, + is_client: bool, + params: Optional[HTTP1ConnectionParameters] = None, + context: Optional[object] = None, + ) -> None: + """ + :arg stream: an `.IOStream` + :arg bool is_client: client or server + :arg params: a `.HTTP1ConnectionParameters` instance or ``None`` + :arg context: an opaque application-defined object that can be accessed + as ``connection.context``. + """ + self.is_client = is_client + self.stream = stream + if params is None: + params = HTTP1ConnectionParameters() + self.params = params + self.context = context + self.no_keep_alive = params.no_keep_alive + # The body limits can be altered by the delegate, so save them + # here instead of just referencing self.params later. + self._max_body_size = self.params.max_body_size or self.stream.max_buffer_size + self._body_timeout = self.params.body_timeout + # _write_finished is set to True when finish() has been called, + # i.e. there will be no more data sent. Data may still be in the + # stream's write buffer. + self._write_finished = False + # True when we have read the entire incoming body. + self._read_finished = False + # _finish_future resolves when all data has been written and flushed + # to the IOStream. + self._finish_future = Future() # type: Future[None] + # If true, the connection should be closed after this request + # (after the response has been written in the server side, + # and after it has been read in the client) + self._disconnect_on_finish = False + self._clear_callbacks() + # Save the start lines after we read or write them; they + # affect later processing (e.g. 304 responses and HEAD methods + # have content-length but no bodies) + self._request_start_line = None # type: Optional[httputil.RequestStartLine] + self._response_start_line = None # type: Optional[httputil.ResponseStartLine] + self._request_headers = None # type: Optional[httputil.HTTPHeaders] + # True if we are writing output with chunked encoding. + self._chunking_output = False + # While reading a body with a content-length, this is the + # amount left to read. + self._expected_content_remaining = None # type: Optional[int] + # A Future for our outgoing writes, returned by IOStream.write. + self._pending_write = None # type: Optional[Future[None]] + + def read_response(self, delegate: httputil.HTTPMessageDelegate) -> Awaitable[bool]: + """Read a single HTTP response. + + Typical client-mode usage is to write a request using `write_headers`, + `write`, and `finish`, and then call ``read_response``. + + :arg delegate: a `.HTTPMessageDelegate` + + Returns a `.Future` that resolves to a bool after the full response has + been read. The result is true if the stream is still open. + """ + if self.params.decompress: + delegate = _GzipMessageDelegate(delegate, self.params.chunk_size) + return self._read_message(delegate) + + async def _read_message(self, delegate: httputil.HTTPMessageDelegate) -> bool: + need_delegate_close = False + try: + header_future = self.stream.read_until_regex( + b"\r?\n\r?\n", max_bytes=self.params.max_header_size + ) + if self.params.header_timeout is None: + header_data = await header_future + else: + try: + header_data = await gen.with_timeout( + self.stream.io_loop.time() + self.params.header_timeout, + header_future, + quiet_exceptions=iostream.StreamClosedError, + ) + except gen.TimeoutError: + self.close() + return False + start_line_str, headers = self._parse_headers(header_data) + if self.is_client: + resp_start_line = httputil.parse_response_start_line(start_line_str) + self._response_start_line = resp_start_line + start_line = ( + resp_start_line + ) # type: Union[httputil.RequestStartLine, httputil.ResponseStartLine] + # TODO: this will need to change to support client-side keepalive + self._disconnect_on_finish = False + else: + req_start_line = httputil.parse_request_start_line(start_line_str) + self._request_start_line = req_start_line + self._request_headers = headers + start_line = req_start_line + self._disconnect_on_finish = not self._can_keep_alive( + req_start_line, headers + ) + need_delegate_close = True + with _ExceptionLoggingContext(app_log): + header_recv_future = delegate.headers_received(start_line, headers) + if header_recv_future is not None: + await header_recv_future + if self.stream is None: + # We've been detached. + need_delegate_close = False + return False + skip_body = False + if self.is_client: + assert isinstance(start_line, httputil.ResponseStartLine) + if ( + self._request_start_line is not None + and self._request_start_line.method == "HEAD" + ): + skip_body = True + code = start_line.code + if code == 304: + # 304 responses may include the content-length header + # but do not actually have a body. + # http://tools.ietf.org/html/rfc7230#section-3.3 + skip_body = True + if 100 <= code < 200: + # 1xx responses should never indicate the presence of + # a body. + if "Content-Length" in headers or "Transfer-Encoding" in headers: + raise httputil.HTTPInputError( + "Response code %d cannot have body" % code + ) + # TODO: client delegates will get headers_received twice + # in the case of a 100-continue. Document or change? + await self._read_message(delegate) + else: + if headers.get("Expect") == "100-continue" and not self._write_finished: + self.stream.write(b"HTTP/1.1 100 (Continue)\r\n\r\n") + if not skip_body: + body_future = self._read_body( + resp_start_line.code if self.is_client else 0, headers, delegate + ) + if body_future is not None: + if self._body_timeout is None: + await body_future + else: + try: + await gen.with_timeout( + self.stream.io_loop.time() + self._body_timeout, + body_future, + quiet_exceptions=iostream.StreamClosedError, + ) + except gen.TimeoutError: + gen_log.info("Timeout reading body from %s", self.context) + self.stream.close() + return False + self._read_finished = True + if not self._write_finished or self.is_client: + need_delegate_close = False + with _ExceptionLoggingContext(app_log): + delegate.finish() + # If we're waiting for the application to produce an asynchronous + # response, and we're not detached, register a close callback + # on the stream (we didn't need one while we were reading) + if ( + not self._finish_future.done() + and self.stream is not None + and not self.stream.closed() + ): + self.stream.set_close_callback(self._on_connection_close) + await self._finish_future + if self.is_client and self._disconnect_on_finish: + self.close() + if self.stream is None: + return False + except httputil.HTTPInputError as e: + gen_log.info("Malformed HTTP message from %s: %s", self.context, e) + if not self.is_client: + await self.stream.write(b"HTTP/1.1 400 Bad Request\r\n\r\n") + self.close() + return False + finally: + if need_delegate_close: + with _ExceptionLoggingContext(app_log): + delegate.on_connection_close() + header_future = None # type: ignore + self._clear_callbacks() + return True + + def _clear_callbacks(self) -> None: + """Clears the callback attributes. + + This allows the request handler to be garbage collected more + quickly in CPython by breaking up reference cycles. + """ + self._write_callback = None + self._write_future = None # type: Optional[Future[None]] + self._close_callback = None # type: Optional[Callable[[], None]] + if self.stream is not None: + self.stream.set_close_callback(None) + + def set_close_callback(self, callback: Optional[Callable[[], None]]) -> None: + """Sets a callback that will be run when the connection is closed. + + Note that this callback is slightly different from + `.HTTPMessageDelegate.on_connection_close`: The + `.HTTPMessageDelegate` method is called when the connection is + closed while receiving a message. This callback is used when + there is not an active delegate (for example, on the server + side this callback is used if the client closes the connection + after sending its request but before receiving all the + response. + """ + self._close_callback = callback + + def _on_connection_close(self) -> None: + # Note that this callback is only registered on the IOStream + # when we have finished reading the request and are waiting for + # the application to produce its response. + if self._close_callback is not None: + callback = self._close_callback + self._close_callback = None + callback() + if not self._finish_future.done(): + future_set_result_unless_cancelled(self._finish_future, None) + self._clear_callbacks() + + def close(self) -> None: + if self.stream is not None: + self.stream.close() + self._clear_callbacks() + if not self._finish_future.done(): + future_set_result_unless_cancelled(self._finish_future, None) + + def detach(self) -> iostream.IOStream: + """Take control of the underlying stream. + + Returns the underlying `.IOStream` object and stops all further + HTTP processing. May only be called during + `.HTTPMessageDelegate.headers_received`. Intended for implementing + protocols like websockets that tunnel over an HTTP handshake. + """ + self._clear_callbacks() + stream = self.stream + self.stream = None # type: ignore + if not self._finish_future.done(): + future_set_result_unless_cancelled(self._finish_future, None) + return stream + + def set_body_timeout(self, timeout: float) -> None: + """Sets the body timeout for a single request. + + Overrides the value from `.HTTP1ConnectionParameters`. + """ + self._body_timeout = timeout + + def set_max_body_size(self, max_body_size: int) -> None: + """Sets the body size limit for a single request. + + Overrides the value from `.HTTP1ConnectionParameters`. + """ + self._max_body_size = max_body_size + + def write_headers( + self, + start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine], + headers: httputil.HTTPHeaders, + chunk: Optional[bytes] = None, + ) -> "Future[None]": + """Implements `.HTTPConnection.write_headers`.""" + lines = [] + if self.is_client: + assert isinstance(start_line, httputil.RequestStartLine) + self._request_start_line = start_line + lines.append(utf8("%s %s HTTP/1.1" % (start_line[0], start_line[1]))) + # Client requests with a non-empty body must have either a + # Content-Length or a Transfer-Encoding. + self._chunking_output = ( + start_line.method in ("POST", "PUT", "PATCH") + and "Content-Length" not in headers + and ( + "Transfer-Encoding" not in headers + or headers["Transfer-Encoding"] == "chunked" + ) + ) + else: + assert isinstance(start_line, httputil.ResponseStartLine) + assert self._request_start_line is not None + assert self._request_headers is not None + self._response_start_line = start_line + lines.append(utf8("HTTP/1.1 %d %s" % (start_line[1], start_line[2]))) + self._chunking_output = ( + # TODO: should this use + # self._request_start_line.version or + # start_line.version? + self._request_start_line.version == "HTTP/1.1" + # Omit payload header field for HEAD request. + and self._request_start_line.method != "HEAD" + # 1xx, 204 and 304 responses have no body (not even a zero-length + # body), and so should not have either Content-Length or + # Transfer-Encoding headers. + and start_line.code not in (204, 304) + and (start_line.code < 100 or start_line.code >= 200) + # No need to chunk the output if a Content-Length is specified. + and "Content-Length" not in headers + # Applications are discouraged from touching Transfer-Encoding, + # but if they do, leave it alone. + and "Transfer-Encoding" not in headers + ) + # If connection to a 1.1 client will be closed, inform client + if ( + self._request_start_line.version == "HTTP/1.1" + and self._disconnect_on_finish + ): + headers["Connection"] = "close" + # If a 1.0 client asked for keep-alive, add the header. + if ( + self._request_start_line.version == "HTTP/1.0" + and self._request_headers.get("Connection", "").lower() == "keep-alive" + ): + headers["Connection"] = "Keep-Alive" + if self._chunking_output: + headers["Transfer-Encoding"] = "chunked" + if not self.is_client and ( + self._request_start_line.method == "HEAD" + or cast(httputil.ResponseStartLine, start_line).code == 304 + ): + self._expected_content_remaining = 0 + elif "Content-Length" in headers: + self._expected_content_remaining = int(headers["Content-Length"]) + else: + self._expected_content_remaining = None + # TODO: headers are supposed to be of type str, but we still have some + # cases that let bytes slip through. Remove these native_str calls when those + # are fixed. + header_lines = ( + native_str(n) + ": " + native_str(v) for n, v in headers.get_all() + ) + lines.extend(line.encode("latin1") for line in header_lines) + for line in lines: + if b"\n" in line: + raise ValueError("Newline in header: " + repr(line)) + future = None + if self.stream.closed(): + future = self._write_future = Future() + future.set_exception(iostream.StreamClosedError()) + future.exception() + else: + future = self._write_future = Future() + data = b"\r\n".join(lines) + b"\r\n\r\n" + if chunk: + data += self._format_chunk(chunk) + self._pending_write = self.stream.write(data) + future_add_done_callback(self._pending_write, self._on_write_complete) + return future + + def _format_chunk(self, chunk: bytes) -> bytes: + if self._expected_content_remaining is not None: + self._expected_content_remaining -= len(chunk) + if self._expected_content_remaining < 0: + # Close the stream now to stop further framing errors. + self.stream.close() + raise httputil.HTTPOutputError( + "Tried to write more data than Content-Length" + ) + if self._chunking_output and chunk: + # Don't write out empty chunks because that means END-OF-STREAM + # with chunked encoding + return utf8("%x" % len(chunk)) + b"\r\n" + chunk + b"\r\n" + else: + return chunk + + def write(self, chunk: bytes) -> "Future[None]": + """Implements `.HTTPConnection.write`. + + For backwards compatibility it is allowed but deprecated to + skip `write_headers` and instead call `write()` with a + pre-encoded header block. + """ + future = None + if self.stream.closed(): + future = self._write_future = Future() + self._write_future.set_exception(iostream.StreamClosedError()) + self._write_future.exception() + else: + future = self._write_future = Future() + self._pending_write = self.stream.write(self._format_chunk(chunk)) + future_add_done_callback(self._pending_write, self._on_write_complete) + return future + + def finish(self) -> None: + """Implements `.HTTPConnection.finish`.""" + if ( + self._expected_content_remaining is not None + and self._expected_content_remaining != 0 + and not self.stream.closed() + ): + self.stream.close() + raise httputil.HTTPOutputError( + "Tried to write %d bytes less than Content-Length" + % self._expected_content_remaining + ) + if self._chunking_output: + if not self.stream.closed(): + self._pending_write = self.stream.write(b"0\r\n\r\n") + self._pending_write.add_done_callback(self._on_write_complete) + self._write_finished = True + # If the app finished the request while we're still reading, + # divert any remaining data away from the delegate and + # close the connection when we're done sending our response. + # Closing the connection is the only way to avoid reading the + # whole input body. + if not self._read_finished: + self._disconnect_on_finish = True + # No more data is coming, so instruct TCP to send any remaining + # data immediately instead of waiting for a full packet or ack. + self.stream.set_nodelay(True) + if self._pending_write is None: + self._finish_request(None) + else: + future_add_done_callback(self._pending_write, self._finish_request) + + def _on_write_complete(self, future: "Future[None]") -> None: + exc = future.exception() + if exc is not None and not isinstance(exc, iostream.StreamClosedError): + future.result() + if self._write_callback is not None: + callback = self._write_callback + self._write_callback = None + self.stream.io_loop.add_callback(callback) + if self._write_future is not None: + future = self._write_future + self._write_future = None + future_set_result_unless_cancelled(future, None) + + def _can_keep_alive( + self, start_line: httputil.RequestStartLine, headers: httputil.HTTPHeaders + ) -> bool: + if self.params.no_keep_alive: + return False + connection_header = headers.get("Connection") + if connection_header is not None: + connection_header = connection_header.lower() + if start_line.version == "HTTP/1.1": + return connection_header != "close" + elif ( + "Content-Length" in headers + or headers.get("Transfer-Encoding", "").lower() == "chunked" + or getattr(start_line, "method", None) in ("HEAD", "GET") + ): + # start_line may be a request or response start line; only + # the former has a method attribute. + return connection_header == "keep-alive" + return False + + def _finish_request(self, future: "Optional[Future[None]]") -> None: + self._clear_callbacks() + if not self.is_client and self._disconnect_on_finish: + self.close() + return + # Turn Nagle's algorithm back on, leaving the stream in its + # default state for the next request. + self.stream.set_nodelay(False) + if not self._finish_future.done(): + future_set_result_unless_cancelled(self._finish_future, None) + + def _parse_headers(self, data: bytes) -> Tuple[str, httputil.HTTPHeaders]: + # The lstrip removes newlines that some implementations sometimes + # insert between messages of a reused connection. Per RFC 7230, + # we SHOULD ignore at least one empty line before the request. + # http://tools.ietf.org/html/rfc7230#section-3.5 + data_str = native_str(data.decode("latin1")).lstrip("\r\n") + # RFC 7230 section allows for both CRLF and bare LF. + eol = data_str.find("\n") + start_line = data_str[:eol].rstrip("\r") + headers = httputil.HTTPHeaders.parse(data_str[eol:]) + return start_line, headers + + def _read_body( + self, + code: int, + headers: httputil.HTTPHeaders, + delegate: httputil.HTTPMessageDelegate, + ) -> Optional[Awaitable[None]]: + if "Content-Length" in headers: + if "Transfer-Encoding" in headers: + # Response cannot contain both Content-Length and + # Transfer-Encoding headers. + # http://tools.ietf.org/html/rfc7230#section-3.3.3 + raise httputil.HTTPInputError( + "Response with both Transfer-Encoding and Content-Length" + ) + if "," in headers["Content-Length"]: + # Proxies sometimes cause Content-Length headers to get + # duplicated. If all the values are identical then we can + # use them but if they differ it's an error. + pieces = re.split(r",\s*", headers["Content-Length"]) + if any(i != pieces[0] for i in pieces): + raise httputil.HTTPInputError( + "Multiple unequal Content-Lengths: %r" + % headers["Content-Length"] + ) + headers["Content-Length"] = pieces[0] + + try: + content_length = int(headers["Content-Length"]) # type: Optional[int] + except ValueError: + # Handles non-integer Content-Length value. + raise httputil.HTTPInputError( + "Only integer Content-Length is allowed: %s" + % headers["Content-Length"] + ) + + if cast(int, content_length) > self._max_body_size: + raise httputil.HTTPInputError("Content-Length too long") + else: + content_length = None + + if code == 204: + # This response code is not allowed to have a non-empty body, + # and has an implicit length of zero instead of read-until-close. + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 + if "Transfer-Encoding" in headers or content_length not in (None, 0): + raise httputil.HTTPInputError( + "Response with code %d should not have body" % code + ) + content_length = 0 + + if content_length is not None: + return self._read_fixed_body(content_length, delegate) + if headers.get("Transfer-Encoding", "").lower() == "chunked": + return self._read_chunked_body(delegate) + if self.is_client: + return self._read_body_until_close(delegate) + return None + + async def _read_fixed_body( + self, content_length: int, delegate: httputil.HTTPMessageDelegate + ) -> None: + while content_length > 0: + body = await self.stream.read_bytes( + min(self.params.chunk_size, content_length), partial=True + ) + content_length -= len(body) + if not self._write_finished or self.is_client: + with _ExceptionLoggingContext(app_log): + ret = delegate.data_received(body) + if ret is not None: + await ret + + async def _read_chunked_body(self, delegate: httputil.HTTPMessageDelegate) -> None: + # TODO: "chunk extensions" http://tools.ietf.org/html/rfc2616#section-3.6.1 + total_size = 0 + while True: + chunk_len_str = await self.stream.read_until(b"\r\n", max_bytes=64) + chunk_len = int(chunk_len_str.strip(), 16) + if chunk_len == 0: + crlf = await self.stream.read_bytes(2) + if crlf != b"\r\n": + raise httputil.HTTPInputError( + "improperly terminated chunked request" + ) + return + total_size += chunk_len + if total_size > self._max_body_size: + raise httputil.HTTPInputError("chunked body too large") + bytes_to_read = chunk_len + while bytes_to_read: + chunk = await self.stream.read_bytes( + min(bytes_to_read, self.params.chunk_size), partial=True + ) + bytes_to_read -= len(chunk) + if not self._write_finished or self.is_client: + with _ExceptionLoggingContext(app_log): + ret = delegate.data_received(chunk) + if ret is not None: + await ret + # chunk ends with \r\n + crlf = await self.stream.read_bytes(2) + assert crlf == b"\r\n" + + async def _read_body_until_close( + self, delegate: httputil.HTTPMessageDelegate + ) -> None: + body = await self.stream.read_until_close() + if not self._write_finished or self.is_client: + with _ExceptionLoggingContext(app_log): + ret = delegate.data_received(body) + if ret is not None: + await ret + + +class _GzipMessageDelegate(httputil.HTTPMessageDelegate): + """Wraps an `HTTPMessageDelegate` to decode ``Content-Encoding: gzip``. + """ + + def __init__(self, delegate: httputil.HTTPMessageDelegate, chunk_size: int) -> None: + self._delegate = delegate + self._chunk_size = chunk_size + self._decompressor = None # type: Optional[GzipDecompressor] + + def headers_received( + self, + start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine], + headers: httputil.HTTPHeaders, + ) -> Optional[Awaitable[None]]: + if headers.get("Content-Encoding") == "gzip": + self._decompressor = GzipDecompressor() + # Downstream delegates will only see uncompressed data, + # so rename the content-encoding header. + # (but note that curl_httpclient doesn't do this). + headers.add("X-Consumed-Content-Encoding", headers["Content-Encoding"]) + del headers["Content-Encoding"] + return self._delegate.headers_received(start_line, headers) + + async def data_received(self, chunk: bytes) -> None: + if self._decompressor: + compressed_data = chunk + while compressed_data: + decompressed = self._decompressor.decompress( + compressed_data, self._chunk_size + ) + if decompressed: + ret = self._delegate.data_received(decompressed) + if ret is not None: + await ret + compressed_data = self._decompressor.unconsumed_tail + if compressed_data and not decompressed: + raise httputil.HTTPInputError( + "encountered unconsumed gzip data without making progress" + ) + else: + ret = self._delegate.data_received(chunk) + if ret is not None: + await ret + + def finish(self) -> None: + if self._decompressor is not None: + tail = self._decompressor.flush() + if tail: + # The tail should always be empty: decompress returned + # all that it can in data_received and the only + # purpose of the flush call is to detect errors such + # as truncated input. If we did legitimately get a new + # chunk at this point we'd need to change the + # interface to make finish() a coroutine. + raise ValueError( + "decompressor.flush returned data; possible truncated input" + ) + return self._delegate.finish() + + def on_connection_close(self) -> None: + return self._delegate.on_connection_close() + + +class HTTP1ServerConnection(object): + """An HTTP/1.x server.""" + + def __init__( + self, + stream: iostream.IOStream, + params: Optional[HTTP1ConnectionParameters] = None, + context: Optional[object] = None, + ) -> None: + """ + :arg stream: an `.IOStream` + :arg params: a `.HTTP1ConnectionParameters` or None + :arg context: an opaque application-defined object that is accessible + as ``connection.context`` + """ + self.stream = stream + if params is None: + params = HTTP1ConnectionParameters() + self.params = params + self.context = context + self._serving_future = None # type: Optional[Future[None]] + + async def close(self) -> None: + """Closes the connection. + + Returns a `.Future` that resolves after the serving loop has exited. + """ + self.stream.close() + # Block until the serving loop is done, but ignore any exceptions + # (start_serving is already responsible for logging them). + assert self._serving_future is not None + try: + await self._serving_future + except Exception: + pass + + def start_serving(self, delegate: httputil.HTTPServerConnectionDelegate) -> None: + """Starts serving requests on this connection. + + :arg delegate: a `.HTTPServerConnectionDelegate` + """ + assert isinstance(delegate, httputil.HTTPServerConnectionDelegate) + fut = gen.convert_yielded(self._server_request_loop(delegate)) + self._serving_future = fut + # Register the future on the IOLoop so its errors get logged. + self.stream.io_loop.add_future(fut, lambda f: f.result()) + + async def _server_request_loop( + self, delegate: httputil.HTTPServerConnectionDelegate + ) -> None: + try: + while True: + conn = HTTP1Connection(self.stream, False, self.params, self.context) + request_delegate = delegate.start_request(self, conn) + try: + ret = await conn.read_response(request_delegate) + except ( + iostream.StreamClosedError, + iostream.UnsatisfiableReadError, + asyncio.CancelledError, + ): + return + except _QuietException: + # This exception was already logged. + conn.close() + return + except Exception: + gen_log.error("Uncaught exception", exc_info=True) + conn.close() + return + if not ret: + return + await asyncio.sleep(0) + finally: + delegate.on_close(self) diff --git a/telegramer/include/tornado/httpclient.py b/telegramer/include/tornado/httpclient.py new file mode 100644 index 0000000..3011c37 --- /dev/null +++ b/telegramer/include/tornado/httpclient.py @@ -0,0 +1,790 @@ +"""Blocking and non-blocking HTTP client interfaces. + +This module defines a common interface shared by two implementations, +``simple_httpclient`` and ``curl_httpclient``. Applications may either +instantiate their chosen implementation class directly or use the +`AsyncHTTPClient` class from this module, which selects an implementation +that can be overridden with the `AsyncHTTPClient.configure` method. + +The default implementation is ``simple_httpclient``, and this is expected +to be suitable for most users' needs. However, some applications may wish +to switch to ``curl_httpclient`` for reasons such as the following: + +* ``curl_httpclient`` has some features not found in ``simple_httpclient``, + including support for HTTP proxies and the ability to use a specified + network interface. + +* ``curl_httpclient`` is more likely to be compatible with sites that are + not-quite-compliant with the HTTP spec, or sites that use little-exercised + features of HTTP. + +* ``curl_httpclient`` is faster. + +Note that if you are using ``curl_httpclient``, it is highly +recommended that you use a recent version of ``libcurl`` and +``pycurl``. Currently the minimum supported version of libcurl is +7.22.0, and the minimum version of pycurl is 7.18.2. It is highly +recommended that your ``libcurl`` installation is built with +asynchronous DNS resolver (threaded or c-ares), otherwise you may +encounter various problems with request timeouts (for more +information, see +http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCONNECTTIMEOUTMS +and comments in curl_httpclient.py). + +To select ``curl_httpclient``, call `AsyncHTTPClient.configure` at startup:: + + AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient") +""" + +import datetime +import functools +from io import BytesIO +import ssl +import time +import weakref + +from tornado.concurrent import ( + Future, + future_set_result_unless_cancelled, + future_set_exception_unless_cancelled, +) +from tornado.escape import utf8, native_str +from tornado import gen, httputil +from tornado.ioloop import IOLoop +from tornado.util import Configurable + +from typing import Type, Any, Union, Dict, Callable, Optional, cast + + +class HTTPClient(object): + """A blocking HTTP client. + + This interface is provided to make it easier to share code between + synchronous and asynchronous applications. Applications that are + running an `.IOLoop` must use `AsyncHTTPClient` instead. + + Typical usage looks like this:: + + http_client = httpclient.HTTPClient() + try: + response = http_client.fetch("http://www.google.com/") + print(response.body) + except httpclient.HTTPError as e: + # HTTPError is raised for non-200 responses; the response + # can be found in e.response. + print("Error: " + str(e)) + except Exception as e: + # Other errors are possible, such as IOError. + print("Error: " + str(e)) + http_client.close() + + .. versionchanged:: 5.0 + + Due to limitations in `asyncio`, it is no longer possible to + use the synchronous ``HTTPClient`` while an `.IOLoop` is running. + Use `AsyncHTTPClient` instead. + + """ + + def __init__( + self, + async_client_class: "Optional[Type[AsyncHTTPClient]]" = None, + **kwargs: Any + ) -> None: + # Initialize self._closed at the beginning of the constructor + # so that an exception raised here doesn't lead to confusing + # failures in __del__. + self._closed = True + self._io_loop = IOLoop(make_current=False) + if async_client_class is None: + async_client_class = AsyncHTTPClient + + # Create the client while our IOLoop is "current", without + # clobbering the thread's real current IOLoop (if any). + async def make_client() -> "AsyncHTTPClient": + await gen.sleep(0) + assert async_client_class is not None + return async_client_class(**kwargs) + + self._async_client = self._io_loop.run_sync(make_client) + self._closed = False + + def __del__(self) -> None: + self.close() + + def close(self) -> None: + """Closes the HTTPClient, freeing any resources used.""" + if not self._closed: + self._async_client.close() + self._io_loop.close() + self._closed = True + + def fetch( + self, request: Union["HTTPRequest", str], **kwargs: Any + ) -> "HTTPResponse": + """Executes a request, returning an `HTTPResponse`. + + The request may be either a string URL or an `HTTPRequest` object. + If it is a string, we construct an `HTTPRequest` using any additional + kwargs: ``HTTPRequest(request, **kwargs)`` + + If an error occurs during the fetch, we raise an `HTTPError` unless + the ``raise_error`` keyword argument is set to False. + """ + response = self._io_loop.run_sync( + functools.partial(self._async_client.fetch, request, **kwargs) + ) + return response + + +class AsyncHTTPClient(Configurable): + """An non-blocking HTTP client. + + Example usage:: + + async def f(): + http_client = AsyncHTTPClient() + try: + response = await http_client.fetch("http://www.google.com") + except Exception as e: + print("Error: %s" % e) + else: + print(response.body) + + The constructor for this class is magic in several respects: It + actually creates an instance of an implementation-specific + subclass, and instances are reused as a kind of pseudo-singleton + (one per `.IOLoop`). The keyword argument ``force_instance=True`` + can be used to suppress this singleton behavior. Unless + ``force_instance=True`` is used, no arguments should be passed to + the `AsyncHTTPClient` constructor. The implementation subclass as + well as arguments to its constructor can be set with the static + method `configure()` + + All `AsyncHTTPClient` implementations support a ``defaults`` + keyword argument, which can be used to set default values for + `HTTPRequest` attributes. For example:: + + AsyncHTTPClient.configure( + None, defaults=dict(user_agent="MyUserAgent")) + # or with force_instance: + client = AsyncHTTPClient(force_instance=True, + defaults=dict(user_agent="MyUserAgent")) + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + + """ + + _instance_cache = None # type: Dict[IOLoop, AsyncHTTPClient] + + @classmethod + def configurable_base(cls) -> Type[Configurable]: + return AsyncHTTPClient + + @classmethod + def configurable_default(cls) -> Type[Configurable]: + from tornado.simple_httpclient import SimpleAsyncHTTPClient + + return SimpleAsyncHTTPClient + + @classmethod + def _async_clients(cls) -> Dict[IOLoop, "AsyncHTTPClient"]: + attr_name = "_async_client_dict_" + cls.__name__ + if not hasattr(cls, attr_name): + setattr(cls, attr_name, weakref.WeakKeyDictionary()) + return getattr(cls, attr_name) + + def __new__(cls, force_instance: bool = False, **kwargs: Any) -> "AsyncHTTPClient": + io_loop = IOLoop.current() + if force_instance: + instance_cache = None + else: + instance_cache = cls._async_clients() + if instance_cache is not None and io_loop in instance_cache: + return instance_cache[io_loop] + instance = super(AsyncHTTPClient, cls).__new__(cls, **kwargs) # type: ignore + # Make sure the instance knows which cache to remove itself from. + # It can't simply call _async_clients() because we may be in + # __new__(AsyncHTTPClient) but instance.__class__ may be + # SimpleAsyncHTTPClient. + instance._instance_cache = instance_cache + if instance_cache is not None: + instance_cache[instance.io_loop] = instance + return instance + + def initialize(self, defaults: Optional[Dict[str, Any]] = None) -> None: + self.io_loop = IOLoop.current() + self.defaults = dict(HTTPRequest._DEFAULTS) + if defaults is not None: + self.defaults.update(defaults) + self._closed = False + + def close(self) -> None: + """Destroys this HTTP client, freeing any file descriptors used. + + This method is **not needed in normal use** due to the way + that `AsyncHTTPClient` objects are transparently reused. + ``close()`` is generally only necessary when either the + `.IOLoop` is also being closed, or the ``force_instance=True`` + argument was used when creating the `AsyncHTTPClient`. + + No other methods may be called on the `AsyncHTTPClient` after + ``close()``. + + """ + if self._closed: + return + self._closed = True + if self._instance_cache is not None: + cached_val = self._instance_cache.pop(self.io_loop, None) + # If there's an object other than self in the instance + # cache for our IOLoop, something has gotten mixed up. A + # value of None appears to be possible when this is called + # from a destructor (HTTPClient.__del__) as the weakref + # gets cleared before the destructor runs. + if cached_val is not None and cached_val is not self: + raise RuntimeError("inconsistent AsyncHTTPClient cache") + + def fetch( + self, + request: Union[str, "HTTPRequest"], + raise_error: bool = True, + **kwargs: Any + ) -> "Future[HTTPResponse]": + """Executes a request, asynchronously returning an `HTTPResponse`. + + The request may be either a string URL or an `HTTPRequest` object. + If it is a string, we construct an `HTTPRequest` using any additional + kwargs: ``HTTPRequest(request, **kwargs)`` + + This method returns a `.Future` whose result is an + `HTTPResponse`. By default, the ``Future`` will raise an + `HTTPError` if the request returned a non-200 response code + (other errors may also be raised if the server could not be + contacted). Instead, if ``raise_error`` is set to False, the + response will always be returned regardless of the response + code. + + If a ``callback`` is given, it will be invoked with the `HTTPResponse`. + In the callback interface, `HTTPError` is not automatically raised. + Instead, you must check the response's ``error`` attribute or + call its `~HTTPResponse.rethrow` method. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + `.Future` instead. + + The ``raise_error=False`` argument only affects the + `HTTPError` raised when a non-200 response code is used, + instead of suppressing all errors. + """ + if self._closed: + raise RuntimeError("fetch() called on closed AsyncHTTPClient") + if not isinstance(request, HTTPRequest): + request = HTTPRequest(url=request, **kwargs) + else: + if kwargs: + raise ValueError( + "kwargs can't be used if request is an HTTPRequest object" + ) + # We may modify this (to add Host, Accept-Encoding, etc), + # so make sure we don't modify the caller's object. This is also + # where normal dicts get converted to HTTPHeaders objects. + request.headers = httputil.HTTPHeaders(request.headers) + request_proxy = _RequestProxy(request, self.defaults) + future = Future() # type: Future[HTTPResponse] + + def handle_response(response: "HTTPResponse") -> None: + if response.error: + if raise_error or not response._error_is_response_code: + future_set_exception_unless_cancelled(future, response.error) + return + future_set_result_unless_cancelled(future, response) + + self.fetch_impl(cast(HTTPRequest, request_proxy), handle_response) + return future + + def fetch_impl( + self, request: "HTTPRequest", callback: Callable[["HTTPResponse"], None] + ) -> None: + raise NotImplementedError() + + @classmethod + def configure( + cls, impl: "Union[None, str, Type[Configurable]]", **kwargs: Any + ) -> None: + """Configures the `AsyncHTTPClient` subclass to use. + + ``AsyncHTTPClient()`` actually creates an instance of a subclass. + This method may be called with either a class object or the + fully-qualified name of such a class (or ``None`` to use the default, + ``SimpleAsyncHTTPClient``) + + If additional keyword arguments are given, they will be passed + to the constructor of each subclass instance created. The + keyword argument ``max_clients`` determines the maximum number + of simultaneous `~AsyncHTTPClient.fetch()` operations that can + execute in parallel on each `.IOLoop`. Additional arguments + may be supported depending on the implementation class in use. + + Example:: + + AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient") + """ + super(AsyncHTTPClient, cls).configure(impl, **kwargs) + + +class HTTPRequest(object): + """HTTP client request object.""" + + _headers = None # type: Union[Dict[str, str], httputil.HTTPHeaders] + + # Default values for HTTPRequest parameters. + # Merged with the values on the request object by AsyncHTTPClient + # implementations. + _DEFAULTS = dict( + connect_timeout=20.0, + request_timeout=20.0, + follow_redirects=True, + max_redirects=5, + decompress_response=True, + proxy_password="", + allow_nonstandard_methods=False, + validate_cert=True, + ) + + def __init__( + self, + url: str, + method: str = "GET", + headers: Optional[Union[Dict[str, str], httputil.HTTPHeaders]] = None, + body: Optional[Union[bytes, str]] = None, + auth_username: Optional[str] = None, + auth_password: Optional[str] = None, + auth_mode: Optional[str] = None, + connect_timeout: Optional[float] = None, + request_timeout: Optional[float] = None, + if_modified_since: Optional[Union[float, datetime.datetime]] = None, + follow_redirects: Optional[bool] = None, + max_redirects: Optional[int] = None, + user_agent: Optional[str] = None, + use_gzip: Optional[bool] = None, + network_interface: Optional[str] = None, + streaming_callback: Optional[Callable[[bytes], None]] = None, + header_callback: Optional[Callable[[str], None]] = None, + prepare_curl_callback: Optional[Callable[[Any], None]] = None, + proxy_host: Optional[str] = None, + proxy_port: Optional[int] = None, + proxy_username: Optional[str] = None, + proxy_password: Optional[str] = None, + proxy_auth_mode: Optional[str] = None, + allow_nonstandard_methods: Optional[bool] = None, + validate_cert: Optional[bool] = None, + ca_certs: Optional[str] = None, + allow_ipv6: Optional[bool] = None, + client_key: Optional[str] = None, + client_cert: Optional[str] = None, + body_producer: Optional[ + Callable[[Callable[[bytes], None]], "Future[None]"] + ] = None, + expect_100_continue: bool = False, + decompress_response: Optional[bool] = None, + ssl_options: Optional[Union[Dict[str, Any], ssl.SSLContext]] = None, + ) -> None: + r"""All parameters except ``url`` are optional. + + :arg str url: URL to fetch + :arg str method: HTTP method, e.g. "GET" or "POST" + :arg headers: Additional HTTP headers to pass on the request + :type headers: `~tornado.httputil.HTTPHeaders` or `dict` + :arg body: HTTP request body as a string (byte or unicode; if unicode + the utf-8 encoding will be used) + :type body: `str` or `bytes` + :arg collections.abc.Callable body_producer: Callable used for + lazy/asynchronous request bodies. + It is called with one argument, a ``write`` function, and should + return a `.Future`. It should call the write function with new + data as it becomes available. The write function returns a + `.Future` which can be used for flow control. + Only one of ``body`` and ``body_producer`` may + be specified. ``body_producer`` is not supported on + ``curl_httpclient``. When using ``body_producer`` it is recommended + to pass a ``Content-Length`` in the headers as otherwise chunked + encoding will be used, and many servers do not support chunked + encoding on requests. New in Tornado 4.0 + :arg str auth_username: Username for HTTP authentication + :arg str auth_password: Password for HTTP authentication + :arg str auth_mode: Authentication mode; default is "basic". + Allowed values are implementation-defined; ``curl_httpclient`` + supports "basic" and "digest"; ``simple_httpclient`` only supports + "basic" + :arg float connect_timeout: Timeout for initial connection in seconds, + default 20 seconds (0 means no timeout) + :arg float request_timeout: Timeout for entire request in seconds, + default 20 seconds (0 means no timeout) + :arg if_modified_since: Timestamp for ``If-Modified-Since`` header + :type if_modified_since: `datetime` or `float` + :arg bool follow_redirects: Should redirects be followed automatically + or return the 3xx response? Default True. + :arg int max_redirects: Limit for ``follow_redirects``, default 5. + :arg str user_agent: String to send as ``User-Agent`` header + :arg bool decompress_response: Request a compressed response from + the server and decompress it after downloading. Default is True. + New in Tornado 4.0. + :arg bool use_gzip: Deprecated alias for ``decompress_response`` + since Tornado 4.0. + :arg str network_interface: Network interface or source IP to use for request. + See ``curl_httpclient`` note below. + :arg collections.abc.Callable streaming_callback: If set, ``streaming_callback`` will + be run with each chunk of data as it is received, and + ``HTTPResponse.body`` and ``HTTPResponse.buffer`` will be empty in + the final response. + :arg collections.abc.Callable header_callback: If set, ``header_callback`` will + be run with each header line as it is received (including the + first line, e.g. ``HTTP/1.0 200 OK\r\n``, and a final line + containing only ``\r\n``. All lines include the trailing newline + characters). ``HTTPResponse.headers`` will be empty in the final + response. This is most useful in conjunction with + ``streaming_callback``, because it's the only way to get access to + header data while the request is in progress. + :arg collections.abc.Callable prepare_curl_callback: If set, will be called with + a ``pycurl.Curl`` object to allow the application to make additional + ``setopt`` calls. + :arg str proxy_host: HTTP proxy hostname. To use proxies, + ``proxy_host`` and ``proxy_port`` must be set; ``proxy_username``, + ``proxy_pass`` and ``proxy_auth_mode`` are optional. Proxies are + currently only supported with ``curl_httpclient``. + :arg int proxy_port: HTTP proxy port + :arg str proxy_username: HTTP proxy username + :arg str proxy_password: HTTP proxy password + :arg str proxy_auth_mode: HTTP proxy Authentication mode; + default is "basic". supports "basic" and "digest" + :arg bool allow_nonstandard_methods: Allow unknown values for ``method`` + argument? Default is False. + :arg bool validate_cert: For HTTPS requests, validate the server's + certificate? Default is True. + :arg str ca_certs: filename of CA certificates in PEM format, + or None to use defaults. See note below when used with + ``curl_httpclient``. + :arg str client_key: Filename for client SSL key, if any. See + note below when used with ``curl_httpclient``. + :arg str client_cert: Filename for client SSL certificate, if any. + See note below when used with ``curl_httpclient``. + :arg ssl.SSLContext ssl_options: `ssl.SSLContext` object for use in + ``simple_httpclient`` (unsupported by ``curl_httpclient``). + Overrides ``validate_cert``, ``ca_certs``, ``client_key``, + and ``client_cert``. + :arg bool allow_ipv6: Use IPv6 when available? Default is True. + :arg bool expect_100_continue: If true, send the + ``Expect: 100-continue`` header and wait for a continue response + before sending the request body. Only supported with + ``simple_httpclient``. + + .. note:: + + When using ``curl_httpclient`` certain options may be + inherited by subsequent fetches because ``pycurl`` does + not allow them to be cleanly reset. This applies to the + ``ca_certs``, ``client_key``, ``client_cert``, and + ``network_interface`` arguments. If you use these + options, you should pass them on every request (you don't + have to always use the same values, but it's not possible + to mix requests that specify these options with ones that + use the defaults). + + .. versionadded:: 3.1 + The ``auth_mode`` argument. + + .. versionadded:: 4.0 + The ``body_producer`` and ``expect_100_continue`` arguments. + + .. versionadded:: 4.2 + The ``ssl_options`` argument. + + .. versionadded:: 4.5 + The ``proxy_auth_mode`` argument. + """ + # Note that some of these attributes go through property setters + # defined below. + self.headers = headers # type: ignore + if if_modified_since: + self.headers["If-Modified-Since"] = httputil.format_timestamp( + if_modified_since + ) + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_username = proxy_username + self.proxy_password = proxy_password + self.proxy_auth_mode = proxy_auth_mode + self.url = url + self.method = method + self.body = body # type: ignore + self.body_producer = body_producer + self.auth_username = auth_username + self.auth_password = auth_password + self.auth_mode = auth_mode + self.connect_timeout = connect_timeout + self.request_timeout = request_timeout + self.follow_redirects = follow_redirects + self.max_redirects = max_redirects + self.user_agent = user_agent + if decompress_response is not None: + self.decompress_response = decompress_response # type: Optional[bool] + else: + self.decompress_response = use_gzip + self.network_interface = network_interface + self.streaming_callback = streaming_callback + self.header_callback = header_callback + self.prepare_curl_callback = prepare_curl_callback + self.allow_nonstandard_methods = allow_nonstandard_methods + self.validate_cert = validate_cert + self.ca_certs = ca_certs + self.allow_ipv6 = allow_ipv6 + self.client_key = client_key + self.client_cert = client_cert + self.ssl_options = ssl_options + self.expect_100_continue = expect_100_continue + self.start_time = time.time() + + @property + def headers(self) -> httputil.HTTPHeaders: + # TODO: headers may actually be a plain dict until fairly late in + # the process (AsyncHTTPClient.fetch), but practically speaking, + # whenever the property is used they're already HTTPHeaders. + return self._headers # type: ignore + + @headers.setter + def headers(self, value: Union[Dict[str, str], httputil.HTTPHeaders]) -> None: + if value is None: + self._headers = httputil.HTTPHeaders() + else: + self._headers = value # type: ignore + + @property + def body(self) -> bytes: + return self._body + + @body.setter + def body(self, value: Union[bytes, str]) -> None: + self._body = utf8(value) + + +class HTTPResponse(object): + """HTTP Response object. + + Attributes: + + * ``request``: HTTPRequest object + + * ``code``: numeric HTTP status code, e.g. 200 or 404 + + * ``reason``: human-readable reason phrase describing the status code + + * ``headers``: `tornado.httputil.HTTPHeaders` object + + * ``effective_url``: final location of the resource after following any + redirects + + * ``buffer``: ``cStringIO`` object for response body + + * ``body``: response body as bytes (created on demand from ``self.buffer``) + + * ``error``: Exception object, if any + + * ``request_time``: seconds from request start to finish. Includes all + network operations from DNS resolution to receiving the last byte of + data. Does not include time spent in the queue (due to the + ``max_clients`` option). If redirects were followed, only includes + the final request. + + * ``start_time``: Time at which the HTTP operation started, based on + `time.time` (not the monotonic clock used by `.IOLoop.time`). May + be ``None`` if the request timed out while in the queue. + + * ``time_info``: dictionary of diagnostic timing information from the + request. Available data are subject to change, but currently uses timings + available from http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html, + plus ``queue``, which is the delay (if any) introduced by waiting for + a slot under `AsyncHTTPClient`'s ``max_clients`` setting. + + .. versionadded:: 5.1 + + Added the ``start_time`` attribute. + + .. versionchanged:: 5.1 + + The ``request_time`` attribute previously included time spent in the queue + for ``simple_httpclient``, but not in ``curl_httpclient``. Now queueing time + is excluded in both implementations. ``request_time`` is now more accurate for + ``curl_httpclient`` because it uses a monotonic clock when available. + """ + + # I'm not sure why these don't get type-inferred from the references in __init__. + error = None # type: Optional[BaseException] + _error_is_response_code = False + request = None # type: HTTPRequest + + def __init__( + self, + request: HTTPRequest, + code: int, + headers: Optional[httputil.HTTPHeaders] = None, + buffer: Optional[BytesIO] = None, + effective_url: Optional[str] = None, + error: Optional[BaseException] = None, + request_time: Optional[float] = None, + time_info: Optional[Dict[str, float]] = None, + reason: Optional[str] = None, + start_time: Optional[float] = None, + ) -> None: + if isinstance(request, _RequestProxy): + self.request = request.request + else: + self.request = request + self.code = code + self.reason = reason or httputil.responses.get(code, "Unknown") + if headers is not None: + self.headers = headers + else: + self.headers = httputil.HTTPHeaders() + self.buffer = buffer + self._body = None # type: Optional[bytes] + if effective_url is None: + self.effective_url = request.url + else: + self.effective_url = effective_url + self._error_is_response_code = False + if error is None: + if self.code < 200 or self.code >= 300: + self._error_is_response_code = True + self.error = HTTPError(self.code, message=self.reason, response=self) + else: + self.error = None + else: + self.error = error + self.start_time = start_time + self.request_time = request_time + self.time_info = time_info or {} + + @property + def body(self) -> bytes: + if self.buffer is None: + return b"" + elif self._body is None: + self._body = self.buffer.getvalue() + + return self._body + + def rethrow(self) -> None: + """If there was an error on the request, raise an `HTTPError`.""" + if self.error: + raise self.error + + def __repr__(self) -> str: + args = ",".join("%s=%r" % i for i in sorted(self.__dict__.items())) + return "%s(%s)" % (self.__class__.__name__, args) + + +class HTTPClientError(Exception): + """Exception thrown for an unsuccessful HTTP request. + + Attributes: + + * ``code`` - HTTP error integer error code, e.g. 404. Error code 599 is + used when no HTTP response was received, e.g. for a timeout. + + * ``response`` - `HTTPResponse` object, if any. + + Note that if ``follow_redirects`` is False, redirects become HTTPErrors, + and you can look at ``error.response.headers['Location']`` to see the + destination of the redirect. + + .. versionchanged:: 5.1 + + Renamed from ``HTTPError`` to ``HTTPClientError`` to avoid collisions with + `tornado.web.HTTPError`. The name ``tornado.httpclient.HTTPError`` remains + as an alias. + """ + + def __init__( + self, + code: int, + message: Optional[str] = None, + response: Optional[HTTPResponse] = None, + ) -> None: + self.code = code + self.message = message or httputil.responses.get(code, "Unknown") + self.response = response + super().__init__(code, message, response) + + def __str__(self) -> str: + return "HTTP %d: %s" % (self.code, self.message) + + # There is a cyclic reference between self and self.response, + # which breaks the default __repr__ implementation. + # (especially on pypy, which doesn't have the same recursion + # detection as cpython). + __repr__ = __str__ + + +HTTPError = HTTPClientError + + +class _RequestProxy(object): + """Combines an object with a dictionary of defaults. + + Used internally by AsyncHTTPClient implementations. + """ + + def __init__( + self, request: HTTPRequest, defaults: Optional[Dict[str, Any]] + ) -> None: + self.request = request + self.defaults = defaults + + def __getattr__(self, name: str) -> Any: + request_attr = getattr(self.request, name) + if request_attr is not None: + return request_attr + elif self.defaults is not None: + return self.defaults.get(name, None) + else: + return None + + +def main() -> None: + from tornado.options import define, options, parse_command_line + + define("print_headers", type=bool, default=False) + define("print_body", type=bool, default=True) + define("follow_redirects", type=bool, default=True) + define("validate_cert", type=bool, default=True) + define("proxy_host", type=str) + define("proxy_port", type=int) + args = parse_command_line() + client = HTTPClient() + for arg in args: + try: + response = client.fetch( + arg, + follow_redirects=options.follow_redirects, + validate_cert=options.validate_cert, + proxy_host=options.proxy_host, + proxy_port=options.proxy_port, + ) + except HTTPError as e: + if e.response is not None: + response = e.response + else: + raise + if options.print_headers: + print(response.headers) + if options.print_body: + print(native_str(response.body)) + client.close() + + +if __name__ == "__main__": + main() diff --git a/telegramer/include/tornado/httpserver.py b/telegramer/include/tornado/httpserver.py new file mode 100644 index 0000000..cd4a468 --- /dev/null +++ b/telegramer/include/tornado/httpserver.py @@ -0,0 +1,398 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A non-blocking, single-threaded HTTP server. + +Typical applications have little direct interaction with the `HTTPServer` +class except to start a server at the beginning of the process +(and even that is often done indirectly via `tornado.web.Application.listen`). + +.. versionchanged:: 4.0 + + The ``HTTPRequest`` class that used to live in this module has been moved + to `tornado.httputil.HTTPServerRequest`. The old name remains as an alias. +""" + +import socket +import ssl + +from tornado.escape import native_str +from tornado.http1connection import HTTP1ServerConnection, HTTP1ConnectionParameters +from tornado import httputil +from tornado import iostream +from tornado import netutil +from tornado.tcpserver import TCPServer +from tornado.util import Configurable + +import typing +from typing import Union, Any, Dict, Callable, List, Type, Tuple, Optional, Awaitable + +if typing.TYPE_CHECKING: + from typing import Set # noqa: F401 + + +class HTTPServer(TCPServer, Configurable, httputil.HTTPServerConnectionDelegate): + r"""A non-blocking, single-threaded HTTP server. + + A server is defined by a subclass of `.HTTPServerConnectionDelegate`, + or, for backwards compatibility, a callback that takes an + `.HTTPServerRequest` as an argument. The delegate is usually a + `tornado.web.Application`. + + `HTTPServer` supports keep-alive connections by default + (automatically for HTTP/1.1, or for HTTP/1.0 when the client + requests ``Connection: keep-alive``). + + If ``xheaders`` is ``True``, we support the + ``X-Real-Ip``/``X-Forwarded-For`` and + ``X-Scheme``/``X-Forwarded-Proto`` headers, which override the + remote IP and URI scheme/protocol for all requests. These headers + are useful when running Tornado behind a reverse proxy or load + balancer. The ``protocol`` argument can also be set to ``https`` + if Tornado is run behind an SSL-decoding proxy that does not set one of + the supported ``xheaders``. + + By default, when parsing the ``X-Forwarded-For`` header, Tornado will + select the last (i.e., the closest) address on the list of hosts as the + remote host IP address. To select the next server in the chain, a list of + trusted downstream hosts may be passed as the ``trusted_downstream`` + argument. These hosts will be skipped when parsing the ``X-Forwarded-For`` + header. + + To make this server serve SSL traffic, send the ``ssl_options`` keyword + argument with an `ssl.SSLContext` object. For compatibility with older + versions of Python ``ssl_options`` may also be a dictionary of keyword + arguments for the `ssl.wrap_socket` method.:: + + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"), + os.path.join(data_dir, "mydomain.key")) + HTTPServer(application, ssl_options=ssl_ctx) + + `HTTPServer` initialization follows one of three patterns (the + initialization methods are defined on `tornado.tcpserver.TCPServer`): + + 1. `~tornado.tcpserver.TCPServer.listen`: simple single-process:: + + server = HTTPServer(app) + server.listen(8888) + IOLoop.current().start() + + In many cases, `tornado.web.Application.listen` can be used to avoid + the need to explicitly create the `HTTPServer`. + + 2. `~tornado.tcpserver.TCPServer.bind`/`~tornado.tcpserver.TCPServer.start`: + simple multi-process:: + + server = HTTPServer(app) + server.bind(8888) + server.start(0) # Forks multiple sub-processes + IOLoop.current().start() + + When using this interface, an `.IOLoop` must *not* be passed + to the `HTTPServer` constructor. `~.TCPServer.start` will always start + the server on the default singleton `.IOLoop`. + + 3. `~tornado.tcpserver.TCPServer.add_sockets`: advanced multi-process:: + + sockets = tornado.netutil.bind_sockets(8888) + tornado.process.fork_processes(0) + server = HTTPServer(app) + server.add_sockets(sockets) + IOLoop.current().start() + + The `~.TCPServer.add_sockets` interface is more complicated, + but it can be used with `tornado.process.fork_processes` to + give you more flexibility in when the fork happens. + `~.TCPServer.add_sockets` can also be used in single-process + servers if you want to create your listening sockets in some + way other than `tornado.netutil.bind_sockets`. + + .. versionchanged:: 4.0 + Added ``decompress_request``, ``chunk_size``, ``max_header_size``, + ``idle_connection_timeout``, ``body_timeout``, ``max_body_size`` + arguments. Added support for `.HTTPServerConnectionDelegate` + instances as ``request_callback``. + + .. versionchanged:: 4.1 + `.HTTPServerConnectionDelegate.start_request` is now called with + two arguments ``(server_conn, request_conn)`` (in accordance with the + documentation) instead of one ``(request_conn)``. + + .. versionchanged:: 4.2 + `HTTPServer` is now a subclass of `tornado.util.Configurable`. + + .. versionchanged:: 4.5 + Added the ``trusted_downstream`` argument. + + .. versionchanged:: 5.0 + The ``io_loop`` argument has been removed. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + # Ignore args to __init__; real initialization belongs in + # initialize since we're Configurable. (there's something + # weird in initialization order between this class, + # Configurable, and TCPServer so we can't leave __init__ out + # completely) + pass + + def initialize( + self, + request_callback: Union[ + httputil.HTTPServerConnectionDelegate, + Callable[[httputil.HTTPServerRequest], None], + ], + no_keep_alive: bool = False, + xheaders: bool = False, + ssl_options: Optional[Union[Dict[str, Any], ssl.SSLContext]] = None, + protocol: Optional[str] = None, + decompress_request: bool = False, + chunk_size: Optional[int] = None, + max_header_size: Optional[int] = None, + idle_connection_timeout: Optional[float] = None, + body_timeout: Optional[float] = None, + max_body_size: Optional[int] = None, + max_buffer_size: Optional[int] = None, + trusted_downstream: Optional[List[str]] = None, + ) -> None: + # This method's signature is not extracted with autodoc + # because we want its arguments to appear on the class + # constructor. When changing this signature, also update the + # copy in httpserver.rst. + self.request_callback = request_callback + self.xheaders = xheaders + self.protocol = protocol + self.conn_params = HTTP1ConnectionParameters( + decompress=decompress_request, + chunk_size=chunk_size, + max_header_size=max_header_size, + header_timeout=idle_connection_timeout or 3600, + max_body_size=max_body_size, + body_timeout=body_timeout, + no_keep_alive=no_keep_alive, + ) + TCPServer.__init__( + self, + ssl_options=ssl_options, + max_buffer_size=max_buffer_size, + read_chunk_size=chunk_size, + ) + self._connections = set() # type: Set[HTTP1ServerConnection] + self.trusted_downstream = trusted_downstream + + @classmethod + def configurable_base(cls) -> Type[Configurable]: + return HTTPServer + + @classmethod + def configurable_default(cls) -> Type[Configurable]: + return HTTPServer + + async def close_all_connections(self) -> None: + """Close all open connections and asynchronously wait for them to finish. + + This method is used in combination with `~.TCPServer.stop` to + support clean shutdowns (especially for unittests). Typical + usage would call ``stop()`` first to stop accepting new + connections, then ``await close_all_connections()`` to wait for + existing connections to finish. + + This method does not currently close open websocket connections. + + Note that this method is a coroutine and must be called with ``await``. + + """ + while self._connections: + # Peek at an arbitrary element of the set + conn = next(iter(self._connections)) + await conn.close() + + def handle_stream(self, stream: iostream.IOStream, address: Tuple) -> None: + context = _HTTPRequestContext( + stream, address, self.protocol, self.trusted_downstream + ) + conn = HTTP1ServerConnection(stream, self.conn_params, context) + self._connections.add(conn) + conn.start_serving(self) + + def start_request( + self, server_conn: object, request_conn: httputil.HTTPConnection + ) -> httputil.HTTPMessageDelegate: + if isinstance(self.request_callback, httputil.HTTPServerConnectionDelegate): + delegate = self.request_callback.start_request(server_conn, request_conn) + else: + delegate = _CallableAdapter(self.request_callback, request_conn) + + if self.xheaders: + delegate = _ProxyAdapter(delegate, request_conn) + + return delegate + + def on_close(self, server_conn: object) -> None: + self._connections.remove(typing.cast(HTTP1ServerConnection, server_conn)) + + +class _CallableAdapter(httputil.HTTPMessageDelegate): + def __init__( + self, + request_callback: Callable[[httputil.HTTPServerRequest], None], + request_conn: httputil.HTTPConnection, + ) -> None: + self.connection = request_conn + self.request_callback = request_callback + self.request = None # type: Optional[httputil.HTTPServerRequest] + self.delegate = None + self._chunks = [] # type: List[bytes] + + def headers_received( + self, + start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine], + headers: httputil.HTTPHeaders, + ) -> Optional[Awaitable[None]]: + self.request = httputil.HTTPServerRequest( + connection=self.connection, + start_line=typing.cast(httputil.RequestStartLine, start_line), + headers=headers, + ) + return None + + def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: + self._chunks.append(chunk) + return None + + def finish(self) -> None: + assert self.request is not None + self.request.body = b"".join(self._chunks) + self.request._parse_body() + self.request_callback(self.request) + + def on_connection_close(self) -> None: + del self._chunks + + +class _HTTPRequestContext(object): + def __init__( + self, + stream: iostream.IOStream, + address: Tuple, + protocol: Optional[str], + trusted_downstream: Optional[List[str]] = None, + ) -> None: + self.address = address + # Save the socket's address family now so we know how to + # interpret self.address even after the stream is closed + # and its socket attribute replaced with None. + if stream.socket is not None: + self.address_family = stream.socket.family + else: + self.address_family = None + # In HTTPServerRequest we want an IP, not a full socket address. + if ( + self.address_family in (socket.AF_INET, socket.AF_INET6) + and address is not None + ): + self.remote_ip = address[0] + else: + # Unix (or other) socket; fake the remote address. + self.remote_ip = "0.0.0.0" + if protocol: + self.protocol = protocol + elif isinstance(stream, iostream.SSLIOStream): + self.protocol = "https" + else: + self.protocol = "http" + self._orig_remote_ip = self.remote_ip + self._orig_protocol = self.protocol + self.trusted_downstream = set(trusted_downstream or []) + + def __str__(self) -> str: + if self.address_family in (socket.AF_INET, socket.AF_INET6): + return self.remote_ip + elif isinstance(self.address, bytes): + # Python 3 with the -bb option warns about str(bytes), + # so convert it explicitly. + # Unix socket addresses are str on mac but bytes on linux. + return native_str(self.address) + else: + return str(self.address) + + def _apply_xheaders(self, headers: httputil.HTTPHeaders) -> None: + """Rewrite the ``remote_ip`` and ``protocol`` fields.""" + # Squid uses X-Forwarded-For, others use X-Real-Ip + ip = headers.get("X-Forwarded-For", self.remote_ip) + # Skip trusted downstream hosts in X-Forwarded-For list + for ip in (cand.strip() for cand in reversed(ip.split(","))): + if ip not in self.trusted_downstream: + break + ip = headers.get("X-Real-Ip", ip) + if netutil.is_valid_ip(ip): + self.remote_ip = ip + # AWS uses X-Forwarded-Proto + proto_header = headers.get( + "X-Scheme", headers.get("X-Forwarded-Proto", self.protocol) + ) + if proto_header: + # use only the last proto entry if there is more than one + # TODO: support trusting multiple layers of proxied protocol + proto_header = proto_header.split(",")[-1].strip() + if proto_header in ("http", "https"): + self.protocol = proto_header + + def _unapply_xheaders(self) -> None: + """Undo changes from `_apply_xheaders`. + + Xheaders are per-request so they should not leak to the next + request on the same connection. + """ + self.remote_ip = self._orig_remote_ip + self.protocol = self._orig_protocol + + +class _ProxyAdapter(httputil.HTTPMessageDelegate): + def __init__( + self, + delegate: httputil.HTTPMessageDelegate, + request_conn: httputil.HTTPConnection, + ) -> None: + self.connection = request_conn + self.delegate = delegate + + def headers_received( + self, + start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine], + headers: httputil.HTTPHeaders, + ) -> Optional[Awaitable[None]]: + # TODO: either make context an official part of the + # HTTPConnection interface or figure out some other way to do this. + self.connection.context._apply_xheaders(headers) # type: ignore + return self.delegate.headers_received(start_line, headers) + + def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: + return self.delegate.data_received(chunk) + + def finish(self) -> None: + self.delegate.finish() + self._cleanup() + + def on_connection_close(self) -> None: + self.delegate.on_connection_close() + self._cleanup() + + def _cleanup(self) -> None: + self.connection.context._unapply_xheaders() # type: ignore + + +HTTPRequest = httputil.HTTPServerRequest diff --git a/telegramer/include/tornado/httputil.py b/telegramer/include/tornado/httputil.py new file mode 100644 index 0000000..bd32cd0 --- /dev/null +++ b/telegramer/include/tornado/httputil.py @@ -0,0 +1,1133 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""HTTP utility code shared by clients and servers. + +This module also defines the `HTTPServerRequest` class which is exposed +via `tornado.web.RequestHandler.request`. +""" + +import calendar +import collections +import copy +import datetime +import email.utils +from functools import lru_cache +from http.client import responses +import http.cookies +import re +from ssl import SSLError +import time +import unicodedata +from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl + +from tornado.escape import native_str, parse_qs_bytes, utf8 +from tornado.log import gen_log +from tornado.util import ObjectDict, unicode_type + + +# responses is unused in this file, but we re-export it to other files. +# Reference it so pyflakes doesn't complain. +responses + +import typing +from typing import ( + Tuple, + Iterable, + List, + Mapping, + Iterator, + Dict, + Union, + Optional, + Awaitable, + Generator, + AnyStr, +) + +if typing.TYPE_CHECKING: + from typing import Deque # noqa: F401 + from asyncio import Future # noqa: F401 + import unittest # noqa: F401 + + +@lru_cache(1000) +def _normalize_header(name: str) -> str: + """Map a header name to Http-Header-Case. + + >>> _normalize_header("coNtent-TYPE") + 'Content-Type' + """ + return "-".join([w.capitalize() for w in name.split("-")]) + + +class HTTPHeaders(collections.abc.MutableMapping): + """A dictionary that maintains ``Http-Header-Case`` for all keys. + + Supports multiple values per key via a pair of new methods, + `add()` and `get_list()`. The regular dictionary interface + returns a single value per key, with multiple values joined by a + comma. + + >>> h = HTTPHeaders({"content-type": "text/html"}) + >>> list(h.keys()) + ['Content-Type'] + >>> h["Content-Type"] + 'text/html' + + >>> h.add("Set-Cookie", "A=B") + >>> h.add("Set-Cookie", "C=D") + >>> h["set-cookie"] + 'A=B,C=D' + >>> h.get_list("set-cookie") + ['A=B', 'C=D'] + + >>> for (k,v) in sorted(h.get_all()): + ... print('%s: %s' % (k,v)) + ... + Content-Type: text/html + Set-Cookie: A=B + Set-Cookie: C=D + """ + + @typing.overload + def __init__(self, __arg: Mapping[str, List[str]]) -> None: + pass + + @typing.overload # noqa: F811 + def __init__(self, __arg: Mapping[str, str]) -> None: + pass + + @typing.overload # noqa: F811 + def __init__(self, *args: Tuple[str, str]) -> None: + pass + + @typing.overload # noqa: F811 + def __init__(self, **kwargs: str) -> None: + pass + + def __init__(self, *args: typing.Any, **kwargs: str) -> None: # noqa: F811 + self._dict = {} # type: typing.Dict[str, str] + self._as_list = {} # type: typing.Dict[str, typing.List[str]] + self._last_key = None # type: Optional[str] + if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders): + # Copy constructor + for k, v in args[0].get_all(): + self.add(k, v) + else: + # Dict-style initialization + self.update(*args, **kwargs) + + # new public methods + + def add(self, name: str, value: str) -> None: + """Adds a new value for the given key.""" + norm_name = _normalize_header(name) + self._last_key = norm_name + if norm_name in self: + self._dict[norm_name] = ( + native_str(self[norm_name]) + "," + native_str(value) + ) + self._as_list[norm_name].append(value) + else: + self[norm_name] = value + + def get_list(self, name: str) -> List[str]: + """Returns all values for the given header as a list.""" + norm_name = _normalize_header(name) + return self._as_list.get(norm_name, []) + + def get_all(self) -> Iterable[Tuple[str, str]]: + """Returns an iterable of all (name, value) pairs. + + If a header has multiple values, multiple pairs will be + returned with the same name. + """ + for name, values in self._as_list.items(): + for value in values: + yield (name, value) + + def parse_line(self, line: str) -> None: + """Updates the dictionary with a single header line. + + >>> h = HTTPHeaders() + >>> h.parse_line("Content-Type: text/html") + >>> h.get('content-type') + 'text/html' + """ + if line[0].isspace(): + # continuation of a multi-line header + if self._last_key is None: + raise HTTPInputError("first header line cannot start with whitespace") + new_part = " " + line.lstrip() + self._as_list[self._last_key][-1] += new_part + self._dict[self._last_key] += new_part + else: + try: + name, value = line.split(":", 1) + except ValueError: + raise HTTPInputError("no colon in header line") + self.add(name, value.strip()) + + @classmethod + def parse(cls, headers: str) -> "HTTPHeaders": + """Returns a dictionary from HTTP header text. + + >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n") + >>> sorted(h.items()) + [('Content-Length', '42'), ('Content-Type', 'text/html')] + + .. versionchanged:: 5.1 + + Raises `HTTPInputError` on malformed headers instead of a + mix of `KeyError`, and `ValueError`. + + """ + h = cls() + # RFC 7230 section 3.5: a recipient MAY recognize a single LF as a line + # terminator and ignore any preceding CR. + for line in headers.split("\n"): + if line.endswith("\r"): + line = line[:-1] + if line: + h.parse_line(line) + return h + + # MutableMapping abstract method implementations. + + def __setitem__(self, name: str, value: str) -> None: + norm_name = _normalize_header(name) + self._dict[norm_name] = value + self._as_list[norm_name] = [value] + + def __getitem__(self, name: str) -> str: + return self._dict[_normalize_header(name)] + + def __delitem__(self, name: str) -> None: + norm_name = _normalize_header(name) + del self._dict[norm_name] + del self._as_list[norm_name] + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self) -> Iterator[typing.Any]: + return iter(self._dict) + + def copy(self) -> "HTTPHeaders": + # defined in dict but not in MutableMapping. + return HTTPHeaders(self) + + # Use our overridden copy method for the copy.copy module. + # This makes shallow copies one level deeper, but preserves + # the appearance that HTTPHeaders is a single container. + __copy__ = copy + + def __str__(self) -> str: + lines = [] + for name, value in self.get_all(): + lines.append("%s: %s\n" % (name, value)) + return "".join(lines) + + __unicode__ = __str__ + + +class HTTPServerRequest(object): + """A single HTTP request. + + All attributes are type `str` unless otherwise noted. + + .. attribute:: method + + HTTP request method, e.g. "GET" or "POST" + + .. attribute:: uri + + The requested uri. + + .. attribute:: path + + The path portion of `uri` + + .. attribute:: query + + The query portion of `uri` + + .. attribute:: version + + HTTP version specified in request, e.g. "HTTP/1.1" + + .. attribute:: headers + + `.HTTPHeaders` dictionary-like object for request headers. Acts like + a case-insensitive dictionary with additional methods for repeated + headers. + + .. attribute:: body + + Request body, if present, as a byte string. + + .. attribute:: remote_ip + + Client's IP address as a string. If ``HTTPServer.xheaders`` is set, + will pass along the real IP address provided by a load balancer + in the ``X-Real-Ip`` or ``X-Forwarded-For`` header. + + .. versionchanged:: 3.1 + The list format of ``X-Forwarded-For`` is now supported. + + .. attribute:: protocol + + The protocol used, either "http" or "https". If ``HTTPServer.xheaders`` + is set, will pass along the protocol used by a load balancer if + reported via an ``X-Scheme`` header. + + .. attribute:: host + + The requested hostname, usually taken from the ``Host`` header. + + .. attribute:: arguments + + GET/POST arguments are available in the arguments property, which + maps arguments names to lists of values (to support multiple values + for individual names). Names are of type `str`, while arguments + are byte strings. Note that this is different from + `.RequestHandler.get_argument`, which returns argument values as + unicode strings. + + .. attribute:: query_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the query string. + + .. versionadded:: 3.2 + + .. attribute:: body_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the request body. + + .. versionadded:: 3.2 + + .. attribute:: files + + File uploads are available in the files property, which maps file + names to lists of `.HTTPFile`. + + .. attribute:: connection + + An HTTP request is attached to a single HTTP connection, which can + be accessed through the "connection" attribute. Since connections + are typically kept open in HTTP/1.1, multiple requests can be handled + sequentially on a single connection. + + .. versionchanged:: 4.0 + Moved from ``tornado.httpserver.HTTPRequest``. + """ + + path = None # type: str + query = None # type: str + + # HACK: Used for stream_request_body + _body_future = None # type: Future[None] + + def __init__( + self, + method: Optional[str] = None, + uri: Optional[str] = None, + version: str = "HTTP/1.0", + headers: Optional[HTTPHeaders] = None, + body: Optional[bytes] = None, + host: Optional[str] = None, + files: Optional[Dict[str, List["HTTPFile"]]] = None, + connection: Optional["HTTPConnection"] = None, + start_line: Optional["RequestStartLine"] = None, + server_connection: Optional[object] = None, + ) -> None: + if start_line is not None: + method, uri, version = start_line + self.method = method + self.uri = uri + self.version = version + self.headers = headers or HTTPHeaders() + self.body = body or b"" + + # set remote IP and protocol + context = getattr(connection, "context", None) + self.remote_ip = getattr(context, "remote_ip", None) + self.protocol = getattr(context, "protocol", "http") + + self.host = host or self.headers.get("Host") or "127.0.0.1" + self.host_name = split_host_and_port(self.host.lower())[0] + self.files = files or {} + self.connection = connection + self.server_connection = server_connection + self._start_time = time.time() + self._finish_time = None + + if uri is not None: + self.path, sep, self.query = uri.partition("?") + self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) + self.body_arguments = {} # type: Dict[str, List[bytes]] + + @property + def cookies(self) -> Dict[str, http.cookies.Morsel]: + """A dictionary of ``http.cookies.Morsel`` objects.""" + if not hasattr(self, "_cookies"): + self._cookies = ( + http.cookies.SimpleCookie() + ) # type: http.cookies.SimpleCookie + if "Cookie" in self.headers: + try: + parsed = parse_cookie(self.headers["Cookie"]) + except Exception: + pass + else: + for k, v in parsed.items(): + try: + self._cookies[k] = v + except Exception: + # SimpleCookie imposes some restrictions on keys; + # parse_cookie does not. Discard any cookies + # with disallowed keys. + pass + return self._cookies + + def full_url(self) -> str: + """Reconstructs the full URL for this request.""" + return self.protocol + "://" + self.host + self.uri + + def request_time(self) -> float: + """Returns the amount of time it took for this request to execute.""" + if self._finish_time is None: + return time.time() - self._start_time + else: + return self._finish_time - self._start_time + + def get_ssl_certificate( + self, binary_form: bool = False + ) -> Union[None, Dict, bytes]: + """Returns the client's SSL certificate, if any. + + To use client certificates, the HTTPServer's + `ssl.SSLContext.verify_mode` field must be set, e.g.:: + + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_ctx.load_cert_chain("foo.crt", "foo.key") + ssl_ctx.load_verify_locations("cacerts.pem") + ssl_ctx.verify_mode = ssl.CERT_REQUIRED + server = HTTPServer(app, ssl_options=ssl_ctx) + + By default, the return value is a dictionary (or None, if no + client certificate is present). If ``binary_form`` is true, a + DER-encoded form of the certificate is returned instead. See + SSLSocket.getpeercert() in the standard library for more + details. + http://docs.python.org/library/ssl.html#sslsocket-objects + """ + try: + if self.connection is None: + return None + # TODO: add a method to HTTPConnection for this so it can work with HTTP/2 + return self.connection.stream.socket.getpeercert( # type: ignore + binary_form=binary_form + ) + except SSLError: + return None + + def _parse_body(self) -> None: + parse_body_arguments( + self.headers.get("Content-Type", ""), + self.body, + self.body_arguments, + self.files, + self.headers, + ) + + for k, v in self.body_arguments.items(): + self.arguments.setdefault(k, []).extend(v) + + def __repr__(self) -> str: + attrs = ("protocol", "host", "method", "uri", "version", "remote_ip") + args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs]) + return "%s(%s)" % (self.__class__.__name__, args) + + +class HTTPInputError(Exception): + """Exception class for malformed HTTP requests or responses + from remote sources. + + .. versionadded:: 4.0 + """ + + pass + + +class HTTPOutputError(Exception): + """Exception class for errors in HTTP output. + + .. versionadded:: 4.0 + """ + + pass + + +class HTTPServerConnectionDelegate(object): + """Implement this interface to handle requests from `.HTTPServer`. + + .. versionadded:: 4.0 + """ + + def start_request( + self, server_conn: object, request_conn: "HTTPConnection" + ) -> "HTTPMessageDelegate": + """This method is called by the server when a new request has started. + + :arg server_conn: is an opaque object representing the long-lived + (e.g. tcp-level) connection. + :arg request_conn: is a `.HTTPConnection` object for a single + request/response exchange. + + This method should return a `.HTTPMessageDelegate`. + """ + raise NotImplementedError() + + def on_close(self, server_conn: object) -> None: + """This method is called when a connection has been closed. + + :arg server_conn: is a server connection that has previously been + passed to ``start_request``. + """ + pass + + +class HTTPMessageDelegate(object): + """Implement this interface to handle an HTTP request or response. + + .. versionadded:: 4.0 + """ + + # TODO: genericize this class to avoid exposing the Union. + def headers_received( + self, + start_line: Union["RequestStartLine", "ResponseStartLine"], + headers: HTTPHeaders, + ) -> Optional[Awaitable[None]]: + """Called when the HTTP headers have been received and parsed. + + :arg start_line: a `.RequestStartLine` or `.ResponseStartLine` + depending on whether this is a client or server message. + :arg headers: a `.HTTPHeaders` instance. + + Some `.HTTPConnection` methods can only be called during + ``headers_received``. + + May return a `.Future`; if it does the body will not be read + until it is done. + """ + pass + + def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: + """Called when a chunk of data has been received. + + May return a `.Future` for flow control. + """ + pass + + def finish(self) -> None: + """Called after the last chunk of data has been received.""" + pass + + def on_connection_close(self) -> None: + """Called if the connection is closed without finishing the request. + + If ``headers_received`` is called, either ``finish`` or + ``on_connection_close`` will be called, but not both. + """ + pass + + +class HTTPConnection(object): + """Applications use this interface to write their responses. + + .. versionadded:: 4.0 + """ + + def write_headers( + self, + start_line: Union["RequestStartLine", "ResponseStartLine"], + headers: HTTPHeaders, + chunk: Optional[bytes] = None, + ) -> "Future[None]": + """Write an HTTP header block. + + :arg start_line: a `.RequestStartLine` or `.ResponseStartLine`. + :arg headers: a `.HTTPHeaders` instance. + :arg chunk: the first (optional) chunk of data. This is an optimization + so that small responses can be written in the same call as their + headers. + + The ``version`` field of ``start_line`` is ignored. + + Returns a future for flow control. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. + """ + raise NotImplementedError() + + def write(self, chunk: bytes) -> "Future[None]": + """Writes a chunk of body data. + + Returns a future for flow control. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. + """ + raise NotImplementedError() + + def finish(self) -> None: + """Indicates that the last body data has been written. + """ + raise NotImplementedError() + + +def url_concat( + url: str, + args: Union[ + None, Dict[str, str], List[Tuple[str, str]], Tuple[Tuple[str, str], ...] + ], +) -> str: + """Concatenate url and arguments regardless of whether + url has existing query parameters. + + ``args`` may be either a dictionary or a list of key-value pairs + (the latter allows for multiple values with the same key. + + >>> url_concat("http://example.com/foo", dict(c="d")) + 'http://example.com/foo?c=d' + >>> url_concat("http://example.com/foo?a=b", dict(c="d")) + 'http://example.com/foo?a=b&c=d' + >>> url_concat("http://example.com/foo?a=b", [("c", "d"), ("c", "d2")]) + 'http://example.com/foo?a=b&c=d&c=d2' + """ + if args is None: + return url + parsed_url = urlparse(url) + if isinstance(args, dict): + parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True) + parsed_query.extend(args.items()) + elif isinstance(args, list) or isinstance(args, tuple): + parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True) + parsed_query.extend(args) + else: + err = "'args' parameter should be dict, list or tuple. Not {0}".format( + type(args) + ) + raise TypeError(err) + final_query = urlencode(parsed_query) + url = urlunparse( + ( + parsed_url[0], + parsed_url[1], + parsed_url[2], + parsed_url[3], + final_query, + parsed_url[5], + ) + ) + return url + + +class HTTPFile(ObjectDict): + """Represents a file uploaded via a form. + + For backwards compatibility, its instance attributes are also + accessible as dictionary keys. + + * ``filename`` + * ``body`` + * ``content_type`` + """ + + pass + + +def _parse_request_range( + range_header: str, +) -> Optional[Tuple[Optional[int], Optional[int]]]: + """Parses a Range header. + + Returns either ``None`` or tuple ``(start, end)``. + Note that while the HTTP headers use inclusive byte positions, + this method returns indexes suitable for use in slices. + + >>> start, end = _parse_request_range("bytes=1-2") + >>> start, end + (1, 3) + >>> [0, 1, 2, 3, 4][start:end] + [1, 2] + >>> _parse_request_range("bytes=6-") + (6, None) + >>> _parse_request_range("bytes=-6") + (-6, None) + >>> _parse_request_range("bytes=-0") + (None, 0) + >>> _parse_request_range("bytes=") + (None, None) + >>> _parse_request_range("foo=42") + >>> _parse_request_range("bytes=1-2,6-10") + + Note: only supports one range (ex, ``bytes=1-2,6-10`` is not allowed). + + See [0] for the details of the range header. + + [0]: http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges + """ + unit, _, value = range_header.partition("=") + unit, value = unit.strip(), value.strip() + if unit != "bytes": + return None + start_b, _, end_b = value.partition("-") + try: + start = _int_or_none(start_b) + end = _int_or_none(end_b) + except ValueError: + return None + if end is not None: + if start is None: + if end != 0: + start = -end + end = None + else: + end += 1 + return (start, end) + + +def _get_content_range(start: Optional[int], end: Optional[int], total: int) -> str: + """Returns a suitable Content-Range header: + + >>> print(_get_content_range(None, 1, 4)) + bytes 0-0/4 + >>> print(_get_content_range(1, 3, 4)) + bytes 1-2/4 + >>> print(_get_content_range(None, None, 4)) + bytes 0-3/4 + """ + start = start or 0 + end = (end or total) - 1 + return "bytes %s-%s/%s" % (start, end, total) + + +def _int_or_none(val: str) -> Optional[int]: + val = val.strip() + if val == "": + return None + return int(val) + + +def parse_body_arguments( + content_type: str, + body: bytes, + arguments: Dict[str, List[bytes]], + files: Dict[str, List[HTTPFile]], + headers: Optional[HTTPHeaders] = None, +) -> None: + """Parses a form request body. + + Supports ``application/x-www-form-urlencoded`` and + ``multipart/form-data``. The ``content_type`` parameter should be + a string and ``body`` should be a byte string. The ``arguments`` + and ``files`` parameters are dictionaries that will be updated + with the parsed contents. + """ + if content_type.startswith("application/x-www-form-urlencoded"): + if headers and "Content-Encoding" in headers: + gen_log.warning( + "Unsupported Content-Encoding: %s", headers["Content-Encoding"] + ) + return + try: + # real charset decoding will happen in RequestHandler.decode_argument() + uri_arguments = parse_qs_bytes(body, keep_blank_values=True) + except Exception as e: + gen_log.warning("Invalid x-www-form-urlencoded body: %s", e) + uri_arguments = {} + for name, values in uri_arguments.items(): + if values: + arguments.setdefault(name, []).extend(values) + elif content_type.startswith("multipart/form-data"): + if headers and "Content-Encoding" in headers: + gen_log.warning( + "Unsupported Content-Encoding: %s", headers["Content-Encoding"] + ) + return + try: + fields = content_type.split(";") + for field in fields: + k, sep, v = field.strip().partition("=") + if k == "boundary" and v: + parse_multipart_form_data(utf8(v), body, arguments, files) + break + else: + raise ValueError("multipart boundary not found") + except Exception as e: + gen_log.warning("Invalid multipart/form-data: %s", e) + + +def parse_multipart_form_data( + boundary: bytes, + data: bytes, + arguments: Dict[str, List[bytes]], + files: Dict[str, List[HTTPFile]], +) -> None: + """Parses a ``multipart/form-data`` body. + + The ``boundary`` and ``data`` parameters are both byte strings. + The dictionaries given in the arguments and files parameters + will be updated with the contents of the body. + + .. versionchanged:: 5.1 + + Now recognizes non-ASCII filenames in RFC 2231/5987 + (``filename*=``) format. + """ + # The standard allows for the boundary to be quoted in the header, + # although it's rare (it happens at least for google app engine + # xmpp). I think we're also supposed to handle backslash-escapes + # here but I'll save that until we see a client that uses them + # in the wild. + if boundary.startswith(b'"') and boundary.endswith(b'"'): + boundary = boundary[1:-1] + final_boundary_index = data.rfind(b"--" + boundary + b"--") + if final_boundary_index == -1: + gen_log.warning("Invalid multipart/form-data: no final boundary") + return + parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n") + for part in parts: + if not part: + continue + eoh = part.find(b"\r\n\r\n") + if eoh == -1: + gen_log.warning("multipart/form-data missing headers") + continue + headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) + disp_header = headers.get("Content-Disposition", "") + disposition, disp_params = _parse_header(disp_header) + if disposition != "form-data" or not part.endswith(b"\r\n"): + gen_log.warning("Invalid multipart/form-data") + continue + value = part[eoh + 4 : -2] + if not disp_params.get("name"): + gen_log.warning("multipart/form-data value missing name") + continue + name = disp_params["name"] + if disp_params.get("filename"): + ctype = headers.get("Content-Type", "application/unknown") + files.setdefault(name, []).append( + HTTPFile( + filename=disp_params["filename"], body=value, content_type=ctype + ) + ) + else: + arguments.setdefault(name, []).append(value) + + +def format_timestamp( + ts: Union[int, float, tuple, time.struct_time, datetime.datetime] +) -> str: + """Formats a timestamp in the format used by HTTP. + + The argument may be a numeric timestamp as returned by `time.time`, + a time tuple as returned by `time.gmtime`, or a `datetime.datetime` + object. + + >>> format_timestamp(1359312200) + 'Sun, 27 Jan 2013 18:43:20 GMT' + """ + if isinstance(ts, (int, float)): + time_num = ts + elif isinstance(ts, (tuple, time.struct_time)): + time_num = calendar.timegm(ts) + elif isinstance(ts, datetime.datetime): + time_num = calendar.timegm(ts.utctimetuple()) + else: + raise TypeError("unknown timestamp type: %r" % ts) + return email.utils.formatdate(time_num, usegmt=True) + + +RequestStartLine = collections.namedtuple( + "RequestStartLine", ["method", "path", "version"] +) + + +_http_version_re = re.compile(r"^HTTP/1\.[0-9]$") + + +def parse_request_start_line(line: str) -> RequestStartLine: + """Returns a (method, path, version) tuple for an HTTP 1.x request line. + + The response is a `collections.namedtuple`. + + >>> parse_request_start_line("GET /foo HTTP/1.1") + RequestStartLine(method='GET', path='/foo', version='HTTP/1.1') + """ + try: + method, path, version = line.split(" ") + except ValueError: + # https://tools.ietf.org/html/rfc7230#section-3.1.1 + # invalid request-line SHOULD respond with a 400 (Bad Request) + raise HTTPInputError("Malformed HTTP request line") + if not _http_version_re.match(version): + raise HTTPInputError( + "Malformed HTTP version in HTTP Request-Line: %r" % version + ) + return RequestStartLine(method, path, version) + + +ResponseStartLine = collections.namedtuple( + "ResponseStartLine", ["version", "code", "reason"] +) + + +_http_response_line_re = re.compile(r"(HTTP/1.[0-9]) ([0-9]+) ([^\r]*)") + + +def parse_response_start_line(line: str) -> ResponseStartLine: + """Returns a (version, code, reason) tuple for an HTTP 1.x response line. + + The response is a `collections.namedtuple`. + + >>> parse_response_start_line("HTTP/1.1 200 OK") + ResponseStartLine(version='HTTP/1.1', code=200, reason='OK') + """ + line = native_str(line) + match = _http_response_line_re.match(line) + if not match: + raise HTTPInputError("Error parsing response start line") + return ResponseStartLine(match.group(1), int(match.group(2)), match.group(3)) + + +# _parseparam and _parse_header are copied and modified from python2.7's cgi.py +# The original 2.7 version of this code did not correctly support some +# combinations of semicolons and double quotes. +# It has also been modified to support valueless parameters as seen in +# websocket extension negotiations, and to support non-ascii values in +# RFC 2231/5987 format. + + +def _parseparam(s: str) -> Generator[str, None, None]: + while s[:1] == ";": + s = s[1:] + end = s.find(";") + while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: + end = s.find(";", end + 1) + if end < 0: + end = len(s) + f = s[:end] + yield f.strip() + s = s[end:] + + +def _parse_header(line: str) -> Tuple[str, Dict[str, str]]: + r"""Parse a Content-type like header. + + Return the main content-type and a dictionary of options. + + >>> d = "form-data; foo=\"b\\\\a\\\"r\"; file*=utf-8''T%C3%A4st" + >>> ct, d = _parse_header(d) + >>> ct + 'form-data' + >>> d['file'] == r'T\u00e4st'.encode('ascii').decode('unicode_escape') + True + >>> d['foo'] + 'b\\a"r' + """ + parts = _parseparam(";" + line) + key = next(parts) + # decode_params treats first argument special, but we already stripped key + params = [("Dummy", "value")] + for p in parts: + i = p.find("=") + if i >= 0: + name = p[:i].strip().lower() + value = p[i + 1 :].strip() + params.append((name, native_str(value))) + decoded_params = email.utils.decode_params(params) + decoded_params.pop(0) # get rid of the dummy again + pdict = {} + for name, decoded_value in decoded_params: + value = email.utils.collapse_rfc2231_value(decoded_value) + if len(value) >= 2 and value[0] == '"' and value[-1] == '"': + value = value[1:-1] + pdict[name] = value + return key, pdict + + +def _encode_header(key: str, pdict: Dict[str, str]) -> str: + """Inverse of _parse_header. + + >>> _encode_header('permessage-deflate', + ... {'client_max_window_bits': 15, 'client_no_context_takeover': None}) + 'permessage-deflate; client_max_window_bits=15; client_no_context_takeover' + """ + if not pdict: + return key + out = [key] + # Sort the parameters just to make it easy to test. + for k, v in sorted(pdict.items()): + if v is None: + out.append(k) + else: + # TODO: quote if necessary. + out.append("%s=%s" % (k, v)) + return "; ".join(out) + + +def encode_username_password( + username: Union[str, bytes], password: Union[str, bytes] +) -> bytes: + """Encodes a username/password pair in the format used by HTTP auth. + + The return value is a byte string in the form ``username:password``. + + .. versionadded:: 5.1 + """ + if isinstance(username, unicode_type): + username = unicodedata.normalize("NFC", username) + if isinstance(password, unicode_type): + password = unicodedata.normalize("NFC", password) + return utf8(username) + b":" + utf8(password) + + +def doctests(): + # type: () -> unittest.TestSuite + import doctest + + return doctest.DocTestSuite() + + +_netloc_re = re.compile(r"^(.+):(\d+)$") + + +def split_host_and_port(netloc: str) -> Tuple[str, Optional[int]]: + """Returns ``(host, port)`` tuple from ``netloc``. + + Returned ``port`` will be ``None`` if not present. + + .. versionadded:: 4.1 + """ + match = _netloc_re.match(netloc) + if match: + host = match.group(1) + port = int(match.group(2)) # type: Optional[int] + else: + host = netloc + port = None + return (host, port) + + +def qs_to_qsl(qs: Dict[str, List[AnyStr]]) -> Iterable[Tuple[str, AnyStr]]: + """Generator converting a result of ``parse_qs`` back to name-value pairs. + + .. versionadded:: 5.0 + """ + for k, vs in qs.items(): + for v in vs: + yield (k, v) + + +_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") +_QuotePatt = re.compile(r"[\\].") +_nulljoin = "".join + + +def _unquote_cookie(s: str) -> str: + """Handle double quotes and escaping in cookie values. + + This method is copied verbatim from the Python 3.5 standard + library (http.cookies._unquote) so we don't have to depend on + non-public interfaces. + """ + # If there aren't any doublequotes, + # then there can't be any special characters. See RFC 2109. + if s is None or len(s) < 2: + return s + if s[0] != '"' or s[-1] != '"': + return s + + # We have to assume that we must decode this string. + # Down to work. + + # Remove the "s + s = s[1:-1] + + # Check for special sequences. Examples: + # \012 --> \n + # \" --> " + # + i = 0 + n = len(s) + res = [] + while 0 <= i < n: + o_match = _OctalPatt.search(s, i) + q_match = _QuotePatt.search(s, i) + if not o_match and not q_match: # Neither matched + res.append(s[i:]) + break + # else: + j = k = -1 + if o_match: + j = o_match.start(0) + if q_match: + k = q_match.start(0) + if q_match and (not o_match or k < j): # QuotePatt matched + res.append(s[i:k]) + res.append(s[k + 1]) + i = k + 2 + else: # OctalPatt matched + res.append(s[i:j]) + res.append(chr(int(s[j + 1 : j + 4], 8))) + i = j + 4 + return _nulljoin(res) + + +def parse_cookie(cookie: str) -> Dict[str, str]: + """Parse a ``Cookie`` HTTP header into a dict of name/value pairs. + + This function attempts to mimic browser cookie parsing behavior; + it specifically does not follow any of the cookie-related RFCs + (because browsers don't either). + + The algorithm used is identical to that used by Django version 1.9.10. + + .. versionadded:: 4.4.2 + """ + cookiedict = {} + for chunk in cookie.split(str(";")): + if str("=") in chunk: + key, val = chunk.split(str("="), 1) + else: + # Assume an empty name per + # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 + key, val = str(""), chunk + key, val = key.strip(), val.strip() + if key or val: + # unquote using Python's algorithm. + cookiedict[key] = _unquote_cookie(val) + return cookiedict diff --git a/telegramer/include/tornado/ioloop.py b/telegramer/include/tornado/ioloop.py new file mode 100644 index 0000000..2cf8844 --- /dev/null +++ b/telegramer/include/tornado/ioloop.py @@ -0,0 +1,944 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""An I/O event loop for non-blocking sockets. + +In Tornado 6.0, `.IOLoop` is a wrapper around the `asyncio` event +loop, with a slightly different interface for historical reasons. +Applications can use either the `.IOLoop` interface or the underlying +`asyncio` event loop directly (unless compatibility with older +versions of Tornado is desired, in which case `.IOLoop` must be used). + +Typical applications will use a single `IOLoop` object, accessed via +`IOLoop.current` class method. The `IOLoop.start` method (or +equivalently, `asyncio.AbstractEventLoop.run_forever`) should usually +be called at the end of the ``main()`` function. Atypical applications +may use more than one `IOLoop`, such as one `IOLoop` per thread, or +per `unittest` case. + +""" + +import asyncio +import concurrent.futures +import datetime +import functools +import logging +import numbers +import os +import sys +import time +import math +import random + +from tornado.concurrent import ( + Future, + is_future, + chain_future, + future_set_exc_info, + future_add_done_callback, +) +from tornado.log import app_log +from tornado.util import Configurable, TimeoutError, import_object + +import typing +from typing import Union, Any, Type, Optional, Callable, TypeVar, Tuple, Awaitable + +if typing.TYPE_CHECKING: + from typing import Dict, List # noqa: F401 + + from typing_extensions import Protocol +else: + Protocol = object + + +class _Selectable(Protocol): + def fileno(self) -> int: + pass + + def close(self) -> None: + pass + + +_T = TypeVar("_T") +_S = TypeVar("_S", bound=_Selectable) + + +class IOLoop(Configurable): + """An I/O event loop. + + As of Tornado 6.0, `IOLoop` is a wrapper around the `asyncio` event + loop. + + Example usage for a simple TCP server: + + .. testcode:: + + import errno + import functools + import socket + + import tornado.ioloop + from tornado.iostream import IOStream + + async def handle_connection(connection, address): + stream = IOStream(connection) + message = await stream.read_until_close() + print("message from client:", message.decode().strip()) + + def connection_ready(sock, fd, events): + while True: + try: + connection, address = sock.accept() + except BlockingIOError: + return + connection.setblocking(0) + io_loop = tornado.ioloop.IOLoop.current() + io_loop.spawn_callback(handle_connection, connection, address) + + if __name__ == '__main__': + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setblocking(0) + sock.bind(("", 8888)) + sock.listen(128) + + io_loop = tornado.ioloop.IOLoop.current() + callback = functools.partial(connection_ready, sock) + io_loop.add_handler(sock.fileno(), callback, io_loop.READ) + io_loop.start() + + .. testoutput:: + :hide: + + By default, a newly-constructed `IOLoop` becomes the thread's current + `IOLoop`, unless there already is a current `IOLoop`. This behavior + can be controlled with the ``make_current`` argument to the `IOLoop` + constructor: if ``make_current=True``, the new `IOLoop` will always + try to become current and it raises an error if there is already a + current instance. If ``make_current=False``, the new `IOLoop` will + not try to become current. + + In general, an `IOLoop` cannot survive a fork or be shared across + processes in any way. When multiple processes are being used, each + process should create its own `IOLoop`, which also implies that + any objects which depend on the `IOLoop` (such as + `.AsyncHTTPClient`) must also be created in the child processes. + As a guideline, anything that starts processes (including the + `tornado.process` and `multiprocessing` modules) should do so as + early as possible, ideally the first thing the application does + after loading its configuration in ``main()``. + + .. versionchanged:: 4.2 + Added the ``make_current`` keyword argument to the `IOLoop` + constructor. + + .. versionchanged:: 5.0 + + Uses the `asyncio` event loop by default. The + ``IOLoop.configure`` method cannot be used on Python 3 except + to redundantly specify the `asyncio` event loop. + + """ + + # These constants were originally based on constants from the epoll module. + NONE = 0 + READ = 0x001 + WRITE = 0x004 + ERROR = 0x018 + + # In Python 3, _ioloop_for_asyncio maps from asyncio loops to IOLoops. + _ioloop_for_asyncio = dict() # type: Dict[asyncio.AbstractEventLoop, IOLoop] + + @classmethod + def configure( + cls, impl: "Union[None, str, Type[Configurable]]", **kwargs: Any + ) -> None: + if asyncio is not None: + from tornado.platform.asyncio import BaseAsyncIOLoop + + if isinstance(impl, str): + impl = import_object(impl) + if isinstance(impl, type) and not issubclass(impl, BaseAsyncIOLoop): + raise RuntimeError( + "only AsyncIOLoop is allowed when asyncio is available" + ) + super(IOLoop, cls).configure(impl, **kwargs) + + @staticmethod + def instance() -> "IOLoop": + """Deprecated alias for `IOLoop.current()`. + + .. versionchanged:: 5.0 + + Previously, this method returned a global singleton + `IOLoop`, in contrast with the per-thread `IOLoop` returned + by `current()`. In nearly all cases the two were the same + (when they differed, it was generally used from non-Tornado + threads to communicate back to the main thread's `IOLoop`). + This distinction is not present in `asyncio`, so in order + to facilitate integration with that package `instance()` + was changed to be an alias to `current()`. Applications + using the cross-thread communications aspect of + `instance()` should instead set their own global variable + to point to the `IOLoop` they want to use. + + .. deprecated:: 5.0 + """ + return IOLoop.current() + + def install(self) -> None: + """Deprecated alias for `make_current()`. + + .. versionchanged:: 5.0 + + Previously, this method would set this `IOLoop` as the + global singleton used by `IOLoop.instance()`. Now that + `instance()` is an alias for `current()`, `install()` + is an alias for `make_current()`. + + .. deprecated:: 5.0 + """ + self.make_current() + + @staticmethod + def clear_instance() -> None: + """Deprecated alias for `clear_current()`. + + .. versionchanged:: 5.0 + + Previously, this method would clear the `IOLoop` used as + the global singleton by `IOLoop.instance()`. Now that + `instance()` is an alias for `current()`, + `clear_instance()` is an alias for `clear_current()`. + + .. deprecated:: 5.0 + + """ + IOLoop.clear_current() + + @typing.overload + @staticmethod + def current() -> "IOLoop": + pass + + @typing.overload + @staticmethod + def current(instance: bool = True) -> Optional["IOLoop"]: # noqa: F811 + pass + + @staticmethod + def current(instance: bool = True) -> Optional["IOLoop"]: # noqa: F811 + """Returns the current thread's `IOLoop`. + + If an `IOLoop` is currently running or has been marked as + current by `make_current`, returns that instance. If there is + no current `IOLoop` and ``instance`` is true, creates one. + + .. versionchanged:: 4.1 + Added ``instance`` argument to control the fallback to + `IOLoop.instance()`. + .. versionchanged:: 5.0 + On Python 3, control of the current `IOLoop` is delegated + to `asyncio`, with this and other methods as pass-through accessors. + The ``instance`` argument now controls whether an `IOLoop` + is created automatically when there is none, instead of + whether we fall back to `IOLoop.instance()` (which is now + an alias for this method). ``instance=False`` is deprecated, + since even if we do not create an `IOLoop`, this method + may initialize the asyncio loop. + """ + try: + loop = asyncio.get_event_loop() + except (RuntimeError, AssertionError): + if not instance: + return None + raise + try: + return IOLoop._ioloop_for_asyncio[loop] + except KeyError: + if instance: + from tornado.platform.asyncio import AsyncIOMainLoop + + current = AsyncIOMainLoop(make_current=True) # type: Optional[IOLoop] + else: + current = None + return current + + def make_current(self) -> None: + """Makes this the `IOLoop` for the current thread. + + An `IOLoop` automatically becomes current for its thread + when it is started, but it is sometimes useful to call + `make_current` explicitly before starting the `IOLoop`, + so that code run at startup time can find the right + instance. + + .. versionchanged:: 4.1 + An `IOLoop` created while there is no current `IOLoop` + will automatically become current. + + .. versionchanged:: 5.0 + This method also sets the current `asyncio` event loop. + """ + # The asyncio event loops override this method. + raise NotImplementedError() + + @staticmethod + def clear_current() -> None: + """Clears the `IOLoop` for the current thread. + + Intended primarily for use by test frameworks in between tests. + + .. versionchanged:: 5.0 + This method also clears the current `asyncio` event loop. + """ + old = IOLoop.current(instance=False) + if old is not None: + old._clear_current_hook() + if asyncio is None: + IOLoop._current.instance = None + + def _clear_current_hook(self) -> None: + """Instance method called when an IOLoop ceases to be current. + + May be overridden by subclasses as a counterpart to make_current. + """ + pass + + @classmethod + def configurable_base(cls) -> Type[Configurable]: + return IOLoop + + @classmethod + def configurable_default(cls) -> Type[Configurable]: + from tornado.platform.asyncio import AsyncIOLoop + + return AsyncIOLoop + + def initialize(self, make_current: Optional[bool] = None) -> None: + if make_current is None: + if IOLoop.current(instance=False) is None: + self.make_current() + elif make_current: + current = IOLoop.current(instance=False) + # AsyncIO loops can already be current by this point. + if current is not None and current is not self: + raise RuntimeError("current IOLoop already exists") + self.make_current() + + def close(self, all_fds: bool = False) -> None: + """Closes the `IOLoop`, freeing any resources used. + + If ``all_fds`` is true, all file descriptors registered on the + IOLoop will be closed (not just the ones created by the + `IOLoop` itself). + + Many applications will only use a single `IOLoop` that runs for the + entire lifetime of the process. In that case closing the `IOLoop` + is not necessary since everything will be cleaned up when the + process exits. `IOLoop.close` is provided mainly for scenarios + such as unit tests, which create and destroy a large number of + ``IOLoops``. + + An `IOLoop` must be completely stopped before it can be closed. This + means that `IOLoop.stop()` must be called *and* `IOLoop.start()` must + be allowed to return before attempting to call `IOLoop.close()`. + Therefore the call to `close` will usually appear just after + the call to `start` rather than near the call to `stop`. + + .. versionchanged:: 3.1 + If the `IOLoop` implementation supports non-integer objects + for "file descriptors", those objects will have their + ``close`` method when ``all_fds`` is true. + """ + raise NotImplementedError() + + @typing.overload + def add_handler( + self, fd: int, handler: Callable[[int, int], None], events: int + ) -> None: + pass + + @typing.overload # noqa: F811 + def add_handler( + self, fd: _S, handler: Callable[[_S, int], None], events: int + ) -> None: + pass + + def add_handler( # noqa: F811 + self, fd: Union[int, _Selectable], handler: Callable[..., None], events: int + ) -> None: + """Registers the given handler to receive the given events for ``fd``. + + The ``fd`` argument may either be an integer file descriptor or + a file-like object with a ``fileno()`` and ``close()`` method. + + The ``events`` argument is a bitwise or of the constants + ``IOLoop.READ``, ``IOLoop.WRITE``, and ``IOLoop.ERROR``. + + When an event occurs, ``handler(fd, events)`` will be run. + + .. versionchanged:: 4.0 + Added the ability to pass file-like objects in addition to + raw file descriptors. + """ + raise NotImplementedError() + + def update_handler(self, fd: Union[int, _Selectable], events: int) -> None: + """Changes the events we listen for ``fd``. + + .. versionchanged:: 4.0 + Added the ability to pass file-like objects in addition to + raw file descriptors. + """ + raise NotImplementedError() + + def remove_handler(self, fd: Union[int, _Selectable]) -> None: + """Stop listening for events on ``fd``. + + .. versionchanged:: 4.0 + Added the ability to pass file-like objects in addition to + raw file descriptors. + """ + raise NotImplementedError() + + def start(self) -> None: + """Starts the I/O loop. + + The loop will run until one of the callbacks calls `stop()`, which + will make the loop stop after the current event iteration completes. + """ + raise NotImplementedError() + + def _setup_logging(self) -> None: + """The IOLoop catches and logs exceptions, so it's + important that log output be visible. However, python's + default behavior for non-root loggers (prior to python + 3.2) is to print an unhelpful "no handlers could be + found" message rather than the actual log entry, so we + must explicitly configure logging if we've made it this + far without anything. + + This method should be called from start() in subclasses. + """ + if not any( + [ + logging.getLogger().handlers, + logging.getLogger("tornado").handlers, + logging.getLogger("tornado.application").handlers, + ] + ): + logging.basicConfig() + + def stop(self) -> None: + """Stop the I/O loop. + + If the event loop is not currently running, the next call to `start()` + will return immediately. + + Note that even after `stop` has been called, the `IOLoop` is not + completely stopped until `IOLoop.start` has also returned. + Some work that was scheduled before the call to `stop` may still + be run before the `IOLoop` shuts down. + """ + raise NotImplementedError() + + def run_sync(self, func: Callable, timeout: Optional[float] = None) -> Any: + """Starts the `IOLoop`, runs the given function, and stops the loop. + + The function must return either an awaitable object or + ``None``. If the function returns an awaitable object, the + `IOLoop` will run until the awaitable is resolved (and + `run_sync()` will return the awaitable's result). If it raises + an exception, the `IOLoop` will stop and the exception will be + re-raised to the caller. + + The keyword-only argument ``timeout`` may be used to set + a maximum duration for the function. If the timeout expires, + a `tornado.util.TimeoutError` is raised. + + This method is useful to allow asynchronous calls in a + ``main()`` function:: + + async def main(): + # do stuff... + + if __name__ == '__main__': + IOLoop.current().run_sync(main) + + .. versionchanged:: 4.3 + Returning a non-``None``, non-awaitable value is now an error. + + .. versionchanged:: 5.0 + If a timeout occurs, the ``func`` coroutine will be cancelled. + + """ + future_cell = [None] # type: List[Optional[Future]] + + def run() -> None: + try: + result = func() + if result is not None: + from tornado.gen import convert_yielded + + result = convert_yielded(result) + except Exception: + fut = Future() # type: Future[Any] + future_cell[0] = fut + future_set_exc_info(fut, sys.exc_info()) + else: + if is_future(result): + future_cell[0] = result + else: + fut = Future() + future_cell[0] = fut + fut.set_result(result) + assert future_cell[0] is not None + self.add_future(future_cell[0], lambda future: self.stop()) + + self.add_callback(run) + if timeout is not None: + + def timeout_callback() -> None: + # If we can cancel the future, do so and wait on it. If not, + # Just stop the loop and return with the task still pending. + # (If we neither cancel nor wait for the task, a warning + # will be logged). + assert future_cell[0] is not None + if not future_cell[0].cancel(): + self.stop() + + timeout_handle = self.add_timeout(self.time() + timeout, timeout_callback) + self.start() + if timeout is not None: + self.remove_timeout(timeout_handle) + assert future_cell[0] is not None + if future_cell[0].cancelled() or not future_cell[0].done(): + raise TimeoutError("Operation timed out after %s seconds" % timeout) + return future_cell[0].result() + + def time(self) -> float: + """Returns the current time according to the `IOLoop`'s clock. + + The return value is a floating-point number relative to an + unspecified time in the past. + + Historically, the IOLoop could be customized to use e.g. + `time.monotonic` instead of `time.time`, but this is not + currently supported and so this method is equivalent to + `time.time`. + + """ + return time.time() + + def add_timeout( + self, + deadline: Union[float, datetime.timedelta], + callback: Callable[..., None], + *args: Any, + **kwargs: Any + ) -> object: + """Runs the ``callback`` at the time ``deadline`` from the I/O loop. + + Returns an opaque handle that may be passed to + `remove_timeout` to cancel. + + ``deadline`` may be a number denoting a time (on the same + scale as `IOLoop.time`, normally `time.time`), or a + `datetime.timedelta` object for a deadline relative to the + current time. Since Tornado 4.0, `call_later` is a more + convenient alternative for the relative case since it does not + require a timedelta object. + + Note that it is not safe to call `add_timeout` from other threads. + Instead, you must use `add_callback` to transfer control to the + `IOLoop`'s thread, and then call `add_timeout` from there. + + Subclasses of IOLoop must implement either `add_timeout` or + `call_at`; the default implementations of each will call + the other. `call_at` is usually easier to implement, but + subclasses that wish to maintain compatibility with Tornado + versions prior to 4.0 must use `add_timeout` instead. + + .. versionchanged:: 4.0 + Now passes through ``*args`` and ``**kwargs`` to the callback. + """ + if isinstance(deadline, numbers.Real): + return self.call_at(deadline, callback, *args, **kwargs) + elif isinstance(deadline, datetime.timedelta): + return self.call_at( + self.time() + deadline.total_seconds(), callback, *args, **kwargs + ) + else: + raise TypeError("Unsupported deadline %r" % deadline) + + def call_later( + self, delay: float, callback: Callable[..., None], *args: Any, **kwargs: Any + ) -> object: + """Runs the ``callback`` after ``delay`` seconds have passed. + + Returns an opaque handle that may be passed to `remove_timeout` + to cancel. Note that unlike the `asyncio` method of the same + name, the returned object does not have a ``cancel()`` method. + + See `add_timeout` for comments on thread-safety and subclassing. + + .. versionadded:: 4.0 + """ + return self.call_at(self.time() + delay, callback, *args, **kwargs) + + def call_at( + self, when: float, callback: Callable[..., None], *args: Any, **kwargs: Any + ) -> object: + """Runs the ``callback`` at the absolute time designated by ``when``. + + ``when`` must be a number using the same reference point as + `IOLoop.time`. + + Returns an opaque handle that may be passed to `remove_timeout` + to cancel. Note that unlike the `asyncio` method of the same + name, the returned object does not have a ``cancel()`` method. + + See `add_timeout` for comments on thread-safety and subclassing. + + .. versionadded:: 4.0 + """ + return self.add_timeout(when, callback, *args, **kwargs) + + def remove_timeout(self, timeout: object) -> None: + """Cancels a pending timeout. + + The argument is a handle as returned by `add_timeout`. It is + safe to call `remove_timeout` even if the callback has already + been run. + """ + raise NotImplementedError() + + def add_callback(self, callback: Callable, *args: Any, **kwargs: Any) -> None: + """Calls the given callback on the next I/O loop iteration. + + It is safe to call this method from any thread at any time, + except from a signal handler. Note that this is the **only** + method in `IOLoop` that makes this thread-safety guarantee; all + other interaction with the `IOLoop` must be done from that + `IOLoop`'s thread. `add_callback()` may be used to transfer + control from other threads to the `IOLoop`'s thread. + + To add a callback from a signal handler, see + `add_callback_from_signal`. + """ + raise NotImplementedError() + + def add_callback_from_signal( + self, callback: Callable, *args: Any, **kwargs: Any + ) -> None: + """Calls the given callback on the next I/O loop iteration. + + Safe for use from a Python signal handler; should not be used + otherwise. + """ + raise NotImplementedError() + + def spawn_callback(self, callback: Callable, *args: Any, **kwargs: Any) -> None: + """Calls the given callback on the next IOLoop iteration. + + As of Tornado 6.0, this method is equivalent to `add_callback`. + + .. versionadded:: 4.0 + """ + self.add_callback(callback, *args, **kwargs) + + def add_future( + self, + future: "Union[Future[_T], concurrent.futures.Future[_T]]", + callback: Callable[["Future[_T]"], None], + ) -> None: + """Schedules a callback on the ``IOLoop`` when the given + `.Future` is finished. + + The callback is invoked with one argument, the + `.Future`. + + This method only accepts `.Future` objects and not other + awaitables (unlike most of Tornado where the two are + interchangeable). + """ + if isinstance(future, Future): + # Note that we specifically do not want the inline behavior of + # tornado.concurrent.future_add_done_callback. We always want + # this callback scheduled on the next IOLoop iteration (which + # asyncio.Future always does). + # + # Wrap the callback in self._run_callback so we control + # the error logging (i.e. it goes to tornado.log.app_log + # instead of asyncio's log). + future.add_done_callback( + lambda f: self._run_callback(functools.partial(callback, future)) + ) + else: + assert is_future(future) + # For concurrent futures, we use self.add_callback, so + # it's fine if future_add_done_callback inlines that call. + future_add_done_callback( + future, lambda f: self.add_callback(callback, future) + ) + + def run_in_executor( + self, + executor: Optional[concurrent.futures.Executor], + func: Callable[..., _T], + *args: Any + ) -> Awaitable[_T]: + """Runs a function in a ``concurrent.futures.Executor``. If + ``executor`` is ``None``, the IO loop's default executor will be used. + + Use `functools.partial` to pass keyword arguments to ``func``. + + .. versionadded:: 5.0 + """ + if executor is None: + if not hasattr(self, "_executor"): + from tornado.process import cpu_count + + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=(cpu_count() * 5) + ) # type: concurrent.futures.Executor + executor = self._executor + c_future = executor.submit(func, *args) + # Concurrent Futures are not usable with await. Wrap this in a + # Tornado Future instead, using self.add_future for thread-safety. + t_future = Future() # type: Future[_T] + self.add_future(c_future, lambda f: chain_future(f, t_future)) + return t_future + + def set_default_executor(self, executor: concurrent.futures.Executor) -> None: + """Sets the default executor to use with :meth:`run_in_executor`. + + .. versionadded:: 5.0 + """ + self._executor = executor + + def _run_callback(self, callback: Callable[[], Any]) -> None: + """Runs a callback with error handling. + + .. versionchanged:: 6.0 + + CancelledErrors are no longer logged. + """ + try: + ret = callback() + if ret is not None: + from tornado import gen + + # Functions that return Futures typically swallow all + # exceptions and store them in the Future. If a Future + # makes it out to the IOLoop, ensure its exception (if any) + # gets logged too. + try: + ret = gen.convert_yielded(ret) + except gen.BadYieldError: + # It's not unusual for add_callback to be used with + # methods returning a non-None and non-yieldable + # result, which should just be ignored. + pass + else: + self.add_future(ret, self._discard_future_result) + except asyncio.CancelledError: + pass + except Exception: + app_log.error("Exception in callback %r", callback, exc_info=True) + + def _discard_future_result(self, future: Future) -> None: + """Avoid unhandled-exception warnings from spawned coroutines.""" + future.result() + + def split_fd( + self, fd: Union[int, _Selectable] + ) -> Tuple[int, Union[int, _Selectable]]: + # """Returns an (fd, obj) pair from an ``fd`` parameter. + + # We accept both raw file descriptors and file-like objects as + # input to `add_handler` and related methods. When a file-like + # object is passed, we must retain the object itself so we can + # close it correctly when the `IOLoop` shuts down, but the + # poller interfaces favor file descriptors (they will accept + # file-like objects and call ``fileno()`` for you, but they + # always return the descriptor itself). + + # This method is provided for use by `IOLoop` subclasses and should + # not generally be used by application code. + + # .. versionadded:: 4.0 + # """ + if isinstance(fd, int): + return fd, fd + return fd.fileno(), fd + + def close_fd(self, fd: Union[int, _Selectable]) -> None: + # """Utility method to close an ``fd``. + + # If ``fd`` is a file-like object, we close it directly; otherwise + # we use `os.close`. + + # This method is provided for use by `IOLoop` subclasses (in + # implementations of ``IOLoop.close(all_fds=True)`` and should + # not generally be used by application code. + + # .. versionadded:: 4.0 + # """ + try: + if isinstance(fd, int): + os.close(fd) + else: + fd.close() + except OSError: + pass + + +class _Timeout(object): + """An IOLoop timeout, a UNIX timestamp and a callback""" + + # Reduce memory overhead when there are lots of pending callbacks + __slots__ = ["deadline", "callback", "tdeadline"] + + def __init__( + self, deadline: float, callback: Callable[[], None], io_loop: IOLoop + ) -> None: + if not isinstance(deadline, numbers.Real): + raise TypeError("Unsupported deadline %r" % deadline) + self.deadline = deadline + self.callback = callback + self.tdeadline = ( + deadline, + next(io_loop._timeout_counter), + ) # type: Tuple[float, int] + + # Comparison methods to sort by deadline, with object id as a tiebreaker + # to guarantee a consistent ordering. The heapq module uses __le__ + # in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons + # use __lt__). + def __lt__(self, other: "_Timeout") -> bool: + return self.tdeadline < other.tdeadline + + def __le__(self, other: "_Timeout") -> bool: + return self.tdeadline <= other.tdeadline + + +class PeriodicCallback(object): + """Schedules the given callback to be called periodically. + + The callback is called every ``callback_time`` milliseconds. + Note that the timeout is given in milliseconds, while most other + time-related functions in Tornado use seconds. + + If ``jitter`` is specified, each callback time will be randomly selected + within a window of ``jitter * callback_time`` milliseconds. + Jitter can be used to reduce alignment of events with similar periods. + A jitter of 0.1 means allowing a 10% variation in callback time. + The window is centered on ``callback_time`` so the total number of calls + within a given interval should not be significantly affected by adding + jitter. + + If the callback runs for longer than ``callback_time`` milliseconds, + subsequent invocations will be skipped to get back on schedule. + + `start` must be called after the `PeriodicCallback` is created. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + + .. versionchanged:: 5.1 + The ``jitter`` argument is added. + """ + + def __init__( + self, callback: Callable[[], None], callback_time: float, jitter: float = 0 + ) -> None: + self.callback = callback + if callback_time <= 0: + raise ValueError("Periodic callback must have a positive callback_time") + self.callback_time = callback_time + self.jitter = jitter + self._running = False + self._timeout = None # type: object + + def start(self) -> None: + """Starts the timer.""" + # Looking up the IOLoop here allows to first instantiate the + # PeriodicCallback in another thread, then start it using + # IOLoop.add_callback(). + self.io_loop = IOLoop.current() + self._running = True + self._next_timeout = self.io_loop.time() + self._schedule_next() + + def stop(self) -> None: + """Stops the timer.""" + self._running = False + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + self._timeout = None + + def is_running(self) -> bool: + """Returns ``True`` if this `.PeriodicCallback` has been started. + + .. versionadded:: 4.1 + """ + return self._running + + def _run(self) -> None: + if not self._running: + return + try: + return self.callback() + except Exception: + app_log.error("Exception in callback %r", self.callback, exc_info=True) + finally: + self._schedule_next() + + def _schedule_next(self) -> None: + if self._running: + self._update_next(self.io_loop.time()) + self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run) + + def _update_next(self, current_time: float) -> None: + callback_time_sec = self.callback_time / 1000.0 + if self.jitter: + # apply jitter fraction + callback_time_sec *= 1 + (self.jitter * (random.random() - 0.5)) + if self._next_timeout <= current_time: + # The period should be measured from the start of one call + # to the start of the next. If one call takes too long, + # skip cycles to get back to a multiple of the original + # schedule. + self._next_timeout += ( + math.floor((current_time - self._next_timeout) / callback_time_sec) + 1 + ) * callback_time_sec + else: + # If the clock moved backwards, ensure we advance the next + # timeout instead of recomputing the same value again. + # This may result in long gaps between callbacks if the + # clock jumps backwards by a lot, but the far more common + # scenario is a small NTP adjustment that should just be + # ignored. + # + # Note that on some systems if time.time() runs slower + # than time.monotonic() (most common on windows), we + # effectively experience a small backwards time jump on + # every iteration because PeriodicCallback uses + # time.time() while asyncio schedules callbacks using + # time.monotonic(). + # https://github.com/tornadoweb/tornado/issues/2333 + self._next_timeout += callback_time_sec diff --git a/telegramer/include/tornado/iostream.py b/telegramer/include/tornado/iostream.py new file mode 100644 index 0000000..19c5485 --- /dev/null +++ b/telegramer/include/tornado/iostream.py @@ -0,0 +1,1660 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Utility classes to write to and read from non-blocking files and sockets. + +Contents: + +* `BaseIOStream`: Generic interface for reading and writing. +* `IOStream`: Implementation of BaseIOStream using non-blocking sockets. +* `SSLIOStream`: SSL-aware version of IOStream. +* `PipeIOStream`: Pipe-based IOStream implementation. +""" + +import asyncio +import collections +import errno +import io +import numbers +import os +import socket +import ssl +import sys +import re + +from tornado.concurrent import Future, future_set_result_unless_cancelled +from tornado import ioloop +from tornado.log import gen_log +from tornado.netutil import ssl_wrap_socket, _client_ssl_defaults, _server_ssl_defaults +from tornado.util import errno_from_exception + +import typing +from typing import ( + Union, + Optional, + Awaitable, + Callable, + Pattern, + Any, + Dict, + TypeVar, + Tuple, +) +from types import TracebackType + +if typing.TYPE_CHECKING: + from typing import Deque, List, Type # noqa: F401 + +_IOStreamType = TypeVar("_IOStreamType", bound="IOStream") + +# These errnos indicate that a connection has been abruptly terminated. +# They should be caught and handled less noisily than other errors. +_ERRNO_CONNRESET = (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE, errno.ETIMEDOUT) + +if hasattr(errno, "WSAECONNRESET"): + _ERRNO_CONNRESET += ( # type: ignore + errno.WSAECONNRESET, # type: ignore + errno.WSAECONNABORTED, # type: ignore + errno.WSAETIMEDOUT, # type: ignore + ) + +if sys.platform == "darwin": + # OSX appears to have a race condition that causes send(2) to return + # EPROTOTYPE if called while a socket is being torn down: + # http://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ + # Since the socket is being closed anyway, treat this as an ECONNRESET + # instead of an unexpected error. + _ERRNO_CONNRESET += (errno.EPROTOTYPE,) # type: ignore + +_WINDOWS = sys.platform.startswith("win") + + +class StreamClosedError(IOError): + """Exception raised by `IOStream` methods when the stream is closed. + + Note that the close callback is scheduled to run *after* other + callbacks on the stream (to allow for buffered data to be processed), + so you may see this error before you see the close callback. + + The ``real_error`` attribute contains the underlying error that caused + the stream to close (if any). + + .. versionchanged:: 4.3 + Added the ``real_error`` attribute. + """ + + def __init__(self, real_error: Optional[BaseException] = None) -> None: + super().__init__("Stream is closed") + self.real_error = real_error + + +class UnsatisfiableReadError(Exception): + """Exception raised when a read cannot be satisfied. + + Raised by ``read_until`` and ``read_until_regex`` with a ``max_bytes`` + argument. + """ + + pass + + +class StreamBufferFullError(Exception): + """Exception raised by `IOStream` methods when the buffer is full. + """ + + +class _StreamBuffer(object): + """ + A specialized buffer that tries to avoid copies when large pieces + of data are encountered. + """ + + def __init__(self) -> None: + # A sequence of (False, bytearray) and (True, memoryview) objects + self._buffers = ( + collections.deque() + ) # type: Deque[Tuple[bool, Union[bytearray, memoryview]]] + # Position in the first buffer + self._first_pos = 0 + self._size = 0 + + def __len__(self) -> int: + return self._size + + # Data above this size will be appended separately instead + # of extending an existing bytearray + _large_buf_threshold = 2048 + + def append(self, data: Union[bytes, bytearray, memoryview]) -> None: + """ + Append the given piece of data (should be a buffer-compatible object). + """ + size = len(data) + if size > self._large_buf_threshold: + if not isinstance(data, memoryview): + data = memoryview(data) + self._buffers.append((True, data)) + elif size > 0: + if self._buffers: + is_memview, b = self._buffers[-1] + new_buf = is_memview or len(b) >= self._large_buf_threshold + else: + new_buf = True + if new_buf: + self._buffers.append((False, bytearray(data))) + else: + b += data # type: ignore + + self._size += size + + def peek(self, size: int) -> memoryview: + """ + Get a view over at most ``size`` bytes (possibly fewer) at the + current buffer position. + """ + assert size > 0 + try: + is_memview, b = self._buffers[0] + except IndexError: + return memoryview(b"") + + pos = self._first_pos + if is_memview: + return typing.cast(memoryview, b[pos : pos + size]) + else: + return memoryview(b)[pos : pos + size] + + def advance(self, size: int) -> None: + """ + Advance the current buffer position by ``size`` bytes. + """ + assert 0 < size <= self._size + self._size -= size + pos = self._first_pos + + buffers = self._buffers + while buffers and size > 0: + is_large, b = buffers[0] + b_remain = len(b) - size - pos + if b_remain <= 0: + buffers.popleft() + size -= len(b) - pos + pos = 0 + elif is_large: + pos += size + size = 0 + else: + # Amortized O(1) shrink for Python 2 + pos += size + if len(b) <= 2 * pos: + del typing.cast(bytearray, b)[:pos] + pos = 0 + size = 0 + + assert size == 0 + self._first_pos = pos + + +class BaseIOStream(object): + """A utility class to write to and read from a non-blocking file or socket. + + We support a non-blocking ``write()`` and a family of ``read_*()`` + methods. When the operation completes, the ``Awaitable`` will resolve + with the data read (or ``None`` for ``write()``). All outstanding + ``Awaitables`` will resolve with a `StreamClosedError` when the + stream is closed; `.BaseIOStream.set_close_callback` can also be used + to be notified of a closed stream. + + When a stream is closed due to an error, the IOStream's ``error`` + attribute contains the exception object. + + Subclasses must implement `fileno`, `close_fd`, `write_to_fd`, + `read_from_fd`, and optionally `get_fd_error`. + + """ + + def __init__( + self, + max_buffer_size: Optional[int] = None, + read_chunk_size: Optional[int] = None, + max_write_buffer_size: Optional[int] = None, + ) -> None: + """`BaseIOStream` constructor. + + :arg max_buffer_size: Maximum amount of incoming data to buffer; + defaults to 100MB. + :arg read_chunk_size: Amount of data to read at one time from the + underlying transport; defaults to 64KB. + :arg max_write_buffer_size: Amount of outgoing data to buffer; + defaults to unlimited. + + .. versionchanged:: 4.0 + Add the ``max_write_buffer_size`` parameter. Changed default + ``read_chunk_size`` to 64KB. + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been + removed. + """ + self.io_loop = ioloop.IOLoop.current() + self.max_buffer_size = max_buffer_size or 104857600 + # A chunk size that is too close to max_buffer_size can cause + # spurious failures. + self.read_chunk_size = min(read_chunk_size or 65536, self.max_buffer_size // 2) + self.max_write_buffer_size = max_write_buffer_size + self.error = None # type: Optional[BaseException] + self._read_buffer = bytearray() + self._read_buffer_pos = 0 + self._read_buffer_size = 0 + self._user_read_buffer = False + self._after_user_read_buffer = None # type: Optional[bytearray] + self._write_buffer = _StreamBuffer() + self._total_write_index = 0 + self._total_write_done_index = 0 + self._read_delimiter = None # type: Optional[bytes] + self._read_regex = None # type: Optional[Pattern] + self._read_max_bytes = None # type: Optional[int] + self._read_bytes = None # type: Optional[int] + self._read_partial = False + self._read_until_close = False + self._read_future = None # type: Optional[Future] + self._write_futures = ( + collections.deque() + ) # type: Deque[Tuple[int, Future[None]]] + self._close_callback = None # type: Optional[Callable[[], None]] + self._connect_future = None # type: Optional[Future[IOStream]] + # _ssl_connect_future should be defined in SSLIOStream + # but it's here so we can clean it up in _signal_closed + # TODO: refactor that so subclasses can add additional futures + # to be cancelled. + self._ssl_connect_future = None # type: Optional[Future[SSLIOStream]] + self._connecting = False + self._state = None # type: Optional[int] + self._closed = False + + def fileno(self) -> Union[int, ioloop._Selectable]: + """Returns the file descriptor for this stream.""" + raise NotImplementedError() + + def close_fd(self) -> None: + """Closes the file underlying this stream. + + ``close_fd`` is called by `BaseIOStream` and should not be called + elsewhere; other users should call `close` instead. + """ + raise NotImplementedError() + + def write_to_fd(self, data: memoryview) -> int: + """Attempts to write ``data`` to the underlying file. + + Returns the number of bytes written. + """ + raise NotImplementedError() + + def read_from_fd(self, buf: Union[bytearray, memoryview]) -> Optional[int]: + """Attempts to read from the underlying file. + + Reads up to ``len(buf)`` bytes, storing them in the buffer. + Returns the number of bytes read. Returns None if there was + nothing to read (the socket returned `~errno.EWOULDBLOCK` or + equivalent), and zero on EOF. + + .. versionchanged:: 5.0 + + Interface redesigned to take a buffer and return a number + of bytes instead of a freshly-allocated object. + """ + raise NotImplementedError() + + def get_fd_error(self) -> Optional[Exception]: + """Returns information about any error on the underlying file. + + This method is called after the `.IOLoop` has signaled an error on the + file descriptor, and should return an Exception (such as `socket.error` + with additional information, or None if no such information is + available. + """ + return None + + def read_until_regex( + self, regex: bytes, max_bytes: Optional[int] = None + ) -> Awaitable[bytes]: + """Asynchronously read until we have matched the given regex. + + The result includes the data that matches the regex and anything + that came before it. + + If ``max_bytes`` is not None, the connection will be closed + if more than ``max_bytes`` bytes have been read and the regex is + not satisfied. + + .. versionchanged:: 4.0 + Added the ``max_bytes`` argument. The ``callback`` argument is + now optional and a `.Future` will be returned if it is omitted. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + `.Future` instead. + + """ + future = self._start_read() + self._read_regex = re.compile(regex) + self._read_max_bytes = max_bytes + try: + self._try_inline_read() + except UnsatisfiableReadError as e: + # Handle this the same way as in _handle_events. + gen_log.info("Unsatisfiable read, closing connection: %s" % e) + self.close(exc_info=e) + return future + except: + # Ensure that the future doesn't log an error because its + # failure was never examined. + future.add_done_callback(lambda f: f.exception()) + raise + return future + + def read_until( + self, delimiter: bytes, max_bytes: Optional[int] = None + ) -> Awaitable[bytes]: + """Asynchronously read until we have found the given delimiter. + + The result includes all the data read including the delimiter. + + If ``max_bytes`` is not None, the connection will be closed + if more than ``max_bytes`` bytes have been read and the delimiter + is not found. + + .. versionchanged:: 4.0 + Added the ``max_bytes`` argument. The ``callback`` argument is + now optional and a `.Future` will be returned if it is omitted. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + `.Future` instead. + """ + future = self._start_read() + self._read_delimiter = delimiter + self._read_max_bytes = max_bytes + try: + self._try_inline_read() + except UnsatisfiableReadError as e: + # Handle this the same way as in _handle_events. + gen_log.info("Unsatisfiable read, closing connection: %s" % e) + self.close(exc_info=e) + return future + except: + future.add_done_callback(lambda f: f.exception()) + raise + return future + + def read_bytes(self, num_bytes: int, partial: bool = False) -> Awaitable[bytes]: + """Asynchronously read a number of bytes. + + If ``partial`` is true, data is returned as soon as we have + any bytes to return (but never more than ``num_bytes``) + + .. versionchanged:: 4.0 + Added the ``partial`` argument. The callback argument is now + optional and a `.Future` will be returned if it is omitted. + + .. versionchanged:: 6.0 + + The ``callback`` and ``streaming_callback`` arguments have + been removed. Use the returned `.Future` (and + ``partial=True`` for ``streaming_callback``) instead. + + """ + future = self._start_read() + assert isinstance(num_bytes, numbers.Integral) + self._read_bytes = num_bytes + self._read_partial = partial + try: + self._try_inline_read() + except: + future.add_done_callback(lambda f: f.exception()) + raise + return future + + def read_into(self, buf: bytearray, partial: bool = False) -> Awaitable[int]: + """Asynchronously read a number of bytes. + + ``buf`` must be a writable buffer into which data will be read. + + If ``partial`` is true, the callback is run as soon as any bytes + have been read. Otherwise, it is run when the ``buf`` has been + entirely filled with read data. + + .. versionadded:: 5.0 + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + `.Future` instead. + + """ + future = self._start_read() + + # First copy data already in read buffer + available_bytes = self._read_buffer_size + n = len(buf) + if available_bytes >= n: + end = self._read_buffer_pos + n + buf[:] = memoryview(self._read_buffer)[self._read_buffer_pos : end] + del self._read_buffer[:end] + self._after_user_read_buffer = self._read_buffer + elif available_bytes > 0: + buf[:available_bytes] = memoryview(self._read_buffer)[ + self._read_buffer_pos : + ] + + # Set up the supplied buffer as our temporary read buffer. + # The original (if it had any data remaining) has been + # saved for later. + self._user_read_buffer = True + self._read_buffer = buf + self._read_buffer_pos = 0 + self._read_buffer_size = available_bytes + self._read_bytes = n + self._read_partial = partial + + try: + self._try_inline_read() + except: + future.add_done_callback(lambda f: f.exception()) + raise + return future + + def read_until_close(self) -> Awaitable[bytes]: + """Asynchronously reads all data from the socket until it is closed. + + This will buffer all available data until ``max_buffer_size`` + is reached. If flow control or cancellation are desired, use a + loop with `read_bytes(partial=True) <.read_bytes>` instead. + + .. versionchanged:: 4.0 + The callback argument is now optional and a `.Future` will + be returned if it is omitted. + + .. versionchanged:: 6.0 + + The ``callback`` and ``streaming_callback`` arguments have + been removed. Use the returned `.Future` (and `read_bytes` + with ``partial=True`` for ``streaming_callback``) instead. + + """ + future = self._start_read() + if self.closed(): + self._finish_read(self._read_buffer_size, False) + return future + self._read_until_close = True + try: + self._try_inline_read() + except: + future.add_done_callback(lambda f: f.exception()) + raise + return future + + def write(self, data: Union[bytes, memoryview]) -> "Future[None]": + """Asynchronously write the given data to this stream. + + This method returns a `.Future` that resolves (with a result + of ``None``) when the write has been completed. + + The ``data`` argument may be of type `bytes` or `memoryview`. + + .. versionchanged:: 4.0 + Now returns a `.Future` if no callback is given. + + .. versionchanged:: 4.5 + Added support for `memoryview` arguments. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + `.Future` instead. + + """ + self._check_closed() + if data: + if ( + self.max_write_buffer_size is not None + and len(self._write_buffer) + len(data) > self.max_write_buffer_size + ): + raise StreamBufferFullError("Reached maximum write buffer size") + self._write_buffer.append(data) + self._total_write_index += len(data) + future = Future() # type: Future[None] + future.add_done_callback(lambda f: f.exception()) + self._write_futures.append((self._total_write_index, future)) + if not self._connecting: + self._handle_write() + if self._write_buffer: + self._add_io_state(self.io_loop.WRITE) + self._maybe_add_error_listener() + return future + + def set_close_callback(self, callback: Optional[Callable[[], None]]) -> None: + """Call the given callback when the stream is closed. + + This mostly is not necessary for applications that use the + `.Future` interface; all outstanding ``Futures`` will resolve + with a `StreamClosedError` when the stream is closed. However, + it is still useful as a way to signal that the stream has been + closed while no other read or write is in progress. + + Unlike other callback-based interfaces, ``set_close_callback`` + was not removed in Tornado 6.0. + """ + self._close_callback = callback + self._maybe_add_error_listener() + + def close( + self, + exc_info: Union[ + None, + bool, + BaseException, + Tuple[ + "Optional[Type[BaseException]]", + Optional[BaseException], + Optional[TracebackType], + ], + ] = False, + ) -> None: + """Close this stream. + + If ``exc_info`` is true, set the ``error`` attribute to the current + exception from `sys.exc_info` (or if ``exc_info`` is a tuple, + use that instead of `sys.exc_info`). + """ + if not self.closed(): + if exc_info: + if isinstance(exc_info, tuple): + self.error = exc_info[1] + elif isinstance(exc_info, BaseException): + self.error = exc_info + else: + exc_info = sys.exc_info() + if any(exc_info): + self.error = exc_info[1] + if self._read_until_close: + self._read_until_close = False + self._finish_read(self._read_buffer_size, False) + elif self._read_future is not None: + # resolve reads that are pending and ready to complete + try: + pos = self._find_read_pos() + except UnsatisfiableReadError: + pass + else: + if pos is not None: + self._read_from_buffer(pos) + if self._state is not None: + self.io_loop.remove_handler(self.fileno()) + self._state = None + self.close_fd() + self._closed = True + self._signal_closed() + + def _signal_closed(self) -> None: + futures = [] # type: List[Future] + if self._read_future is not None: + futures.append(self._read_future) + self._read_future = None + futures += [future for _, future in self._write_futures] + self._write_futures.clear() + if self._connect_future is not None: + futures.append(self._connect_future) + self._connect_future = None + for future in futures: + if not future.done(): + future.set_exception(StreamClosedError(real_error=self.error)) + # Reference the exception to silence warnings. Annoyingly, + # this raises if the future was cancelled, but just + # returns any other error. + try: + future.exception() + except asyncio.CancelledError: + pass + if self._ssl_connect_future is not None: + # _ssl_connect_future expects to see the real exception (typically + # an ssl.SSLError), not just StreamClosedError. + if not self._ssl_connect_future.done(): + if self.error is not None: + self._ssl_connect_future.set_exception(self.error) + else: + self._ssl_connect_future.set_exception(StreamClosedError()) + self._ssl_connect_future.exception() + self._ssl_connect_future = None + if self._close_callback is not None: + cb = self._close_callback + self._close_callback = None + self.io_loop.add_callback(cb) + # Clear the buffers so they can be cleared immediately even + # if the IOStream object is kept alive by a reference cycle. + # TODO: Clear the read buffer too; it currently breaks some tests. + self._write_buffer = None # type: ignore + + def reading(self) -> bool: + """Returns ``True`` if we are currently reading from the stream.""" + return self._read_future is not None + + def writing(self) -> bool: + """Returns ``True`` if we are currently writing to the stream.""" + return bool(self._write_buffer) + + def closed(self) -> bool: + """Returns ``True`` if the stream has been closed.""" + return self._closed + + def set_nodelay(self, value: bool) -> None: + """Sets the no-delay flag for this stream. + + By default, data written to TCP streams may be held for a time + to make the most efficient use of bandwidth (according to + Nagle's algorithm). The no-delay flag requests that data be + written as soon as possible, even if doing so would consume + additional bandwidth. + + This flag is currently defined only for TCP-based ``IOStreams``. + + .. versionadded:: 3.1 + """ + pass + + def _handle_connect(self) -> None: + raise NotImplementedError() + + def _handle_events(self, fd: Union[int, ioloop._Selectable], events: int) -> None: + if self.closed(): + gen_log.warning("Got events for closed stream %s", fd) + return + try: + if self._connecting: + # Most IOLoops will report a write failed connect + # with the WRITE event, but SelectIOLoop reports a + # READ as well so we must check for connecting before + # either. + self._handle_connect() + if self.closed(): + return + if events & self.io_loop.READ: + self._handle_read() + if self.closed(): + return + if events & self.io_loop.WRITE: + self._handle_write() + if self.closed(): + return + if events & self.io_loop.ERROR: + self.error = self.get_fd_error() + # We may have queued up a user callback in _handle_read or + # _handle_write, so don't close the IOStream until those + # callbacks have had a chance to run. + self.io_loop.add_callback(self.close) + return + state = self.io_loop.ERROR + if self.reading(): + state |= self.io_loop.READ + if self.writing(): + state |= self.io_loop.WRITE + if state == self.io_loop.ERROR and self._read_buffer_size == 0: + # If the connection is idle, listen for reads too so + # we can tell if the connection is closed. If there is + # data in the read buffer we won't run the close callback + # yet anyway, so we don't need to listen in this case. + state |= self.io_loop.READ + if state != self._state: + assert ( + self._state is not None + ), "shouldn't happen: _handle_events without self._state" + self._state = state + self.io_loop.update_handler(self.fileno(), self._state) + except UnsatisfiableReadError as e: + gen_log.info("Unsatisfiable read, closing connection: %s" % e) + self.close(exc_info=e) + except Exception as e: + gen_log.error("Uncaught exception, closing connection.", exc_info=True) + self.close(exc_info=e) + raise + + def _read_to_buffer_loop(self) -> Optional[int]: + # This method is called from _handle_read and _try_inline_read. + if self._read_bytes is not None: + target_bytes = self._read_bytes # type: Optional[int] + elif self._read_max_bytes is not None: + target_bytes = self._read_max_bytes + elif self.reading(): + # For read_until without max_bytes, or + # read_until_close, read as much as we can before + # scanning for the delimiter. + target_bytes = None + else: + target_bytes = 0 + next_find_pos = 0 + while not self.closed(): + # Read from the socket until we get EWOULDBLOCK or equivalent. + # SSL sockets do some internal buffering, and if the data is + # sitting in the SSL object's buffer select() and friends + # can't see it; the only way to find out if it's there is to + # try to read it. + if self._read_to_buffer() == 0: + break + + # If we've read all the bytes we can use, break out of + # this loop. + + # If we've reached target_bytes, we know we're done. + if target_bytes is not None and self._read_buffer_size >= target_bytes: + break + + # Otherwise, we need to call the more expensive find_read_pos. + # It's inefficient to do this on every read, so instead + # do it on the first read and whenever the read buffer + # size has doubled. + if self._read_buffer_size >= next_find_pos: + pos = self._find_read_pos() + if pos is not None: + return pos + next_find_pos = self._read_buffer_size * 2 + return self._find_read_pos() + + def _handle_read(self) -> None: + try: + pos = self._read_to_buffer_loop() + except UnsatisfiableReadError: + raise + except asyncio.CancelledError: + raise + except Exception as e: + gen_log.warning("error on read: %s" % e) + self.close(exc_info=e) + return + if pos is not None: + self._read_from_buffer(pos) + + def _start_read(self) -> Future: + if self._read_future is not None: + # It is an error to start a read while a prior read is unresolved. + # However, if the prior read is unresolved because the stream was + # closed without satisfying it, it's better to raise + # StreamClosedError instead of AssertionError. In particular, this + # situation occurs in harmless situations in http1connection.py and + # an AssertionError would be logged noisily. + # + # On the other hand, it is legal to start a new read while the + # stream is closed, in case the read can be satisfied from the + # read buffer. So we only want to check the closed status of the + # stream if we need to decide what kind of error to raise for + # "already reading". + # + # These conditions have proven difficult to test; we have no + # unittests that reliably verify this behavior so be careful + # when making changes here. See #2651 and #2719. + self._check_closed() + assert self._read_future is None, "Already reading" + self._read_future = Future() + return self._read_future + + def _finish_read(self, size: int, streaming: bool) -> None: + if self._user_read_buffer: + self._read_buffer = self._after_user_read_buffer or bytearray() + self._after_user_read_buffer = None + self._read_buffer_pos = 0 + self._read_buffer_size = len(self._read_buffer) + self._user_read_buffer = False + result = size # type: Union[int, bytes] + else: + result = self._consume(size) + if self._read_future is not None: + future = self._read_future + self._read_future = None + future_set_result_unless_cancelled(future, result) + self._maybe_add_error_listener() + + def _try_inline_read(self) -> None: + """Attempt to complete the current read operation from buffered data. + + If the read can be completed without blocking, schedules the + read callback on the next IOLoop iteration; otherwise starts + listening for reads on the socket. + """ + # See if we've already got the data from a previous read + pos = self._find_read_pos() + if pos is not None: + self._read_from_buffer(pos) + return + self._check_closed() + pos = self._read_to_buffer_loop() + if pos is not None: + self._read_from_buffer(pos) + return + # We couldn't satisfy the read inline, so make sure we're + # listening for new data unless the stream is closed. + if not self.closed(): + self._add_io_state(ioloop.IOLoop.READ) + + def _read_to_buffer(self) -> Optional[int]: + """Reads from the socket and appends the result to the read buffer. + + Returns the number of bytes read. Returns 0 if there is nothing + to read (i.e. the read returns EWOULDBLOCK or equivalent). On + error closes the socket and raises an exception. + """ + try: + while True: + try: + if self._user_read_buffer: + buf = memoryview(self._read_buffer)[ + self._read_buffer_size : + ] # type: Union[memoryview, bytearray] + else: + buf = bytearray(self.read_chunk_size) + bytes_read = self.read_from_fd(buf) + except (socket.error, IOError, OSError) as e: + # ssl.SSLError is a subclass of socket.error + if self._is_connreset(e): + # Treat ECONNRESET as a connection close rather than + # an error to minimize log spam (the exception will + # be available on self.error for apps that care). + self.close(exc_info=e) + return None + self.close(exc_info=e) + raise + break + if bytes_read is None: + return 0 + elif bytes_read == 0: + self.close() + return 0 + if not self._user_read_buffer: + self._read_buffer += memoryview(buf)[:bytes_read] + self._read_buffer_size += bytes_read + finally: + # Break the reference to buf so we don't waste a chunk's worth of + # memory in case an exception hangs on to our stack frame. + del buf + if self._read_buffer_size > self.max_buffer_size: + gen_log.error("Reached maximum read buffer size") + self.close() + raise StreamBufferFullError("Reached maximum read buffer size") + return bytes_read + + def _read_from_buffer(self, pos: int) -> None: + """Attempts to complete the currently-pending read from the buffer. + + The argument is either a position in the read buffer or None, + as returned by _find_read_pos. + """ + self._read_bytes = self._read_delimiter = self._read_regex = None + self._read_partial = False + self._finish_read(pos, False) + + def _find_read_pos(self) -> Optional[int]: + """Attempts to find a position in the read buffer that satisfies + the currently-pending read. + + Returns a position in the buffer if the current read can be satisfied, + or None if it cannot. + """ + if self._read_bytes is not None and ( + self._read_buffer_size >= self._read_bytes + or (self._read_partial and self._read_buffer_size > 0) + ): + num_bytes = min(self._read_bytes, self._read_buffer_size) + return num_bytes + elif self._read_delimiter is not None: + # Multi-byte delimiters (e.g. '\r\n') may straddle two + # chunks in the read buffer, so we can't easily find them + # without collapsing the buffer. However, since protocols + # using delimited reads (as opposed to reads of a known + # length) tend to be "line" oriented, the delimiter is likely + # to be in the first few chunks. Merge the buffer gradually + # since large merges are relatively expensive and get undone in + # _consume(). + if self._read_buffer: + loc = self._read_buffer.find( + self._read_delimiter, self._read_buffer_pos + ) + if loc != -1: + loc -= self._read_buffer_pos + delimiter_len = len(self._read_delimiter) + self._check_max_bytes(self._read_delimiter, loc + delimiter_len) + return loc + delimiter_len + self._check_max_bytes(self._read_delimiter, self._read_buffer_size) + elif self._read_regex is not None: + if self._read_buffer: + m = self._read_regex.search(self._read_buffer, self._read_buffer_pos) + if m is not None: + loc = m.end() - self._read_buffer_pos + self._check_max_bytes(self._read_regex, loc) + return loc + self._check_max_bytes(self._read_regex, self._read_buffer_size) + return None + + def _check_max_bytes(self, delimiter: Union[bytes, Pattern], size: int) -> None: + if self._read_max_bytes is not None and size > self._read_max_bytes: + raise UnsatisfiableReadError( + "delimiter %r not found within %d bytes" + % (delimiter, self._read_max_bytes) + ) + + def _handle_write(self) -> None: + while True: + size = len(self._write_buffer) + if not size: + break + assert size > 0 + try: + if _WINDOWS: + # On windows, socket.send blows up if given a + # write buffer that's too large, instead of just + # returning the number of bytes it was able to + # process. Therefore we must not call socket.send + # with more than 128KB at a time. + size = 128 * 1024 + + num_bytes = self.write_to_fd(self._write_buffer.peek(size)) + if num_bytes == 0: + break + self._write_buffer.advance(num_bytes) + self._total_write_done_index += num_bytes + except BlockingIOError: + break + except (socket.error, IOError, OSError) as e: + if not self._is_connreset(e): + # Broken pipe errors are usually caused by connection + # reset, and its better to not log EPIPE errors to + # minimize log spam + gen_log.warning("Write error on %s: %s", self.fileno(), e) + self.close(exc_info=e) + return + + while self._write_futures: + index, future = self._write_futures[0] + if index > self._total_write_done_index: + break + self._write_futures.popleft() + future_set_result_unless_cancelled(future, None) + + def _consume(self, loc: int) -> bytes: + # Consume loc bytes from the read buffer and return them + if loc == 0: + return b"" + assert loc <= self._read_buffer_size + # Slice the bytearray buffer into bytes, without intermediate copying + b = ( + memoryview(self._read_buffer)[ + self._read_buffer_pos : self._read_buffer_pos + loc + ] + ).tobytes() + self._read_buffer_pos += loc + self._read_buffer_size -= loc + # Amortized O(1) shrink + # (this heuristic is implemented natively in Python 3.4+ + # but is replicated here for Python 2) + if self._read_buffer_pos > self._read_buffer_size: + del self._read_buffer[: self._read_buffer_pos] + self._read_buffer_pos = 0 + return b + + def _check_closed(self) -> None: + if self.closed(): + raise StreamClosedError(real_error=self.error) + + def _maybe_add_error_listener(self) -> None: + # This method is part of an optimization: to detect a connection that + # is closed when we're not actively reading or writing, we must listen + # for read events. However, it is inefficient to do this when the + # connection is first established because we are going to read or write + # immediately anyway. Instead, we insert checks at various times to + # see if the connection is idle and add the read listener then. + if self._state is None or self._state == ioloop.IOLoop.ERROR: + if ( + not self.closed() + and self._read_buffer_size == 0 + and self._close_callback is not None + ): + self._add_io_state(ioloop.IOLoop.READ) + + def _add_io_state(self, state: int) -> None: + """Adds `state` (IOLoop.{READ,WRITE} flags) to our event handler. + + Implementation notes: Reads and writes have a fast path and a + slow path. The fast path reads synchronously from socket + buffers, while the slow path uses `_add_io_state` to schedule + an IOLoop callback. + + To detect closed connections, we must have called + `_add_io_state` at some point, but we want to delay this as + much as possible so we don't have to set an `IOLoop.ERROR` + listener that will be overwritten by the next slow-path + operation. If a sequence of fast-path ops do not end in a + slow-path op, (e.g. for an @asynchronous long-poll request), + we must add the error handler. + + TODO: reevaluate this now that callbacks are gone. + + """ + if self.closed(): + # connection has been closed, so there can be no future events + return + if self._state is None: + self._state = ioloop.IOLoop.ERROR | state + self.io_loop.add_handler(self.fileno(), self._handle_events, self._state) + elif not self._state & state: + self._state = self._state | state + self.io_loop.update_handler(self.fileno(), self._state) + + def _is_connreset(self, exc: BaseException) -> bool: + """Return ``True`` if exc is ECONNRESET or equivalent. + + May be overridden in subclasses. + """ + return ( + isinstance(exc, (socket.error, IOError)) + and errno_from_exception(exc) in _ERRNO_CONNRESET + ) + + +class IOStream(BaseIOStream): + r"""Socket-based `IOStream` implementation. + + This class supports the read and write methods from `BaseIOStream` + plus a `connect` method. + + The ``socket`` parameter may either be connected or unconnected. + For server operations the socket is the result of calling + `socket.accept `. For client operations the + socket is created with `socket.socket`, and may either be + connected before passing it to the `IOStream` or connected with + `IOStream.connect`. + + A very simple (and broken) HTTP client using this class: + + .. testcode:: + + import tornado.ioloop + import tornado.iostream + import socket + + async def main(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + stream = tornado.iostream.IOStream(s) + await stream.connect(("friendfeed.com", 80)) + await stream.write(b"GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n") + header_data = await stream.read_until(b"\r\n\r\n") + headers = {} + for line in header_data.split(b"\r\n"): + parts = line.split(b":") + if len(parts) == 2: + headers[parts[0].strip()] = parts[1].strip() + body_data = await stream.read_bytes(int(headers[b"Content-Length"])) + print(body_data) + stream.close() + + if __name__ == '__main__': + tornado.ioloop.IOLoop.current().run_sync(main) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + stream = tornado.iostream.IOStream(s) + stream.connect(("friendfeed.com", 80), send_request) + tornado.ioloop.IOLoop.current().start() + + .. testoutput:: + :hide: + + """ + + def __init__(self, socket: socket.socket, *args: Any, **kwargs: Any) -> None: + self.socket = socket + self.socket.setblocking(False) + super().__init__(*args, **kwargs) + + def fileno(self) -> Union[int, ioloop._Selectable]: + return self.socket + + def close_fd(self) -> None: + self.socket.close() + self.socket = None # type: ignore + + def get_fd_error(self) -> Optional[Exception]: + errno = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + return socket.error(errno, os.strerror(errno)) + + def read_from_fd(self, buf: Union[bytearray, memoryview]) -> Optional[int]: + try: + return self.socket.recv_into(buf, len(buf)) + except BlockingIOError: + return None + finally: + del buf + + def write_to_fd(self, data: memoryview) -> int: + try: + return self.socket.send(data) # type: ignore + finally: + # Avoid keeping to data, which can be a memoryview. + # See https://github.com/tornadoweb/tornado/pull/2008 + del data + + def connect( + self: _IOStreamType, address: Any, server_hostname: Optional[str] = None + ) -> "Future[_IOStreamType]": + """Connects the socket to a remote address without blocking. + + May only be called if the socket passed to the constructor was + not previously connected. The address parameter is in the + same format as for `socket.connect ` for + the type of socket passed to the IOStream constructor, + e.g. an ``(ip, port)`` tuple. Hostnames are accepted here, + but will be resolved synchronously and block the IOLoop. + If you have a hostname instead of an IP address, the `.TCPClient` + class is recommended instead of calling this method directly. + `.TCPClient` will do asynchronous DNS resolution and handle + both IPv4 and IPv6. + + If ``callback`` is specified, it will be called with no + arguments when the connection is completed; if not this method + returns a `.Future` (whose result after a successful + connection will be the stream itself). + + In SSL mode, the ``server_hostname`` parameter will be used + for certificate validation (unless disabled in the + ``ssl_options``) and SNI (if supported; requires Python + 2.7.9+). + + Note that it is safe to call `IOStream.write + ` while the connection is pending, in + which case the data will be written as soon as the connection + is ready. Calling `IOStream` read methods before the socket is + connected works on some platforms but is non-portable. + + .. versionchanged:: 4.0 + If no callback is given, returns a `.Future`. + + .. versionchanged:: 4.2 + SSL certificates are validated by default; pass + ``ssl_options=dict(cert_reqs=ssl.CERT_NONE)`` or a + suitably-configured `ssl.SSLContext` to the + `SSLIOStream` constructor to disable. + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + `.Future` instead. + + """ + self._connecting = True + future = Future() # type: Future[_IOStreamType] + self._connect_future = typing.cast("Future[IOStream]", future) + try: + self.socket.connect(address) + except BlockingIOError: + # In non-blocking mode we expect connect() to raise an + # exception with EINPROGRESS or EWOULDBLOCK. + pass + except socket.error as e: + # On freebsd, other errors such as ECONNREFUSED may be + # returned immediately when attempting to connect to + # localhost, so handle them the same way as an error + # reported later in _handle_connect. + if future is None: + gen_log.warning("Connect error on fd %s: %s", self.socket.fileno(), e) + self.close(exc_info=e) + return future + self._add_io_state(self.io_loop.WRITE) + return future + + def start_tls( + self, + server_side: bool, + ssl_options: Optional[Union[Dict[str, Any], ssl.SSLContext]] = None, + server_hostname: Optional[str] = None, + ) -> Awaitable["SSLIOStream"]: + """Convert this `IOStream` to an `SSLIOStream`. + + This enables protocols that begin in clear-text mode and + switch to SSL after some initial negotiation (such as the + ``STARTTLS`` extension to SMTP and IMAP). + + This method cannot be used if there are outstanding reads + or writes on the stream, or if there is any data in the + IOStream's buffer (data in the operating system's socket + buffer is allowed). This means it must generally be used + immediately after reading or writing the last clear-text + data. It can also be used immediately after connecting, + before any reads or writes. + + The ``ssl_options`` argument may be either an `ssl.SSLContext` + object or a dictionary of keyword arguments for the + `ssl.wrap_socket` function. The ``server_hostname`` argument + will be used for certificate validation unless disabled + in the ``ssl_options``. + + This method returns a `.Future` whose result is the new + `SSLIOStream`. After this method has been called, + any other operation on the original stream is undefined. + + If a close callback is defined on this stream, it will be + transferred to the new stream. + + .. versionadded:: 4.0 + + .. versionchanged:: 4.2 + SSL certificates are validated by default; pass + ``ssl_options=dict(cert_reqs=ssl.CERT_NONE)`` or a + suitably-configured `ssl.SSLContext` to disable. + """ + if ( + self._read_future + or self._write_futures + or self._connect_future + or self._closed + or self._read_buffer + or self._write_buffer + ): + raise ValueError("IOStream is not idle; cannot convert to SSL") + if ssl_options is None: + if server_side: + ssl_options = _server_ssl_defaults + else: + ssl_options = _client_ssl_defaults + + socket = self.socket + self.io_loop.remove_handler(socket) + self.socket = None # type: ignore + socket = ssl_wrap_socket( + socket, + ssl_options, + server_hostname=server_hostname, + server_side=server_side, + do_handshake_on_connect=False, + ) + orig_close_callback = self._close_callback + self._close_callback = None + + future = Future() # type: Future[SSLIOStream] + ssl_stream = SSLIOStream(socket, ssl_options=ssl_options) + ssl_stream.set_close_callback(orig_close_callback) + ssl_stream._ssl_connect_future = future + ssl_stream.max_buffer_size = self.max_buffer_size + ssl_stream.read_chunk_size = self.read_chunk_size + return future + + def _handle_connect(self) -> None: + try: + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + except socket.error as e: + # Hurd doesn't allow SO_ERROR for loopback sockets because all + # errors for such sockets are reported synchronously. + if errno_from_exception(e) == errno.ENOPROTOOPT: + err = 0 + if err != 0: + self.error = socket.error(err, os.strerror(err)) + # IOLoop implementations may vary: some of them return + # an error state before the socket becomes writable, so + # in that case a connection failure would be handled by the + # error path in _handle_events instead of here. + if self._connect_future is None: + gen_log.warning( + "Connect error on fd %s: %s", + self.socket.fileno(), + errno.errorcode[err], + ) + self.close() + return + if self._connect_future is not None: + future = self._connect_future + self._connect_future = None + future_set_result_unless_cancelled(future, self) + self._connecting = False + + def set_nodelay(self, value: bool) -> None: + if self.socket is not None and self.socket.family in ( + socket.AF_INET, + socket.AF_INET6, + ): + try: + self.socket.setsockopt( + socket.IPPROTO_TCP, socket.TCP_NODELAY, 1 if value else 0 + ) + except socket.error as e: + # Sometimes setsockopt will fail if the socket is closed + # at the wrong time. This can happen with HTTPServer + # resetting the value to ``False`` between requests. + if e.errno != errno.EINVAL and not self._is_connreset(e): + raise + + +class SSLIOStream(IOStream): + """A utility class to write to and read from a non-blocking SSL socket. + + If the socket passed to the constructor is already connected, + it should be wrapped with:: + + ssl.wrap_socket(sock, do_handshake_on_connect=False, **kwargs) + + before constructing the `SSLIOStream`. Unconnected sockets will be + wrapped when `IOStream.connect` is finished. + """ + + socket = None # type: ssl.SSLSocket + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """The ``ssl_options`` keyword argument may either be an + `ssl.SSLContext` object or a dictionary of keywords arguments + for `ssl.wrap_socket` + """ + self._ssl_options = kwargs.pop("ssl_options", _client_ssl_defaults) + super().__init__(*args, **kwargs) + self._ssl_accepting = True + self._handshake_reading = False + self._handshake_writing = False + self._server_hostname = None # type: Optional[str] + + # If the socket is already connected, attempt to start the handshake. + try: + self.socket.getpeername() + except socket.error: + pass + else: + # Indirectly start the handshake, which will run on the next + # IOLoop iteration and then the real IO state will be set in + # _handle_events. + self._add_io_state(self.io_loop.WRITE) + + def reading(self) -> bool: + return self._handshake_reading or super().reading() + + def writing(self) -> bool: + return self._handshake_writing or super().writing() + + def _do_ssl_handshake(self) -> None: + # Based on code from test_ssl.py in the python stdlib + try: + self._handshake_reading = False + self._handshake_writing = False + self.socket.do_handshake() + except ssl.SSLError as err: + if err.args[0] == ssl.SSL_ERROR_WANT_READ: + self._handshake_reading = True + return + elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE: + self._handshake_writing = True + return + elif err.args[0] in (ssl.SSL_ERROR_EOF, ssl.SSL_ERROR_ZERO_RETURN): + return self.close(exc_info=err) + elif err.args[0] == ssl.SSL_ERROR_SSL: + try: + peer = self.socket.getpeername() + except Exception: + peer = "(not connected)" + gen_log.warning( + "SSL Error on %s %s: %s", self.socket.fileno(), peer, err + ) + return self.close(exc_info=err) + raise + except ssl.CertificateError as err: + # CertificateError can happen during handshake (hostname + # verification) and should be passed to user. Starting + # in Python 3.7, this error is a subclass of SSLError + # and will be handled by the previous block instead. + return self.close(exc_info=err) + except socket.error as err: + # Some port scans (e.g. nmap in -sT mode) have been known + # to cause do_handshake to raise EBADF and ENOTCONN, so make + # those errors quiet as well. + # https://groups.google.com/forum/?fromgroups#!topic/python-tornado/ApucKJat1_0 + # Errno 0 is also possible in some cases (nc -z). + # https://github.com/tornadoweb/tornado/issues/2504 + if self._is_connreset(err) or err.args[0] in ( + 0, + errno.EBADF, + errno.ENOTCONN, + ): + return self.close(exc_info=err) + raise + except AttributeError as err: + # On Linux, if the connection was reset before the call to + # wrap_socket, do_handshake will fail with an + # AttributeError. + return self.close(exc_info=err) + else: + self._ssl_accepting = False + if not self._verify_cert(self.socket.getpeercert()): + self.close() + return + self._finish_ssl_connect() + + def _finish_ssl_connect(self) -> None: + if self._ssl_connect_future is not None: + future = self._ssl_connect_future + self._ssl_connect_future = None + future_set_result_unless_cancelled(future, self) + + def _verify_cert(self, peercert: Any) -> bool: + """Returns ``True`` if peercert is valid according to the configured + validation mode and hostname. + + The ssl handshake already tested the certificate for a valid + CA signature; the only thing that remains is to check + the hostname. + """ + if isinstance(self._ssl_options, dict): + verify_mode = self._ssl_options.get("cert_reqs", ssl.CERT_NONE) + elif isinstance(self._ssl_options, ssl.SSLContext): + verify_mode = self._ssl_options.verify_mode + assert verify_mode in (ssl.CERT_NONE, ssl.CERT_REQUIRED, ssl.CERT_OPTIONAL) + if verify_mode == ssl.CERT_NONE or self._server_hostname is None: + return True + cert = self.socket.getpeercert() + if cert is None and verify_mode == ssl.CERT_REQUIRED: + gen_log.warning("No SSL certificate given") + return False + try: + ssl.match_hostname(peercert, self._server_hostname) + except ssl.CertificateError as e: + gen_log.warning("Invalid SSL certificate: %s" % e) + return False + else: + return True + + def _handle_read(self) -> None: + if self._ssl_accepting: + self._do_ssl_handshake() + return + super()._handle_read() + + def _handle_write(self) -> None: + if self._ssl_accepting: + self._do_ssl_handshake() + return + super()._handle_write() + + def connect( + self, address: Tuple, server_hostname: Optional[str] = None + ) -> "Future[SSLIOStream]": + self._server_hostname = server_hostname + # Ignore the result of connect(). If it fails, + # wait_for_handshake will raise an error too. This is + # necessary for the old semantics of the connect callback + # (which takes no arguments). In 6.0 this can be refactored to + # be a regular coroutine. + # TODO: This is trickier than it looks, since if write() + # is called with a connect() pending, we want the connect + # to resolve before the write. Or do we care about this? + # (There's a test for it, but I think in practice users + # either wait for the connect before performing a write or + # they don't care about the connect Future at all) + fut = super().connect(address) + fut.add_done_callback(lambda f: f.exception()) + return self.wait_for_handshake() + + def _handle_connect(self) -> None: + # Call the superclass method to check for errors. + super()._handle_connect() + if self.closed(): + return + # When the connection is complete, wrap the socket for SSL + # traffic. Note that we do this by overriding _handle_connect + # instead of by passing a callback to super().connect because + # user callbacks are enqueued asynchronously on the IOLoop, + # but since _handle_events calls _handle_connect immediately + # followed by _handle_write we need this to be synchronous. + # + # The IOLoop will get confused if we swap out self.socket while the + # fd is registered, so remove it now and re-register after + # wrap_socket(). + self.io_loop.remove_handler(self.socket) + old_state = self._state + assert old_state is not None + self._state = None + self.socket = ssl_wrap_socket( + self.socket, + self._ssl_options, + server_hostname=self._server_hostname, + do_handshake_on_connect=False, + ) + self._add_io_state(old_state) + + def wait_for_handshake(self) -> "Future[SSLIOStream]": + """Wait for the initial SSL handshake to complete. + + If a ``callback`` is given, it will be called with no + arguments once the handshake is complete; otherwise this + method returns a `.Future` which will resolve to the + stream itself after the handshake is complete. + + Once the handshake is complete, information such as + the peer's certificate and NPN/ALPN selections may be + accessed on ``self.socket``. + + This method is intended for use on server-side streams + or after using `IOStream.start_tls`; it should not be used + with `IOStream.connect` (which already waits for the + handshake to complete). It may only be called once per stream. + + .. versionadded:: 4.2 + + .. versionchanged:: 6.0 + + The ``callback`` argument was removed. Use the returned + `.Future` instead. + + """ + if self._ssl_connect_future is not None: + raise RuntimeError("Already waiting") + future = self._ssl_connect_future = Future() + if not self._ssl_accepting: + self._finish_ssl_connect() + return future + + def write_to_fd(self, data: memoryview) -> int: + try: + return self.socket.send(data) # type: ignore + except ssl.SSLError as e: + if e.args[0] == ssl.SSL_ERROR_WANT_WRITE: + # In Python 3.5+, SSLSocket.send raises a WANT_WRITE error if + # the socket is not writeable; we need to transform this into + # an EWOULDBLOCK socket.error or a zero return value, + # either of which will be recognized by the caller of this + # method. Prior to Python 3.5, an unwriteable socket would + # simply return 0 bytes written. + return 0 + raise + finally: + # Avoid keeping to data, which can be a memoryview. + # See https://github.com/tornadoweb/tornado/pull/2008 + del data + + def read_from_fd(self, buf: Union[bytearray, memoryview]) -> Optional[int]: + try: + if self._ssl_accepting: + # If the handshake hasn't finished yet, there can't be anything + # to read (attempting to read may or may not raise an exception + # depending on the SSL version) + return None + try: + return self.socket.recv_into(buf, len(buf)) + except ssl.SSLError as e: + # SSLError is a subclass of socket.error, so this except + # block must come first. + if e.args[0] == ssl.SSL_ERROR_WANT_READ: + return None + else: + raise + except BlockingIOError: + return None + finally: + del buf + + def _is_connreset(self, e: BaseException) -> bool: + if isinstance(e, ssl.SSLError) and e.args[0] == ssl.SSL_ERROR_EOF: + return True + return super()._is_connreset(e) + + +class PipeIOStream(BaseIOStream): + """Pipe-based `IOStream` implementation. + + The constructor takes an integer file descriptor (such as one returned + by `os.pipe`) rather than an open file object. Pipes are generally + one-way, so a `PipeIOStream` can be used for reading or writing but not + both. + + ``PipeIOStream`` is only available on Unix-based platforms. + """ + + def __init__(self, fd: int, *args: Any, **kwargs: Any) -> None: + self.fd = fd + self._fio = io.FileIO(self.fd, "r+") + os.set_blocking(fd, False) + super().__init__(*args, **kwargs) + + def fileno(self) -> int: + return self.fd + + def close_fd(self) -> None: + self._fio.close() + + def write_to_fd(self, data: memoryview) -> int: + try: + return os.write(self.fd, data) # type: ignore + finally: + # Avoid keeping to data, which can be a memoryview. + # See https://github.com/tornadoweb/tornado/pull/2008 + del data + + def read_from_fd(self, buf: Union[bytearray, memoryview]) -> Optional[int]: + try: + return self._fio.readinto(buf) # type: ignore + except (IOError, OSError) as e: + if errno_from_exception(e) == errno.EBADF: + # If the writing half of a pipe is closed, select will + # report it as readable but reads will fail with EBADF. + self.close(exc_info=e) + return None + else: + raise + finally: + del buf + + +def doctests() -> Any: + import doctest + + return doctest.DocTestSuite() diff --git a/telegramer/include/tornado/locale.py b/telegramer/include/tornado/locale.py new file mode 100644 index 0000000..adb1f77 --- /dev/null +++ b/telegramer/include/tornado/locale.py @@ -0,0 +1,581 @@ +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Translation methods for generating localized strings. + +To load a locale and generate a translated string:: + + user_locale = tornado.locale.get("es_LA") + print(user_locale.translate("Sign out")) + +`tornado.locale.get()` returns the closest matching locale, not necessarily the +specific locale you requested. You can support pluralization with +additional arguments to `~Locale.translate()`, e.g.:: + + people = [...] + message = user_locale.translate( + "%(list)s is online", "%(list)s are online", len(people)) + print(message % {"list": user_locale.list(people)}) + +The first string is chosen if ``len(people) == 1``, otherwise the second +string is chosen. + +Applications should call one of `load_translations` (which uses a simple +CSV format) or `load_gettext_translations` (which uses the ``.mo`` format +supported by `gettext` and related tools). If neither method is called, +the `Locale.translate` method will simply return the original string. +""" + +import codecs +import csv +import datetime +import gettext +import os +import re + +from tornado import escape +from tornado.log import gen_log + +from tornado._locale_data import LOCALE_NAMES + +from typing import Iterable, Any, Union, Dict, Optional + +_default_locale = "en_US" +_translations = {} # type: Dict[str, Any] +_supported_locales = frozenset([_default_locale]) +_use_gettext = False +CONTEXT_SEPARATOR = "\x04" + + +def get(*locale_codes: str) -> "Locale": + """Returns the closest match for the given locale codes. + + We iterate over all given locale codes in order. If we have a tight + or a loose match for the code (e.g., "en" for "en_US"), we return + the locale. Otherwise we move to the next code in the list. + + By default we return ``en_US`` if no translations are found for any of + the specified locales. You can change the default locale with + `set_default_locale()`. + """ + return Locale.get_closest(*locale_codes) + + +def set_default_locale(code: str) -> None: + """Sets the default locale. + + The default locale is assumed to be the language used for all strings + in the system. The translations loaded from disk are mappings from + the default locale to the destination locale. Consequently, you don't + need to create a translation file for the default locale. + """ + global _default_locale + global _supported_locales + _default_locale = code + _supported_locales = frozenset(list(_translations.keys()) + [_default_locale]) + + +def load_translations(directory: str, encoding: Optional[str] = None) -> None: + """Loads translations from CSV files in a directory. + + Translations are strings with optional Python-style named placeholders + (e.g., ``My name is %(name)s``) and their associated translations. + + The directory should have translation files of the form ``LOCALE.csv``, + e.g. ``es_GT.csv``. The CSV files should have two or three columns: string, + translation, and an optional plural indicator. Plural indicators should + be one of "plural" or "singular". A given string can have both singular + and plural forms. For example ``%(name)s liked this`` may have a + different verb conjugation depending on whether %(name)s is one + name or a list of names. There should be two rows in the CSV file for + that string, one with plural indicator "singular", and one "plural". + For strings with no verbs that would change on translation, simply + use "unknown" or the empty string (or don't include the column at all). + + The file is read using the `csv` module in the default "excel" dialect. + In this format there should not be spaces after the commas. + + If no ``encoding`` parameter is given, the encoding will be + detected automatically (among UTF-8 and UTF-16) if the file + contains a byte-order marker (BOM), defaulting to UTF-8 if no BOM + is present. + + Example translation ``es_LA.csv``:: + + "I love you","Te amo" + "%(name)s liked this","A %(name)s les gustó esto","plural" + "%(name)s liked this","A %(name)s le gustó esto","singular" + + .. versionchanged:: 4.3 + Added ``encoding`` parameter. Added support for BOM-based encoding + detection, UTF-16, and UTF-8-with-BOM. + """ + global _translations + global _supported_locales + _translations = {} + for path in os.listdir(directory): + if not path.endswith(".csv"): + continue + locale, extension = path.split(".") + if not re.match("[a-z]+(_[A-Z]+)?$", locale): + gen_log.error( + "Unrecognized locale %r (path: %s)", + locale, + os.path.join(directory, path), + ) + continue + full_path = os.path.join(directory, path) + if encoding is None: + # Try to autodetect encoding based on the BOM. + with open(full_path, "rb") as bf: + data = bf.read(len(codecs.BOM_UTF16_LE)) + if data in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE): + encoding = "utf-16" + else: + # utf-8-sig is "utf-8 with optional BOM". It's discouraged + # in most cases but is common with CSV files because Excel + # cannot read utf-8 files without a BOM. + encoding = "utf-8-sig" + # python 3: csv.reader requires a file open in text mode. + # Specify an encoding to avoid dependence on $LANG environment variable. + with open(full_path, encoding=encoding) as f: + _translations[locale] = {} + for i, row in enumerate(csv.reader(f)): + if not row or len(row) < 2: + continue + row = [escape.to_unicode(c).strip() for c in row] + english, translation = row[:2] + if len(row) > 2: + plural = row[2] or "unknown" + else: + plural = "unknown" + if plural not in ("plural", "singular", "unknown"): + gen_log.error( + "Unrecognized plural indicator %r in %s line %d", + plural, + path, + i + 1, + ) + continue + _translations[locale].setdefault(plural, {})[english] = translation + _supported_locales = frozenset(list(_translations.keys()) + [_default_locale]) + gen_log.debug("Supported locales: %s", sorted(_supported_locales)) + + +def load_gettext_translations(directory: str, domain: str) -> None: + """Loads translations from `gettext`'s locale tree + + Locale tree is similar to system's ``/usr/share/locale``, like:: + + {directory}/{lang}/LC_MESSAGES/{domain}.mo + + Three steps are required to have your app translated: + + 1. Generate POT translation file:: + + xgettext --language=Python --keyword=_:1,2 -d mydomain file1.py file2.html etc + + 2. Merge against existing POT file:: + + msgmerge old.po mydomain.po > new.po + + 3. Compile:: + + msgfmt mydomain.po -o {directory}/pt_BR/LC_MESSAGES/mydomain.mo + """ + global _translations + global _supported_locales + global _use_gettext + _translations = {} + for lang in os.listdir(directory): + if lang.startswith("."): + continue # skip .svn, etc + if os.path.isfile(os.path.join(directory, lang)): + continue + try: + os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain + ".mo")) + _translations[lang] = gettext.translation( + domain, directory, languages=[lang] + ) + except Exception as e: + gen_log.error("Cannot load translation for '%s': %s", lang, str(e)) + continue + _supported_locales = frozenset(list(_translations.keys()) + [_default_locale]) + _use_gettext = True + gen_log.debug("Supported locales: %s", sorted(_supported_locales)) + + +def get_supported_locales() -> Iterable[str]: + """Returns a list of all the supported locale codes.""" + return _supported_locales + + +class Locale(object): + """Object representing a locale. + + After calling one of `load_translations` or `load_gettext_translations`, + call `get` or `get_closest` to get a Locale object. + """ + + _cache = {} # type: Dict[str, Locale] + + @classmethod + def get_closest(cls, *locale_codes: str) -> "Locale": + """Returns the closest match for the given locale code.""" + for code in locale_codes: + if not code: + continue + code = code.replace("-", "_") + parts = code.split("_") + if len(parts) > 2: + continue + elif len(parts) == 2: + code = parts[0].lower() + "_" + parts[1].upper() + if code in _supported_locales: + return cls.get(code) + if parts[0].lower() in _supported_locales: + return cls.get(parts[0].lower()) + return cls.get(_default_locale) + + @classmethod + def get(cls, code: str) -> "Locale": + """Returns the Locale for the given locale code. + + If it is not supported, we raise an exception. + """ + if code not in cls._cache: + assert code in _supported_locales + translations = _translations.get(code, None) + if translations is None: + locale = CSVLocale(code, {}) # type: Locale + elif _use_gettext: + locale = GettextLocale(code, translations) + else: + locale = CSVLocale(code, translations) + cls._cache[code] = locale + return cls._cache[code] + + def __init__(self, code: str) -> None: + self.code = code + self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown") + self.rtl = False + for prefix in ["fa", "ar", "he"]: + if self.code.startswith(prefix): + self.rtl = True + break + + # Initialize strings for date formatting + _ = self.translate + self._months = [ + _("January"), + _("February"), + _("March"), + _("April"), + _("May"), + _("June"), + _("July"), + _("August"), + _("September"), + _("October"), + _("November"), + _("December"), + ] + self._weekdays = [ + _("Monday"), + _("Tuesday"), + _("Wednesday"), + _("Thursday"), + _("Friday"), + _("Saturday"), + _("Sunday"), + ] + + def translate( + self, + message: str, + plural_message: Optional[str] = None, + count: Optional[int] = None, + ) -> str: + """Returns the translation for the given message for this locale. + + If ``plural_message`` is given, you must also provide + ``count``. We return ``plural_message`` when ``count != 1``, + and we return the singular form for the given message when + ``count == 1``. + """ + raise NotImplementedError() + + def pgettext( + self, + context: str, + message: str, + plural_message: Optional[str] = None, + count: Optional[int] = None, + ) -> str: + raise NotImplementedError() + + def format_date( + self, + date: Union[int, float, datetime.datetime], + gmt_offset: int = 0, + relative: bool = True, + shorter: bool = False, + full_format: bool = False, + ) -> str: + """Formats the given date (which should be GMT). + + By default, we return a relative time (e.g., "2 minutes ago"). You + can return an absolute date string with ``relative=False``. + + You can force a full format date ("July 10, 1980") with + ``full_format=True``. + + This method is primarily intended for dates in the past. + For dates in the future, we fall back to full format. + """ + if isinstance(date, (int, float)): + date = datetime.datetime.utcfromtimestamp(date) + now = datetime.datetime.utcnow() + if date > now: + if relative and (date - now).seconds < 60: + # Due to click skew, things are some things slightly + # in the future. Round timestamps in the immediate + # future down to now in relative mode. + date = now + else: + # Otherwise, future dates always use the full format. + full_format = True + local_date = date - datetime.timedelta(minutes=gmt_offset) + local_now = now - datetime.timedelta(minutes=gmt_offset) + local_yesterday = local_now - datetime.timedelta(hours=24) + difference = now - date + seconds = difference.seconds + days = difference.days + + _ = self.translate + format = None + if not full_format: + if relative and days == 0: + if seconds < 50: + return _("1 second ago", "%(seconds)d seconds ago", seconds) % { + "seconds": seconds + } + + if seconds < 50 * 60: + minutes = round(seconds / 60.0) + return _("1 minute ago", "%(minutes)d minutes ago", minutes) % { + "minutes": minutes + } + + hours = round(seconds / (60.0 * 60)) + return _("1 hour ago", "%(hours)d hours ago", hours) % {"hours": hours} + + if days == 0: + format = _("%(time)s") + elif days == 1 and local_date.day == local_yesterday.day and relative: + format = _("yesterday") if shorter else _("yesterday at %(time)s") + elif days < 5: + format = _("%(weekday)s") if shorter else _("%(weekday)s at %(time)s") + elif days < 334: # 11mo, since confusing for same month last year + format = ( + _("%(month_name)s %(day)s") + if shorter + else _("%(month_name)s %(day)s at %(time)s") + ) + + if format is None: + format = ( + _("%(month_name)s %(day)s, %(year)s") + if shorter + else _("%(month_name)s %(day)s, %(year)s at %(time)s") + ) + + tfhour_clock = self.code not in ("en", "en_US", "zh_CN") + if tfhour_clock: + str_time = "%d:%02d" % (local_date.hour, local_date.minute) + elif self.code == "zh_CN": + str_time = "%s%d:%02d" % ( + (u"\u4e0a\u5348", u"\u4e0b\u5348")[local_date.hour >= 12], + local_date.hour % 12 or 12, + local_date.minute, + ) + else: + str_time = "%d:%02d %s" % ( + local_date.hour % 12 or 12, + local_date.minute, + ("am", "pm")[local_date.hour >= 12], + ) + + return format % { + "month_name": self._months[local_date.month - 1], + "weekday": self._weekdays[local_date.weekday()], + "day": str(local_date.day), + "year": str(local_date.year), + "time": str_time, + } + + def format_day( + self, date: datetime.datetime, gmt_offset: int = 0, dow: bool = True + ) -> bool: + """Formats the given date as a day of week. + + Example: "Monday, January 22". You can remove the day of week with + ``dow=False``. + """ + local_date = date - datetime.timedelta(minutes=gmt_offset) + _ = self.translate + if dow: + return _("%(weekday)s, %(month_name)s %(day)s") % { + "month_name": self._months[local_date.month - 1], + "weekday": self._weekdays[local_date.weekday()], + "day": str(local_date.day), + } + else: + return _("%(month_name)s %(day)s") % { + "month_name": self._months[local_date.month - 1], + "day": str(local_date.day), + } + + def list(self, parts: Any) -> str: + """Returns a comma-separated list for the given list of parts. + + The format is, e.g., "A, B and C", "A and B" or just "A" for lists + of size 1. + """ + _ = self.translate + if len(parts) == 0: + return "" + if len(parts) == 1: + return parts[0] + comma = u" \u0648 " if self.code.startswith("fa") else u", " + return _("%(commas)s and %(last)s") % { + "commas": comma.join(parts[:-1]), + "last": parts[len(parts) - 1], + } + + def friendly_number(self, value: int) -> str: + """Returns a comma-separated number for the given integer.""" + if self.code not in ("en", "en_US"): + return str(value) + s = str(value) + parts = [] + while s: + parts.append(s[-3:]) + s = s[:-3] + return ",".join(reversed(parts)) + + +class CSVLocale(Locale): + """Locale implementation using tornado's CSV translation format.""" + + def __init__(self, code: str, translations: Dict[str, Dict[str, str]]) -> None: + self.translations = translations + super().__init__(code) + + def translate( + self, + message: str, + plural_message: Optional[str] = None, + count: Optional[int] = None, + ) -> str: + if plural_message is not None: + assert count is not None + if count != 1: + message = plural_message + message_dict = self.translations.get("plural", {}) + else: + message_dict = self.translations.get("singular", {}) + else: + message_dict = self.translations.get("unknown", {}) + return message_dict.get(message, message) + + def pgettext( + self, + context: str, + message: str, + plural_message: Optional[str] = None, + count: Optional[int] = None, + ) -> str: + if self.translations: + gen_log.warning("pgettext is not supported by CSVLocale") + return self.translate(message, plural_message, count) + + +class GettextLocale(Locale): + """Locale implementation using the `gettext` module.""" + + def __init__(self, code: str, translations: gettext.NullTranslations) -> None: + self.ngettext = translations.ngettext + self.gettext = translations.gettext + # self.gettext must exist before __init__ is called, since it + # calls into self.translate + super().__init__(code) + + def translate( + self, + message: str, + plural_message: Optional[str] = None, + count: Optional[int] = None, + ) -> str: + if plural_message is not None: + assert count is not None + return self.ngettext(message, plural_message, count) + else: + return self.gettext(message) + + def pgettext( + self, + context: str, + message: str, + plural_message: Optional[str] = None, + count: Optional[int] = None, + ) -> str: + """Allows to set context for translation, accepts plural forms. + + Usage example:: + + pgettext("law", "right") + pgettext("good", "right") + + Plural message example:: + + pgettext("organization", "club", "clubs", len(clubs)) + pgettext("stick", "club", "clubs", len(clubs)) + + To generate POT file with context, add following options to step 1 + of `load_gettext_translations` sequence:: + + xgettext [basic options] --keyword=pgettext:1c,2 --keyword=pgettext:1c,2,3 + + .. versionadded:: 4.2 + """ + if plural_message is not None: + assert count is not None + msgs_with_ctxt = ( + "%s%s%s" % (context, CONTEXT_SEPARATOR, message), + "%s%s%s" % (context, CONTEXT_SEPARATOR, plural_message), + count, + ) + result = self.ngettext(*msgs_with_ctxt) + if CONTEXT_SEPARATOR in result: + # Translation not found + result = self.ngettext(message, plural_message, count) + return result + else: + msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message) + result = self.gettext(msg_with_ctxt) + if CONTEXT_SEPARATOR in result: + # Translation not found + result = message + return result diff --git a/telegramer/include/tornado/locks.py b/telegramer/include/tornado/locks.py new file mode 100644 index 0000000..0898eba --- /dev/null +++ b/telegramer/include/tornado/locks.py @@ -0,0 +1,571 @@ +# Copyright 2015 The Tornado Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import collections +import datetime +import types + +from tornado import gen, ioloop +from tornado.concurrent import Future, future_set_result_unless_cancelled + +from typing import Union, Optional, Type, Any, Awaitable +import typing + +if typing.TYPE_CHECKING: + from typing import Deque, Set # noqa: F401 + +__all__ = ["Condition", "Event", "Semaphore", "BoundedSemaphore", "Lock"] + + +class _TimeoutGarbageCollector(object): + """Base class for objects that periodically clean up timed-out waiters. + + Avoids memory leak in a common pattern like: + + while True: + yield condition.wait(short_timeout) + print('looping....') + """ + + def __init__(self) -> None: + self._waiters = collections.deque() # type: Deque[Future] + self._timeouts = 0 + + def _garbage_collect(self) -> None: + # Occasionally clear timed-out waiters. + self._timeouts += 1 + if self._timeouts > 100: + self._timeouts = 0 + self._waiters = collections.deque(w for w in self._waiters if not w.done()) + + +class Condition(_TimeoutGarbageCollector): + """A condition allows one or more coroutines to wait until notified. + + Like a standard `threading.Condition`, but does not need an underlying lock + that is acquired and released. + + With a `Condition`, coroutines can wait to be notified by other coroutines: + + .. testcode:: + + from tornado import gen + from tornado.ioloop import IOLoop + from tornado.locks import Condition + + condition = Condition() + + async def waiter(): + print("I'll wait right here") + await condition.wait() + print("I'm done waiting") + + async def notifier(): + print("About to notify") + condition.notify() + print("Done notifying") + + async def runner(): + # Wait for waiter() and notifier() in parallel + await gen.multi([waiter(), notifier()]) + + IOLoop.current().run_sync(runner) + + .. testoutput:: + + I'll wait right here + About to notify + Done notifying + I'm done waiting + + `wait` takes an optional ``timeout`` argument, which is either an absolute + timestamp:: + + io_loop = IOLoop.current() + + # Wait up to 1 second for a notification. + await condition.wait(timeout=io_loop.time() + 1) + + ...or a `datetime.timedelta` for a timeout relative to the current time:: + + # Wait up to 1 second. + await condition.wait(timeout=datetime.timedelta(seconds=1)) + + The method returns False if there's no notification before the deadline. + + .. versionchanged:: 5.0 + Previously, waiters could be notified synchronously from within + `notify`. Now, the notification will always be received on the + next iteration of the `.IOLoop`. + """ + + def __init__(self) -> None: + super().__init__() + self.io_loop = ioloop.IOLoop.current() + + def __repr__(self) -> str: + result = "<%s" % (self.__class__.__name__,) + if self._waiters: + result += " waiters[%s]" % len(self._waiters) + return result + ">" + + def wait( + self, timeout: Optional[Union[float, datetime.timedelta]] = None + ) -> Awaitable[bool]: + """Wait for `.notify`. + + Returns a `.Future` that resolves ``True`` if the condition is notified, + or ``False`` after a timeout. + """ + waiter = Future() # type: Future[bool] + self._waiters.append(waiter) + if timeout: + + def on_timeout() -> None: + if not waiter.done(): + future_set_result_unless_cancelled(waiter, False) + self._garbage_collect() + + io_loop = ioloop.IOLoop.current() + timeout_handle = io_loop.add_timeout(timeout, on_timeout) + waiter.add_done_callback(lambda _: io_loop.remove_timeout(timeout_handle)) + return waiter + + def notify(self, n: int = 1) -> None: + """Wake ``n`` waiters.""" + waiters = [] # Waiters we plan to run right now. + while n and self._waiters: + waiter = self._waiters.popleft() + if not waiter.done(): # Might have timed out. + n -= 1 + waiters.append(waiter) + + for waiter in waiters: + future_set_result_unless_cancelled(waiter, True) + + def notify_all(self) -> None: + """Wake all waiters.""" + self.notify(len(self._waiters)) + + +class Event(object): + """An event blocks coroutines until its internal flag is set to True. + + Similar to `threading.Event`. + + A coroutine can wait for an event to be set. Once it is set, calls to + ``yield event.wait()`` will not block unless the event has been cleared: + + .. testcode:: + + from tornado import gen + from tornado.ioloop import IOLoop + from tornado.locks import Event + + event = Event() + + async def waiter(): + print("Waiting for event") + await event.wait() + print("Not waiting this time") + await event.wait() + print("Done") + + async def setter(): + print("About to set the event") + event.set() + + async def runner(): + await gen.multi([waiter(), setter()]) + + IOLoop.current().run_sync(runner) + + .. testoutput:: + + Waiting for event + About to set the event + Not waiting this time + Done + """ + + def __init__(self) -> None: + self._value = False + self._waiters = set() # type: Set[Future[None]] + + def __repr__(self) -> str: + return "<%s %s>" % ( + self.__class__.__name__, + "set" if self.is_set() else "clear", + ) + + def is_set(self) -> bool: + """Return ``True`` if the internal flag is true.""" + return self._value + + def set(self) -> None: + """Set the internal flag to ``True``. All waiters are awakened. + + Calling `.wait` once the flag is set will not block. + """ + if not self._value: + self._value = True + + for fut in self._waiters: + if not fut.done(): + fut.set_result(None) + + def clear(self) -> None: + """Reset the internal flag to ``False``. + + Calls to `.wait` will block until `.set` is called. + """ + self._value = False + + def wait( + self, timeout: Optional[Union[float, datetime.timedelta]] = None + ) -> Awaitable[None]: + """Block until the internal flag is true. + + Returns an awaitable, which raises `tornado.util.TimeoutError` after a + timeout. + """ + fut = Future() # type: Future[None] + if self._value: + fut.set_result(None) + return fut + self._waiters.add(fut) + fut.add_done_callback(lambda fut: self._waiters.remove(fut)) + if timeout is None: + return fut + else: + timeout_fut = gen.with_timeout(timeout, fut) + # This is a slightly clumsy workaround for the fact that + # gen.with_timeout doesn't cancel its futures. Cancelling + # fut will remove it from the waiters list. + timeout_fut.add_done_callback( + lambda tf: fut.cancel() if not fut.done() else None + ) + return timeout_fut + + +class _ReleasingContextManager(object): + """Releases a Lock or Semaphore at the end of a "with" statement. + + with (yield semaphore.acquire()): + pass + + # Now semaphore.release() has been called. + """ + + def __init__(self, obj: Any) -> None: + self._obj = obj + + def __enter__(self) -> None: + pass + + def __exit__( + self, + exc_type: "Optional[Type[BaseException]]", + exc_val: Optional[BaseException], + exc_tb: Optional[types.TracebackType], + ) -> None: + self._obj.release() + + +class Semaphore(_TimeoutGarbageCollector): + """A lock that can be acquired a fixed number of times before blocking. + + A Semaphore manages a counter representing the number of `.release` calls + minus the number of `.acquire` calls, plus an initial value. The `.acquire` + method blocks if necessary until it can return without making the counter + negative. + + Semaphores limit access to a shared resource. To allow access for two + workers at a time: + + .. testsetup:: semaphore + + from collections import deque + + from tornado import gen + from tornado.ioloop import IOLoop + from tornado.concurrent import Future + + # Ensure reliable doctest output: resolve Futures one at a time. + futures_q = deque([Future() for _ in range(3)]) + + async def simulator(futures): + for f in futures: + # simulate the asynchronous passage of time + await gen.sleep(0) + await gen.sleep(0) + f.set_result(None) + + IOLoop.current().add_callback(simulator, list(futures_q)) + + def use_some_resource(): + return futures_q.popleft() + + .. testcode:: semaphore + + from tornado import gen + from tornado.ioloop import IOLoop + from tornado.locks import Semaphore + + sem = Semaphore(2) + + async def worker(worker_id): + await sem.acquire() + try: + print("Worker %d is working" % worker_id) + await use_some_resource() + finally: + print("Worker %d is done" % worker_id) + sem.release() + + async def runner(): + # Join all workers. + await gen.multi([worker(i) for i in range(3)]) + + IOLoop.current().run_sync(runner) + + .. testoutput:: semaphore + + Worker 0 is working + Worker 1 is working + Worker 0 is done + Worker 2 is working + Worker 1 is done + Worker 2 is done + + Workers 0 and 1 are allowed to run concurrently, but worker 2 waits until + the semaphore has been released once, by worker 0. + + The semaphore can be used as an async context manager:: + + async def worker(worker_id): + async with sem: + print("Worker %d is working" % worker_id) + await use_some_resource() + + # Now the semaphore has been released. + print("Worker %d is done" % worker_id) + + For compatibility with older versions of Python, `.acquire` is a + context manager, so ``worker`` could also be written as:: + + @gen.coroutine + def worker(worker_id): + with (yield sem.acquire()): + print("Worker %d is working" % worker_id) + yield use_some_resource() + + # Now the semaphore has been released. + print("Worker %d is done" % worker_id) + + .. versionchanged:: 4.3 + Added ``async with`` support in Python 3.5. + + """ + + def __init__(self, value: int = 1) -> None: + super().__init__() + if value < 0: + raise ValueError("semaphore initial value must be >= 0") + + self._value = value + + def __repr__(self) -> str: + res = super().__repr__() + extra = ( + "locked" if self._value == 0 else "unlocked,value:{0}".format(self._value) + ) + if self._waiters: + extra = "{0},waiters:{1}".format(extra, len(self._waiters)) + return "<{0} [{1}]>".format(res[1:-1], extra) + + def release(self) -> None: + """Increment the counter and wake one waiter.""" + self._value += 1 + while self._waiters: + waiter = self._waiters.popleft() + if not waiter.done(): + self._value -= 1 + + # If the waiter is a coroutine paused at + # + # with (yield semaphore.acquire()): + # + # then the context manager's __exit__ calls release() at the end + # of the "with" block. + waiter.set_result(_ReleasingContextManager(self)) + break + + def acquire( + self, timeout: Optional[Union[float, datetime.timedelta]] = None + ) -> Awaitable[_ReleasingContextManager]: + """Decrement the counter. Returns an awaitable. + + Block if the counter is zero and wait for a `.release`. The awaitable + raises `.TimeoutError` after the deadline. + """ + waiter = Future() # type: Future[_ReleasingContextManager] + if self._value > 0: + self._value -= 1 + waiter.set_result(_ReleasingContextManager(self)) + else: + self._waiters.append(waiter) + if timeout: + + def on_timeout() -> None: + if not waiter.done(): + waiter.set_exception(gen.TimeoutError()) + self._garbage_collect() + + io_loop = ioloop.IOLoop.current() + timeout_handle = io_loop.add_timeout(timeout, on_timeout) + waiter.add_done_callback( + lambda _: io_loop.remove_timeout(timeout_handle) + ) + return waiter + + def __enter__(self) -> None: + raise RuntimeError("Use 'async with' instead of 'with' for Semaphore") + + def __exit__( + self, + typ: "Optional[Type[BaseException]]", + value: Optional[BaseException], + traceback: Optional[types.TracebackType], + ) -> None: + self.__enter__() + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + typ: "Optional[Type[BaseException]]", + value: Optional[BaseException], + tb: Optional[types.TracebackType], + ) -> None: + self.release() + + +class BoundedSemaphore(Semaphore): + """A semaphore that prevents release() being called too many times. + + If `.release` would increment the semaphore's value past the initial + value, it raises `ValueError`. Semaphores are mostly used to guard + resources with limited capacity, so a semaphore released too many times + is a sign of a bug. + """ + + def __init__(self, value: int = 1) -> None: + super().__init__(value=value) + self._initial_value = value + + def release(self) -> None: + """Increment the counter and wake one waiter.""" + if self._value >= self._initial_value: + raise ValueError("Semaphore released too many times") + super().release() + + +class Lock(object): + """A lock for coroutines. + + A Lock begins unlocked, and `acquire` locks it immediately. While it is + locked, a coroutine that yields `acquire` waits until another coroutine + calls `release`. + + Releasing an unlocked lock raises `RuntimeError`. + + A Lock can be used as an async context manager with the ``async + with`` statement: + + >>> from tornado import locks + >>> lock = locks.Lock() + >>> + >>> async def f(): + ... async with lock: + ... # Do something holding the lock. + ... pass + ... + ... # Now the lock is released. + + For compatibility with older versions of Python, the `.acquire` + method asynchronously returns a regular context manager: + + >>> async def f2(): + ... with (yield lock.acquire()): + ... # Do something holding the lock. + ... pass + ... + ... # Now the lock is released. + + .. versionchanged:: 4.3 + Added ``async with`` support in Python 3.5. + + """ + + def __init__(self) -> None: + self._block = BoundedSemaphore(value=1) + + def __repr__(self) -> str: + return "<%s _block=%s>" % (self.__class__.__name__, self._block) + + def acquire( + self, timeout: Optional[Union[float, datetime.timedelta]] = None + ) -> Awaitable[_ReleasingContextManager]: + """Attempt to lock. Returns an awaitable. + + Returns an awaitable, which raises `tornado.util.TimeoutError` after a + timeout. + """ + return self._block.acquire(timeout) + + def release(self) -> None: + """Unlock. + + The first coroutine in line waiting for `acquire` gets the lock. + + If not locked, raise a `RuntimeError`. + """ + try: + self._block.release() + except ValueError: + raise RuntimeError("release unlocked lock") + + def __enter__(self) -> None: + raise RuntimeError("Use `async with` instead of `with` for Lock") + + def __exit__( + self, + typ: "Optional[Type[BaseException]]", + value: Optional[BaseException], + tb: Optional[types.TracebackType], + ) -> None: + self.__enter__() + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + typ: "Optional[Type[BaseException]]", + value: Optional[BaseException], + tb: Optional[types.TracebackType], + ) -> None: + self.release() diff --git a/telegramer/include/tornado/log.py b/telegramer/include/tornado/log.py new file mode 100644 index 0000000..810a037 --- /dev/null +++ b/telegramer/include/tornado/log.py @@ -0,0 +1,339 @@ +# +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Logging support for Tornado. + +Tornado uses three logger streams: + +* ``tornado.access``: Per-request logging for Tornado's HTTP servers (and + potentially other servers in the future) +* ``tornado.application``: Logging of errors from application code (i.e. + uncaught exceptions from callbacks) +* ``tornado.general``: General-purpose logging, including any errors + or warnings from Tornado itself. + +These streams may be configured independently using the standard library's +`logging` module. For example, you may wish to send ``tornado.access`` logs +to a separate file for analysis. +""" +import logging +import logging.handlers +import sys + +from tornado.escape import _unicode +from tornado.util import unicode_type, basestring_type + +try: + import colorama # type: ignore +except ImportError: + colorama = None + +try: + import curses +except ImportError: + curses = None # type: ignore + +from typing import Dict, Any, cast, Optional + +# Logger objects for internal tornado use +access_log = logging.getLogger("tornado.access") +app_log = logging.getLogger("tornado.application") +gen_log = logging.getLogger("tornado.general") + + +def _stderr_supports_color() -> bool: + try: + if hasattr(sys.stderr, "isatty") and sys.stderr.isatty(): + if curses: + curses.setupterm() + if curses.tigetnum("colors") > 0: + return True + elif colorama: + if sys.stderr is getattr( + colorama.initialise, "wrapped_stderr", object() + ): + return True + except Exception: + # Very broad exception handling because it's always better to + # fall back to non-colored logs than to break at startup. + pass + return False + + +def _safe_unicode(s: Any) -> str: + try: + return _unicode(s) + except UnicodeDecodeError: + return repr(s) + + +class LogFormatter(logging.Formatter): + """Log formatter used in Tornado. + + Key features of this formatter are: + + * Color support when logging to a terminal that supports it. + * Timestamps on every log line. + * Robust against str/bytes encoding problems. + + This formatter is enabled automatically by + `tornado.options.parse_command_line` or `tornado.options.parse_config_file` + (unless ``--logging=none`` is used). + + Color support on Windows versions that do not support ANSI color codes is + enabled by use of the colorama__ library. Applications that wish to use + this must first initialize colorama with a call to ``colorama.init``. + See the colorama documentation for details. + + __ https://pypi.python.org/pypi/colorama + + .. versionchanged:: 4.5 + Added support for ``colorama``. Changed the constructor + signature to be compatible with `logging.config.dictConfig`. + """ + + DEFAULT_FORMAT = "%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s" # noqa: E501 + DEFAULT_DATE_FORMAT = "%y%m%d %H:%M:%S" + DEFAULT_COLORS = { + logging.DEBUG: 4, # Blue + logging.INFO: 2, # Green + logging.WARNING: 3, # Yellow + logging.ERROR: 1, # Red + logging.CRITICAL: 5, # Magenta + } + + def __init__( + self, + fmt: str = DEFAULT_FORMAT, + datefmt: str = DEFAULT_DATE_FORMAT, + style: str = "%", + color: bool = True, + colors: Dict[int, int] = DEFAULT_COLORS, + ) -> None: + r""" + :arg bool color: Enables color support. + :arg str fmt: Log message format. + It will be applied to the attributes dict of log records. The + text between ``%(color)s`` and ``%(end_color)s`` will be colored + depending on the level if color support is on. + :arg dict colors: color mappings from logging level to terminal color + code + :arg str datefmt: Datetime format. + Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``. + + .. versionchanged:: 3.2 + + Added ``fmt`` and ``datefmt`` arguments. + """ + logging.Formatter.__init__(self, datefmt=datefmt) + self._fmt = fmt + + self._colors = {} # type: Dict[int, str] + if color and _stderr_supports_color(): + if curses is not None: + fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or b"" + + for levelno, code in colors.items(): + # Convert the terminal control characters from + # bytes to unicode strings for easier use with the + # logging module. + self._colors[levelno] = unicode_type( + curses.tparm(fg_color, code), "ascii" + ) + self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii") + else: + # If curses is not present (currently we'll only get here for + # colorama on windows), assume hard-coded ANSI color codes. + for levelno, code in colors.items(): + self._colors[levelno] = "\033[2;3%dm" % code + self._normal = "\033[0m" + else: + self._normal = "" + + def format(self, record: Any) -> str: + try: + message = record.getMessage() + assert isinstance(message, basestring_type) # guaranteed by logging + # Encoding notes: The logging module prefers to work with character + # strings, but only enforces that log messages are instances of + # basestring. In python 2, non-ascii bytestrings will make + # their way through the logging framework until they blow up with + # an unhelpful decoding error (with this formatter it happens + # when we attach the prefix, but there are other opportunities for + # exceptions further along in the framework). + # + # If a byte string makes it this far, convert it to unicode to + # ensure it will make it out to the logs. Use repr() as a fallback + # to ensure that all byte strings can be converted successfully, + # but don't do it by default so we don't add extra quotes to ascii + # bytestrings. This is a bit of a hacky place to do this, but + # it's worth it since the encoding errors that would otherwise + # result are so useless (and tornado is fond of using utf8-encoded + # byte strings wherever possible). + record.message = _safe_unicode(message) + except Exception as e: + record.message = "Bad message (%r): %r" % (e, record.__dict__) + + record.asctime = self.formatTime(record, cast(str, self.datefmt)) + + if record.levelno in self._colors: + record.color = self._colors[record.levelno] + record.end_color = self._normal + else: + record.color = record.end_color = "" + + formatted = self._fmt % record.__dict__ + + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + # exc_text contains multiple lines. We need to _safe_unicode + # each line separately so that non-utf8 bytes don't cause + # all the newlines to turn into '\n'. + lines = [formatted.rstrip()] + lines.extend(_safe_unicode(ln) for ln in record.exc_text.split("\n")) + formatted = "\n".join(lines) + return formatted.replace("\n", "\n ") + + +def enable_pretty_logging( + options: Any = None, logger: Optional[logging.Logger] = None +) -> None: + """Turns on formatted logging output as configured. + + This is called automatically by `tornado.options.parse_command_line` + and `tornado.options.parse_config_file`. + """ + if options is None: + import tornado.options + + options = tornado.options.options + if options.logging is None or options.logging.lower() == "none": + return + if logger is None: + logger = logging.getLogger() + logger.setLevel(getattr(logging, options.logging.upper())) + if options.log_file_prefix: + rotate_mode = options.log_rotate_mode + if rotate_mode == "size": + channel = logging.handlers.RotatingFileHandler( + filename=options.log_file_prefix, + maxBytes=options.log_file_max_size, + backupCount=options.log_file_num_backups, + encoding="utf-8", + ) # type: logging.Handler + elif rotate_mode == "time": + channel = logging.handlers.TimedRotatingFileHandler( + filename=options.log_file_prefix, + when=options.log_rotate_when, + interval=options.log_rotate_interval, + backupCount=options.log_file_num_backups, + encoding="utf-8", + ) + else: + error_message = ( + "The value of log_rotate_mode option should be " + + '"size" or "time", not "%s".' % rotate_mode + ) + raise ValueError(error_message) + channel.setFormatter(LogFormatter(color=False)) + logger.addHandler(channel) + + if options.log_to_stderr or (options.log_to_stderr is None and not logger.handlers): + # Set up color if we are in a tty and curses is installed + channel = logging.StreamHandler() + channel.setFormatter(LogFormatter()) + logger.addHandler(channel) + + +def define_logging_options(options: Any = None) -> None: + """Add logging-related flags to ``options``. + + These options are present automatically on the default options instance; + this method is only necessary if you have created your own `.OptionParser`. + + .. versionadded:: 4.2 + This function existed in prior versions but was broken and undocumented until 4.2. + """ + if options is None: + # late import to prevent cycle + import tornado.options + + options = tornado.options.options + options.define( + "logging", + default="info", + help=( + "Set the Python log level. If 'none', tornado won't touch the " + "logging configuration." + ), + metavar="debug|info|warning|error|none", + ) + options.define( + "log_to_stderr", + type=bool, + default=None, + help=( + "Send log output to stderr (colorized if possible). " + "By default use stderr if --log_file_prefix is not set and " + "no other logging is configured." + ), + ) + options.define( + "log_file_prefix", + type=str, + default=None, + metavar="PATH", + help=( + "Path prefix for log files. " + "Note that if you are running multiple tornado processes, " + "log_file_prefix must be different for each of them (e.g. " + "include the port number)" + ), + ) + options.define( + "log_file_max_size", + type=int, + default=100 * 1000 * 1000, + help="max size of log files before rollover", + ) + options.define( + "log_file_num_backups", type=int, default=10, help="number of log files to keep" + ) + + options.define( + "log_rotate_when", + type=str, + default="midnight", + help=( + "specify the type of TimedRotatingFileHandler interval " + "other options:('S', 'M', 'H', 'D', 'W0'-'W6')" + ), + ) + options.define( + "log_rotate_interval", + type=int, + default=1, + help="The interval value of timed rotating", + ) + + options.define( + "log_rotate_mode", + type=str, + default="size", + help="The mode of rotating files(time or size)", + ) + + options.add_parse_callback(lambda: enable_pretty_logging(options)) diff --git a/telegramer/include/tornado/netutil.py b/telegramer/include/tornado/netutil.py new file mode 100644 index 0000000..868d3e9 --- /dev/null +++ b/telegramer/include/tornado/netutil.py @@ -0,0 +1,617 @@ +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Miscellaneous network utility code.""" + +import concurrent.futures +import errno +import os +import sys +import socket +import ssl +import stat + +from tornado.concurrent import dummy_executor, run_on_executor +from tornado.ioloop import IOLoop +from tornado.util import Configurable, errno_from_exception + +from typing import List, Callable, Any, Type, Dict, Union, Tuple, Awaitable, Optional + +# Note that the naming of ssl.Purpose is confusing; the purpose +# of a context is to authentiate the opposite side of the connection. +_client_ssl_defaults = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) +_server_ssl_defaults = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) +if hasattr(ssl, "OP_NO_COMPRESSION"): + # See netutil.ssl_options_to_context + _client_ssl_defaults.options |= ssl.OP_NO_COMPRESSION + _server_ssl_defaults.options |= ssl.OP_NO_COMPRESSION + +# ThreadedResolver runs getaddrinfo on a thread. If the hostname is unicode, +# getaddrinfo attempts to import encodings.idna. If this is done at +# module-import time, the import lock is already held by the main thread, +# leading to deadlock. Avoid it by caching the idna encoder on the main +# thread now. +u"foo".encode("idna") + +# For undiagnosed reasons, 'latin1' codec may also need to be preloaded. +u"foo".encode("latin1") + +# Default backlog used when calling sock.listen() +_DEFAULT_BACKLOG = 128 + + +def bind_sockets( + port: int, + address: Optional[str] = None, + family: socket.AddressFamily = socket.AF_UNSPEC, + backlog: int = _DEFAULT_BACKLOG, + flags: Optional[int] = None, + reuse_port: bool = False, +) -> List[socket.socket]: + """Creates listening sockets bound to the given port and address. + + Returns a list of socket objects (multiple sockets are returned if + the given address maps to multiple IP addresses, which is most common + for mixed IPv4 and IPv6 use). + + Address may be either an IP address or hostname. If it's a hostname, + the server will listen on all IP addresses associated with the + name. Address may be an empty string or None to listen on all + available interfaces. Family may be set to either `socket.AF_INET` + or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise + both will be used if available. + + The ``backlog`` argument has the same meaning as for + `socket.listen() `. + + ``flags`` is a bitmask of AI_* flags to `~socket.getaddrinfo`, like + ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``. + + ``reuse_port`` option sets ``SO_REUSEPORT`` option for every socket + in the list. If your platform doesn't support this option ValueError will + be raised. + """ + if reuse_port and not hasattr(socket, "SO_REUSEPORT"): + raise ValueError("the platform doesn't support SO_REUSEPORT") + + sockets = [] + if address == "": + address = None + if not socket.has_ipv6 and family == socket.AF_UNSPEC: + # Python can be compiled with --disable-ipv6, which causes + # operations on AF_INET6 sockets to fail, but does not + # automatically exclude those results from getaddrinfo + # results. + # http://bugs.python.org/issue16208 + family = socket.AF_INET + if flags is None: + flags = socket.AI_PASSIVE + bound_port = None + unique_addresses = set() # type: set + for res in sorted( + socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags), + key=lambda x: x[0], + ): + if res in unique_addresses: + continue + + unique_addresses.add(res) + + af, socktype, proto, canonname, sockaddr = res + if ( + sys.platform == "darwin" + and address == "localhost" + and af == socket.AF_INET6 + and sockaddr[3] != 0 + ): + # Mac OS X includes a link-local address fe80::1%lo0 in the + # getaddrinfo results for 'localhost'. However, the firewall + # doesn't understand that this is a local address and will + # prompt for access (often repeatedly, due to an apparent + # bug in its ability to remember granting access to an + # application). Skip these addresses. + continue + try: + sock = socket.socket(af, socktype, proto) + except socket.error as e: + if errno_from_exception(e) == errno.EAFNOSUPPORT: + continue + raise + if os.name != "nt": + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except socket.error as e: + if errno_from_exception(e) != errno.ENOPROTOOPT: + # Hurd doesn't support SO_REUSEADDR. + raise + if reuse_port: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + if af == socket.AF_INET6: + # On linux, ipv6 sockets accept ipv4 too by default, + # but this makes it impossible to bind to both + # 0.0.0.0 in ipv4 and :: in ipv6. On other systems, + # separate sockets *must* be used to listen for both ipv4 + # and ipv6. For consistency, always disable ipv4 on our + # ipv6 sockets and use a separate ipv4 socket when needed. + # + # Python 2.x on windows doesn't have IPPROTO_IPV6. + if hasattr(socket, "IPPROTO_IPV6"): + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + + # automatic port allocation with port=None + # should bind on the same port on IPv4 and IPv6 + host, requested_port = sockaddr[:2] + if requested_port == 0 and bound_port is not None: + sockaddr = tuple([host, bound_port] + list(sockaddr[2:])) + + sock.setblocking(False) + try: + sock.bind(sockaddr) + except OSError as e: + if ( + errno_from_exception(e) == errno.EADDRNOTAVAIL + and address == "localhost" + and sockaddr[0] == "::1" + ): + # On some systems (most notably docker with default + # configurations), ipv6 is partially disabled: + # socket.has_ipv6 is true, we can create AF_INET6 + # sockets, and getaddrinfo("localhost", ..., + # AF_PASSIVE) resolves to ::1, but we get an error + # when binding. + # + # Swallow the error, but only for this specific case. + # If EADDRNOTAVAIL occurs in other situations, it + # might be a real problem like a typo in a + # configuration. + sock.close() + continue + else: + raise + bound_port = sock.getsockname()[1] + sock.listen(backlog) + sockets.append(sock) + return sockets + + +if hasattr(socket, "AF_UNIX"): + + def bind_unix_socket( + file: str, mode: int = 0o600, backlog: int = _DEFAULT_BACKLOG + ) -> socket.socket: + """Creates a listening unix socket. + + If a socket with the given name already exists, it will be deleted. + If any other file with that name exists, an exception will be + raised. + + Returns a socket object (not a list of socket objects like + `bind_sockets`) + """ + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except socket.error as e: + if errno_from_exception(e) != errno.ENOPROTOOPT: + # Hurd doesn't support SO_REUSEADDR + raise + sock.setblocking(False) + try: + st = os.stat(file) + except FileNotFoundError: + pass + else: + if stat.S_ISSOCK(st.st_mode): + os.remove(file) + else: + raise ValueError("File %s exists and is not a socket", file) + sock.bind(file) + os.chmod(file, mode) + sock.listen(backlog) + return sock + + +def add_accept_handler( + sock: socket.socket, callback: Callable[[socket.socket, Any], None] +) -> Callable[[], None]: + """Adds an `.IOLoop` event handler to accept new connections on ``sock``. + + When a connection is accepted, ``callback(connection, address)`` will + be run (``connection`` is a socket object, and ``address`` is the + address of the other end of the connection). Note that this signature + is different from the ``callback(fd, events)`` signature used for + `.IOLoop` handlers. + + A callable is returned which, when called, will remove the `.IOLoop` + event handler and stop processing further incoming connections. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + + .. versionchanged:: 5.0 + A callable is returned (``None`` was returned before). + """ + io_loop = IOLoop.current() + removed = [False] + + def accept_handler(fd: socket.socket, events: int) -> None: + # More connections may come in while we're handling callbacks; + # to prevent starvation of other tasks we must limit the number + # of connections we accept at a time. Ideally we would accept + # up to the number of connections that were waiting when we + # entered this method, but this information is not available + # (and rearranging this method to call accept() as many times + # as possible before running any callbacks would have adverse + # effects on load balancing in multiprocess configurations). + # Instead, we use the (default) listen backlog as a rough + # heuristic for the number of connections we can reasonably + # accept at once. + for i in range(_DEFAULT_BACKLOG): + if removed[0]: + # The socket was probably closed + return + try: + connection, address = sock.accept() + except BlockingIOError: + # EWOULDBLOCK indicates we have accepted every + # connection that is available. + return + except ConnectionAbortedError: + # ECONNABORTED indicates that there was a connection + # but it was closed while still in the accept queue. + # (observed on FreeBSD). + continue + callback(connection, address) + + def remove_handler() -> None: + io_loop.remove_handler(sock) + removed[0] = True + + io_loop.add_handler(sock, accept_handler, IOLoop.READ) + return remove_handler + + +def is_valid_ip(ip: str) -> bool: + """Returns ``True`` if the given string is a well-formed IP address. + + Supports IPv4 and IPv6. + """ + if not ip or "\x00" in ip: + # getaddrinfo resolves empty strings to localhost, and truncates + # on zero bytes. + return False + try: + res = socket.getaddrinfo( + ip, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_NUMERICHOST + ) + return bool(res) + except socket.gaierror as e: + if e.args[0] == socket.EAI_NONAME: + return False + raise + return True + + +class Resolver(Configurable): + """Configurable asynchronous DNS resolver interface. + + By default, a blocking implementation is used (which simply calls + `socket.getaddrinfo`). An alternative implementation can be + chosen with the `Resolver.configure <.Configurable.configure>` + class method:: + + Resolver.configure('tornado.netutil.ThreadedResolver') + + The implementations of this interface included with Tornado are + + * `tornado.netutil.DefaultExecutorResolver` + * `tornado.netutil.BlockingResolver` (deprecated) + * `tornado.netutil.ThreadedResolver` (deprecated) + * `tornado.netutil.OverrideResolver` + * `tornado.platform.twisted.TwistedResolver` + * `tornado.platform.caresresolver.CaresResolver` + + .. versionchanged:: 5.0 + The default implementation has changed from `BlockingResolver` to + `DefaultExecutorResolver`. + """ + + @classmethod + def configurable_base(cls) -> Type["Resolver"]: + return Resolver + + @classmethod + def configurable_default(cls) -> Type["Resolver"]: + return DefaultExecutorResolver + + def resolve( + self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC + ) -> Awaitable[List[Tuple[int, Any]]]: + """Resolves an address. + + The ``host`` argument is a string which may be a hostname or a + literal IP address. + + Returns a `.Future` whose result is a list of (family, + address) pairs, where address is a tuple suitable to pass to + `socket.connect ` (i.e. a ``(host, + port)`` pair for IPv4; additional fields may be present for + IPv6). If a ``callback`` is passed, it will be run with the + result as an argument when it is complete. + + :raises IOError: if the address cannot be resolved. + + .. versionchanged:: 4.4 + Standardized all implementations to raise `IOError`. + + .. versionchanged:: 6.0 The ``callback`` argument was removed. + Use the returned awaitable object instead. + + """ + raise NotImplementedError() + + def close(self) -> None: + """Closes the `Resolver`, freeing any resources used. + + .. versionadded:: 3.1 + + """ + pass + + +def _resolve_addr( + host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC +) -> List[Tuple[int, Any]]: + # On Solaris, getaddrinfo fails if the given port is not found + # in /etc/services and no socket type is given, so we must pass + # one here. The socket type used here doesn't seem to actually + # matter (we discard the one we get back in the results), + # so the addresses we return should still be usable with SOCK_DGRAM. + addrinfo = socket.getaddrinfo(host, port, family, socket.SOCK_STREAM) + results = [] + for fam, socktype, proto, canonname, address in addrinfo: + results.append((fam, address)) + return results # type: ignore + + +class DefaultExecutorResolver(Resolver): + """Resolver implementation using `.IOLoop.run_in_executor`. + + .. versionadded:: 5.0 + """ + + async def resolve( + self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC + ) -> List[Tuple[int, Any]]: + result = await IOLoop.current().run_in_executor( + None, _resolve_addr, host, port, family + ) + return result + + +class ExecutorResolver(Resolver): + """Resolver implementation using a `concurrent.futures.Executor`. + + Use this instead of `ThreadedResolver` when you require additional + control over the executor being used. + + The executor will be shut down when the resolver is closed unless + ``close_resolver=False``; use this if you want to reuse the same + executor elsewhere. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + + .. deprecated:: 5.0 + The default `Resolver` now uses `.IOLoop.run_in_executor`; use that instead + of this class. + """ + + def initialize( + self, + executor: Optional[concurrent.futures.Executor] = None, + close_executor: bool = True, + ) -> None: + self.io_loop = IOLoop.current() + if executor is not None: + self.executor = executor + self.close_executor = close_executor + else: + self.executor = dummy_executor + self.close_executor = False + + def close(self) -> None: + if self.close_executor: + self.executor.shutdown() + self.executor = None # type: ignore + + @run_on_executor + def resolve( + self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC + ) -> List[Tuple[int, Any]]: + return _resolve_addr(host, port, family) + + +class BlockingResolver(ExecutorResolver): + """Default `Resolver` implementation, using `socket.getaddrinfo`. + + The `.IOLoop` will be blocked during the resolution, although the + callback will not be run until the next `.IOLoop` iteration. + + .. deprecated:: 5.0 + The default `Resolver` now uses `.IOLoop.run_in_executor`; use that instead + of this class. + """ + + def initialize(self) -> None: # type: ignore + super().initialize() + + +class ThreadedResolver(ExecutorResolver): + """Multithreaded non-blocking `Resolver` implementation. + + Requires the `concurrent.futures` package to be installed + (available in the standard library since Python 3.2, + installable with ``pip install futures`` in older versions). + + The thread pool size can be configured with:: + + Resolver.configure('tornado.netutil.ThreadedResolver', + num_threads=10) + + .. versionchanged:: 3.1 + All ``ThreadedResolvers`` share a single thread pool, whose + size is set by the first one to be created. + + .. deprecated:: 5.0 + The default `Resolver` now uses `.IOLoop.run_in_executor`; use that instead + of this class. + """ + + _threadpool = None # type: ignore + _threadpool_pid = None # type: int + + def initialize(self, num_threads: int = 10) -> None: # type: ignore + threadpool = ThreadedResolver._create_threadpool(num_threads) + super().initialize(executor=threadpool, close_executor=False) + + @classmethod + def _create_threadpool( + cls, num_threads: int + ) -> concurrent.futures.ThreadPoolExecutor: + pid = os.getpid() + if cls._threadpool_pid != pid: + # Threads cannot survive after a fork, so if our pid isn't what it + # was when we created the pool then delete it. + cls._threadpool = None + if cls._threadpool is None: + cls._threadpool = concurrent.futures.ThreadPoolExecutor(num_threads) + cls._threadpool_pid = pid + return cls._threadpool + + +class OverrideResolver(Resolver): + """Wraps a resolver with a mapping of overrides. + + This can be used to make local DNS changes (e.g. for testing) + without modifying system-wide settings. + + The mapping can be in three formats:: + + { + # Hostname to host or ip + "example.com": "127.0.1.1", + + # Host+port to host+port + ("login.example.com", 443): ("localhost", 1443), + + # Host+port+address family to host+port + ("login.example.com", 443, socket.AF_INET6): ("::1", 1443), + } + + .. versionchanged:: 5.0 + Added support for host-port-family triplets. + """ + + def initialize(self, resolver: Resolver, mapping: dict) -> None: + self.resolver = resolver + self.mapping = mapping + + def close(self) -> None: + self.resolver.close() + + def resolve( + self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC + ) -> Awaitable[List[Tuple[int, Any]]]: + if (host, port, family) in self.mapping: + host, port = self.mapping[(host, port, family)] + elif (host, port) in self.mapping: + host, port = self.mapping[(host, port)] + elif host in self.mapping: + host = self.mapping[host] + return self.resolver.resolve(host, port, family) + + +# These are the keyword arguments to ssl.wrap_socket that must be translated +# to their SSLContext equivalents (the other arguments are still passed +# to SSLContext.wrap_socket). +_SSL_CONTEXT_KEYWORDS = frozenset( + ["ssl_version", "certfile", "keyfile", "cert_reqs", "ca_certs", "ciphers"] +) + + +def ssl_options_to_context( + ssl_options: Union[Dict[str, Any], ssl.SSLContext] +) -> ssl.SSLContext: + """Try to convert an ``ssl_options`` dictionary to an + `~ssl.SSLContext` object. + + The ``ssl_options`` dictionary contains keywords to be passed to + `ssl.wrap_socket`. In Python 2.7.9+, `ssl.SSLContext` objects can + be used instead. This function converts the dict form to its + `~ssl.SSLContext` equivalent, and may be used when a component which + accepts both forms needs to upgrade to the `~ssl.SSLContext` version + to use features like SNI or NPN. + """ + if isinstance(ssl_options, ssl.SSLContext): + return ssl_options + assert isinstance(ssl_options, dict) + assert all(k in _SSL_CONTEXT_KEYWORDS for k in ssl_options), ssl_options + # Can't use create_default_context since this interface doesn't + # tell us client vs server. + context = ssl.SSLContext(ssl_options.get("ssl_version", ssl.PROTOCOL_SSLv23)) + if "certfile" in ssl_options: + context.load_cert_chain( + ssl_options["certfile"], ssl_options.get("keyfile", None) + ) + if "cert_reqs" in ssl_options: + context.verify_mode = ssl_options["cert_reqs"] + if "ca_certs" in ssl_options: + context.load_verify_locations(ssl_options["ca_certs"]) + if "ciphers" in ssl_options: + context.set_ciphers(ssl_options["ciphers"]) + if hasattr(ssl, "OP_NO_COMPRESSION"): + # Disable TLS compression to avoid CRIME and related attacks. + # This constant depends on openssl version 1.0. + # TODO: Do we need to do this ourselves or can we trust + # the defaults? + context.options |= ssl.OP_NO_COMPRESSION + return context + + +def ssl_wrap_socket( + socket: socket.socket, + ssl_options: Union[Dict[str, Any], ssl.SSLContext], + server_hostname: Optional[str] = None, + **kwargs: Any +) -> ssl.SSLSocket: + """Returns an ``ssl.SSLSocket`` wrapping the given socket. + + ``ssl_options`` may be either an `ssl.SSLContext` object or a + dictionary (as accepted by `ssl_options_to_context`). Additional + keyword arguments are passed to ``wrap_socket`` (either the + `~ssl.SSLContext` method or the `ssl` module function as + appropriate). + """ + context = ssl_options_to_context(ssl_options) + if ssl.HAS_SNI: + # In python 3.4, wrap_socket only accepts the server_hostname + # argument if HAS_SNI is true. + # TODO: add a unittest (python added server-side SNI support in 3.4) + # In the meantime it can be manually tested with + # python3 -m tornado.httpclient https://sni.velox.ch + return context.wrap_socket(socket, server_hostname=server_hostname, **kwargs) + else: + return context.wrap_socket(socket, **kwargs) diff --git a/telegramer/include/tornado/options.py b/telegramer/include/tornado/options.py new file mode 100644 index 0000000..f0b89a9 --- /dev/null +++ b/telegramer/include/tornado/options.py @@ -0,0 +1,735 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A command line parsing module that lets modules define their own options. + +This module is inspired by Google's `gflags +`_. The primary difference +with libraries such as `argparse` is that a global registry is used so +that options may be defined in any module (it also enables +`tornado.log` by default). The rest of Tornado does not depend on this +module, so feel free to use `argparse` or other configuration +libraries if you prefer them. + +Options must be defined with `tornado.options.define` before use, +generally at the top level of a module. The options are then +accessible as attributes of `tornado.options.options`:: + + # myapp/db.py + from tornado.options import define, options + + define("mysql_host", default="127.0.0.1:3306", help="Main user DB") + define("memcache_hosts", default="127.0.0.1:11011", multiple=True, + help="Main user memcache servers") + + def connect(): + db = database.Connection(options.mysql_host) + ... + + # myapp/server.py + from tornado.options import define, options + + define("port", default=8080, help="port to listen on") + + def start_server(): + app = make_app() + app.listen(options.port) + +The ``main()`` method of your application does not need to be aware of all of +the options used throughout your program; they are all automatically loaded +when the modules are loaded. However, all modules that define options +must have been imported before the command line is parsed. + +Your ``main()`` method can parse the command line or parse a config file with +either `parse_command_line` or `parse_config_file`:: + + import myapp.db, myapp.server + import tornado.options + + if __name__ == '__main__': + tornado.options.parse_command_line() + # or + tornado.options.parse_config_file("/etc/server.conf") + +.. note:: + + When using multiple ``parse_*`` functions, pass ``final=False`` to all + but the last one, or side effects may occur twice (in particular, + this can result in log messages being doubled). + +`tornado.options.options` is a singleton instance of `OptionParser`, and +the top-level functions in this module (`define`, `parse_command_line`, etc) +simply call methods on it. You may create additional `OptionParser` +instances to define isolated sets of options, such as for subcommands. + +.. note:: + + By default, several options are defined that will configure the + standard `logging` module when `parse_command_line` or `parse_config_file` + are called. If you want Tornado to leave the logging configuration + alone so you can manage it yourself, either pass ``--logging=none`` + on the command line or do the following to disable it in code:: + + from tornado.options import options, parse_command_line + options.logging = None + parse_command_line() + +.. versionchanged:: 4.3 + Dashes and underscores are fully interchangeable in option names; + options can be defined, set, and read with any mix of the two. + Dashes are typical for command-line usage while config files require + underscores. +""" + +import datetime +import numbers +import re +import sys +import os +import textwrap + +from tornado.escape import _unicode, native_str +from tornado.log import define_logging_options +from tornado.util import basestring_type, exec_in + +from typing import ( + Any, + Iterator, + Iterable, + Tuple, + Set, + Dict, + Callable, + List, + TextIO, + Optional, +) + + +class Error(Exception): + """Exception raised by errors in the options module.""" + + pass + + +class OptionParser(object): + """A collection of options, a dictionary with object-like access. + + Normally accessed via static functions in the `tornado.options` module, + which reference a global instance. + """ + + def __init__(self) -> None: + # we have to use self.__dict__ because we override setattr. + self.__dict__["_options"] = {} + self.__dict__["_parse_callbacks"] = [] + self.define( + "help", + type=bool, + help="show this help information", + callback=self._help_callback, + ) + + def _normalize_name(self, name: str) -> str: + return name.replace("_", "-") + + def __getattr__(self, name: str) -> Any: + name = self._normalize_name(name) + if isinstance(self._options.get(name), _Option): + return self._options[name].value() + raise AttributeError("Unrecognized option %r" % name) + + def __setattr__(self, name: str, value: Any) -> None: + name = self._normalize_name(name) + if isinstance(self._options.get(name), _Option): + return self._options[name].set(value) + raise AttributeError("Unrecognized option %r" % name) + + def __iter__(self) -> Iterator: + return (opt.name for opt in self._options.values()) + + def __contains__(self, name: str) -> bool: + name = self._normalize_name(name) + return name in self._options + + def __getitem__(self, name: str) -> Any: + return self.__getattr__(name) + + def __setitem__(self, name: str, value: Any) -> None: + return self.__setattr__(name, value) + + def items(self) -> Iterable[Tuple[str, Any]]: + """An iterable of (name, value) pairs. + + .. versionadded:: 3.1 + """ + return [(opt.name, opt.value()) for name, opt in self._options.items()] + + def groups(self) -> Set[str]: + """The set of option-groups created by ``define``. + + .. versionadded:: 3.1 + """ + return set(opt.group_name for opt in self._options.values()) + + def group_dict(self, group: str) -> Dict[str, Any]: + """The names and values of options in a group. + + Useful for copying options into Application settings:: + + from tornado.options import define, parse_command_line, options + + define('template_path', group='application') + define('static_path', group='application') + + parse_command_line() + + application = Application( + handlers, **options.group_dict('application')) + + .. versionadded:: 3.1 + """ + return dict( + (opt.name, opt.value()) + for name, opt in self._options.items() + if not group or group == opt.group_name + ) + + def as_dict(self) -> Dict[str, Any]: + """The names and values of all options. + + .. versionadded:: 3.1 + """ + return dict((opt.name, opt.value()) for name, opt in self._options.items()) + + def define( + self, + name: str, + default: Any = None, + type: Optional[type] = None, + help: Optional[str] = None, + metavar: Optional[str] = None, + multiple: bool = False, + group: Optional[str] = None, + callback: Optional[Callable[[Any], None]] = None, + ) -> None: + """Defines a new command line option. + + ``type`` can be any of `str`, `int`, `float`, `bool`, + `~datetime.datetime`, or `~datetime.timedelta`. If no ``type`` + is given but a ``default`` is, ``type`` is the type of + ``default``. Otherwise, ``type`` defaults to `str`. + + If ``multiple`` is True, the option value is a list of ``type`` + instead of an instance of ``type``. + + ``help`` and ``metavar`` are used to construct the + automatically generated command line help string. The help + message is formatted like:: + + --name=METAVAR help string + + ``group`` is used to group the defined options in logical + groups. By default, command line options are grouped by the + file in which they are defined. + + Command line option names must be unique globally. + + If a ``callback`` is given, it will be run with the new value whenever + the option is changed. This can be used to combine command-line + and file-based options:: + + define("config", type=str, help="path to config file", + callback=lambda path: parse_config_file(path, final=False)) + + With this definition, options in the file specified by ``--config`` will + override options set earlier on the command line, but can be overridden + by later flags. + + """ + normalized = self._normalize_name(name) + if normalized in self._options: + raise Error( + "Option %r already defined in %s" + % (normalized, self._options[normalized].file_name) + ) + frame = sys._getframe(0) + options_file = frame.f_code.co_filename + + # Can be called directly, or through top level define() fn, in which + # case, step up above that frame to look for real caller. + if ( + frame.f_back.f_code.co_filename == options_file + and frame.f_back.f_code.co_name == "define" + ): + frame = frame.f_back + + file_name = frame.f_back.f_code.co_filename + if file_name == options_file: + file_name = "" + if type is None: + if not multiple and default is not None: + type = default.__class__ + else: + type = str + if group: + group_name = group # type: Optional[str] + else: + group_name = file_name + option = _Option( + name, + file_name=file_name, + default=default, + type=type, + help=help, + metavar=metavar, + multiple=multiple, + group_name=group_name, + callback=callback, + ) + self._options[normalized] = option + + def parse_command_line( + self, args: Optional[List[str]] = None, final: bool = True + ) -> List[str]: + """Parses all options given on the command line (defaults to + `sys.argv`). + + Options look like ``--option=value`` and are parsed according + to their ``type``. For boolean options, ``--option`` is + equivalent to ``--option=true`` + + If the option has ``multiple=True``, comma-separated values + are accepted. For multi-value integer options, the syntax + ``x:y`` is also accepted and equivalent to ``range(x, y)``. + + Note that ``args[0]`` is ignored since it is the program name + in `sys.argv`. + + We return a list of all arguments that are not parsed as options. + + If ``final`` is ``False``, parse callbacks will not be run. + This is useful for applications that wish to combine configurations + from multiple sources. + + """ + if args is None: + args = sys.argv + remaining = [] # type: List[str] + for i in range(1, len(args)): + # All things after the last option are command line arguments + if not args[i].startswith("-"): + remaining = args[i:] + break + if args[i] == "--": + remaining = args[i + 1 :] + break + arg = args[i].lstrip("-") + name, equals, value = arg.partition("=") + name = self._normalize_name(name) + if name not in self._options: + self.print_help() + raise Error("Unrecognized command line option: %r" % name) + option = self._options[name] + if not equals: + if option.type == bool: + value = "true" + else: + raise Error("Option %r requires a value" % name) + option.parse(value) + + if final: + self.run_parse_callbacks() + + return remaining + + def parse_config_file(self, path: str, final: bool = True) -> None: + """Parses and loads the config file at the given path. + + The config file contains Python code that will be executed (so + it is **not safe** to use untrusted config files). Anything in + the global namespace that matches a defined option will be + used to set that option's value. + + Options may either be the specified type for the option or + strings (in which case they will be parsed the same way as in + `.parse_command_line`) + + Example (using the options defined in the top-level docs of + this module):: + + port = 80 + mysql_host = 'mydb.example.com:3306' + # Both lists and comma-separated strings are allowed for + # multiple=True. + memcache_hosts = ['cache1.example.com:11011', + 'cache2.example.com:11011'] + memcache_hosts = 'cache1.example.com:11011,cache2.example.com:11011' + + If ``final`` is ``False``, parse callbacks will not be run. + This is useful for applications that wish to combine configurations + from multiple sources. + + .. note:: + + `tornado.options` is primarily a command-line library. + Config file support is provided for applications that wish + to use it, but applications that prefer config files may + wish to look at other libraries instead. + + .. versionchanged:: 4.1 + Config files are now always interpreted as utf-8 instead of + the system default encoding. + + .. versionchanged:: 4.4 + The special variable ``__file__`` is available inside config + files, specifying the absolute path to the config file itself. + + .. versionchanged:: 5.1 + Added the ability to set options via strings in config files. + + """ + config = {"__file__": os.path.abspath(path)} + with open(path, "rb") as f: + exec_in(native_str(f.read()), config, config) + for name in config: + normalized = self._normalize_name(name) + if normalized in self._options: + option = self._options[normalized] + if option.multiple: + if not isinstance(config[name], (list, str)): + raise Error( + "Option %r is required to be a list of %s " + "or a comma-separated string" + % (option.name, option.type.__name__) + ) + + if type(config[name]) == str and option.type != str: + option.parse(config[name]) + else: + option.set(config[name]) + + if final: + self.run_parse_callbacks() + + def print_help(self, file: Optional[TextIO] = None) -> None: + """Prints all the command line options to stderr (or another file).""" + if file is None: + file = sys.stderr + print("Usage: %s [OPTIONS]" % sys.argv[0], file=file) + print("\nOptions:\n", file=file) + by_group = {} # type: Dict[str, List[_Option]] + for option in self._options.values(): + by_group.setdefault(option.group_name, []).append(option) + + for filename, o in sorted(by_group.items()): + if filename: + print("\n%s options:\n" % os.path.normpath(filename), file=file) + o.sort(key=lambda option: option.name) + for option in o: + # Always print names with dashes in a CLI context. + prefix = self._normalize_name(option.name) + if option.metavar: + prefix += "=" + option.metavar + description = option.help or "" + if option.default is not None and option.default != "": + description += " (default %s)" % option.default + lines = textwrap.wrap(description, 79 - 35) + if len(prefix) > 30 or len(lines) == 0: + lines.insert(0, "") + print(" --%-30s %s" % (prefix, lines[0]), file=file) + for line in lines[1:]: + print("%-34s %s" % (" ", line), file=file) + print(file=file) + + def _help_callback(self, value: bool) -> None: + if value: + self.print_help() + sys.exit(0) + + def add_parse_callback(self, callback: Callable[[], None]) -> None: + """Adds a parse callback, to be invoked when option parsing is done.""" + self._parse_callbacks.append(callback) + + def run_parse_callbacks(self) -> None: + for callback in self._parse_callbacks: + callback() + + def mockable(self) -> "_Mockable": + """Returns a wrapper around self that is compatible with + `mock.patch `. + + The `mock.patch ` function (included in + the standard library `unittest.mock` package since Python 3.3, + or in the third-party ``mock`` package for older versions of + Python) is incompatible with objects like ``options`` that + override ``__getattr__`` and ``__setattr__``. This function + returns an object that can be used with `mock.patch.object + ` to modify option values:: + + with mock.patch.object(options.mockable(), 'name', value): + assert options.name == value + """ + return _Mockable(self) + + +class _Mockable(object): + """`mock.patch` compatible wrapper for `OptionParser`. + + As of ``mock`` version 1.0.1, when an object uses ``__getattr__`` + hooks instead of ``__dict__``, ``patch.__exit__`` tries to delete + the attribute it set instead of setting a new one (assuming that + the object does not capture ``__setattr__``, so the patch + created a new attribute in ``__dict__``). + + _Mockable's getattr and setattr pass through to the underlying + OptionParser, and delattr undoes the effect of a previous setattr. + """ + + def __init__(self, options: OptionParser) -> None: + # Modify __dict__ directly to bypass __setattr__ + self.__dict__["_options"] = options + self.__dict__["_originals"] = {} + + def __getattr__(self, name: str) -> Any: + return getattr(self._options, name) + + def __setattr__(self, name: str, value: Any) -> None: + assert name not in self._originals, "don't reuse mockable objects" + self._originals[name] = getattr(self._options, name) + setattr(self._options, name, value) + + def __delattr__(self, name: str) -> None: + setattr(self._options, name, self._originals.pop(name)) + + +class _Option(object): + # This class could almost be made generic, but the way the types + # interact with the multiple argument makes this tricky. (default + # and the callback use List[T], but type is still Type[T]). + UNSET = object() + + def __init__( + self, + name: str, + default: Any = None, + type: Optional[type] = None, + help: Optional[str] = None, + metavar: Optional[str] = None, + multiple: bool = False, + file_name: Optional[str] = None, + group_name: Optional[str] = None, + callback: Optional[Callable[[Any], None]] = None, + ) -> None: + if default is None and multiple: + default = [] + self.name = name + if type is None: + raise ValueError("type must not be None") + self.type = type + self.help = help + self.metavar = metavar + self.multiple = multiple + self.file_name = file_name + self.group_name = group_name + self.callback = callback + self.default = default + self._value = _Option.UNSET # type: Any + + def value(self) -> Any: + return self.default if self._value is _Option.UNSET else self._value + + def parse(self, value: str) -> Any: + _parse = { + datetime.datetime: self._parse_datetime, + datetime.timedelta: self._parse_timedelta, + bool: self._parse_bool, + basestring_type: self._parse_string, + }.get( + self.type, self.type + ) # type: Callable[[str], Any] + if self.multiple: + self._value = [] + for part in value.split(","): + if issubclass(self.type, numbers.Integral): + # allow ranges of the form X:Y (inclusive at both ends) + lo_str, _, hi_str = part.partition(":") + lo = _parse(lo_str) + hi = _parse(hi_str) if hi_str else lo + self._value.extend(range(lo, hi + 1)) + else: + self._value.append(_parse(part)) + else: + self._value = _parse(value) + if self.callback is not None: + self.callback(self._value) + return self.value() + + def set(self, value: Any) -> None: + if self.multiple: + if not isinstance(value, list): + raise Error( + "Option %r is required to be a list of %s" + % (self.name, self.type.__name__) + ) + for item in value: + if item is not None and not isinstance(item, self.type): + raise Error( + "Option %r is required to be a list of %s" + % (self.name, self.type.__name__) + ) + else: + if value is not None and not isinstance(value, self.type): + raise Error( + "Option %r is required to be a %s (%s given)" + % (self.name, self.type.__name__, type(value)) + ) + self._value = value + if self.callback is not None: + self.callback(self._value) + + # Supported date/time formats in our options + _DATETIME_FORMATS = [ + "%a %b %d %H:%M:%S %Y", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%dT%H:%M", + "%Y%m%d %H:%M:%S", + "%Y%m%d %H:%M", + "%Y-%m-%d", + "%Y%m%d", + "%H:%M:%S", + "%H:%M", + ] + + def _parse_datetime(self, value: str) -> datetime.datetime: + for format in self._DATETIME_FORMATS: + try: + return datetime.datetime.strptime(value, format) + except ValueError: + pass + raise Error("Unrecognized date/time format: %r" % value) + + _TIMEDELTA_ABBREV_DICT = { + "h": "hours", + "m": "minutes", + "min": "minutes", + "s": "seconds", + "sec": "seconds", + "ms": "milliseconds", + "us": "microseconds", + "d": "days", + "w": "weeks", + } + + _FLOAT_PATTERN = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?" + + _TIMEDELTA_PATTERN = re.compile( + r"\s*(%s)\s*(\w*)\s*" % _FLOAT_PATTERN, re.IGNORECASE + ) + + def _parse_timedelta(self, value: str) -> datetime.timedelta: + try: + sum = datetime.timedelta() + start = 0 + while start < len(value): + m = self._TIMEDELTA_PATTERN.match(value, start) + if not m: + raise Exception() + num = float(m.group(1)) + units = m.group(2) or "seconds" + units = self._TIMEDELTA_ABBREV_DICT.get(units, units) + sum += datetime.timedelta(**{units: num}) + start = m.end() + return sum + except Exception: + raise + + def _parse_bool(self, value: str) -> bool: + return value.lower() not in ("false", "0", "f") + + def _parse_string(self, value: str) -> str: + return _unicode(value) + + +options = OptionParser() +"""Global options object. + +All defined options are available as attributes on this object. +""" + + +def define( + name: str, + default: Any = None, + type: Optional[type] = None, + help: Optional[str] = None, + metavar: Optional[str] = None, + multiple: bool = False, + group: Optional[str] = None, + callback: Optional[Callable[[Any], None]] = None, +) -> None: + """Defines an option in the global namespace. + + See `OptionParser.define`. + """ + return options.define( + name, + default=default, + type=type, + help=help, + metavar=metavar, + multiple=multiple, + group=group, + callback=callback, + ) + + +def parse_command_line( + args: Optional[List[str]] = None, final: bool = True +) -> List[str]: + """Parses global options from the command line. + + See `OptionParser.parse_command_line`. + """ + return options.parse_command_line(args, final=final) + + +def parse_config_file(path: str, final: bool = True) -> None: + """Parses global options from a config file. + + See `OptionParser.parse_config_file`. + """ + return options.parse_config_file(path, final=final) + + +def print_help(file: Optional[TextIO] = None) -> None: + """Prints all the command line options to stderr (or another file). + + See `OptionParser.print_help`. + """ + return options.print_help(file) + + +def add_parse_callback(callback: Callable[[], None]) -> None: + """Adds a parse callback, to be invoked when option parsing is done. + + See `OptionParser.add_parse_callback` + """ + options.add_parse_callback(callback) + + +# Default options +define_logging_options(options) diff --git a/telegramer/include/tornado/platform/__init__.py b/telegramer/include/tornado/platform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegramer/include/tornado/platform/asyncio.py b/telegramer/include/tornado/platform/asyncio.py new file mode 100644 index 0000000..012948b --- /dev/null +++ b/telegramer/include/tornado/platform/asyncio.py @@ -0,0 +1,611 @@ +"""Bridges between the `asyncio` module and Tornado IOLoop. + +.. versionadded:: 3.2 + +This module integrates Tornado with the ``asyncio`` module introduced +in Python 3.4. This makes it possible to combine the two libraries on +the same event loop. + +.. deprecated:: 5.0 + + While the code in this module is still used, it is now enabled + automatically when `asyncio` is available, so applications should + no longer need to refer to this module directly. + +.. note:: + + Tornado is designed to use a selector-based event loop. On Windows, + where a proactor-based event loop has been the default since Python 3.8, + a selector event loop is emulated by running ``select`` on a separate thread. + Configuring ``asyncio`` to use a selector event loop may improve performance + of Tornado (but may reduce performance of other ``asyncio``-based libraries + in the same process). +""" + +import asyncio +import atexit +import concurrent.futures +import errno +import functools +import select +import socket +import sys +import threading +import typing +from tornado.gen import convert_yielded +from tornado.ioloop import IOLoop, _Selectable + +from typing import Any, TypeVar, Awaitable, Callable, Union, Optional, List, Tuple, Dict + +if typing.TYPE_CHECKING: + from typing import Set # noqa: F401 + from typing_extensions import Protocol + + class _HasFileno(Protocol): + def fileno(self) -> int: + pass + + _FileDescriptorLike = Union[int, _HasFileno] + +_T = TypeVar("_T") + + +# Collection of selector thread event loops to shut down on exit. +_selector_loops = set() # type: Set[AddThreadSelectorEventLoop] + + +def _atexit_callback() -> None: + for loop in _selector_loops: + with loop._select_cond: + loop._closing_selector = True + loop._select_cond.notify() + try: + loop._waker_w.send(b"a") + except BlockingIOError: + pass + # If we don't join our (daemon) thread here, we may get a deadlock + # during interpreter shutdown. I don't really understand why. This + # deadlock happens every time in CI (both travis and appveyor) but + # I've never been able to reproduce locally. + loop._thread.join() + _selector_loops.clear() + + +atexit.register(_atexit_callback) + + +class BaseAsyncIOLoop(IOLoop): + def initialize( # type: ignore + self, asyncio_loop: asyncio.AbstractEventLoop, **kwargs: Any + ) -> None: + # asyncio_loop is always the real underlying IOLoop. This is used in + # ioloop.py to maintain the asyncio-to-ioloop mappings. + self.asyncio_loop = asyncio_loop + # selector_loop is an event loop that implements the add_reader family of + # methods. Usually the same as asyncio_loop but differs on platforms such + # as windows where the default event loop does not implement these methods. + self.selector_loop = asyncio_loop + if hasattr(asyncio, "ProactorEventLoop") and isinstance( + asyncio_loop, asyncio.ProactorEventLoop # type: ignore + ): + # Ignore this line for mypy because the abstract method checker + # doesn't understand dynamic proxies. + self.selector_loop = AddThreadSelectorEventLoop(asyncio_loop) # type: ignore + # Maps fd to (fileobj, handler function) pair (as in IOLoop.add_handler) + self.handlers = {} # type: Dict[int, Tuple[Union[int, _Selectable], Callable]] + # Set of fds listening for reads/writes + self.readers = set() # type: Set[int] + self.writers = set() # type: Set[int] + self.closing = False + # If an asyncio loop was closed through an asyncio interface + # instead of IOLoop.close(), we'd never hear about it and may + # have left a dangling reference in our map. In case an + # application (or, more likely, a test suite) creates and + # destroys a lot of event loops in this way, check here to + # ensure that we don't have a lot of dead loops building up in + # the map. + # + # TODO(bdarnell): consider making self.asyncio_loop a weakref + # for AsyncIOMainLoop and make _ioloop_for_asyncio a + # WeakKeyDictionary. + for loop in list(IOLoop._ioloop_for_asyncio): + if loop.is_closed(): + del IOLoop._ioloop_for_asyncio[loop] + IOLoop._ioloop_for_asyncio[asyncio_loop] = self + + self._thread_identity = 0 + + super().initialize(**kwargs) + + def assign_thread_identity() -> None: + self._thread_identity = threading.get_ident() + + self.add_callback(assign_thread_identity) + + def close(self, all_fds: bool = False) -> None: + self.closing = True + for fd in list(self.handlers): + fileobj, handler_func = self.handlers[fd] + self.remove_handler(fd) + if all_fds: + self.close_fd(fileobj) + # Remove the mapping before closing the asyncio loop. If this + # happened in the other order, we could race against another + # initialize() call which would see the closed asyncio loop, + # assume it was closed from the asyncio side, and do this + # cleanup for us, leading to a KeyError. + del IOLoop._ioloop_for_asyncio[self.asyncio_loop] + if self.selector_loop is not self.asyncio_loop: + self.selector_loop.close() + self.asyncio_loop.close() + + def add_handler( + self, fd: Union[int, _Selectable], handler: Callable[..., None], events: int + ) -> None: + fd, fileobj = self.split_fd(fd) + if fd in self.handlers: + raise ValueError("fd %s added twice" % fd) + self.handlers[fd] = (fileobj, handler) + if events & IOLoop.READ: + self.selector_loop.add_reader(fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + if events & IOLoop.WRITE: + self.selector_loop.add_writer(fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + + def update_handler(self, fd: Union[int, _Selectable], events: int) -> None: + fd, fileobj = self.split_fd(fd) + if events & IOLoop.READ: + if fd not in self.readers: + self.selector_loop.add_reader(fd, self._handle_events, fd, IOLoop.READ) + self.readers.add(fd) + else: + if fd in self.readers: + self.selector_loop.remove_reader(fd) + self.readers.remove(fd) + if events & IOLoop.WRITE: + if fd not in self.writers: + self.selector_loop.add_writer(fd, self._handle_events, fd, IOLoop.WRITE) + self.writers.add(fd) + else: + if fd in self.writers: + self.selector_loop.remove_writer(fd) + self.writers.remove(fd) + + def remove_handler(self, fd: Union[int, _Selectable]) -> None: + fd, fileobj = self.split_fd(fd) + if fd not in self.handlers: + return + if fd in self.readers: + self.selector_loop.remove_reader(fd) + self.readers.remove(fd) + if fd in self.writers: + self.selector_loop.remove_writer(fd) + self.writers.remove(fd) + del self.handlers[fd] + + def _handle_events(self, fd: int, events: int) -> None: + fileobj, handler_func = self.handlers[fd] + handler_func(fileobj, events) + + def start(self) -> None: + try: + old_loop = asyncio.get_event_loop() + except (RuntimeError, AssertionError): + old_loop = None # type: ignore + try: + self._setup_logging() + asyncio.set_event_loop(self.asyncio_loop) + self.asyncio_loop.run_forever() + finally: + asyncio.set_event_loop(old_loop) + + def stop(self) -> None: + self.asyncio_loop.stop() + + def call_at( + self, when: float, callback: Callable[..., None], *args: Any, **kwargs: Any + ) -> object: + # asyncio.call_at supports *args but not **kwargs, so bind them here. + # We do not synchronize self.time and asyncio_loop.time, so + # convert from absolute to relative. + return self.asyncio_loop.call_later( + max(0, when - self.time()), + self._run_callback, + functools.partial(callback, *args, **kwargs), + ) + + def remove_timeout(self, timeout: object) -> None: + timeout.cancel() # type: ignore + + def add_callback(self, callback: Callable, *args: Any, **kwargs: Any) -> None: + if threading.get_ident() == self._thread_identity: + call_soon = self.asyncio_loop.call_soon + else: + call_soon = self.asyncio_loop.call_soon_threadsafe + try: + call_soon(self._run_callback, functools.partial(callback, *args, **kwargs)) + except RuntimeError: + # "Event loop is closed". Swallow the exception for + # consistency with PollIOLoop (and logical consistency + # with the fact that we can't guarantee that an + # add_callback that completes without error will + # eventually execute). + pass + except AttributeError: + # ProactorEventLoop may raise this instead of RuntimeError + # if call_soon_threadsafe races with a call to close(). + # Swallow it too for consistency. + pass + + def add_callback_from_signal( + self, callback: Callable, *args: Any, **kwargs: Any + ) -> None: + try: + self.asyncio_loop.call_soon_threadsafe( + self._run_callback, functools.partial(callback, *args, **kwargs) + ) + except RuntimeError: + pass + + def run_in_executor( + self, + executor: Optional[concurrent.futures.Executor], + func: Callable[..., _T], + *args: Any + ) -> Awaitable[_T]: + return self.asyncio_loop.run_in_executor(executor, func, *args) + + def set_default_executor(self, executor: concurrent.futures.Executor) -> None: + return self.asyncio_loop.set_default_executor(executor) + + +class AsyncIOMainLoop(BaseAsyncIOLoop): + """``AsyncIOMainLoop`` creates an `.IOLoop` that corresponds to the + current ``asyncio`` event loop (i.e. the one returned by + ``asyncio.get_event_loop()``). + + .. deprecated:: 5.0 + + Now used automatically when appropriate; it is no longer necessary + to refer to this class directly. + + .. versionchanged:: 5.0 + + Closing an `AsyncIOMainLoop` now closes the underlying asyncio loop. + """ + + def initialize(self, **kwargs: Any) -> None: # type: ignore + super().initialize(asyncio.get_event_loop(), **kwargs) + + def make_current(self) -> None: + # AsyncIOMainLoop already refers to the current asyncio loop so + # nothing to do here. + pass + + +class AsyncIOLoop(BaseAsyncIOLoop): + """``AsyncIOLoop`` is an `.IOLoop` that runs on an ``asyncio`` event loop. + This class follows the usual Tornado semantics for creating new + ``IOLoops``; these loops are not necessarily related to the + ``asyncio`` default event loop. + + Each ``AsyncIOLoop`` creates a new ``asyncio.EventLoop``; this object + can be accessed with the ``asyncio_loop`` attribute. + + .. versionchanged:: 5.0 + + When an ``AsyncIOLoop`` becomes the current `.IOLoop`, it also sets + the current `asyncio` event loop. + + .. deprecated:: 5.0 + + Now used automatically when appropriate; it is no longer necessary + to refer to this class directly. + """ + + def initialize(self, **kwargs: Any) -> None: # type: ignore + self.is_current = False + loop = asyncio.new_event_loop() + try: + super().initialize(loop, **kwargs) + except Exception: + # If initialize() does not succeed (taking ownership of the loop), + # we have to close it. + loop.close() + raise + + def close(self, all_fds: bool = False) -> None: + if self.is_current: + self.clear_current() + super().close(all_fds=all_fds) + + def make_current(self) -> None: + if not self.is_current: + try: + self.old_asyncio = asyncio.get_event_loop() + except (RuntimeError, AssertionError): + self.old_asyncio = None # type: ignore + self.is_current = True + asyncio.set_event_loop(self.asyncio_loop) + + def _clear_current_hook(self) -> None: + if self.is_current: + asyncio.set_event_loop(self.old_asyncio) + self.is_current = False + + +def to_tornado_future(asyncio_future: asyncio.Future) -> asyncio.Future: + """Convert an `asyncio.Future` to a `tornado.concurrent.Future`. + + .. versionadded:: 4.1 + + .. deprecated:: 5.0 + Tornado ``Futures`` have been merged with `asyncio.Future`, + so this method is now a no-op. + """ + return asyncio_future + + +def to_asyncio_future(tornado_future: asyncio.Future) -> asyncio.Future: + """Convert a Tornado yieldable object to an `asyncio.Future`. + + .. versionadded:: 4.1 + + .. versionchanged:: 4.3 + Now accepts any yieldable object, not just + `tornado.concurrent.Future`. + + .. deprecated:: 5.0 + Tornado ``Futures`` have been merged with `asyncio.Future`, + so this method is now equivalent to `tornado.gen.convert_yielded`. + """ + return convert_yielded(tornado_future) + + +if sys.platform == "win32" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"): + # "Any thread" and "selector" should be orthogonal, but there's not a clean + # interface for composing policies so pick the right base. + _BasePolicy = asyncio.WindowsSelectorEventLoopPolicy # type: ignore +else: + _BasePolicy = asyncio.DefaultEventLoopPolicy + + +class AnyThreadEventLoopPolicy(_BasePolicy): # type: ignore + """Event loop policy that allows loop creation on any thread. + + The default `asyncio` event loop policy only automatically creates + event loops in the main threads. Other threads must create event + loops explicitly or `asyncio.get_event_loop` (and therefore + `.IOLoop.current`) will fail. Installing this policy allows event + loops to be created automatically on any thread, matching the + behavior of Tornado versions prior to 5.0 (or 5.0 on Python 2). + + Usage:: + + asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy()) + + .. versionadded:: 5.0 + + """ + + def get_event_loop(self) -> asyncio.AbstractEventLoop: + try: + return super().get_event_loop() + except (RuntimeError, AssertionError): + # This was an AssertionError in Python 3.4.2 (which ships with Debian Jessie) + # and changed to a RuntimeError in 3.4.3. + # "There is no current event loop in thread %r" + loop = self.new_event_loop() + self.set_event_loop(loop) + return loop + + +class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop): + """Wrap an event loop to add implementations of the ``add_reader`` method family. + + Instances of this class start a second thread to run a selector. + This thread is completely hidden from the user; all callbacks are + run on the wrapped event loop's thread. + + This class is used automatically by Tornado; applications should not need + to refer to it directly. + + It is safe to wrap any event loop with this class, although it only makes sense + for event loops that do not implement the ``add_reader`` family of methods + themselves (i.e. ``WindowsProactorEventLoop``) + + Closing the ``AddThreadSelectorEventLoop`` also closes the wrapped event loop. + + """ + + # This class is a __getattribute__-based proxy. All attributes other than those + # in this set are proxied through to the underlying loop. + MY_ATTRIBUTES = { + "_consume_waker", + "_select_cond", + "_select_args", + "_closing_selector", + "_thread", + "_handle_event", + "_readers", + "_real_loop", + "_start_select", + "_run_select", + "_handle_select", + "_wake_selector", + "_waker_r", + "_waker_w", + "_writers", + "add_reader", + "add_writer", + "close", + "remove_reader", + "remove_writer", + } + + def __getattribute__(self, name: str) -> Any: + if name in AddThreadSelectorEventLoop.MY_ATTRIBUTES: + return super().__getattribute__(name) + return getattr(self._real_loop, name) + + def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None: + self._real_loop = real_loop + + # Create a thread to run the select system call. We manage this thread + # manually so we can trigger a clean shutdown from an atexit hook. Note + # that due to the order of operations at shutdown, only daemon threads + # can be shut down in this way (non-daemon threads would require the + # introduction of a new hook: https://bugs.python.org/issue41962) + self._select_cond = threading.Condition() + self._select_args = ( + None + ) # type: Optional[Tuple[List[_FileDescriptorLike], List[_FileDescriptorLike]]] + self._closing_selector = False + self._thread = threading.Thread( + name="Tornado selector", daemon=True, target=self._run_select, + ) + self._thread.start() + # Start the select loop once the loop is started. + self._real_loop.call_soon(self._start_select) + + self._readers = {} # type: Dict[_FileDescriptorLike, Callable] + self._writers = {} # type: Dict[_FileDescriptorLike, Callable] + + # Writing to _waker_w will wake up the selector thread, which + # watches for _waker_r to be readable. + self._waker_r, self._waker_w = socket.socketpair() + self._waker_r.setblocking(False) + self._waker_w.setblocking(False) + _selector_loops.add(self) + self.add_reader(self._waker_r, self._consume_waker) + + def __del__(self) -> None: + # If the top-level application code uses asyncio interfaces to + # start and stop the event loop, no objects created in Tornado + # can get a clean shutdown notification. If we're just left to + # be GC'd, we must explicitly close our sockets to avoid + # logging warnings. + _selector_loops.discard(self) + self._waker_r.close() + self._waker_w.close() + + def close(self) -> None: + with self._select_cond: + self._closing_selector = True + self._select_cond.notify() + self._wake_selector() + self._thread.join() + _selector_loops.discard(self) + self._waker_r.close() + self._waker_w.close() + self._real_loop.close() + + def _wake_selector(self) -> None: + try: + self._waker_w.send(b"a") + except BlockingIOError: + pass + + def _consume_waker(self) -> None: + try: + self._waker_r.recv(1024) + except BlockingIOError: + pass + + def _start_select(self) -> None: + # Capture reader and writer sets here in the event loop + # thread to avoid any problems with concurrent + # modification while the select loop uses them. + with self._select_cond: + assert self._select_args is None + self._select_args = (list(self._readers.keys()), list(self._writers.keys())) + self._select_cond.notify() + + def _run_select(self) -> None: + while True: + with self._select_cond: + while self._select_args is None and not self._closing_selector: + self._select_cond.wait() + if self._closing_selector: + return + assert self._select_args is not None + to_read, to_write = self._select_args + self._select_args = None + + # We use the simpler interface of the select module instead of + # the more stateful interface in the selectors module because + # this class is only intended for use on windows, where + # select.select is the only option. The selector interface + # does not have well-documented thread-safety semantics that + # we can rely on so ensuring proper synchronization would be + # tricky. + try: + # On windows, selecting on a socket for write will not + # return the socket when there is an error (but selecting + # for reads works). Also select for errors when selecting + # for writes, and merge the results. + # + # This pattern is also used in + # https://github.com/python/cpython/blob/v3.8.0/Lib/selectors.py#L312-L317 + rs, ws, xs = select.select(to_read, to_write, to_write) + ws = ws + xs + except OSError as e: + # After remove_reader or remove_writer is called, the file + # descriptor may subsequently be closed on the event loop + # thread. It's possible that this select thread hasn't + # gotten into the select system call by the time that + # happens in which case (at least on macOS), select may + # raise a "bad file descriptor" error. If we get that + # error, check and see if we're also being woken up by + # polling the waker alone. If we are, just return to the + # event loop and we'll get the updated set of file + # descriptors on the next iteration. Otherwise, raise the + # original error. + if e.errno == getattr(errno, "WSAENOTSOCK", errno.EBADF): + rs, _, _ = select.select([self._waker_r.fileno()], [], [], 0) + if rs: + ws = [] + else: + raise + else: + raise + self._real_loop.call_soon_threadsafe(self._handle_select, rs, ws) + + def _handle_select( + self, rs: List["_FileDescriptorLike"], ws: List["_FileDescriptorLike"] + ) -> None: + for r in rs: + self._handle_event(r, self._readers) + for w in ws: + self._handle_event(w, self._writers) + self._start_select() + + def _handle_event( + self, fd: "_FileDescriptorLike", cb_map: Dict["_FileDescriptorLike", Callable], + ) -> None: + try: + callback = cb_map[fd] + except KeyError: + return + callback() + + def add_reader( + self, fd: "_FileDescriptorLike", callback: Callable[..., None], *args: Any + ) -> None: + self._readers[fd] = functools.partial(callback, *args) + self._wake_selector() + + def add_writer( + self, fd: "_FileDescriptorLike", callback: Callable[..., None], *args: Any + ) -> None: + self._writers[fd] = functools.partial(callback, *args) + self._wake_selector() + + def remove_reader(self, fd: "_FileDescriptorLike") -> None: + del self._readers[fd] + self._wake_selector() + + def remove_writer(self, fd: "_FileDescriptorLike") -> None: + del self._writers[fd] + self._wake_selector() diff --git a/telegramer/include/tornado/platform/caresresolver.py b/telegramer/include/tornado/platform/caresresolver.py new file mode 100644 index 0000000..e2c5009 --- /dev/null +++ b/telegramer/include/tornado/platform/caresresolver.py @@ -0,0 +1,89 @@ +import pycares # type: ignore +import socket + +from tornado.concurrent import Future +from tornado import gen +from tornado.ioloop import IOLoop +from tornado.netutil import Resolver, is_valid_ip + +import typing + +if typing.TYPE_CHECKING: + from typing import Generator, Any, List, Tuple, Dict # noqa: F401 + + +class CaresResolver(Resolver): + """Name resolver based on the c-ares library. + + This is a non-blocking and non-threaded resolver. It may not produce + the same results as the system resolver, but can be used for non-blocking + resolution when threads cannot be used. + + c-ares fails to resolve some names when ``family`` is ``AF_UNSPEC``, + so it is only recommended for use in ``AF_INET`` (i.e. IPv4). This is + the default for ``tornado.simple_httpclient``, but other libraries + may default to ``AF_UNSPEC``. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + """ + + def initialize(self) -> None: + self.io_loop = IOLoop.current() + self.channel = pycares.Channel(sock_state_cb=self._sock_state_cb) + self.fds = {} # type: Dict[int, int] + + def _sock_state_cb(self, fd: int, readable: bool, writable: bool) -> None: + state = (IOLoop.READ if readable else 0) | (IOLoop.WRITE if writable else 0) + if not state: + self.io_loop.remove_handler(fd) + del self.fds[fd] + elif fd in self.fds: + self.io_loop.update_handler(fd, state) + self.fds[fd] = state + else: + self.io_loop.add_handler(fd, self._handle_events, state) + self.fds[fd] = state + + def _handle_events(self, fd: int, events: int) -> None: + read_fd = pycares.ARES_SOCKET_BAD + write_fd = pycares.ARES_SOCKET_BAD + if events & IOLoop.READ: + read_fd = fd + if events & IOLoop.WRITE: + write_fd = fd + self.channel.process_fd(read_fd, write_fd) + + @gen.coroutine + def resolve( + self, host: str, port: int, family: int = 0 + ) -> "Generator[Any, Any, List[Tuple[int, Any]]]": + if is_valid_ip(host): + addresses = [host] + else: + # gethostbyname doesn't take callback as a kwarg + fut = Future() # type: Future[Tuple[Any, Any]] + self.channel.gethostbyname( + host, family, lambda result, error: fut.set_result((result, error)) + ) + result, error = yield fut + if error: + raise IOError( + "C-Ares returned error %s: %s while resolving %s" + % (error, pycares.errno.strerror(error), host) + ) + addresses = result.addresses + addrinfo = [] + for address in addresses: + if "." in address: + address_family = socket.AF_INET + elif ":" in address: + address_family = socket.AF_INET6 + else: + address_family = socket.AF_UNSPEC + if family != socket.AF_UNSPEC and family != address_family: + raise IOError( + "Requested socket family %d but got %d" % (family, address_family) + ) + addrinfo.append((typing.cast(int, address_family), (address, port))) + return addrinfo diff --git a/telegramer/include/tornado/platform/twisted.py b/telegramer/include/tornado/platform/twisted.py new file mode 100644 index 0000000..0987a84 --- /dev/null +++ b/telegramer/include/tornado/platform/twisted.py @@ -0,0 +1,146 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Bridges between the Twisted package and Tornado. +""" + +import socket +import sys + +import twisted.internet.abstract # type: ignore +import twisted.internet.asyncioreactor # type: ignore +from twisted.internet.defer import Deferred # type: ignore +from twisted.python import failure # type: ignore +import twisted.names.cache # type: ignore +import twisted.names.client # type: ignore +import twisted.names.hosts # type: ignore +import twisted.names.resolve # type: ignore + + +from tornado.concurrent import Future, future_set_exc_info +from tornado.escape import utf8 +from tornado import gen +from tornado.netutil import Resolver + +import typing + +if typing.TYPE_CHECKING: + from typing import Generator, Any, List, Tuple # noqa: F401 + + +class TwistedResolver(Resolver): + """Twisted-based asynchronous resolver. + + This is a non-blocking and non-threaded resolver. It is + recommended only when threads cannot be used, since it has + limitations compared to the standard ``getaddrinfo``-based + `~tornado.netutil.Resolver` and + `~tornado.netutil.DefaultExecutorResolver`. Specifically, it returns at + most one result, and arguments other than ``host`` and ``family`` + are ignored. It may fail to resolve when ``family`` is not + ``socket.AF_UNSPEC``. + + Requires Twisted 12.1 or newer. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + """ + + def initialize(self) -> None: + # partial copy of twisted.names.client.createResolver, which doesn't + # allow for a reactor to be passed in. + self.reactor = twisted.internet.asyncioreactor.AsyncioSelectorReactor() + + host_resolver = twisted.names.hosts.Resolver("/etc/hosts") + cache_resolver = twisted.names.cache.CacheResolver(reactor=self.reactor) + real_resolver = twisted.names.client.Resolver( + "/etc/resolv.conf", reactor=self.reactor + ) + self.resolver = twisted.names.resolve.ResolverChain( + [host_resolver, cache_resolver, real_resolver] + ) + + @gen.coroutine + def resolve( + self, host: str, port: int, family: int = 0 + ) -> "Generator[Any, Any, List[Tuple[int, Any]]]": + # getHostByName doesn't accept IP addresses, so if the input + # looks like an IP address just return it immediately. + if twisted.internet.abstract.isIPAddress(host): + resolved = host + resolved_family = socket.AF_INET + elif twisted.internet.abstract.isIPv6Address(host): + resolved = host + resolved_family = socket.AF_INET6 + else: + deferred = self.resolver.getHostByName(utf8(host)) + fut = Future() # type: Future[Any] + deferred.addBoth(fut.set_result) + resolved = yield fut + if isinstance(resolved, failure.Failure): + try: + resolved.raiseException() + except twisted.names.error.DomainError as e: + raise IOError(e) + elif twisted.internet.abstract.isIPAddress(resolved): + resolved_family = socket.AF_INET + elif twisted.internet.abstract.isIPv6Address(resolved): + resolved_family = socket.AF_INET6 + else: + resolved_family = socket.AF_UNSPEC + if family != socket.AF_UNSPEC and family != resolved_family: + raise Exception( + "Requested socket family %d but got %d" % (family, resolved_family) + ) + result = [(typing.cast(int, resolved_family), (resolved, port))] + return result + + +def install() -> None: + """Install ``AsyncioSelectorReactor`` as the default Twisted reactor. + + .. deprecated:: 5.1 + + This function is provided for backwards compatibility; code + that does not require compatibility with older versions of + Tornado should use + ``twisted.internet.asyncioreactor.install()`` directly. + + .. versionchanged:: 6.0.3 + + In Tornado 5.x and before, this function installed a reactor + based on the Tornado ``IOLoop``. When that reactor + implementation was removed in Tornado 6.0.0, this function was + removed as well. It was restored in Tornado 6.0.3 using the + ``asyncio`` reactor instead. + + """ + from twisted.internet.asyncioreactor import install + + install() + + +if hasattr(gen.convert_yielded, "register"): + + @gen.convert_yielded.register(Deferred) # type: ignore + def _(d: Deferred) -> Future: + f = Future() # type: Future[Any] + + def errback(failure: failure.Failure) -> None: + try: + failure.raiseException() + # Should never happen, but just in case + raise Exception("errback called without error") + except: + future_set_exc_info(f, sys.exc_info()) + + d.addCallbacks(f.set_result, errback) + return f diff --git a/telegramer/include/tornado/process.py b/telegramer/include/tornado/process.py new file mode 100644 index 0000000..26428fe --- /dev/null +++ b/telegramer/include/tornado/process.py @@ -0,0 +1,373 @@ +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Utilities for working with multiple processes, including both forking +the server into multiple processes and managing subprocesses. +""" + +import os +import multiprocessing +import signal +import subprocess +import sys +import time + +from binascii import hexlify + +from tornado.concurrent import ( + Future, + future_set_result_unless_cancelled, + future_set_exception_unless_cancelled, +) +from tornado import ioloop +from tornado.iostream import PipeIOStream +from tornado.log import gen_log + +import typing +from typing import Optional, Any, Callable + +if typing.TYPE_CHECKING: + from typing import List # noqa: F401 + +# Re-export this exception for convenience. +CalledProcessError = subprocess.CalledProcessError + + +def cpu_count() -> int: + """Returns the number of processors on this machine.""" + if multiprocessing is None: + return 1 + try: + return multiprocessing.cpu_count() + except NotImplementedError: + pass + try: + return os.sysconf("SC_NPROCESSORS_CONF") # type: ignore + except (AttributeError, ValueError): + pass + gen_log.error("Could not detect number of processors; assuming 1") + return 1 + + +def _reseed_random() -> None: + if "random" not in sys.modules: + return + import random + + # If os.urandom is available, this method does the same thing as + # random.seed (at least as of python 2.6). If os.urandom is not + # available, we mix in the pid in addition to a timestamp. + try: + seed = int(hexlify(os.urandom(16)), 16) + except NotImplementedError: + seed = int(time.time() * 1000) ^ os.getpid() + random.seed(seed) + + +_task_id = None + + +def fork_processes( + num_processes: Optional[int], max_restarts: Optional[int] = None +) -> int: + """Starts multiple worker processes. + + If ``num_processes`` is None or <= 0, we detect the number of cores + available on this machine and fork that number of child + processes. If ``num_processes`` is given and > 0, we fork that + specific number of sub-processes. + + Since we use processes and not threads, there is no shared memory + between any server code. + + Note that multiple processes are not compatible with the autoreload + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). + When using multiple processes, no IOLoops can be created or + referenced until after the call to ``fork_processes``. + + In each child process, ``fork_processes`` returns its *task id*, a + number between 0 and ``num_processes``. Processes that exit + abnormally (due to a signal or non-zero exit status) are restarted + with the same id (up to ``max_restarts`` times). In the parent + process, ``fork_processes`` calls ``sys.exit(0)`` after all child + processes have exited normally. + + max_restarts defaults to 100. + + Availability: Unix + """ + if sys.platform == "win32": + # The exact form of this condition matters to mypy; it understands + # if but not assert in this context. + raise Exception("fork not available on windows") + if max_restarts is None: + max_restarts = 100 + + global _task_id + assert _task_id is None + if num_processes is None or num_processes <= 0: + num_processes = cpu_count() + gen_log.info("Starting %d processes", num_processes) + children = {} + + def start_child(i: int) -> Optional[int]: + pid = os.fork() + if pid == 0: + # child process + _reseed_random() + global _task_id + _task_id = i + return i + else: + children[pid] = i + return None + + for i in range(num_processes): + id = start_child(i) + if id is not None: + return id + num_restarts = 0 + while children: + pid, status = os.wait() + if pid not in children: + continue + id = children.pop(pid) + if os.WIFSIGNALED(status): + gen_log.warning( + "child %d (pid %d) killed by signal %d, restarting", + id, + pid, + os.WTERMSIG(status), + ) + elif os.WEXITSTATUS(status) != 0: + gen_log.warning( + "child %d (pid %d) exited with status %d, restarting", + id, + pid, + os.WEXITSTATUS(status), + ) + else: + gen_log.info("child %d (pid %d) exited normally", id, pid) + continue + num_restarts += 1 + if num_restarts > max_restarts: + raise RuntimeError("Too many child restarts, giving up") + new_id = start_child(id) + if new_id is not None: + return new_id + # All child processes exited cleanly, so exit the master process + # instead of just returning to right after the call to + # fork_processes (which will probably just start up another IOLoop + # unless the caller checks the return value). + sys.exit(0) + + +def task_id() -> Optional[int]: + """Returns the current task id, if any. + + Returns None if this process was not created by `fork_processes`. + """ + global _task_id + return _task_id + + +class Subprocess(object): + """Wraps ``subprocess.Popen`` with IOStream support. + + The constructor is the same as ``subprocess.Popen`` with the following + additions: + + * ``stdin``, ``stdout``, and ``stderr`` may have the value + ``tornado.process.Subprocess.STREAM``, which will make the corresponding + attribute of the resulting Subprocess a `.PipeIOStream`. If this option + is used, the caller is responsible for closing the streams when done + with them. + + The ``Subprocess.STREAM`` option and the ``set_exit_callback`` and + ``wait_for_exit`` methods do not work on Windows. There is + therefore no reason to use this class instead of + ``subprocess.Popen`` on that platform. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + + """ + + STREAM = object() + + _initialized = False + _waiting = {} # type: ignore + _old_sigchld = None + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.io_loop = ioloop.IOLoop.current() + # All FDs we create should be closed on error; those in to_close + # should be closed in the parent process on success. + pipe_fds = [] # type: List[int] + to_close = [] # type: List[int] + if kwargs.get("stdin") is Subprocess.STREAM: + in_r, in_w = os.pipe() + kwargs["stdin"] = in_r + pipe_fds.extend((in_r, in_w)) + to_close.append(in_r) + self.stdin = PipeIOStream(in_w) + if kwargs.get("stdout") is Subprocess.STREAM: + out_r, out_w = os.pipe() + kwargs["stdout"] = out_w + pipe_fds.extend((out_r, out_w)) + to_close.append(out_w) + self.stdout = PipeIOStream(out_r) + if kwargs.get("stderr") is Subprocess.STREAM: + err_r, err_w = os.pipe() + kwargs["stderr"] = err_w + pipe_fds.extend((err_r, err_w)) + to_close.append(err_w) + self.stderr = PipeIOStream(err_r) + try: + self.proc = subprocess.Popen(*args, **kwargs) + except: + for fd in pipe_fds: + os.close(fd) + raise + for fd in to_close: + os.close(fd) + self.pid = self.proc.pid + for attr in ["stdin", "stdout", "stderr"]: + if not hasattr(self, attr): # don't clobber streams set above + setattr(self, attr, getattr(self.proc, attr)) + self._exit_callback = None # type: Optional[Callable[[int], None]] + self.returncode = None # type: Optional[int] + + def set_exit_callback(self, callback: Callable[[int], None]) -> None: + """Runs ``callback`` when this process exits. + + The callback takes one argument, the return code of the process. + + This method uses a ``SIGCHLD`` handler, which is a global setting + and may conflict if you have other libraries trying to handle the + same signal. If you are using more than one ``IOLoop`` it may + be necessary to call `Subprocess.initialize` first to designate + one ``IOLoop`` to run the signal handlers. + + In many cases a close callback on the stdout or stderr streams + can be used as an alternative to an exit callback if the + signal handler is causing a problem. + + Availability: Unix + """ + self._exit_callback = callback + Subprocess.initialize() + Subprocess._waiting[self.pid] = self + Subprocess._try_cleanup_process(self.pid) + + def wait_for_exit(self, raise_error: bool = True) -> "Future[int]": + """Returns a `.Future` which resolves when the process exits. + + Usage:: + + ret = yield proc.wait_for_exit() + + This is a coroutine-friendly alternative to `set_exit_callback` + (and a replacement for the blocking `subprocess.Popen.wait`). + + By default, raises `subprocess.CalledProcessError` if the process + has a non-zero exit status. Use ``wait_for_exit(raise_error=False)`` + to suppress this behavior and return the exit status without raising. + + .. versionadded:: 4.2 + + Availability: Unix + """ + future = Future() # type: Future[int] + + def callback(ret: int) -> None: + if ret != 0 and raise_error: + # Unfortunately we don't have the original args any more. + future_set_exception_unless_cancelled( + future, CalledProcessError(ret, "unknown") + ) + else: + future_set_result_unless_cancelled(future, ret) + + self.set_exit_callback(callback) + return future + + @classmethod + def initialize(cls) -> None: + """Initializes the ``SIGCHLD`` handler. + + The signal handler is run on an `.IOLoop` to avoid locking issues. + Note that the `.IOLoop` used for signal handling need not be the + same one used by individual Subprocess objects (as long as the + ``IOLoops`` are each running in separate threads). + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been + removed. + + Availability: Unix + """ + if cls._initialized: + return + io_loop = ioloop.IOLoop.current() + cls._old_sigchld = signal.signal( + signal.SIGCHLD, + lambda sig, frame: io_loop.add_callback_from_signal(cls._cleanup), + ) + cls._initialized = True + + @classmethod + def uninitialize(cls) -> None: + """Removes the ``SIGCHLD`` handler.""" + if not cls._initialized: + return + signal.signal(signal.SIGCHLD, cls._old_sigchld) + cls._initialized = False + + @classmethod + def _cleanup(cls) -> None: + for pid in list(cls._waiting.keys()): # make a copy + cls._try_cleanup_process(pid) + + @classmethod + def _try_cleanup_process(cls, pid: int) -> None: + try: + ret_pid, status = os.waitpid(pid, os.WNOHANG) # type: ignore + except ChildProcessError: + return + if ret_pid == 0: + return + assert ret_pid == pid + subproc = cls._waiting.pop(pid) + subproc.io_loop.add_callback_from_signal(subproc._set_returncode, status) + + def _set_returncode(self, status: int) -> None: + if sys.platform == "win32": + self.returncode = -1 + else: + if os.WIFSIGNALED(status): + self.returncode = -os.WTERMSIG(status) + else: + assert os.WIFEXITED(status) + self.returncode = os.WEXITSTATUS(status) + # We've taken over wait() duty from the subprocess.Popen + # object. If we don't inform it of the process's return code, + # it will log a warning at destruction in python 3.6+. + self.proc.returncode = self.returncode + if self._exit_callback: + callback = self._exit_callback + self._exit_callback = None + callback(self.returncode) diff --git a/telegramer/include/tornado/py.typed b/telegramer/include/tornado/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/telegramer/include/tornado/queues.py b/telegramer/include/tornado/queues.py new file mode 100644 index 0000000..1e87f62 --- /dev/null +++ b/telegramer/include/tornado/queues.py @@ -0,0 +1,414 @@ +# Copyright 2015 The Tornado Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Asynchronous queues for coroutines. These classes are very similar +to those provided in the standard library's `asyncio package +`_. + +.. warning:: + + Unlike the standard library's `queue` module, the classes defined here + are *not* thread-safe. To use these queues from another thread, + use `.IOLoop.add_callback` to transfer control to the `.IOLoop` thread + before calling any queue methods. + +""" + +import collections +import datetime +import heapq + +from tornado import gen, ioloop +from tornado.concurrent import Future, future_set_result_unless_cancelled +from tornado.locks import Event + +from typing import Union, TypeVar, Generic, Awaitable, Optional +import typing + +if typing.TYPE_CHECKING: + from typing import Deque, Tuple, Any # noqa: F401 + +_T = TypeVar("_T") + +__all__ = ["Queue", "PriorityQueue", "LifoQueue", "QueueFull", "QueueEmpty"] + + +class QueueEmpty(Exception): + """Raised by `.Queue.get_nowait` when the queue has no items.""" + + pass + + +class QueueFull(Exception): + """Raised by `.Queue.put_nowait` when a queue is at its maximum size.""" + + pass + + +def _set_timeout( + future: Future, timeout: Union[None, float, datetime.timedelta] +) -> None: + if timeout: + + def on_timeout() -> None: + if not future.done(): + future.set_exception(gen.TimeoutError()) + + io_loop = ioloop.IOLoop.current() + timeout_handle = io_loop.add_timeout(timeout, on_timeout) + future.add_done_callback(lambda _: io_loop.remove_timeout(timeout_handle)) + + +class _QueueIterator(Generic[_T]): + def __init__(self, q: "Queue[_T]") -> None: + self.q = q + + def __anext__(self) -> Awaitable[_T]: + return self.q.get() + + +class Queue(Generic[_T]): + """Coordinate producer and consumer coroutines. + + If maxsize is 0 (the default) the queue size is unbounded. + + .. testcode:: + + from tornado import gen + from tornado.ioloop import IOLoop + from tornado.queues import Queue + + q = Queue(maxsize=2) + + async def consumer(): + async for item in q: + try: + print('Doing work on %s' % item) + await gen.sleep(0.01) + finally: + q.task_done() + + async def producer(): + for item in range(5): + await q.put(item) + print('Put %s' % item) + + async def main(): + # Start consumer without waiting (since it never finishes). + IOLoop.current().spawn_callback(consumer) + await producer() # Wait for producer to put all tasks. + await q.join() # Wait for consumer to finish all tasks. + print('Done') + + IOLoop.current().run_sync(main) + + .. testoutput:: + + Put 0 + Put 1 + Doing work on 0 + Put 2 + Doing work on 1 + Put 3 + Doing work on 2 + Put 4 + Doing work on 3 + Doing work on 4 + Done + + + In versions of Python without native coroutines (before 3.5), + ``consumer()`` could be written as:: + + @gen.coroutine + def consumer(): + while True: + item = yield q.get() + try: + print('Doing work on %s' % item) + yield gen.sleep(0.01) + finally: + q.task_done() + + .. versionchanged:: 4.3 + Added ``async for`` support in Python 3.5. + + """ + + # Exact type depends on subclass. Could be another generic + # parameter and use protocols to be more precise here. + _queue = None # type: Any + + def __init__(self, maxsize: int = 0) -> None: + if maxsize is None: + raise TypeError("maxsize can't be None") + + if maxsize < 0: + raise ValueError("maxsize can't be negative") + + self._maxsize = maxsize + self._init() + self._getters = collections.deque([]) # type: Deque[Future[_T]] + self._putters = collections.deque([]) # type: Deque[Tuple[_T, Future[None]]] + self._unfinished_tasks = 0 + self._finished = Event() + self._finished.set() + + @property + def maxsize(self) -> int: + """Number of items allowed in the queue.""" + return self._maxsize + + def qsize(self) -> int: + """Number of items in the queue.""" + return len(self._queue) + + def empty(self) -> bool: + return not self._queue + + def full(self) -> bool: + if self.maxsize == 0: + return False + else: + return self.qsize() >= self.maxsize + + def put( + self, item: _T, timeout: Optional[Union[float, datetime.timedelta]] = None + ) -> "Future[None]": + """Put an item into the queue, perhaps waiting until there is room. + + Returns a Future, which raises `tornado.util.TimeoutError` after a + timeout. + + ``timeout`` may be a number denoting a time (on the same + scale as `tornado.ioloop.IOLoop.time`, normally `time.time`), or a + `datetime.timedelta` object for a deadline relative to the + current time. + """ + future = Future() # type: Future[None] + try: + self.put_nowait(item) + except QueueFull: + self._putters.append((item, future)) + _set_timeout(future, timeout) + else: + future.set_result(None) + return future + + def put_nowait(self, item: _T) -> None: + """Put an item into the queue without blocking. + + If no free slot is immediately available, raise `QueueFull`. + """ + self._consume_expired() + if self._getters: + assert self.empty(), "queue non-empty, why are getters waiting?" + getter = self._getters.popleft() + self.__put_internal(item) + future_set_result_unless_cancelled(getter, self._get()) + elif self.full(): + raise QueueFull + else: + self.__put_internal(item) + + def get( + self, timeout: Optional[Union[float, datetime.timedelta]] = None + ) -> Awaitable[_T]: + """Remove and return an item from the queue. + + Returns an awaitable which resolves once an item is available, or raises + `tornado.util.TimeoutError` after a timeout. + + ``timeout`` may be a number denoting a time (on the same + scale as `tornado.ioloop.IOLoop.time`, normally `time.time`), or a + `datetime.timedelta` object for a deadline relative to the + current time. + + .. note:: + + The ``timeout`` argument of this method differs from that + of the standard library's `queue.Queue.get`. That method + interprets numeric values as relative timeouts; this one + interprets them as absolute deadlines and requires + ``timedelta`` objects for relative timeouts (consistent + with other timeouts in Tornado). + + """ + future = Future() # type: Future[_T] + try: + future.set_result(self.get_nowait()) + except QueueEmpty: + self._getters.append(future) + _set_timeout(future, timeout) + return future + + def get_nowait(self) -> _T: + """Remove and return an item from the queue without blocking. + + Return an item if one is immediately available, else raise + `QueueEmpty`. + """ + self._consume_expired() + if self._putters: + assert self.full(), "queue not full, why are putters waiting?" + item, putter = self._putters.popleft() + self.__put_internal(item) + future_set_result_unless_cancelled(putter, None) + return self._get() + elif self.qsize(): + return self._get() + else: + raise QueueEmpty + + def task_done(self) -> None: + """Indicate that a formerly enqueued task is complete. + + Used by queue consumers. For each `.get` used to fetch a task, a + subsequent call to `.task_done` tells the queue that the processing + on the task is complete. + + If a `.join` is blocking, it resumes when all items have been + processed; that is, when every `.put` is matched by a `.task_done`. + + Raises `ValueError` if called more times than `.put`. + """ + if self._unfinished_tasks <= 0: + raise ValueError("task_done() called too many times") + self._unfinished_tasks -= 1 + if self._unfinished_tasks == 0: + self._finished.set() + + def join( + self, timeout: Optional[Union[float, datetime.timedelta]] = None + ) -> Awaitable[None]: + """Block until all items in the queue are processed. + + Returns an awaitable, which raises `tornado.util.TimeoutError` after a + timeout. + """ + return self._finished.wait(timeout) + + def __aiter__(self) -> _QueueIterator[_T]: + return _QueueIterator(self) + + # These three are overridable in subclasses. + def _init(self) -> None: + self._queue = collections.deque() + + def _get(self) -> _T: + return self._queue.popleft() + + def _put(self, item: _T) -> None: + self._queue.append(item) + + # End of the overridable methods. + + def __put_internal(self, item: _T) -> None: + self._unfinished_tasks += 1 + self._finished.clear() + self._put(item) + + def _consume_expired(self) -> None: + # Remove timed-out waiters. + while self._putters and self._putters[0][1].done(): + self._putters.popleft() + + while self._getters and self._getters[0].done(): + self._getters.popleft() + + def __repr__(self) -> str: + return "<%s at %s %s>" % (type(self).__name__, hex(id(self)), self._format()) + + def __str__(self) -> str: + return "<%s %s>" % (type(self).__name__, self._format()) + + def _format(self) -> str: + result = "maxsize=%r" % (self.maxsize,) + if getattr(self, "_queue", None): + result += " queue=%r" % self._queue + if self._getters: + result += " getters[%s]" % len(self._getters) + if self._putters: + result += " putters[%s]" % len(self._putters) + if self._unfinished_tasks: + result += " tasks=%s" % self._unfinished_tasks + return result + + +class PriorityQueue(Queue): + """A `.Queue` that retrieves entries in priority order, lowest first. + + Entries are typically tuples like ``(priority number, data)``. + + .. testcode:: + + from tornado.queues import PriorityQueue + + q = PriorityQueue() + q.put((1, 'medium-priority item')) + q.put((0, 'high-priority item')) + q.put((10, 'low-priority item')) + + print(q.get_nowait()) + print(q.get_nowait()) + print(q.get_nowait()) + + .. testoutput:: + + (0, 'high-priority item') + (1, 'medium-priority item') + (10, 'low-priority item') + """ + + def _init(self) -> None: + self._queue = [] + + def _put(self, item: _T) -> None: + heapq.heappush(self._queue, item) + + def _get(self) -> _T: + return heapq.heappop(self._queue) + + +class LifoQueue(Queue): + """A `.Queue` that retrieves the most recently put items first. + + .. testcode:: + + from tornado.queues import LifoQueue + + q = LifoQueue() + q.put(3) + q.put(2) + q.put(1) + + print(q.get_nowait()) + print(q.get_nowait()) + print(q.get_nowait()) + + .. testoutput:: + + 1 + 2 + 3 + """ + + def _init(self) -> None: + self._queue = [] + + def _put(self, item: _T) -> None: + self._queue.append(item) + + def _get(self) -> _T: + return self._queue.pop() diff --git a/telegramer/include/tornado/routing.py b/telegramer/include/tornado/routing.py new file mode 100644 index 0000000..a145d71 --- /dev/null +++ b/telegramer/include/tornado/routing.py @@ -0,0 +1,717 @@ +# Copyright 2015 The Tornado Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Flexible routing implementation. + +Tornado routes HTTP requests to appropriate handlers using `Router` +class implementations. The `tornado.web.Application` class is a +`Router` implementation and may be used directly, or the classes in +this module may be used for additional flexibility. The `RuleRouter` +class can match on more criteria than `.Application`, or the `Router` +interface can be subclassed for maximum customization. + +`Router` interface extends `~.httputil.HTTPServerConnectionDelegate` +to provide additional routing capabilities. This also means that any +`Router` implementation can be used directly as a ``request_callback`` +for `~.httpserver.HTTPServer` constructor. + +`Router` subclass must implement a ``find_handler`` method to provide +a suitable `~.httputil.HTTPMessageDelegate` instance to handle the +request: + +.. code-block:: python + + class CustomRouter(Router): + def find_handler(self, request, **kwargs): + # some routing logic providing a suitable HTTPMessageDelegate instance + return MessageDelegate(request.connection) + + class MessageDelegate(HTTPMessageDelegate): + def __init__(self, connection): + self.connection = connection + + def finish(self): + self.connection.write_headers( + ResponseStartLine("HTTP/1.1", 200, "OK"), + HTTPHeaders({"Content-Length": "2"}), + b"OK") + self.connection.finish() + + router = CustomRouter() + server = HTTPServer(router) + +The main responsibility of `Router` implementation is to provide a +mapping from a request to `~.httputil.HTTPMessageDelegate` instance +that will handle this request. In the example above we can see that +routing is possible even without instantiating an `~.web.Application`. + +For routing to `~.web.RequestHandler` implementations we need an +`~.web.Application` instance. `~.web.Application.get_handler_delegate` +provides a convenient way to create `~.httputil.HTTPMessageDelegate` +for a given request and `~.web.RequestHandler`. + +Here is a simple example of how we can we route to +`~.web.RequestHandler` subclasses by HTTP method: + +.. code-block:: python + + resources = {} + + class GetResource(RequestHandler): + def get(self, path): + if path not in resources: + raise HTTPError(404) + + self.finish(resources[path]) + + class PostResource(RequestHandler): + def post(self, path): + resources[path] = self.request.body + + class HTTPMethodRouter(Router): + def __init__(self, app): + self.app = app + + def find_handler(self, request, **kwargs): + handler = GetResource if request.method == "GET" else PostResource + return self.app.get_handler_delegate(request, handler, path_args=[request.path]) + + router = HTTPMethodRouter(Application()) + server = HTTPServer(router) + +`ReversibleRouter` interface adds the ability to distinguish between +the routes and reverse them to the original urls using route's name +and additional arguments. `~.web.Application` is itself an +implementation of `ReversibleRouter` class. + +`RuleRouter` and `ReversibleRuleRouter` are implementations of +`Router` and `ReversibleRouter` interfaces and can be used for +creating rule-based routing configurations. + +Rules are instances of `Rule` class. They contain a `Matcher`, which +provides the logic for determining whether the rule is a match for a +particular request and a target, which can be one of the following. + +1) An instance of `~.httputil.HTTPServerConnectionDelegate`: + +.. code-block:: python + + router = RuleRouter([ + Rule(PathMatches("/handler"), ConnectionDelegate()), + # ... more rules + ]) + + class ConnectionDelegate(HTTPServerConnectionDelegate): + def start_request(self, server_conn, request_conn): + return MessageDelegate(request_conn) + +2) A callable accepting a single argument of `~.httputil.HTTPServerRequest` type: + +.. code-block:: python + + router = RuleRouter([ + Rule(PathMatches("/callable"), request_callable) + ]) + + def request_callable(request): + request.write(b"HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nOK") + request.finish() + +3) Another `Router` instance: + +.. code-block:: python + + router = RuleRouter([ + Rule(PathMatches("/router.*"), CustomRouter()) + ]) + +Of course a nested `RuleRouter` or a `~.web.Application` is allowed: + +.. code-block:: python + + router = RuleRouter([ + Rule(HostMatches("example.com"), RuleRouter([ + Rule(PathMatches("/app1/.*"), Application([(r"/app1/handler", Handler)])), + ])) + ]) + + server = HTTPServer(router) + +In the example below `RuleRouter` is used to route between applications: + +.. code-block:: python + + app1 = Application([ + (r"/app1/handler", Handler1), + # other handlers ... + ]) + + app2 = Application([ + (r"/app2/handler", Handler2), + # other handlers ... + ]) + + router = RuleRouter([ + Rule(PathMatches("/app1.*"), app1), + Rule(PathMatches("/app2.*"), app2) + ]) + + server = HTTPServer(router) + +For more information on application-level routing see docs for `~.web.Application`. + +.. versionadded:: 4.5 + +""" + +import re +from functools import partial + +from tornado import httputil +from tornado.httpserver import _CallableAdapter +from tornado.escape import url_escape, url_unescape, utf8 +from tornado.log import app_log +from tornado.util import basestring_type, import_object, re_unescape, unicode_type + +from typing import Any, Union, Optional, Awaitable, List, Dict, Pattern, Tuple, overload + + +class Router(httputil.HTTPServerConnectionDelegate): + """Abstract router interface.""" + + def find_handler( + self, request: httputil.HTTPServerRequest, **kwargs: Any + ) -> Optional[httputil.HTTPMessageDelegate]: + """Must be implemented to return an appropriate instance of `~.httputil.HTTPMessageDelegate` + that can serve the request. + Routing implementations may pass additional kwargs to extend the routing logic. + + :arg httputil.HTTPServerRequest request: current HTTP request. + :arg kwargs: additional keyword arguments passed by routing implementation. + :returns: an instance of `~.httputil.HTTPMessageDelegate` that will be used to + process the request. + """ + raise NotImplementedError() + + def start_request( + self, server_conn: object, request_conn: httputil.HTTPConnection + ) -> httputil.HTTPMessageDelegate: + return _RoutingDelegate(self, server_conn, request_conn) + + +class ReversibleRouter(Router): + """Abstract router interface for routers that can handle named routes + and support reversing them to original urls. + """ + + def reverse_url(self, name: str, *args: Any) -> Optional[str]: + """Returns url string for a given route name and arguments + or ``None`` if no match is found. + + :arg str name: route name. + :arg args: url parameters. + :returns: parametrized url string for a given route name (or ``None``). + """ + raise NotImplementedError() + + +class _RoutingDelegate(httputil.HTTPMessageDelegate): + def __init__( + self, router: Router, server_conn: object, request_conn: httputil.HTTPConnection + ) -> None: + self.server_conn = server_conn + self.request_conn = request_conn + self.delegate = None # type: Optional[httputil.HTTPMessageDelegate] + self.router = router # type: Router + + def headers_received( + self, + start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine], + headers: httputil.HTTPHeaders, + ) -> Optional[Awaitable[None]]: + assert isinstance(start_line, httputil.RequestStartLine) + request = httputil.HTTPServerRequest( + connection=self.request_conn, + server_connection=self.server_conn, + start_line=start_line, + headers=headers, + ) + + self.delegate = self.router.find_handler(request) + if self.delegate is None: + app_log.debug( + "Delegate for %s %s request not found", + start_line.method, + start_line.path, + ) + self.delegate = _DefaultMessageDelegate(self.request_conn) + + return self.delegate.headers_received(start_line, headers) + + def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: + assert self.delegate is not None + return self.delegate.data_received(chunk) + + def finish(self) -> None: + assert self.delegate is not None + self.delegate.finish() + + def on_connection_close(self) -> None: + assert self.delegate is not None + self.delegate.on_connection_close() + + +class _DefaultMessageDelegate(httputil.HTTPMessageDelegate): + def __init__(self, connection: httputil.HTTPConnection) -> None: + self.connection = connection + + def finish(self) -> None: + self.connection.write_headers( + httputil.ResponseStartLine("HTTP/1.1", 404, "Not Found"), + httputil.HTTPHeaders(), + ) + self.connection.finish() + + +# _RuleList can either contain pre-constructed Rules or a sequence of +# arguments to be passed to the Rule constructor. +_RuleList = List[ + Union[ + "Rule", + List[Any], # Can't do detailed typechecking of lists. + Tuple[Union[str, "Matcher"], Any], + Tuple[Union[str, "Matcher"], Any, Dict[str, Any]], + Tuple[Union[str, "Matcher"], Any, Dict[str, Any], str], + ] +] + + +class RuleRouter(Router): + """Rule-based router implementation.""" + + def __init__(self, rules: Optional[_RuleList] = None) -> None: + """Constructs a router from an ordered list of rules:: + + RuleRouter([ + Rule(PathMatches("/handler"), Target), + # ... more rules + ]) + + You can also omit explicit `Rule` constructor and use tuples of arguments:: + + RuleRouter([ + (PathMatches("/handler"), Target), + ]) + + `PathMatches` is a default matcher, so the example above can be simplified:: + + RuleRouter([ + ("/handler", Target), + ]) + + In the examples above, ``Target`` can be a nested `Router` instance, an instance of + `~.httputil.HTTPServerConnectionDelegate` or an old-style callable, + accepting a request argument. + + :arg rules: a list of `Rule` instances or tuples of `Rule` + constructor arguments. + """ + self.rules = [] # type: List[Rule] + if rules: + self.add_rules(rules) + + def add_rules(self, rules: _RuleList) -> None: + """Appends new rules to the router. + + :arg rules: a list of Rule instances (or tuples of arguments, which are + passed to Rule constructor). + """ + for rule in rules: + if isinstance(rule, (tuple, list)): + assert len(rule) in (2, 3, 4) + if isinstance(rule[0], basestring_type): + rule = Rule(PathMatches(rule[0]), *rule[1:]) + else: + rule = Rule(*rule) + + self.rules.append(self.process_rule(rule)) + + def process_rule(self, rule: "Rule") -> "Rule": + """Override this method for additional preprocessing of each rule. + + :arg Rule rule: a rule to be processed. + :returns: the same or modified Rule instance. + """ + return rule + + def find_handler( + self, request: httputil.HTTPServerRequest, **kwargs: Any + ) -> Optional[httputil.HTTPMessageDelegate]: + for rule in self.rules: + target_params = rule.matcher.match(request) + if target_params is not None: + if rule.target_kwargs: + target_params["target_kwargs"] = rule.target_kwargs + + delegate = self.get_target_delegate( + rule.target, request, **target_params + ) + + if delegate is not None: + return delegate + + return None + + def get_target_delegate( + self, target: Any, request: httputil.HTTPServerRequest, **target_params: Any + ) -> Optional[httputil.HTTPMessageDelegate]: + """Returns an instance of `~.httputil.HTTPMessageDelegate` for a + Rule's target. This method is called by `~.find_handler` and can be + extended to provide additional target types. + + :arg target: a Rule's target. + :arg httputil.HTTPServerRequest request: current request. + :arg target_params: additional parameters that can be useful + for `~.httputil.HTTPMessageDelegate` creation. + """ + if isinstance(target, Router): + return target.find_handler(request, **target_params) + + elif isinstance(target, httputil.HTTPServerConnectionDelegate): + assert request.connection is not None + return target.start_request(request.server_connection, request.connection) + + elif callable(target): + assert request.connection is not None + return _CallableAdapter( + partial(target, **target_params), request.connection + ) + + return None + + +class ReversibleRuleRouter(ReversibleRouter, RuleRouter): + """A rule-based router that implements ``reverse_url`` method. + + Each rule added to this router may have a ``name`` attribute that can be + used to reconstruct an original uri. The actual reconstruction takes place + in a rule's matcher (see `Matcher.reverse`). + """ + + def __init__(self, rules: Optional[_RuleList] = None) -> None: + self.named_rules = {} # type: Dict[str, Any] + super().__init__(rules) + + def process_rule(self, rule: "Rule") -> "Rule": + rule = super().process_rule(rule) + + if rule.name: + if rule.name in self.named_rules: + app_log.warning( + "Multiple handlers named %s; replacing previous value", rule.name + ) + self.named_rules[rule.name] = rule + + return rule + + def reverse_url(self, name: str, *args: Any) -> Optional[str]: + if name in self.named_rules: + return self.named_rules[name].matcher.reverse(*args) + + for rule in self.rules: + if isinstance(rule.target, ReversibleRouter): + reversed_url = rule.target.reverse_url(name, *args) + if reversed_url is not None: + return reversed_url + + return None + + +class Rule(object): + """A routing rule.""" + + def __init__( + self, + matcher: "Matcher", + target: Any, + target_kwargs: Optional[Dict[str, Any]] = None, + name: Optional[str] = None, + ) -> None: + """Constructs a Rule instance. + + :arg Matcher matcher: a `Matcher` instance used for determining + whether the rule should be considered a match for a specific + request. + :arg target: a Rule's target (typically a ``RequestHandler`` or + `~.httputil.HTTPServerConnectionDelegate` subclass or even a nested `Router`, + depending on routing implementation). + :arg dict target_kwargs: a dict of parameters that can be useful + at the moment of target instantiation (for example, ``status_code`` + for a ``RequestHandler`` subclass). They end up in + ``target_params['target_kwargs']`` of `RuleRouter.get_target_delegate` + method. + :arg str name: the name of the rule that can be used to find it + in `ReversibleRouter.reverse_url` implementation. + """ + if isinstance(target, str): + # import the Module and instantiate the class + # Must be a fully qualified name (module.ClassName) + target = import_object(target) + + self.matcher = matcher # type: Matcher + self.target = target + self.target_kwargs = target_kwargs if target_kwargs else {} + self.name = name + + def reverse(self, *args: Any) -> Optional[str]: + return self.matcher.reverse(*args) + + def __repr__(self) -> str: + return "%s(%r, %s, kwargs=%r, name=%r)" % ( + self.__class__.__name__, + self.matcher, + self.target, + self.target_kwargs, + self.name, + ) + + +class Matcher(object): + """Represents a matcher for request features.""" + + def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]: + """Matches current instance against the request. + + :arg httputil.HTTPServerRequest request: current HTTP request + :returns: a dict of parameters to be passed to the target handler + (for example, ``handler_kwargs``, ``path_args``, ``path_kwargs`` + can be passed for proper `~.web.RequestHandler` instantiation). + An empty dict is a valid (and common) return value to indicate a match + when the argument-passing features are not used. + ``None`` must be returned to indicate that there is no match.""" + raise NotImplementedError() + + def reverse(self, *args: Any) -> Optional[str]: + """Reconstructs full url from matcher instance and additional arguments.""" + return None + + +class AnyMatches(Matcher): + """Matches any request.""" + + def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]: + return {} + + +class HostMatches(Matcher): + """Matches requests from hosts specified by ``host_pattern`` regex.""" + + def __init__(self, host_pattern: Union[str, Pattern]) -> None: + if isinstance(host_pattern, basestring_type): + if not host_pattern.endswith("$"): + host_pattern += "$" + self.host_pattern = re.compile(host_pattern) + else: + self.host_pattern = host_pattern + + def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]: + if self.host_pattern.match(request.host_name): + return {} + + return None + + +class DefaultHostMatches(Matcher): + """Matches requests from host that is equal to application's default_host. + Always returns no match if ``X-Real-Ip`` header is present. + """ + + def __init__(self, application: Any, host_pattern: Pattern) -> None: + self.application = application + self.host_pattern = host_pattern + + def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]: + # Look for default host if not behind load balancer (for debugging) + if "X-Real-Ip" not in request.headers: + if self.host_pattern.match(self.application.default_host): + return {} + return None + + +class PathMatches(Matcher): + """Matches requests with paths specified by ``path_pattern`` regex.""" + + def __init__(self, path_pattern: Union[str, Pattern]) -> None: + if isinstance(path_pattern, basestring_type): + if not path_pattern.endswith("$"): + path_pattern += "$" + self.regex = re.compile(path_pattern) + else: + self.regex = path_pattern + + assert len(self.regex.groupindex) in (0, self.regex.groups), ( + "groups in url regexes must either be all named or all " + "positional: %r" % self.regex.pattern + ) + + self._path, self._group_count = self._find_groups() + + def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]: + match = self.regex.match(request.path) + if match is None: + return None + if not self.regex.groups: + return {} + + path_args = [] # type: List[bytes] + path_kwargs = {} # type: Dict[str, bytes] + + # Pass matched groups to the handler. Since + # match.groups() includes both named and + # unnamed groups, we want to use either groups + # or groupdict but not both. + if self.regex.groupindex: + path_kwargs = dict( + (str(k), _unquote_or_none(v)) for (k, v) in match.groupdict().items() + ) + else: + path_args = [_unquote_or_none(s) for s in match.groups()] + + return dict(path_args=path_args, path_kwargs=path_kwargs) + + def reverse(self, *args: Any) -> Optional[str]: + if self._path is None: + raise ValueError("Cannot reverse url regex " + self.regex.pattern) + assert len(args) == self._group_count, ( + "required number of arguments " "not found" + ) + if not len(args): + return self._path + converted_args = [] + for a in args: + if not isinstance(a, (unicode_type, bytes)): + a = str(a) + converted_args.append(url_escape(utf8(a), plus=False)) + return self._path % tuple(converted_args) + + def _find_groups(self) -> Tuple[Optional[str], Optional[int]]: + """Returns a tuple (reverse string, group count) for a url. + + For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method + would return ('/%s/%s/', 2). + """ + pattern = self.regex.pattern + if pattern.startswith("^"): + pattern = pattern[1:] + if pattern.endswith("$"): + pattern = pattern[:-1] + + if self.regex.groups != pattern.count("("): + # The pattern is too complicated for our simplistic matching, + # so we can't support reversing it. + return None, None + + pieces = [] + for fragment in pattern.split("("): + if ")" in fragment: + paren_loc = fragment.index(")") + if paren_loc >= 0: + try: + unescaped_fragment = re_unescape(fragment[paren_loc + 1 :]) + except ValueError: + # If we can't unescape part of it, we can't + # reverse this url. + return (None, None) + pieces.append("%s" + unescaped_fragment) + else: + try: + unescaped_fragment = re_unescape(fragment) + except ValueError: + # If we can't unescape part of it, we can't + # reverse this url. + return (None, None) + pieces.append(unescaped_fragment) + + return "".join(pieces), self.regex.groups + + +class URLSpec(Rule): + """Specifies mappings between URLs and handlers. + + .. versionchanged: 4.5 + `URLSpec` is now a subclass of a `Rule` with `PathMatches` matcher and is preserved for + backwards compatibility. + """ + + def __init__( + self, + pattern: Union[str, Pattern], + handler: Any, + kwargs: Optional[Dict[str, Any]] = None, + name: Optional[str] = None, + ) -> None: + """Parameters: + + * ``pattern``: Regular expression to be matched. Any capturing + groups in the regex will be passed in to the handler's + get/post/etc methods as arguments (by keyword if named, by + position if unnamed. Named and unnamed capturing groups + may not be mixed in the same rule). + + * ``handler``: `~.web.RequestHandler` subclass to be invoked. + + * ``kwargs`` (optional): A dictionary of additional arguments + to be passed to the handler's constructor. + + * ``name`` (optional): A name for this handler. Used by + `~.web.Application.reverse_url`. + + """ + matcher = PathMatches(pattern) + super().__init__(matcher, handler, kwargs, name) + + self.regex = matcher.regex + self.handler_class = self.target + self.kwargs = kwargs + + def __repr__(self) -> str: + return "%s(%r, %s, kwargs=%r, name=%r)" % ( + self.__class__.__name__, + self.regex.pattern, + self.handler_class, + self.kwargs, + self.name, + ) + + +@overload +def _unquote_or_none(s: str) -> bytes: + pass + + +@overload # noqa: F811 +def _unquote_or_none(s: None) -> None: + pass + + +def _unquote_or_none(s: Optional[str]) -> Optional[bytes]: # noqa: F811 + """None-safe wrapper around url_unescape to handle unmatched optional + groups correctly. + + Note that args are passed as bytes so the handler can decide what + encoding to use. + """ + if s is None: + return s + return url_unescape(s, encoding=None, plus=False) diff --git a/telegramer/include/tornado/simple_httpclient.py b/telegramer/include/tornado/simple_httpclient.py new file mode 100644 index 0000000..f99f391 --- /dev/null +++ b/telegramer/include/tornado/simple_httpclient.py @@ -0,0 +1,699 @@ +from tornado.escape import _unicode +from tornado import gen, version +from tornado.httpclient import ( + HTTPResponse, + HTTPError, + AsyncHTTPClient, + main, + _RequestProxy, + HTTPRequest, +) +from tornado import httputil +from tornado.http1connection import HTTP1Connection, HTTP1ConnectionParameters +from tornado.ioloop import IOLoop +from tornado.iostream import StreamClosedError, IOStream +from tornado.netutil import ( + Resolver, + OverrideResolver, + _client_ssl_defaults, + is_valid_ip, +) +from tornado.log import gen_log +from tornado.tcpclient import TCPClient + +import base64 +import collections +import copy +import functools +import re +import socket +import ssl +import sys +import time +from io import BytesIO +import urllib.parse + +from typing import Dict, Any, Callable, Optional, Type, Union +from types import TracebackType +import typing + +if typing.TYPE_CHECKING: + from typing import Deque, Tuple, List # noqa: F401 + + +class HTTPTimeoutError(HTTPError): + """Error raised by SimpleAsyncHTTPClient on timeout. + + For historical reasons, this is a subclass of `.HTTPClientError` + which simulates a response code of 599. + + .. versionadded:: 5.1 + """ + + def __init__(self, message: str) -> None: + super().__init__(599, message=message) + + def __str__(self) -> str: + return self.message or "Timeout" + + +class HTTPStreamClosedError(HTTPError): + """Error raised by SimpleAsyncHTTPClient when the underlying stream is closed. + + When a more specific exception is available (such as `ConnectionResetError`), + it may be raised instead of this one. + + For historical reasons, this is a subclass of `.HTTPClientError` + which simulates a response code of 599. + + .. versionadded:: 5.1 + """ + + def __init__(self, message: str) -> None: + super().__init__(599, message=message) + + def __str__(self) -> str: + return self.message or "Stream closed" + + +class SimpleAsyncHTTPClient(AsyncHTTPClient): + """Non-blocking HTTP client with no external dependencies. + + This class implements an HTTP 1.1 client on top of Tornado's IOStreams. + Some features found in the curl-based AsyncHTTPClient are not yet + supported. In particular, proxies are not supported, connections + are not reused, and callers cannot select the network interface to be + used. + """ + + def initialize( # type: ignore + self, + max_clients: int = 10, + hostname_mapping: Optional[Dict[str, str]] = None, + max_buffer_size: int = 104857600, + resolver: Optional[Resolver] = None, + defaults: Optional[Dict[str, Any]] = None, + max_header_size: Optional[int] = None, + max_body_size: Optional[int] = None, + ) -> None: + """Creates a AsyncHTTPClient. + + Only a single AsyncHTTPClient instance exists per IOLoop + in order to provide limitations on the number of pending connections. + ``force_instance=True`` may be used to suppress this behavior. + + Note that because of this implicit reuse, unless ``force_instance`` + is used, only the first call to the constructor actually uses + its arguments. It is recommended to use the ``configure`` method + instead of the constructor to ensure that arguments take effect. + + ``max_clients`` is the number of concurrent requests that can be + in progress; when this limit is reached additional requests will be + queued. Note that time spent waiting in this queue still counts + against the ``request_timeout``. + + ``hostname_mapping`` is a dictionary mapping hostnames to IP addresses. + It can be used to make local DNS changes when modifying system-wide + settings like ``/etc/hosts`` is not possible or desirable (e.g. in + unittests). + + ``max_buffer_size`` (default 100MB) is the number of bytes + that can be read into memory at once. ``max_body_size`` + (defaults to ``max_buffer_size``) is the largest response body + that the client will accept. Without a + ``streaming_callback``, the smaller of these two limits + applies; with a ``streaming_callback`` only ``max_body_size`` + does. + + .. versionchanged:: 4.2 + Added the ``max_body_size`` argument. + """ + super().initialize(defaults=defaults) + self.max_clients = max_clients + self.queue = ( + collections.deque() + ) # type: Deque[Tuple[object, HTTPRequest, Callable[[HTTPResponse], None]]] + self.active = ( + {} + ) # type: Dict[object, Tuple[HTTPRequest, Callable[[HTTPResponse], None]]] + self.waiting = ( + {} + ) # type: Dict[object, Tuple[HTTPRequest, Callable[[HTTPResponse], None], object]] + self.max_buffer_size = max_buffer_size + self.max_header_size = max_header_size + self.max_body_size = max_body_size + # TCPClient could create a Resolver for us, but we have to do it + # ourselves to support hostname_mapping. + if resolver: + self.resolver = resolver + self.own_resolver = False + else: + self.resolver = Resolver() + self.own_resolver = True + if hostname_mapping is not None: + self.resolver = OverrideResolver( + resolver=self.resolver, mapping=hostname_mapping + ) + self.tcp_client = TCPClient(resolver=self.resolver) + + def close(self) -> None: + super().close() + if self.own_resolver: + self.resolver.close() + self.tcp_client.close() + + def fetch_impl( + self, request: HTTPRequest, callback: Callable[[HTTPResponse], None] + ) -> None: + key = object() + self.queue.append((key, request, callback)) + assert request.connect_timeout is not None + assert request.request_timeout is not None + timeout_handle = None + if len(self.active) >= self.max_clients: + timeout = ( + min(request.connect_timeout, request.request_timeout) + or request.connect_timeout + or request.request_timeout + ) # min but skip zero + if timeout: + timeout_handle = self.io_loop.add_timeout( + self.io_loop.time() + timeout, + functools.partial(self._on_timeout, key, "in request queue"), + ) + self.waiting[key] = (request, callback, timeout_handle) + self._process_queue() + if self.queue: + gen_log.debug( + "max_clients limit reached, request queued. " + "%d active, %d queued requests." % (len(self.active), len(self.queue)) + ) + + def _process_queue(self) -> None: + while self.queue and len(self.active) < self.max_clients: + key, request, callback = self.queue.popleft() + if key not in self.waiting: + continue + self._remove_timeout(key) + self.active[key] = (request, callback) + release_callback = functools.partial(self._release_fetch, key) + self._handle_request(request, release_callback, callback) + + def _connection_class(self) -> type: + return _HTTPConnection + + def _handle_request( + self, + request: HTTPRequest, + release_callback: Callable[[], None], + final_callback: Callable[[HTTPResponse], None], + ) -> None: + self._connection_class()( + self, + request, + release_callback, + final_callback, + self.max_buffer_size, + self.tcp_client, + self.max_header_size, + self.max_body_size, + ) + + def _release_fetch(self, key: object) -> None: + del self.active[key] + self._process_queue() + + def _remove_timeout(self, key: object) -> None: + if key in self.waiting: + request, callback, timeout_handle = self.waiting[key] + if timeout_handle is not None: + self.io_loop.remove_timeout(timeout_handle) + del self.waiting[key] + + def _on_timeout(self, key: object, info: Optional[str] = None) -> None: + """Timeout callback of request. + + Construct a timeout HTTPResponse when a timeout occurs. + + :arg object key: A simple object to mark the request. + :info string key: More detailed timeout information. + """ + request, callback, timeout_handle = self.waiting[key] + self.queue.remove((key, request, callback)) + + error_message = "Timeout {0}".format(info) if info else "Timeout" + timeout_response = HTTPResponse( + request, + 599, + error=HTTPTimeoutError(error_message), + request_time=self.io_loop.time() - request.start_time, + ) + self.io_loop.add_callback(callback, timeout_response) + del self.waiting[key] + + +class _HTTPConnection(httputil.HTTPMessageDelegate): + _SUPPORTED_METHODS = set( + ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] + ) + + def __init__( + self, + client: Optional[SimpleAsyncHTTPClient], + request: HTTPRequest, + release_callback: Callable[[], None], + final_callback: Callable[[HTTPResponse], None], + max_buffer_size: int, + tcp_client: TCPClient, + max_header_size: int, + max_body_size: int, + ) -> None: + self.io_loop = IOLoop.current() + self.start_time = self.io_loop.time() + self.start_wall_time = time.time() + self.client = client + self.request = request + self.release_callback = release_callback + self.final_callback = final_callback + self.max_buffer_size = max_buffer_size + self.tcp_client = tcp_client + self.max_header_size = max_header_size + self.max_body_size = max_body_size + self.code = None # type: Optional[int] + self.headers = None # type: Optional[httputil.HTTPHeaders] + self.chunks = [] # type: List[bytes] + self._decompressor = None + # Timeout handle returned by IOLoop.add_timeout + self._timeout = None # type: object + self._sockaddr = None + IOLoop.current().add_future( + gen.convert_yielded(self.run()), lambda f: f.result() + ) + + async def run(self) -> None: + try: + self.parsed = urllib.parse.urlsplit(_unicode(self.request.url)) + if self.parsed.scheme not in ("http", "https"): + raise ValueError("Unsupported url scheme: %s" % self.request.url) + # urlsplit results have hostname and port results, but they + # didn't support ipv6 literals until python 2.7. + netloc = self.parsed.netloc + if "@" in netloc: + userpass, _, netloc = netloc.rpartition("@") + host, port = httputil.split_host_and_port(netloc) + if port is None: + port = 443 if self.parsed.scheme == "https" else 80 + if re.match(r"^\[.*\]$", host): + # raw ipv6 addresses in urls are enclosed in brackets + host = host[1:-1] + self.parsed_hostname = host # save final host for _on_connect + + if self.request.allow_ipv6 is False: + af = socket.AF_INET + else: + af = socket.AF_UNSPEC + + ssl_options = self._get_ssl_options(self.parsed.scheme) + + source_ip = None + if self.request.network_interface: + if is_valid_ip(self.request.network_interface): + source_ip = self.request.network_interface + else: + raise ValueError( + "Unrecognized IPv4 or IPv6 address for network_interface, got %r" + % (self.request.network_interface,) + ) + + timeout = ( + min(self.request.connect_timeout, self.request.request_timeout) + or self.request.connect_timeout + or self.request.request_timeout + ) # min but skip zero + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + functools.partial(self._on_timeout, "while connecting"), + ) + stream = await self.tcp_client.connect( + host, + port, + af=af, + ssl_options=ssl_options, + max_buffer_size=self.max_buffer_size, + source_ip=source_ip, + ) + + if self.final_callback is None: + # final_callback is cleared if we've hit our timeout. + stream.close() + return + self.stream = stream + self.stream.set_close_callback(self.on_connection_close) + self._remove_timeout() + if self.final_callback is None: + return + if self.request.request_timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + self.request.request_timeout, + functools.partial(self._on_timeout, "during request"), + ) + if ( + self.request.method not in self._SUPPORTED_METHODS + and not self.request.allow_nonstandard_methods + ): + raise KeyError("unknown method %s" % self.request.method) + for key in ( + "proxy_host", + "proxy_port", + "proxy_username", + "proxy_password", + "proxy_auth_mode", + ): + if getattr(self.request, key, None): + raise NotImplementedError("%s not supported" % key) + if "Connection" not in self.request.headers: + self.request.headers["Connection"] = "close" + if "Host" not in self.request.headers: + if "@" in self.parsed.netloc: + self.request.headers["Host"] = self.parsed.netloc.rpartition("@")[ + -1 + ] + else: + self.request.headers["Host"] = self.parsed.netloc + username, password = None, None + if self.parsed.username is not None: + username, password = self.parsed.username, self.parsed.password + elif self.request.auth_username is not None: + username = self.request.auth_username + password = self.request.auth_password or "" + if username is not None: + assert password is not None + if self.request.auth_mode not in (None, "basic"): + raise ValueError("unsupported auth_mode %s", self.request.auth_mode) + self.request.headers["Authorization"] = "Basic " + _unicode( + base64.b64encode( + httputil.encode_username_password(username, password) + ) + ) + if self.request.user_agent: + self.request.headers["User-Agent"] = self.request.user_agent + elif self.request.headers.get("User-Agent") is None: + self.request.headers["User-Agent"] = "Tornado/{}".format(version) + if not self.request.allow_nonstandard_methods: + # Some HTTP methods nearly always have bodies while others + # almost never do. Fail in this case unless the user has + # opted out of sanity checks with allow_nonstandard_methods. + body_expected = self.request.method in ("POST", "PATCH", "PUT") + body_present = ( + self.request.body is not None + or self.request.body_producer is not None + ) + if (body_expected and not body_present) or ( + body_present and not body_expected + ): + raise ValueError( + "Body must %sbe None for method %s (unless " + "allow_nonstandard_methods is true)" + % ("not " if body_expected else "", self.request.method) + ) + if self.request.expect_100_continue: + self.request.headers["Expect"] = "100-continue" + if self.request.body is not None: + # When body_producer is used the caller is responsible for + # setting Content-Length (or else chunked encoding will be used). + self.request.headers["Content-Length"] = str(len(self.request.body)) + if ( + self.request.method == "POST" + and "Content-Type" not in self.request.headers + ): + self.request.headers[ + "Content-Type" + ] = "application/x-www-form-urlencoded" + if self.request.decompress_response: + self.request.headers["Accept-Encoding"] = "gzip" + req_path = (self.parsed.path or "/") + ( + ("?" + self.parsed.query) if self.parsed.query else "" + ) + self.connection = self._create_connection(stream) + start_line = httputil.RequestStartLine(self.request.method, req_path, "") + self.connection.write_headers(start_line, self.request.headers) + if self.request.expect_100_continue: + await self.connection.read_response(self) + else: + await self._write_body(True) + except Exception: + if not self._handle_exception(*sys.exc_info()): + raise + + def _get_ssl_options( + self, scheme: str + ) -> Union[None, Dict[str, Any], ssl.SSLContext]: + if scheme == "https": + if self.request.ssl_options is not None: + return self.request.ssl_options + # If we are using the defaults, don't construct a + # new SSLContext. + if ( + self.request.validate_cert + and self.request.ca_certs is None + and self.request.client_cert is None + and self.request.client_key is None + ): + return _client_ssl_defaults + ssl_ctx = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, cafile=self.request.ca_certs + ) + if not self.request.validate_cert: + ssl_ctx.check_hostname = False + ssl_ctx.verify_mode = ssl.CERT_NONE + if self.request.client_cert is not None: + ssl_ctx.load_cert_chain( + self.request.client_cert, self.request.client_key + ) + if hasattr(ssl, "OP_NO_COMPRESSION"): + # See netutil.ssl_options_to_context + ssl_ctx.options |= ssl.OP_NO_COMPRESSION + return ssl_ctx + return None + + def _on_timeout(self, info: Optional[str] = None) -> None: + """Timeout callback of _HTTPConnection instance. + + Raise a `HTTPTimeoutError` when a timeout occurs. + + :info string key: More detailed timeout information. + """ + self._timeout = None + error_message = "Timeout {0}".format(info) if info else "Timeout" + if self.final_callback is not None: + self._handle_exception( + HTTPTimeoutError, HTTPTimeoutError(error_message), None + ) + + def _remove_timeout(self) -> None: + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + self._timeout = None + + def _create_connection(self, stream: IOStream) -> HTTP1Connection: + stream.set_nodelay(True) + connection = HTTP1Connection( + stream, + True, + HTTP1ConnectionParameters( + no_keep_alive=True, + max_header_size=self.max_header_size, + max_body_size=self.max_body_size, + decompress=bool(self.request.decompress_response), + ), + self._sockaddr, + ) + return connection + + async def _write_body(self, start_read: bool) -> None: + if self.request.body is not None: + self.connection.write(self.request.body) + elif self.request.body_producer is not None: + fut = self.request.body_producer(self.connection.write) + if fut is not None: + await fut + self.connection.finish() + if start_read: + try: + await self.connection.read_response(self) + except StreamClosedError: + if not self._handle_exception(*sys.exc_info()): + raise + + def _release(self) -> None: + if self.release_callback is not None: + release_callback = self.release_callback + self.release_callback = None # type: ignore + release_callback() + + def _run_callback(self, response: HTTPResponse) -> None: + self._release() + if self.final_callback is not None: + final_callback = self.final_callback + self.final_callback = None # type: ignore + self.io_loop.add_callback(final_callback, response) + + def _handle_exception( + self, + typ: "Optional[Type[BaseException]]", + value: Optional[BaseException], + tb: Optional[TracebackType], + ) -> bool: + if self.final_callback: + self._remove_timeout() + if isinstance(value, StreamClosedError): + if value.real_error is None: + value = HTTPStreamClosedError("Stream closed") + else: + value = value.real_error + self._run_callback( + HTTPResponse( + self.request, + 599, + error=value, + request_time=self.io_loop.time() - self.start_time, + start_time=self.start_wall_time, + ) + ) + + if hasattr(self, "stream"): + # TODO: this may cause a StreamClosedError to be raised + # by the connection's Future. Should we cancel the + # connection more gracefully? + self.stream.close() + return True + else: + # If our callback has already been called, we are probably + # catching an exception that is not caused by us but rather + # some child of our callback. Rather than drop it on the floor, + # pass it along, unless it's just the stream being closed. + return isinstance(value, StreamClosedError) + + def on_connection_close(self) -> None: + if self.final_callback is not None: + message = "Connection closed" + if self.stream.error: + raise self.stream.error + try: + raise HTTPStreamClosedError(message) + except HTTPStreamClosedError: + self._handle_exception(*sys.exc_info()) + + async def headers_received( + self, + first_line: Union[httputil.ResponseStartLine, httputil.RequestStartLine], + headers: httputil.HTTPHeaders, + ) -> None: + assert isinstance(first_line, httputil.ResponseStartLine) + if self.request.expect_100_continue and first_line.code == 100: + await self._write_body(False) + return + self.code = first_line.code + self.reason = first_line.reason + self.headers = headers + + if self._should_follow_redirect(): + return + + if self.request.header_callback is not None: + # Reassemble the start line. + self.request.header_callback("%s %s %s\r\n" % first_line) + for k, v in self.headers.get_all(): + self.request.header_callback("%s: %s\r\n" % (k, v)) + self.request.header_callback("\r\n") + + def _should_follow_redirect(self) -> bool: + if self.request.follow_redirects: + assert self.request.max_redirects is not None + return ( + self.code in (301, 302, 303, 307, 308) + and self.request.max_redirects > 0 + and self.headers is not None + and self.headers.get("Location") is not None + ) + return False + + def finish(self) -> None: + assert self.code is not None + data = b"".join(self.chunks) + self._remove_timeout() + original_request = getattr(self.request, "original_request", self.request) + if self._should_follow_redirect(): + assert isinstance(self.request, _RequestProxy) + new_request = copy.copy(self.request.request) + new_request.url = urllib.parse.urljoin( + self.request.url, self.headers["Location"] + ) + new_request.max_redirects = self.request.max_redirects - 1 + del new_request.headers["Host"] + # https://tools.ietf.org/html/rfc7231#section-6.4 + # + # The original HTTP spec said that after a 301 or 302 + # redirect, the request method should be preserved. + # However, browsers implemented this by changing the + # method to GET, and the behavior stuck. 303 redirects + # always specified this POST-to-GET behavior, arguably + # for *all* methods, but libcurl < 7.70 only does this + # for POST, while libcurl >= 7.70 does it for other methods. + if (self.code == 303 and self.request.method != "HEAD") or ( + self.code in (301, 302) and self.request.method == "POST" + ): + new_request.method = "GET" + new_request.body = None + for h in [ + "Content-Length", + "Content-Type", + "Content-Encoding", + "Transfer-Encoding", + ]: + try: + del self.request.headers[h] + except KeyError: + pass + new_request.original_request = original_request + final_callback = self.final_callback + self.final_callback = None + self._release() + fut = self.client.fetch(new_request, raise_error=False) + fut.add_done_callback(lambda f: final_callback(f.result())) + self._on_end_request() + return + if self.request.streaming_callback: + buffer = BytesIO() + else: + buffer = BytesIO(data) # TODO: don't require one big string? + response = HTTPResponse( + original_request, + self.code, + reason=getattr(self, "reason", None), + headers=self.headers, + request_time=self.io_loop.time() - self.start_time, + start_time=self.start_wall_time, + buffer=buffer, + effective_url=self.request.url, + ) + self._run_callback(response) + self._on_end_request() + + def _on_end_request(self) -> None: + self.stream.close() + + def data_received(self, chunk: bytes) -> None: + if self._should_follow_redirect(): + # We're going to follow a redirect so just discard the body. + return + if self.request.streaming_callback is not None: + self.request.streaming_callback(chunk) + else: + self.chunks.append(chunk) + + +if __name__ == "__main__": + AsyncHTTPClient.configure(SimpleAsyncHTTPClient) + main() diff --git a/telegramer/include/tornado/speedups.c b/telegramer/include/tornado/speedups.c new file mode 100644 index 0000000..525d660 --- /dev/null +++ b/telegramer/include/tornado/speedups.c @@ -0,0 +1,70 @@ +#define PY_SSIZE_T_CLEAN +#include +#include + +static PyObject* websocket_mask(PyObject* self, PyObject* args) { + const char* mask; + Py_ssize_t mask_len; + uint32_t uint32_mask; + uint64_t uint64_mask; + const char* data; + Py_ssize_t data_len; + Py_ssize_t i; + PyObject* result; + char* buf; + + if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len)) { + return NULL; + } + + uint32_mask = ((uint32_t*)mask)[0]; + + result = PyBytes_FromStringAndSize(NULL, data_len); + if (!result) { + return NULL; + } + buf = PyBytes_AsString(result); + + if (sizeof(size_t) >= 8) { + uint64_mask = uint32_mask; + uint64_mask = (uint64_mask << 32) | uint32_mask; + + while (data_len >= 8) { + ((uint64_t*)buf)[0] = ((uint64_t*)data)[0] ^ uint64_mask; + data += 8; + buf += 8; + data_len -= 8; + } + } + + while (data_len >= 4) { + ((uint32_t*)buf)[0] = ((uint32_t*)data)[0] ^ uint32_mask; + data += 4; + buf += 4; + data_len -= 4; + } + + for (i = 0; i < data_len; i++) { + buf[i] = data[i] ^ mask[i]; + } + + return result; +} + +static PyMethodDef methods[] = { + {"websocket_mask", websocket_mask, METH_VARARGS, ""}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef speedupsmodule = { + PyModuleDef_HEAD_INIT, + "speedups", + NULL, + -1, + methods +}; + +PyMODINIT_FUNC +PyInit_speedups(void) { + return PyModule_Create(&speedupsmodule); +} diff --git a/telegramer/include/tornado/tcpclient.py b/telegramer/include/tornado/tcpclient.py new file mode 100644 index 0000000..e2d682e --- /dev/null +++ b/telegramer/include/tornado/tcpclient.py @@ -0,0 +1,328 @@ +# +# Copyright 2014 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A non-blocking TCP connection factory. +""" + +import functools +import socket +import numbers +import datetime +import ssl + +from tornado.concurrent import Future, future_add_done_callback +from tornado.ioloop import IOLoop +from tornado.iostream import IOStream +from tornado import gen +from tornado.netutil import Resolver +from tornado.gen import TimeoutError + +from typing import Any, Union, Dict, Tuple, List, Callable, Iterator, Optional, Set + +_INITIAL_CONNECT_TIMEOUT = 0.3 + + +class _Connector(object): + """A stateless implementation of the "Happy Eyeballs" algorithm. + + "Happy Eyeballs" is documented in RFC6555 as the recommended practice + for when both IPv4 and IPv6 addresses are available. + + In this implementation, we partition the addresses by family, and + make the first connection attempt to whichever address was + returned first by ``getaddrinfo``. If that connection fails or + times out, we begin a connection in parallel to the first address + of the other family. If there are additional failures we retry + with other addresses, keeping one connection attempt per family + in flight at a time. + + http://tools.ietf.org/html/rfc6555 + + """ + + def __init__( + self, + addrinfo: List[Tuple], + connect: Callable[ + [socket.AddressFamily, Tuple], Tuple[IOStream, "Future[IOStream]"] + ], + ) -> None: + self.io_loop = IOLoop.current() + self.connect = connect + + self.future = ( + Future() + ) # type: Future[Tuple[socket.AddressFamily, Any, IOStream]] + self.timeout = None # type: Optional[object] + self.connect_timeout = None # type: Optional[object] + self.last_error = None # type: Optional[Exception] + self.remaining = len(addrinfo) + self.primary_addrs, self.secondary_addrs = self.split(addrinfo) + self.streams = set() # type: Set[IOStream] + + @staticmethod + def split( + addrinfo: List[Tuple], + ) -> Tuple[ + List[Tuple[socket.AddressFamily, Tuple]], + List[Tuple[socket.AddressFamily, Tuple]], + ]: + """Partition the ``addrinfo`` list by address family. + + Returns two lists. The first list contains the first entry from + ``addrinfo`` and all others with the same family, and the + second list contains all other addresses (normally one list will + be AF_INET and the other AF_INET6, although non-standard resolvers + may return additional families). + """ + primary = [] + secondary = [] + primary_af = addrinfo[0][0] + for af, addr in addrinfo: + if af == primary_af: + primary.append((af, addr)) + else: + secondary.append((af, addr)) + return primary, secondary + + def start( + self, + timeout: float = _INITIAL_CONNECT_TIMEOUT, + connect_timeout: Optional[Union[float, datetime.timedelta]] = None, + ) -> "Future[Tuple[socket.AddressFamily, Any, IOStream]]": + self.try_connect(iter(self.primary_addrs)) + self.set_timeout(timeout) + if connect_timeout is not None: + self.set_connect_timeout(connect_timeout) + return self.future + + def try_connect(self, addrs: Iterator[Tuple[socket.AddressFamily, Tuple]]) -> None: + try: + af, addr = next(addrs) + except StopIteration: + # We've reached the end of our queue, but the other queue + # might still be working. Send a final error on the future + # only when both queues are finished. + if self.remaining == 0 and not self.future.done(): + self.future.set_exception( + self.last_error or IOError("connection failed") + ) + return + stream, future = self.connect(af, addr) + self.streams.add(stream) + future_add_done_callback( + future, functools.partial(self.on_connect_done, addrs, af, addr) + ) + + def on_connect_done( + self, + addrs: Iterator[Tuple[socket.AddressFamily, Tuple]], + af: socket.AddressFamily, + addr: Tuple, + future: "Future[IOStream]", + ) -> None: + self.remaining -= 1 + try: + stream = future.result() + except Exception as e: + if self.future.done(): + return + # Error: try again (but remember what happened so we have an + # error to raise in the end) + self.last_error = e + self.try_connect(addrs) + if self.timeout is not None: + # If the first attempt failed, don't wait for the + # timeout to try an address from the secondary queue. + self.io_loop.remove_timeout(self.timeout) + self.on_timeout() + return + self.clear_timeouts() + if self.future.done(): + # This is a late arrival; just drop it. + stream.close() + else: + self.streams.discard(stream) + self.future.set_result((af, addr, stream)) + self.close_streams() + + def set_timeout(self, timeout: float) -> None: + self.timeout = self.io_loop.add_timeout( + self.io_loop.time() + timeout, self.on_timeout + ) + + def on_timeout(self) -> None: + self.timeout = None + if not self.future.done(): + self.try_connect(iter(self.secondary_addrs)) + + def clear_timeout(self) -> None: + if self.timeout is not None: + self.io_loop.remove_timeout(self.timeout) + + def set_connect_timeout( + self, connect_timeout: Union[float, datetime.timedelta] + ) -> None: + self.connect_timeout = self.io_loop.add_timeout( + connect_timeout, self.on_connect_timeout + ) + + def on_connect_timeout(self) -> None: + if not self.future.done(): + self.future.set_exception(TimeoutError()) + self.close_streams() + + def clear_timeouts(self) -> None: + if self.timeout is not None: + self.io_loop.remove_timeout(self.timeout) + if self.connect_timeout is not None: + self.io_loop.remove_timeout(self.connect_timeout) + + def close_streams(self) -> None: + for stream in self.streams: + stream.close() + + +class TCPClient(object): + """A non-blocking TCP connection factory. + + .. versionchanged:: 5.0 + The ``io_loop`` argument (deprecated since version 4.1) has been removed. + """ + + def __init__(self, resolver: Optional[Resolver] = None) -> None: + if resolver is not None: + self.resolver = resolver + self._own_resolver = False + else: + self.resolver = Resolver() + self._own_resolver = True + + def close(self) -> None: + if self._own_resolver: + self.resolver.close() + + async def connect( + self, + host: str, + port: int, + af: socket.AddressFamily = socket.AF_UNSPEC, + ssl_options: Optional[Union[Dict[str, Any], ssl.SSLContext]] = None, + max_buffer_size: Optional[int] = None, + source_ip: Optional[str] = None, + source_port: Optional[int] = None, + timeout: Optional[Union[float, datetime.timedelta]] = None, + ) -> IOStream: + """Connect to the given host and port. + + Asynchronously returns an `.IOStream` (or `.SSLIOStream` if + ``ssl_options`` is not None). + + Using the ``source_ip`` kwarg, one can specify the source + IP address to use when establishing the connection. + In case the user needs to resolve and + use a specific interface, it has to be handled outside + of Tornado as this depends very much on the platform. + + Raises `TimeoutError` if the input future does not complete before + ``timeout``, which may be specified in any form allowed by + `.IOLoop.add_timeout` (i.e. a `datetime.timedelta` or an absolute time + relative to `.IOLoop.time`) + + Similarly, when the user requires a certain source port, it can + be specified using the ``source_port`` arg. + + .. versionchanged:: 4.5 + Added the ``source_ip`` and ``source_port`` arguments. + + .. versionchanged:: 5.0 + Added the ``timeout`` argument. + """ + if timeout is not None: + if isinstance(timeout, numbers.Real): + timeout = IOLoop.current().time() + timeout + elif isinstance(timeout, datetime.timedelta): + timeout = IOLoop.current().time() + timeout.total_seconds() + else: + raise TypeError("Unsupported timeout %r" % timeout) + if timeout is not None: + addrinfo = await gen.with_timeout( + timeout, self.resolver.resolve(host, port, af) + ) + else: + addrinfo = await self.resolver.resolve(host, port, af) + connector = _Connector( + addrinfo, + functools.partial( + self._create_stream, + max_buffer_size, + source_ip=source_ip, + source_port=source_port, + ), + ) + af, addr, stream = await connector.start(connect_timeout=timeout) + # TODO: For better performance we could cache the (af, addr) + # information here and re-use it on subsequent connections to + # the same host. (http://tools.ietf.org/html/rfc6555#section-4.2) + if ssl_options is not None: + if timeout is not None: + stream = await gen.with_timeout( + timeout, + stream.start_tls( + False, ssl_options=ssl_options, server_hostname=host + ), + ) + else: + stream = await stream.start_tls( + False, ssl_options=ssl_options, server_hostname=host + ) + return stream + + def _create_stream( + self, + max_buffer_size: int, + af: socket.AddressFamily, + addr: Tuple, + source_ip: Optional[str] = None, + source_port: Optional[int] = None, + ) -> Tuple[IOStream, "Future[IOStream]"]: + # Always connect in plaintext; we'll convert to ssl if necessary + # after one connection has completed. + source_port_bind = source_port if isinstance(source_port, int) else 0 + source_ip_bind = source_ip + if source_port_bind and not source_ip: + # User required a specific port, but did not specify + # a certain source IP, will bind to the default loopback. + source_ip_bind = "::1" if af == socket.AF_INET6 else "127.0.0.1" + # Trying to use the same address family as the requested af socket: + # - 127.0.0.1 for IPv4 + # - ::1 for IPv6 + socket_obj = socket.socket(af) + if source_port_bind or source_ip_bind: + # If the user requires binding also to a specific IP/port. + try: + socket_obj.bind((source_ip_bind, source_port_bind)) + except socket.error: + socket_obj.close() + # Fail loudly if unable to use the IP/port. + raise + try: + stream = IOStream(socket_obj, max_buffer_size=max_buffer_size) + except socket.error as e: + fu = Future() # type: Future[IOStream] + fu.set_exception(e) + return stream, fu + else: + return stream, stream.connect(addr) diff --git a/telegramer/include/tornado/tcpserver.py b/telegramer/include/tornado/tcpserver.py new file mode 100644 index 0000000..476ffc9 --- /dev/null +++ b/telegramer/include/tornado/tcpserver.py @@ -0,0 +1,334 @@ +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A non-blocking, single-threaded TCP server.""" + +import errno +import os +import socket +import ssl + +from tornado import gen +from tornado.log import app_log +from tornado.ioloop import IOLoop +from tornado.iostream import IOStream, SSLIOStream +from tornado.netutil import bind_sockets, add_accept_handler, ssl_wrap_socket +from tornado import process +from tornado.util import errno_from_exception + +import typing +from typing import Union, Dict, Any, Iterable, Optional, Awaitable + +if typing.TYPE_CHECKING: + from typing import Callable, List # noqa: F401 + + +class TCPServer(object): + r"""A non-blocking, single-threaded TCP server. + + To use `TCPServer`, define a subclass which overrides the `handle_stream` + method. For example, a simple echo server could be defined like this:: + + from tornado.tcpserver import TCPServer + from tornado.iostream import StreamClosedError + from tornado import gen + + class EchoServer(TCPServer): + async def handle_stream(self, stream, address): + while True: + try: + data = await stream.read_until(b"\n") + await stream.write(data) + except StreamClosedError: + break + + To make this server serve SSL traffic, send the ``ssl_options`` keyword + argument with an `ssl.SSLContext` object. For compatibility with older + versions of Python ``ssl_options`` may also be a dictionary of keyword + arguments for the `ssl.wrap_socket` method.:: + + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"), + os.path.join(data_dir, "mydomain.key")) + TCPServer(ssl_options=ssl_ctx) + + `TCPServer` initialization follows one of three patterns: + + 1. `listen`: simple single-process:: + + server = TCPServer() + server.listen(8888) + IOLoop.current().start() + + 2. `bind`/`start`: simple multi-process:: + + server = TCPServer() + server.bind(8888) + server.start(0) # Forks multiple sub-processes + IOLoop.current().start() + + When using this interface, an `.IOLoop` must *not* be passed + to the `TCPServer` constructor. `start` will always start + the server on the default singleton `.IOLoop`. + + 3. `add_sockets`: advanced multi-process:: + + sockets = bind_sockets(8888) + tornado.process.fork_processes(0) + server = TCPServer() + server.add_sockets(sockets) + IOLoop.current().start() + + The `add_sockets` interface is more complicated, but it can be + used with `tornado.process.fork_processes` to give you more + flexibility in when the fork happens. `add_sockets` can + also be used in single-process servers if you want to create + your listening sockets in some way other than + `~tornado.netutil.bind_sockets`. + + .. versionadded:: 3.1 + The ``max_buffer_size`` argument. + + .. versionchanged:: 5.0 + The ``io_loop`` argument has been removed. + """ + + def __init__( + self, + ssl_options: Optional[Union[Dict[str, Any], ssl.SSLContext]] = None, + max_buffer_size: Optional[int] = None, + read_chunk_size: Optional[int] = None, + ) -> None: + self.ssl_options = ssl_options + self._sockets = {} # type: Dict[int, socket.socket] + self._handlers = {} # type: Dict[int, Callable[[], None]] + self._pending_sockets = [] # type: List[socket.socket] + self._started = False + self._stopped = False + self.max_buffer_size = max_buffer_size + self.read_chunk_size = read_chunk_size + + # Verify the SSL options. Otherwise we don't get errors until clients + # connect. This doesn't verify that the keys are legitimate, but + # the SSL module doesn't do that until there is a connected socket + # which seems like too much work + if self.ssl_options is not None and isinstance(self.ssl_options, dict): + # Only certfile is required: it can contain both keys + if "certfile" not in self.ssl_options: + raise KeyError('missing key "certfile" in ssl_options') + + if not os.path.exists(self.ssl_options["certfile"]): + raise ValueError( + 'certfile "%s" does not exist' % self.ssl_options["certfile"] + ) + if "keyfile" in self.ssl_options and not os.path.exists( + self.ssl_options["keyfile"] + ): + raise ValueError( + 'keyfile "%s" does not exist' % self.ssl_options["keyfile"] + ) + + def listen(self, port: int, address: str = "") -> None: + """Starts accepting connections on the given port. + + This method may be called more than once to listen on multiple ports. + `listen` takes effect immediately; it is not necessary to call + `TCPServer.start` afterwards. It is, however, necessary to start + the `.IOLoop`. + """ + sockets = bind_sockets(port, address=address) + self.add_sockets(sockets) + + def add_sockets(self, sockets: Iterable[socket.socket]) -> None: + """Makes this server start accepting connections on the given sockets. + + The ``sockets`` parameter is a list of socket objects such as + those returned by `~tornado.netutil.bind_sockets`. + `add_sockets` is typically used in combination with that + method and `tornado.process.fork_processes` to provide greater + control over the initialization of a multi-process server. + """ + for sock in sockets: + self._sockets[sock.fileno()] = sock + self._handlers[sock.fileno()] = add_accept_handler( + sock, self._handle_connection + ) + + def add_socket(self, socket: socket.socket) -> None: + """Singular version of `add_sockets`. Takes a single socket object.""" + self.add_sockets([socket]) + + def bind( + self, + port: int, + address: Optional[str] = None, + family: socket.AddressFamily = socket.AF_UNSPEC, + backlog: int = 128, + reuse_port: bool = False, + ) -> None: + """Binds this server to the given port on the given address. + + To start the server, call `start`. If you want to run this server + in a single process, you can call `listen` as a shortcut to the + sequence of `bind` and `start` calls. + + Address may be either an IP address or hostname. If it's a hostname, + the server will listen on all IP addresses associated with the + name. Address may be an empty string or None to listen on all + available interfaces. Family may be set to either `socket.AF_INET` + or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise + both will be used if available. + + The ``backlog`` argument has the same meaning as for + `socket.listen `. The ``reuse_port`` argument + has the same meaning as for `.bind_sockets`. + + This method may be called multiple times prior to `start` to listen + on multiple ports or interfaces. + + .. versionchanged:: 4.4 + Added the ``reuse_port`` argument. + """ + sockets = bind_sockets( + port, address=address, family=family, backlog=backlog, reuse_port=reuse_port + ) + if self._started: + self.add_sockets(sockets) + else: + self._pending_sockets.extend(sockets) + + def start( + self, num_processes: Optional[int] = 1, max_restarts: Optional[int] = None + ) -> None: + """Starts this server in the `.IOLoop`. + + By default, we run the server in this process and do not fork any + additional child process. + + If num_processes is ``None`` or <= 0, we detect the number of cores + available on this machine and fork that number of child + processes. If num_processes is given and > 1, we fork that + specific number of sub-processes. + + Since we use processes and not threads, there is no shared memory + between any server code. + + Note that multiple processes are not compatible with the autoreload + module (or the ``autoreload=True`` option to `tornado.web.Application` + which defaults to True when ``debug=True``). + When using multiple processes, no IOLoops can be created or + referenced until after the call to ``TCPServer.start(n)``. + + Values of ``num_processes`` other than 1 are not supported on Windows. + + The ``max_restarts`` argument is passed to `.fork_processes`. + + .. versionchanged:: 6.0 + + Added ``max_restarts`` argument. + """ + assert not self._started + self._started = True + if num_processes != 1: + process.fork_processes(num_processes, max_restarts) + sockets = self._pending_sockets + self._pending_sockets = [] + self.add_sockets(sockets) + + def stop(self) -> None: + """Stops listening for new connections. + + Requests currently in progress may still continue after the + server is stopped. + """ + if self._stopped: + return + self._stopped = True + for fd, sock in self._sockets.items(): + assert sock.fileno() == fd + # Unregister socket from IOLoop + self._handlers.pop(fd)() + sock.close() + + def handle_stream( + self, stream: IOStream, address: tuple + ) -> Optional[Awaitable[None]]: + """Override to handle a new `.IOStream` from an incoming connection. + + This method may be a coroutine; if so any exceptions it raises + asynchronously will be logged. Accepting of incoming connections + will not be blocked by this coroutine. + + If this `TCPServer` is configured for SSL, ``handle_stream`` + may be called before the SSL handshake has completed. Use + `.SSLIOStream.wait_for_handshake` if you need to verify the client's + certificate or use NPN/ALPN. + + .. versionchanged:: 4.2 + Added the option for this method to be a coroutine. + """ + raise NotImplementedError() + + def _handle_connection(self, connection: socket.socket, address: Any) -> None: + if self.ssl_options is not None: + assert ssl, "Python 2.6+ and OpenSSL required for SSL" + try: + connection = ssl_wrap_socket( + connection, + self.ssl_options, + server_side=True, + do_handshake_on_connect=False, + ) + except ssl.SSLError as err: + if err.args[0] == ssl.SSL_ERROR_EOF: + return connection.close() + else: + raise + except socket.error as err: + # If the connection is closed immediately after it is created + # (as in a port scan), we can get one of several errors. + # wrap_socket makes an internal call to getpeername, + # which may return either EINVAL (Mac OS X) or ENOTCONN + # (Linux). If it returns ENOTCONN, this error is + # silently swallowed by the ssl module, so we need to + # catch another error later on (AttributeError in + # SSLIOStream._do_ssl_handshake). + # To test this behavior, try nmap with the -sT flag. + # https://github.com/tornadoweb/tornado/pull/750 + if errno_from_exception(err) in (errno.ECONNABORTED, errno.EINVAL): + return connection.close() + else: + raise + try: + if self.ssl_options is not None: + stream = SSLIOStream( + connection, + max_buffer_size=self.max_buffer_size, + read_chunk_size=self.read_chunk_size, + ) # type: IOStream + else: + stream = IOStream( + connection, + max_buffer_size=self.max_buffer_size, + read_chunk_size=self.read_chunk_size, + ) + + future = self.handle_stream(stream, address) + if future is not None: + IOLoop.current().add_future( + gen.convert_yielded(future), lambda f: f.result() + ) + except Exception: + app_log.error("Error in connection callback", exc_info=True) diff --git a/telegramer/include/tornado/template.py b/telegramer/include/tornado/template.py new file mode 100644 index 0000000..2e6e0a2 --- /dev/null +++ b/telegramer/include/tornado/template.py @@ -0,0 +1,1048 @@ +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A simple template system that compiles templates to Python code. + +Basic usage looks like:: + + t = template.Template("{{ myvalue }}") + print(t.generate(myvalue="XXX")) + +`Loader` is a class that loads templates from a root directory and caches +the compiled templates:: + + loader = template.Loader("/home/btaylor") + print(loader.load("test.html").generate(myvalue="XXX")) + +We compile all templates to raw Python. Error-reporting is currently... uh, +interesting. Syntax for the templates:: + + ### base.html + + + {% block title %}Default title{% end %} + + +
    + {% for student in students %} + {% block student %} +
  • {{ escape(student.name) }}
  • + {% end %} + {% end %} +
+ + + + ### bold.html + {% extends "base.html" %} + + {% block title %}A bolder title{% end %} + + {% block student %} +
  • {{ escape(student.name) }}
  • + {% end %} + +Unlike most other template systems, we do not put any restrictions on the +expressions you can include in your statements. ``if`` and ``for`` blocks get +translated exactly into Python, so you can do complex expressions like:: + + {% for student in [p for p in people if p.student and p.age > 23] %} +
  • {{ escape(student.name) }}
  • + {% end %} + +Translating directly to Python means you can apply functions to expressions +easily, like the ``escape()`` function in the examples above. You can pass +functions in to your template just like any other variable +(In a `.RequestHandler`, override `.RequestHandler.get_template_namespace`):: + + ### Python code + def add(x, y): + return x + y + template.execute(add=add) + + ### The template + {{ add(1, 2) }} + +We provide the functions `escape() <.xhtml_escape>`, `.url_escape()`, +`.json_encode()`, and `.squeeze()` to all templates by default. + +Typical applications do not create `Template` or `Loader` instances by +hand, but instead use the `~.RequestHandler.render` and +`~.RequestHandler.render_string` methods of +`tornado.web.RequestHandler`, which load templates automatically based +on the ``template_path`` `.Application` setting. + +Variable names beginning with ``_tt_`` are reserved by the template +system and should not be used by application code. + +Syntax Reference +---------------- + +Template expressions are surrounded by double curly braces: ``{{ ... }}``. +The contents may be any python expression, which will be escaped according +to the current autoescape setting and inserted into the output. Other +template directives use ``{% %}``. + +To comment out a section so that it is omitted from the output, surround it +with ``{# ... #}``. + + +To include a literal ``{{``, ``{%``, or ``{#`` in the output, escape them as +``{{!``, ``{%!``, and ``{#!``, respectively. + + +``{% apply *function* %}...{% end %}`` + Applies a function to the output of all template code between ``apply`` + and ``end``:: + + {% apply linkify %}{{name}} said: {{message}}{% end %} + + Note that as an implementation detail apply blocks are implemented + as nested functions and thus may interact strangely with variables + set via ``{% set %}``, or the use of ``{% break %}`` or ``{% continue %}`` + within loops. + +``{% autoescape *function* %}`` + Sets the autoescape mode for the current file. This does not affect + other files, even those referenced by ``{% include %}``. Note that + autoescaping can also be configured globally, at the `.Application` + or `Loader`.:: + + {% autoescape xhtml_escape %} + {% autoescape None %} + +``{% block *name* %}...{% end %}`` + Indicates a named, replaceable block for use with ``{% extends %}``. + Blocks in the parent template will be replaced with the contents of + the same-named block in a child template.:: + + + {% block title %}Default title{% end %} + + + {% extends "base.html" %} + {% block title %}My page title{% end %} + +``{% comment ... %}`` + A comment which will be removed from the template output. Note that + there is no ``{% end %}`` tag; the comment goes from the word ``comment`` + to the closing ``%}`` tag. + +``{% extends *filename* %}`` + Inherit from another template. Templates that use ``extends`` should + contain one or more ``block`` tags to replace content from the parent + template. Anything in the child template not contained in a ``block`` + tag will be ignored. For an example, see the ``{% block %}`` tag. + +``{% for *var* in *expr* %}...{% end %}`` + Same as the python ``for`` statement. ``{% break %}`` and + ``{% continue %}`` may be used inside the loop. + +``{% from *x* import *y* %}`` + Same as the python ``import`` statement. + +``{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}`` + Conditional statement - outputs the first section whose condition is + true. (The ``elif`` and ``else`` sections are optional) + +``{% import *module* %}`` + Same as the python ``import`` statement. + +``{% include *filename* %}`` + Includes another template file. The included file can see all the local + variables as if it were copied directly to the point of the ``include`` + directive (the ``{% autoescape %}`` directive is an exception). + Alternately, ``{% module Template(filename, **kwargs) %}`` may be used + to include another template with an isolated namespace. + +``{% module *expr* %}`` + Renders a `~tornado.web.UIModule`. The output of the ``UIModule`` is + not escaped:: + + {% module Template("foo.html", arg=42) %} + + ``UIModules`` are a feature of the `tornado.web.RequestHandler` + class (and specifically its ``render`` method) and will not work + when the template system is used on its own in other contexts. + +``{% raw *expr* %}`` + Outputs the result of the given expression without autoescaping. + +``{% set *x* = *y* %}`` + Sets a local variable. + +``{% try %}...{% except %}...{% else %}...{% finally %}...{% end %}`` + Same as the python ``try`` statement. + +``{% while *condition* %}... {% end %}`` + Same as the python ``while`` statement. ``{% break %}`` and + ``{% continue %}`` may be used inside the loop. + +``{% whitespace *mode* %}`` + Sets the whitespace mode for the remainder of the current file + (or until the next ``{% whitespace %}`` directive). See + `filter_whitespace` for available options. New in Tornado 4.3. +""" + +import datetime +from io import StringIO +import linecache +import os.path +import posixpath +import re +import threading + +from tornado import escape +from tornado.log import app_log +from tornado.util import ObjectDict, exec_in, unicode_type + +from typing import Any, Union, Callable, List, Dict, Iterable, Optional, TextIO +import typing + +if typing.TYPE_CHECKING: + from typing import Tuple, ContextManager # noqa: F401 + +_DEFAULT_AUTOESCAPE = "xhtml_escape" + + +class _UnsetMarker: + pass + + +_UNSET = _UnsetMarker() + + +def filter_whitespace(mode: str, text: str) -> str: + """Transform whitespace in ``text`` according to ``mode``. + + Available modes are: + + * ``all``: Return all whitespace unmodified. + * ``single``: Collapse consecutive whitespace with a single whitespace + character, preserving newlines. + * ``oneline``: Collapse all runs of whitespace into a single space + character, removing all newlines in the process. + + .. versionadded:: 4.3 + """ + if mode == "all": + return text + elif mode == "single": + text = re.sub(r"([\t ]+)", " ", text) + text = re.sub(r"(\s*\n\s*)", "\n", text) + return text + elif mode == "oneline": + return re.sub(r"(\s+)", " ", text) + else: + raise Exception("invalid whitespace mode %s" % mode) + + +class Template(object): + """A compiled template. + + We compile into Python from the given template_string. You can generate + the template from variables with generate(). + """ + + # note that the constructor's signature is not extracted with + # autodoc because _UNSET looks like garbage. When changing + # this signature update website/sphinx/template.rst too. + def __init__( + self, + template_string: Union[str, bytes], + name: str = "", + loader: Optional["BaseLoader"] = None, + compress_whitespace: Union[bool, _UnsetMarker] = _UNSET, + autoescape: Optional[Union[str, _UnsetMarker]] = _UNSET, + whitespace: Optional[str] = None, + ) -> None: + """Construct a Template. + + :arg str template_string: the contents of the template file. + :arg str name: the filename from which the template was loaded + (used for error message). + :arg tornado.template.BaseLoader loader: the `~tornado.template.BaseLoader` responsible + for this template, used to resolve ``{% include %}`` and ``{% extend %}`` directives. + :arg bool compress_whitespace: Deprecated since Tornado 4.3. + Equivalent to ``whitespace="single"`` if true and + ``whitespace="all"`` if false. + :arg str autoescape: The name of a function in the template + namespace, or ``None`` to disable escaping by default. + :arg str whitespace: A string specifying treatment of whitespace; + see `filter_whitespace` for options. + + .. versionchanged:: 4.3 + Added ``whitespace`` parameter; deprecated ``compress_whitespace``. + """ + self.name = escape.native_str(name) + + if compress_whitespace is not _UNSET: + # Convert deprecated compress_whitespace (bool) to whitespace (str). + if whitespace is not None: + raise Exception("cannot set both whitespace and compress_whitespace") + whitespace = "single" if compress_whitespace else "all" + if whitespace is None: + if loader and loader.whitespace: + whitespace = loader.whitespace + else: + # Whitespace defaults by filename. + if name.endswith(".html") or name.endswith(".js"): + whitespace = "single" + else: + whitespace = "all" + # Validate the whitespace setting. + assert whitespace is not None + filter_whitespace(whitespace, "") + + if not isinstance(autoescape, _UnsetMarker): + self.autoescape = autoescape # type: Optional[str] + elif loader: + self.autoescape = loader.autoescape + else: + self.autoescape = _DEFAULT_AUTOESCAPE + + self.namespace = loader.namespace if loader else {} + reader = _TemplateReader(name, escape.native_str(template_string), whitespace) + self.file = _File(self, _parse(reader, self)) + self.code = self._generate_python(loader) + self.loader = loader + try: + # Under python2.5, the fake filename used here must match + # the module name used in __name__ below. + # The dont_inherit flag prevents template.py's future imports + # from being applied to the generated code. + self.compiled = compile( + escape.to_unicode(self.code), + "%s.generated.py" % self.name.replace(".", "_"), + "exec", + dont_inherit=True, + ) + except Exception: + formatted_code = _format_code(self.code).rstrip() + app_log.error("%s code:\n%s", self.name, formatted_code) + raise + + def generate(self, **kwargs: Any) -> bytes: + """Generate this template with the given arguments.""" + namespace = { + "escape": escape.xhtml_escape, + "xhtml_escape": escape.xhtml_escape, + "url_escape": escape.url_escape, + "json_encode": escape.json_encode, + "squeeze": escape.squeeze, + "linkify": escape.linkify, + "datetime": datetime, + "_tt_utf8": escape.utf8, # for internal use + "_tt_string_types": (unicode_type, bytes), + # __name__ and __loader__ allow the traceback mechanism to find + # the generated source code. + "__name__": self.name.replace(".", "_"), + "__loader__": ObjectDict(get_source=lambda name: self.code), + } + namespace.update(self.namespace) + namespace.update(kwargs) + exec_in(self.compiled, namespace) + execute = typing.cast(Callable[[], bytes], namespace["_tt_execute"]) + # Clear the traceback module's cache of source data now that + # we've generated a new template (mainly for this module's + # unittests, where different tests reuse the same name). + linecache.clearcache() + return execute() + + def _generate_python(self, loader: Optional["BaseLoader"]) -> str: + buffer = StringIO() + try: + # named_blocks maps from names to _NamedBlock objects + named_blocks = {} # type: Dict[str, _NamedBlock] + ancestors = self._get_ancestors(loader) + ancestors.reverse() + for ancestor in ancestors: + ancestor.find_named_blocks(loader, named_blocks) + writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template) + ancestors[0].generate(writer) + return buffer.getvalue() + finally: + buffer.close() + + def _get_ancestors(self, loader: Optional["BaseLoader"]) -> List["_File"]: + ancestors = [self.file] + for chunk in self.file.body.chunks: + if isinstance(chunk, _ExtendsBlock): + if not loader: + raise ParseError( + "{% extends %} block found, but no " "template loader" + ) + template = loader.load(chunk.name, self.name) + ancestors.extend(template._get_ancestors(loader)) + return ancestors + + +class BaseLoader(object): + """Base class for template loaders. + + You must use a template loader to use template constructs like + ``{% extends %}`` and ``{% include %}``. The loader caches all + templates after they are loaded the first time. + """ + + def __init__( + self, + autoescape: str = _DEFAULT_AUTOESCAPE, + namespace: Optional[Dict[str, Any]] = None, + whitespace: Optional[str] = None, + ) -> None: + """Construct a template loader. + + :arg str autoescape: The name of a function in the template + namespace, such as "xhtml_escape", or ``None`` to disable + autoescaping by default. + :arg dict namespace: A dictionary to be added to the default template + namespace, or ``None``. + :arg str whitespace: A string specifying default behavior for + whitespace in templates; see `filter_whitespace` for options. + Default is "single" for files ending in ".html" and ".js" and + "all" for other files. + + .. versionchanged:: 4.3 + Added ``whitespace`` parameter. + """ + self.autoescape = autoescape + self.namespace = namespace or {} + self.whitespace = whitespace + self.templates = {} # type: Dict[str, Template] + # self.lock protects self.templates. It's a reentrant lock + # because templates may load other templates via `include` or + # `extends`. Note that thanks to the GIL this code would be safe + # even without the lock, but could lead to wasted work as multiple + # threads tried to compile the same template simultaneously. + self.lock = threading.RLock() + + def reset(self) -> None: + """Resets the cache of compiled templates.""" + with self.lock: + self.templates = {} + + def resolve_path(self, name: str, parent_path: Optional[str] = None) -> str: + """Converts a possibly-relative path to absolute (used internally).""" + raise NotImplementedError() + + def load(self, name: str, parent_path: Optional[str] = None) -> Template: + """Loads a template.""" + name = self.resolve_path(name, parent_path=parent_path) + with self.lock: + if name not in self.templates: + self.templates[name] = self._create_template(name) + return self.templates[name] + + def _create_template(self, name: str) -> Template: + raise NotImplementedError() + + +class Loader(BaseLoader): + """A template loader that loads from a single root directory. + """ + + def __init__(self, root_directory: str, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.root = os.path.abspath(root_directory) + + def resolve_path(self, name: str, parent_path: Optional[str] = None) -> str: + if ( + parent_path + and not parent_path.startswith("<") + and not parent_path.startswith("/") + and not name.startswith("/") + ): + current_path = os.path.join(self.root, parent_path) + file_dir = os.path.dirname(os.path.abspath(current_path)) + relative_path = os.path.abspath(os.path.join(file_dir, name)) + if relative_path.startswith(self.root): + name = relative_path[len(self.root) + 1 :] + return name + + def _create_template(self, name: str) -> Template: + path = os.path.join(self.root, name) + with open(path, "rb") as f: + template = Template(f.read(), name=name, loader=self) + return template + + +class DictLoader(BaseLoader): + """A template loader that loads from a dictionary.""" + + def __init__(self, dict: Dict[str, str], **kwargs: Any) -> None: + super().__init__(**kwargs) + self.dict = dict + + def resolve_path(self, name: str, parent_path: Optional[str] = None) -> str: + if ( + parent_path + and not parent_path.startswith("<") + and not parent_path.startswith("/") + and not name.startswith("/") + ): + file_dir = posixpath.dirname(parent_path) + name = posixpath.normpath(posixpath.join(file_dir, name)) + return name + + def _create_template(self, name: str) -> Template: + return Template(self.dict[name], name=name, loader=self) + + +class _Node(object): + def each_child(self) -> Iterable["_Node"]: + return () + + def generate(self, writer: "_CodeWriter") -> None: + raise NotImplementedError() + + def find_named_blocks( + self, loader: Optional[BaseLoader], named_blocks: Dict[str, "_NamedBlock"] + ) -> None: + for child in self.each_child(): + child.find_named_blocks(loader, named_blocks) + + +class _File(_Node): + def __init__(self, template: Template, body: "_ChunkList") -> None: + self.template = template + self.body = body + self.line = 0 + + def generate(self, writer: "_CodeWriter") -> None: + writer.write_line("def _tt_execute():", self.line) + with writer.indent(): + writer.write_line("_tt_buffer = []", self.line) + writer.write_line("_tt_append = _tt_buffer.append", self.line) + self.body.generate(writer) + writer.write_line("return _tt_utf8('').join(_tt_buffer)", self.line) + + def each_child(self) -> Iterable["_Node"]: + return (self.body,) + + +class _ChunkList(_Node): + def __init__(self, chunks: List[_Node]) -> None: + self.chunks = chunks + + def generate(self, writer: "_CodeWriter") -> None: + for chunk in self.chunks: + chunk.generate(writer) + + def each_child(self) -> Iterable["_Node"]: + return self.chunks + + +class _NamedBlock(_Node): + def __init__(self, name: str, body: _Node, template: Template, line: int) -> None: + self.name = name + self.body = body + self.template = template + self.line = line + + def each_child(self) -> Iterable["_Node"]: + return (self.body,) + + def generate(self, writer: "_CodeWriter") -> None: + block = writer.named_blocks[self.name] + with writer.include(block.template, self.line): + block.body.generate(writer) + + def find_named_blocks( + self, loader: Optional[BaseLoader], named_blocks: Dict[str, "_NamedBlock"] + ) -> None: + named_blocks[self.name] = self + _Node.find_named_blocks(self, loader, named_blocks) + + +class _ExtendsBlock(_Node): + def __init__(self, name: str) -> None: + self.name = name + + +class _IncludeBlock(_Node): + def __init__(self, name: str, reader: "_TemplateReader", line: int) -> None: + self.name = name + self.template_name = reader.name + self.line = line + + def find_named_blocks( + self, loader: Optional[BaseLoader], named_blocks: Dict[str, _NamedBlock] + ) -> None: + assert loader is not None + included = loader.load(self.name, self.template_name) + included.file.find_named_blocks(loader, named_blocks) + + def generate(self, writer: "_CodeWriter") -> None: + assert writer.loader is not None + included = writer.loader.load(self.name, self.template_name) + with writer.include(included, self.line): + included.file.body.generate(writer) + + +class _ApplyBlock(_Node): + def __init__(self, method: str, line: int, body: _Node) -> None: + self.method = method + self.line = line + self.body = body + + def each_child(self) -> Iterable["_Node"]: + return (self.body,) + + def generate(self, writer: "_CodeWriter") -> None: + method_name = "_tt_apply%d" % writer.apply_counter + writer.apply_counter += 1 + writer.write_line("def %s():" % method_name, self.line) + with writer.indent(): + writer.write_line("_tt_buffer = []", self.line) + writer.write_line("_tt_append = _tt_buffer.append", self.line) + self.body.generate(writer) + writer.write_line("return _tt_utf8('').join(_tt_buffer)", self.line) + writer.write_line( + "_tt_append(_tt_utf8(%s(%s())))" % (self.method, method_name), self.line + ) + + +class _ControlBlock(_Node): + def __init__(self, statement: str, line: int, body: _Node) -> None: + self.statement = statement + self.line = line + self.body = body + + def each_child(self) -> Iterable[_Node]: + return (self.body,) + + def generate(self, writer: "_CodeWriter") -> None: + writer.write_line("%s:" % self.statement, self.line) + with writer.indent(): + self.body.generate(writer) + # Just in case the body was empty + writer.write_line("pass", self.line) + + +class _IntermediateControlBlock(_Node): + def __init__(self, statement: str, line: int) -> None: + self.statement = statement + self.line = line + + def generate(self, writer: "_CodeWriter") -> None: + # In case the previous block was empty + writer.write_line("pass", self.line) + writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1) + + +class _Statement(_Node): + def __init__(self, statement: str, line: int) -> None: + self.statement = statement + self.line = line + + def generate(self, writer: "_CodeWriter") -> None: + writer.write_line(self.statement, self.line) + + +class _Expression(_Node): + def __init__(self, expression: str, line: int, raw: bool = False) -> None: + self.expression = expression + self.line = line + self.raw = raw + + def generate(self, writer: "_CodeWriter") -> None: + writer.write_line("_tt_tmp = %s" % self.expression, self.line) + writer.write_line( + "if isinstance(_tt_tmp, _tt_string_types):" " _tt_tmp = _tt_utf8(_tt_tmp)", + self.line, + ) + writer.write_line("else: _tt_tmp = _tt_utf8(str(_tt_tmp))", self.line) + if not self.raw and writer.current_template.autoescape is not None: + # In python3 functions like xhtml_escape return unicode, + # so we have to convert to utf8 again. + writer.write_line( + "_tt_tmp = _tt_utf8(%s(_tt_tmp))" % writer.current_template.autoescape, + self.line, + ) + writer.write_line("_tt_append(_tt_tmp)", self.line) + + +class _Module(_Expression): + def __init__(self, expression: str, line: int) -> None: + super().__init__("_tt_modules." + expression, line, raw=True) + + +class _Text(_Node): + def __init__(self, value: str, line: int, whitespace: str) -> None: + self.value = value + self.line = line + self.whitespace = whitespace + + def generate(self, writer: "_CodeWriter") -> None: + value = self.value + + # Compress whitespace if requested, with a crude heuristic to avoid + # altering preformatted whitespace. + if "
    " not in value:
    +            value = filter_whitespace(self.whitespace, value)
    +
    +        if value:
    +            writer.write_line("_tt_append(%r)" % escape.utf8(value), self.line)
    +
    +
    +class ParseError(Exception):
    +    """Raised for template syntax errors.
    +
    +    ``ParseError`` instances have ``filename`` and ``lineno`` attributes
    +    indicating the position of the error.
    +
    +    .. versionchanged:: 4.3
    +       Added ``filename`` and ``lineno`` attributes.
    +    """
    +
    +    def __init__(
    +        self, message: str, filename: Optional[str] = None, lineno: int = 0
    +    ) -> None:
    +        self.message = message
    +        # The names "filename" and "lineno" are chosen for consistency
    +        # with python SyntaxError.
    +        self.filename = filename
    +        self.lineno = lineno
    +
    +    def __str__(self) -> str:
    +        return "%s at %s:%d" % (self.message, self.filename, self.lineno)
    +
    +
    +class _CodeWriter(object):
    +    def __init__(
    +        self,
    +        file: TextIO,
    +        named_blocks: Dict[str, _NamedBlock],
    +        loader: Optional[BaseLoader],
    +        current_template: Template,
    +    ) -> None:
    +        self.file = file
    +        self.named_blocks = named_blocks
    +        self.loader = loader
    +        self.current_template = current_template
    +        self.apply_counter = 0
    +        self.include_stack = []  # type: List[Tuple[Template, int]]
    +        self._indent = 0
    +
    +    def indent_size(self) -> int:
    +        return self._indent
    +
    +    def indent(self) -> "ContextManager":
    +        class Indenter(object):
    +            def __enter__(_) -> "_CodeWriter":
    +                self._indent += 1
    +                return self
    +
    +            def __exit__(_, *args: Any) -> None:
    +                assert self._indent > 0
    +                self._indent -= 1
    +
    +        return Indenter()
    +
    +    def include(self, template: Template, line: int) -> "ContextManager":
    +        self.include_stack.append((self.current_template, line))
    +        self.current_template = template
    +
    +        class IncludeTemplate(object):
    +            def __enter__(_) -> "_CodeWriter":
    +                return self
    +
    +            def __exit__(_, *args: Any) -> None:
    +                self.current_template = self.include_stack.pop()[0]
    +
    +        return IncludeTemplate()
    +
    +    def write_line(
    +        self, line: str, line_number: int, indent: Optional[int] = None
    +    ) -> None:
    +        if indent is None:
    +            indent = self._indent
    +        line_comment = "  # %s:%d" % (self.current_template.name, line_number)
    +        if self.include_stack:
    +            ancestors = [
    +                "%s:%d" % (tmpl.name, lineno) for (tmpl, lineno) in self.include_stack
    +            ]
    +            line_comment += " (via %s)" % ", ".join(reversed(ancestors))
    +        print("    " * indent + line + line_comment, file=self.file)
    +
    +
    +class _TemplateReader(object):
    +    def __init__(self, name: str, text: str, whitespace: str) -> None:
    +        self.name = name
    +        self.text = text
    +        self.whitespace = whitespace
    +        self.line = 1
    +        self.pos = 0
    +
    +    def find(self, needle: str, start: int = 0, end: Optional[int] = None) -> int:
    +        assert start >= 0, start
    +        pos = self.pos
    +        start += pos
    +        if end is None:
    +            index = self.text.find(needle, start)
    +        else:
    +            end += pos
    +            assert end >= start
    +            index = self.text.find(needle, start, end)
    +        if index != -1:
    +            index -= pos
    +        return index
    +
    +    def consume(self, count: Optional[int] = None) -> str:
    +        if count is None:
    +            count = len(self.text) - self.pos
    +        newpos = self.pos + count
    +        self.line += self.text.count("\n", self.pos, newpos)
    +        s = self.text[self.pos : newpos]
    +        self.pos = newpos
    +        return s
    +
    +    def remaining(self) -> int:
    +        return len(self.text) - self.pos
    +
    +    def __len__(self) -> int:
    +        return self.remaining()
    +
    +    def __getitem__(self, key: Union[int, slice]) -> str:
    +        if isinstance(key, slice):
    +            size = len(self)
    +            start, stop, step = key.indices(size)
    +            if start is None:
    +                start = self.pos
    +            else:
    +                start += self.pos
    +            if stop is not None:
    +                stop += self.pos
    +            return self.text[slice(start, stop, step)]
    +        elif key < 0:
    +            return self.text[key]
    +        else:
    +            return self.text[self.pos + key]
    +
    +    def __str__(self) -> str:
    +        return self.text[self.pos :]
    +
    +    def raise_parse_error(self, msg: str) -> None:
    +        raise ParseError(msg, self.name, self.line)
    +
    +
    +def _format_code(code: str) -> str:
    +    lines = code.splitlines()
    +    format = "%%%dd  %%s\n" % len(repr(len(lines) + 1))
    +    return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
    +
    +
    +def _parse(
    +    reader: _TemplateReader,
    +    template: Template,
    +    in_block: Optional[str] = None,
    +    in_loop: Optional[str] = None,
    +) -> _ChunkList:
    +    body = _ChunkList([])
    +    while True:
    +        # Find next template directive
    +        curly = 0
    +        while True:
    +            curly = reader.find("{", curly)
    +            if curly == -1 or curly + 1 == reader.remaining():
    +                # EOF
    +                if in_block:
    +                    reader.raise_parse_error(
    +                        "Missing {%% end %%} block for %s" % in_block
    +                    )
    +                body.chunks.append(
    +                    _Text(reader.consume(), reader.line, reader.whitespace)
    +                )
    +                return body
    +            # If the first curly brace is not the start of a special token,
    +            # start searching from the character after it
    +            if reader[curly + 1] not in ("{", "%", "#"):
    +                curly += 1
    +                continue
    +            # When there are more than 2 curlies in a row, use the
    +            # innermost ones.  This is useful when generating languages
    +            # like latex where curlies are also meaningful
    +            if (
    +                curly + 2 < reader.remaining()
    +                and reader[curly + 1] == "{"
    +                and reader[curly + 2] == "{"
    +            ):
    +                curly += 1
    +                continue
    +            break
    +
    +        # Append any text before the special token
    +        if curly > 0:
    +            cons = reader.consume(curly)
    +            body.chunks.append(_Text(cons, reader.line, reader.whitespace))
    +
    +        start_brace = reader.consume(2)
    +        line = reader.line
    +
    +        # Template directives may be escaped as "{{!" or "{%!".
    +        # In this case output the braces and consume the "!".
    +        # This is especially useful in conjunction with jquery templates,
    +        # which also use double braces.
    +        if reader.remaining() and reader[0] == "!":
    +            reader.consume(1)
    +            body.chunks.append(_Text(start_brace, line, reader.whitespace))
    +            continue
    +
    +        # Comment
    +        if start_brace == "{#":
    +            end = reader.find("#}")
    +            if end == -1:
    +                reader.raise_parse_error("Missing end comment #}")
    +            contents = reader.consume(end).strip()
    +            reader.consume(2)
    +            continue
    +
    +        # Expression
    +        if start_brace == "{{":
    +            end = reader.find("}}")
    +            if end == -1:
    +                reader.raise_parse_error("Missing end expression }}")
    +            contents = reader.consume(end).strip()
    +            reader.consume(2)
    +            if not contents:
    +                reader.raise_parse_error("Empty expression")
    +            body.chunks.append(_Expression(contents, line))
    +            continue
    +
    +        # Block
    +        assert start_brace == "{%", start_brace
    +        end = reader.find("%}")
    +        if end == -1:
    +            reader.raise_parse_error("Missing end block %}")
    +        contents = reader.consume(end).strip()
    +        reader.consume(2)
    +        if not contents:
    +            reader.raise_parse_error("Empty block tag ({% %})")
    +
    +        operator, space, suffix = contents.partition(" ")
    +        suffix = suffix.strip()
    +
    +        # Intermediate ("else", "elif", etc) blocks
    +        intermediate_blocks = {
    +            "else": set(["if", "for", "while", "try"]),
    +            "elif": set(["if"]),
    +            "except": set(["try"]),
    +            "finally": set(["try"]),
    +        }
    +        allowed_parents = intermediate_blocks.get(operator)
    +        if allowed_parents is not None:
    +            if not in_block:
    +                reader.raise_parse_error(
    +                    "%s outside %s block" % (operator, allowed_parents)
    +                )
    +            if in_block not in allowed_parents:
    +                reader.raise_parse_error(
    +                    "%s block cannot be attached to %s block" % (operator, in_block)
    +                )
    +            body.chunks.append(_IntermediateControlBlock(contents, line))
    +            continue
    +
    +        # End tag
    +        elif operator == "end":
    +            if not in_block:
    +                reader.raise_parse_error("Extra {% end %} block")
    +            return body
    +
    +        elif operator in (
    +            "extends",
    +            "include",
    +            "set",
    +            "import",
    +            "from",
    +            "comment",
    +            "autoescape",
    +            "whitespace",
    +            "raw",
    +            "module",
    +        ):
    +            if operator == "comment":
    +                continue
    +            if operator == "extends":
    +                suffix = suffix.strip('"').strip("'")
    +                if not suffix:
    +                    reader.raise_parse_error("extends missing file path")
    +                block = _ExtendsBlock(suffix)  # type: _Node
    +            elif operator in ("import", "from"):
    +                if not suffix:
    +                    reader.raise_parse_error("import missing statement")
    +                block = _Statement(contents, line)
    +            elif operator == "include":
    +                suffix = suffix.strip('"').strip("'")
    +                if not suffix:
    +                    reader.raise_parse_error("include missing file path")
    +                block = _IncludeBlock(suffix, reader, line)
    +            elif operator == "set":
    +                if not suffix:
    +                    reader.raise_parse_error("set missing statement")
    +                block = _Statement(suffix, line)
    +            elif operator == "autoescape":
    +                fn = suffix.strip()  # type: Optional[str]
    +                if fn == "None":
    +                    fn = None
    +                template.autoescape = fn
    +                continue
    +            elif operator == "whitespace":
    +                mode = suffix.strip()
    +                # Validate the selected mode
    +                filter_whitespace(mode, "")
    +                reader.whitespace = mode
    +                continue
    +            elif operator == "raw":
    +                block = _Expression(suffix, line, raw=True)
    +            elif operator == "module":
    +                block = _Module(suffix, line)
    +            body.chunks.append(block)
    +            continue
    +
    +        elif operator in ("apply", "block", "try", "if", "for", "while"):
    +            # parse inner body recursively
    +            if operator in ("for", "while"):
    +                block_body = _parse(reader, template, operator, operator)
    +            elif operator == "apply":
    +                # apply creates a nested function so syntactically it's not
    +                # in the loop.
    +                block_body = _parse(reader, template, operator, None)
    +            else:
    +                block_body = _parse(reader, template, operator, in_loop)
    +
    +            if operator == "apply":
    +                if not suffix:
    +                    reader.raise_parse_error("apply missing method name")
    +                block = _ApplyBlock(suffix, line, block_body)
    +            elif operator == "block":
    +                if not suffix:
    +                    reader.raise_parse_error("block missing name")
    +                block = _NamedBlock(suffix, block_body, template, line)
    +            else:
    +                block = _ControlBlock(contents, line, block_body)
    +            body.chunks.append(block)
    +            continue
    +
    +        elif operator in ("break", "continue"):
    +            if not in_loop:
    +                reader.raise_parse_error(
    +                    "%s outside %s block" % (operator, set(["for", "while"]))
    +                )
    +            body.chunks.append(_Statement(contents, line))
    +            continue
    +
    +        else:
    +            reader.raise_parse_error("unknown operator: %r" % operator)
    diff --git a/telegramer/include/tornado/testing.py b/telegramer/include/tornado/testing.py
    new file mode 100644
    index 0000000..3351b92
    --- /dev/null
    +++ b/telegramer/include/tornado/testing.py
    @@ -0,0 +1,818 @@
    +"""Support classes for automated testing.
    +
    +* `AsyncTestCase` and `AsyncHTTPTestCase`:  Subclasses of unittest.TestCase
    +  with additional support for testing asynchronous (`.IOLoop`-based) code.
    +
    +* `ExpectLog`: Make test logs less spammy.
    +
    +* `main()`: A simple test runner (wrapper around unittest.main()) with support
    +  for the tornado.autoreload module to rerun the tests when code changes.
    +"""
    +
    +import asyncio
    +from collections.abc import Generator
    +import functools
    +import inspect
    +import logging
    +import os
    +import re
    +import signal
    +import socket
    +import sys
    +import unittest
    +
    +from tornado import gen
    +from tornado.httpclient import AsyncHTTPClient, HTTPResponse
    +from tornado.httpserver import HTTPServer
    +from tornado.ioloop import IOLoop, TimeoutError
    +from tornado import netutil
    +from tornado.platform.asyncio import AsyncIOMainLoop
    +from tornado.process import Subprocess
    +from tornado.log import app_log
    +from tornado.util import raise_exc_info, basestring_type
    +from tornado.web import Application
    +
    +import typing
    +from typing import Tuple, Any, Callable, Type, Dict, Union, Optional
    +from types import TracebackType
    +
    +if typing.TYPE_CHECKING:
    +    # Coroutine wasn't added to typing until 3.5.3, so only import it
    +    # when mypy is running and use forward references.
    +    from typing import Coroutine  # noqa: F401
    +
    +    _ExcInfoTuple = Tuple[
    +        Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]
    +    ]
    +
    +
    +_NON_OWNED_IOLOOPS = AsyncIOMainLoop
    +
    +
    +def bind_unused_port(reuse_port: bool = False) -> Tuple[socket.socket, int]:
    +    """Binds a server socket to an available port on localhost.
    +
    +    Returns a tuple (socket, port).
    +
    +    .. versionchanged:: 4.4
    +       Always binds to ``127.0.0.1`` without resolving the name
    +       ``localhost``.
    +    """
    +    sock = netutil.bind_sockets(
    +        0, "127.0.0.1", family=socket.AF_INET, reuse_port=reuse_port
    +    )[0]
    +    port = sock.getsockname()[1]
    +    return sock, port
    +
    +
    +def get_async_test_timeout() -> float:
    +    """Get the global timeout setting for async tests.
    +
    +    Returns a float, the timeout in seconds.
    +
    +    .. versionadded:: 3.1
    +    """
    +    env = os.environ.get("ASYNC_TEST_TIMEOUT")
    +    if env is not None:
    +        try:
    +            return float(env)
    +        except ValueError:
    +            pass
    +    return 5
    +
    +
    +class _TestMethodWrapper(object):
    +    """Wraps a test method to raise an error if it returns a value.
    +
    +    This is mainly used to detect undecorated generators (if a test
    +    method yields it must use a decorator to consume the generator),
    +    but will also detect other kinds of return values (these are not
    +    necessarily errors, but we alert anyway since there is no good
    +    reason to return a value from a test).
    +    """
    +
    +    def __init__(self, orig_method: Callable) -> None:
    +        self.orig_method = orig_method
    +
    +    def __call__(self, *args: Any, **kwargs: Any) -> None:
    +        result = self.orig_method(*args, **kwargs)
    +        if isinstance(result, Generator) or inspect.iscoroutine(result):
    +            raise TypeError(
    +                "Generator and coroutine test methods should be"
    +                " decorated with tornado.testing.gen_test"
    +            )
    +        elif result is not None:
    +            raise ValueError("Return value from test method ignored: %r" % result)
    +
    +    def __getattr__(self, name: str) -> Any:
    +        """Proxy all unknown attributes to the original method.
    +
    +        This is important for some of the decorators in the `unittest`
    +        module, such as `unittest.skipIf`.
    +        """
    +        return getattr(self.orig_method, name)
    +
    +
    +class AsyncTestCase(unittest.TestCase):
    +    """`~unittest.TestCase` subclass for testing `.IOLoop`-based
    +    asynchronous code.
    +
    +    The unittest framework is synchronous, so the test must be
    +    complete by the time the test method returns. This means that
    +    asynchronous code cannot be used in quite the same way as usual
    +    and must be adapted to fit. To write your tests with coroutines,
    +    decorate your test methods with `tornado.testing.gen_test` instead
    +    of `tornado.gen.coroutine`.
    +
    +    This class also provides the (deprecated) `stop()` and `wait()`
    +    methods for a more manual style of testing. The test method itself
    +    must call ``self.wait()``, and asynchronous callbacks should call
    +    ``self.stop()`` to signal completion.
    +
    +    By default, a new `.IOLoop` is constructed for each test and is available
    +    as ``self.io_loop``.  If the code being tested requires a
    +    global `.IOLoop`, subclasses should override `get_new_ioloop` to return it.
    +
    +    The `.IOLoop`'s ``start`` and ``stop`` methods should not be
    +    called directly.  Instead, use `self.stop ` and `self.wait
    +    `.  Arguments passed to ``self.stop`` are returned from
    +    ``self.wait``.  It is possible to have multiple ``wait``/``stop``
    +    cycles in the same test.
    +
    +    Example::
    +
    +        # This test uses coroutine style.
    +        class MyTestCase(AsyncTestCase):
    +            @tornado.testing.gen_test
    +            def test_http_fetch(self):
    +                client = AsyncHTTPClient()
    +                response = yield client.fetch("http://www.tornadoweb.org")
    +                # Test contents of response
    +                self.assertIn("FriendFeed", response.body)
    +
    +        # This test uses argument passing between self.stop and self.wait.
    +        class MyTestCase2(AsyncTestCase):
    +            def test_http_fetch(self):
    +                client = AsyncHTTPClient()
    +                client.fetch("http://www.tornadoweb.org/", self.stop)
    +                response = self.wait()
    +                # Test contents of response
    +                self.assertIn("FriendFeed", response.body)
    +    """
    +
    +    def __init__(self, methodName: str = "runTest") -> None:
    +        super().__init__(methodName)
    +        self.__stopped = False
    +        self.__running = False
    +        self.__failure = None  # type: Optional[_ExcInfoTuple]
    +        self.__stop_args = None  # type: Any
    +        self.__timeout = None  # type: Optional[object]
    +
    +        # It's easy to forget the @gen_test decorator, but if you do
    +        # the test will silently be ignored because nothing will consume
    +        # the generator.  Replace the test method with a wrapper that will
    +        # make sure it's not an undecorated generator.
    +        setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName)))
    +
    +        # Not used in this class itself, but used by @gen_test
    +        self._test_generator = None  # type: Optional[Union[Generator, Coroutine]]
    +
    +    def setUp(self) -> None:
    +        super().setUp()
    +        self.io_loop = self.get_new_ioloop()
    +        self.io_loop.make_current()
    +
    +    def tearDown(self) -> None:
    +        # Native coroutines tend to produce warnings if they're not
    +        # allowed to run to completion. It's difficult to ensure that
    +        # this always happens in tests, so cancel any tasks that are
    +        # still pending by the time we get here.
    +        asyncio_loop = self.io_loop.asyncio_loop  # type: ignore
    +        if hasattr(asyncio, "all_tasks"):  # py37
    +            tasks = asyncio.all_tasks(asyncio_loop)  # type: ignore
    +        else:
    +            tasks = asyncio.Task.all_tasks(asyncio_loop)
    +        # Tasks that are done may still appear here and may contain
    +        # non-cancellation exceptions, so filter them out.
    +        tasks = [t for t in tasks if not t.done()]
    +        for t in tasks:
    +            t.cancel()
    +        # Allow the tasks to run and finalize themselves (which means
    +        # raising a CancelledError inside the coroutine). This may
    +        # just transform the "task was destroyed but it is pending"
    +        # warning into a "uncaught CancelledError" warning, but
    +        # catching CancelledErrors in coroutines that may leak is
    +        # simpler than ensuring that no coroutines leak.
    +        if tasks:
    +            done, pending = self.io_loop.run_sync(lambda: asyncio.wait(tasks))
    +            assert not pending
    +            # If any task failed with anything but a CancelledError, raise it.
    +            for f in done:
    +                try:
    +                    f.result()
    +                except asyncio.CancelledError:
    +                    pass
    +
    +        # Clean up Subprocess, so it can be used again with a new ioloop.
    +        Subprocess.uninitialize()
    +        self.io_loop.clear_current()
    +        if not isinstance(self.io_loop, _NON_OWNED_IOLOOPS):
    +            # Try to clean up any file descriptors left open in the ioloop.
    +            # This avoids leaks, especially when tests are run repeatedly
    +            # in the same process with autoreload (because curl does not
    +            # set FD_CLOEXEC on its file descriptors)
    +            self.io_loop.close(all_fds=True)
    +        super().tearDown()
    +        # In case an exception escaped or the StackContext caught an exception
    +        # when there wasn't a wait() to re-raise it, do so here.
    +        # This is our last chance to raise an exception in a way that the
    +        # unittest machinery understands.
    +        self.__rethrow()
    +
    +    def get_new_ioloop(self) -> IOLoop:
    +        """Returns the `.IOLoop` to use for this test.
    +
    +        By default, a new `.IOLoop` is created for each test.
    +        Subclasses may override this method to return
    +        `.IOLoop.current()` if it is not appropriate to use a new
    +        `.IOLoop` in each tests (for example, if there are global
    +        singletons using the default `.IOLoop`) or if a per-test event
    +        loop is being provided by another system (such as
    +        ``pytest-asyncio``).
    +        """
    +        return IOLoop()
    +
    +    def _handle_exception(
    +        self, typ: Type[Exception], value: Exception, tb: TracebackType
    +    ) -> bool:
    +        if self.__failure is None:
    +            self.__failure = (typ, value, tb)
    +        else:
    +            app_log.error(
    +                "multiple unhandled exceptions in test", exc_info=(typ, value, tb)
    +            )
    +        self.stop()
    +        return True
    +
    +    def __rethrow(self) -> None:
    +        if self.__failure is not None:
    +            failure = self.__failure
    +            self.__failure = None
    +            raise_exc_info(failure)
    +
    +    def run(
    +        self, result: Optional[unittest.TestResult] = None
    +    ) -> Optional[unittest.TestResult]:
    +        ret = super().run(result)
    +        # As a last resort, if an exception escaped super.run() and wasn't
    +        # re-raised in tearDown, raise it here.  This will cause the
    +        # unittest run to fail messily, but that's better than silently
    +        # ignoring an error.
    +        self.__rethrow()
    +        return ret
    +
    +    def stop(self, _arg: Any = None, **kwargs: Any) -> None:
    +        """Stops the `.IOLoop`, causing one pending (or future) call to `wait()`
    +        to return.
    +
    +        Keyword arguments or a single positional argument passed to `stop()` are
    +        saved and will be returned by `wait()`.
    +
    +        .. deprecated:: 5.1
    +
    +           `stop` and `wait` are deprecated; use ``@gen_test`` instead.
    +        """
    +        assert _arg is None or not kwargs
    +        self.__stop_args = kwargs or _arg
    +        if self.__running:
    +            self.io_loop.stop()
    +            self.__running = False
    +        self.__stopped = True
    +
    +    def wait(
    +        self,
    +        condition: Optional[Callable[..., bool]] = None,
    +        timeout: Optional[float] = None,
    +    ) -> Any:
    +        """Runs the `.IOLoop` until stop is called or timeout has passed.
    +
    +        In the event of a timeout, an exception will be thrown. The
    +        default timeout is 5 seconds; it may be overridden with a
    +        ``timeout`` keyword argument or globally with the
    +        ``ASYNC_TEST_TIMEOUT`` environment variable.
    +
    +        If ``condition`` is not ``None``, the `.IOLoop` will be restarted
    +        after `stop()` until ``condition()`` returns ``True``.
    +
    +        .. versionchanged:: 3.1
    +           Added the ``ASYNC_TEST_TIMEOUT`` environment variable.
    +
    +        .. deprecated:: 5.1
    +
    +           `stop` and `wait` are deprecated; use ``@gen_test`` instead.
    +        """
    +        if timeout is None:
    +            timeout = get_async_test_timeout()
    +
    +        if not self.__stopped:
    +            if timeout:
    +
    +                def timeout_func() -> None:
    +                    try:
    +                        raise self.failureException(
    +                            "Async operation timed out after %s seconds" % timeout
    +                        )
    +                    except Exception:
    +                        self.__failure = sys.exc_info()
    +                    self.stop()
    +
    +                self.__timeout = self.io_loop.add_timeout(
    +                    self.io_loop.time() + timeout, timeout_func
    +                )
    +            while True:
    +                self.__running = True
    +                self.io_loop.start()
    +                if self.__failure is not None or condition is None or condition():
    +                    break
    +            if self.__timeout is not None:
    +                self.io_loop.remove_timeout(self.__timeout)
    +                self.__timeout = None
    +        assert self.__stopped
    +        self.__stopped = False
    +        self.__rethrow()
    +        result = self.__stop_args
    +        self.__stop_args = None
    +        return result
    +
    +
    +class AsyncHTTPTestCase(AsyncTestCase):
    +    """A test case that starts up an HTTP server.
    +
    +    Subclasses must override `get_app()`, which returns the
    +    `tornado.web.Application` (or other `.HTTPServer` callback) to be tested.
    +    Tests will typically use the provided ``self.http_client`` to fetch
    +    URLs from this server.
    +
    +    Example, assuming the "Hello, world" example from the user guide is in
    +    ``hello.py``::
    +
    +        import hello
    +
    +        class TestHelloApp(AsyncHTTPTestCase):
    +            def get_app(self):
    +                return hello.make_app()
    +
    +            def test_homepage(self):
    +                response = self.fetch('/')
    +                self.assertEqual(response.code, 200)
    +                self.assertEqual(response.body, 'Hello, world')
    +
    +    That call to ``self.fetch()`` is equivalent to ::
    +
    +        self.http_client.fetch(self.get_url('/'), self.stop)
    +        response = self.wait()
    +
    +    which illustrates how AsyncTestCase can turn an asynchronous operation,
    +    like ``http_client.fetch()``, into a synchronous operation. If you need
    +    to do other asynchronous operations in tests, you'll probably need to use
    +    ``stop()`` and ``wait()`` yourself.
    +    """
    +
    +    def setUp(self) -> None:
    +        super().setUp()
    +        sock, port = bind_unused_port()
    +        self.__port = port
    +
    +        self.http_client = self.get_http_client()
    +        self._app = self.get_app()
    +        self.http_server = self.get_http_server()
    +        self.http_server.add_sockets([sock])
    +
    +    def get_http_client(self) -> AsyncHTTPClient:
    +        return AsyncHTTPClient()
    +
    +    def get_http_server(self) -> HTTPServer:
    +        return HTTPServer(self._app, **self.get_httpserver_options())
    +
    +    def get_app(self) -> Application:
    +        """Should be overridden by subclasses to return a
    +        `tornado.web.Application` or other `.HTTPServer` callback.
    +        """
    +        raise NotImplementedError()
    +
    +    def fetch(
    +        self, path: str, raise_error: bool = False, **kwargs: Any
    +    ) -> HTTPResponse:
    +        """Convenience method to synchronously fetch a URL.
    +
    +        The given path will be appended to the local server's host and
    +        port.  Any additional keyword arguments will be passed directly to
    +        `.AsyncHTTPClient.fetch` (and so could be used to pass
    +        ``method="POST"``, ``body="..."``, etc).
    +
    +        If the path begins with http:// or https://, it will be treated as a
    +        full URL and will be fetched as-is.
    +
    +        If ``raise_error`` is ``True``, a `tornado.httpclient.HTTPError` will
    +        be raised if the response code is not 200. This is the same behavior
    +        as the ``raise_error`` argument to `.AsyncHTTPClient.fetch`, but
    +        the default is ``False`` here (it's ``True`` in `.AsyncHTTPClient`)
    +        because tests often need to deal with non-200 response codes.
    +
    +        .. versionchanged:: 5.0
    +           Added support for absolute URLs.
    +
    +        .. versionchanged:: 5.1
    +
    +           Added the ``raise_error`` argument.
    +
    +        .. deprecated:: 5.1
    +
    +           This method currently turns any exception into an
    +           `.HTTPResponse` with status code 599. In Tornado 6.0,
    +           errors other than `tornado.httpclient.HTTPError` will be
    +           passed through, and ``raise_error=False`` will only
    +           suppress errors that would be raised due to non-200
    +           response codes.
    +
    +        """
    +        if path.lower().startswith(("http://", "https://")):
    +            url = path
    +        else:
    +            url = self.get_url(path)
    +        return self.io_loop.run_sync(
    +            lambda: self.http_client.fetch(url, raise_error=raise_error, **kwargs),
    +            timeout=get_async_test_timeout(),
    +        )
    +
    +    def get_httpserver_options(self) -> Dict[str, Any]:
    +        """May be overridden by subclasses to return additional
    +        keyword arguments for the server.
    +        """
    +        return {}
    +
    +    def get_http_port(self) -> int:
    +        """Returns the port used by the server.
    +
    +        A new port is chosen for each test.
    +        """
    +        return self.__port
    +
    +    def get_protocol(self) -> str:
    +        return "http"
    +
    +    def get_url(self, path: str) -> str:
    +        """Returns an absolute url for the given path on the test server."""
    +        return "%s://127.0.0.1:%s%s" % (self.get_protocol(), self.get_http_port(), path)
    +
    +    def tearDown(self) -> None:
    +        self.http_server.stop()
    +        self.io_loop.run_sync(
    +            self.http_server.close_all_connections, timeout=get_async_test_timeout()
    +        )
    +        self.http_client.close()
    +        del self.http_server
    +        del self._app
    +        super().tearDown()
    +
    +
    +class AsyncHTTPSTestCase(AsyncHTTPTestCase):
    +    """A test case that starts an HTTPS server.
    +
    +    Interface is generally the same as `AsyncHTTPTestCase`.
    +    """
    +
    +    def get_http_client(self) -> AsyncHTTPClient:
    +        return AsyncHTTPClient(force_instance=True, defaults=dict(validate_cert=False))
    +
    +    def get_httpserver_options(self) -> Dict[str, Any]:
    +        return dict(ssl_options=self.get_ssl_options())
    +
    +    def get_ssl_options(self) -> Dict[str, Any]:
    +        """May be overridden by subclasses to select SSL options.
    +
    +        By default includes a self-signed testing certificate.
    +        """
    +        return AsyncHTTPSTestCase.default_ssl_options()
    +
    +    @staticmethod
    +    def default_ssl_options() -> Dict[str, Any]:
    +        # Testing keys were generated with:
    +        # openssl req -new -keyout tornado/test/test.key \
    +        #                     -out tornado/test/test.crt -nodes -days 3650 -x509
    +        module_dir = os.path.dirname(__file__)
    +        return dict(
    +            certfile=os.path.join(module_dir, "test", "test.crt"),
    +            keyfile=os.path.join(module_dir, "test", "test.key"),
    +        )
    +
    +    def get_protocol(self) -> str:
    +        return "https"
    +
    +
    +@typing.overload
    +def gen_test(
    +    *, timeout: Optional[float] = None
    +) -> Callable[[Callable[..., Union[Generator, "Coroutine"]]], Callable[..., None]]:
    +    pass
    +
    +
    +@typing.overload  # noqa: F811
    +def gen_test(func: Callable[..., Union[Generator, "Coroutine"]]) -> Callable[..., None]:
    +    pass
    +
    +
    +def gen_test(  # noqa: F811
    +    func: Optional[Callable[..., Union[Generator, "Coroutine"]]] = None,
    +    timeout: Optional[float] = None,
    +) -> Union[
    +    Callable[..., None],
    +    Callable[[Callable[..., Union[Generator, "Coroutine"]]], Callable[..., None]],
    +]:
    +    """Testing equivalent of ``@gen.coroutine``, to be applied to test methods.
    +
    +    ``@gen.coroutine`` cannot be used on tests because the `.IOLoop` is not
    +    already running.  ``@gen_test`` should be applied to test methods
    +    on subclasses of `AsyncTestCase`.
    +
    +    Example::
    +
    +        class MyTest(AsyncHTTPTestCase):
    +            @gen_test
    +            def test_something(self):
    +                response = yield self.http_client.fetch(self.get_url('/'))
    +
    +    By default, ``@gen_test`` times out after 5 seconds. The timeout may be
    +    overridden globally with the ``ASYNC_TEST_TIMEOUT`` environment variable,
    +    or for each test with the ``timeout`` keyword argument::
    +
    +        class MyTest(AsyncHTTPTestCase):
    +            @gen_test(timeout=10)
    +            def test_something_slow(self):
    +                response = yield self.http_client.fetch(self.get_url('/'))
    +
    +    Note that ``@gen_test`` is incompatible with `AsyncTestCase.stop`,
    +    `AsyncTestCase.wait`, and `AsyncHTTPTestCase.fetch`. Use ``yield
    +    self.http_client.fetch(self.get_url())`` as shown above instead.
    +
    +    .. versionadded:: 3.1
    +       The ``timeout`` argument and ``ASYNC_TEST_TIMEOUT`` environment
    +       variable.
    +
    +    .. versionchanged:: 4.0
    +       The wrapper now passes along ``*args, **kwargs`` so it can be used
    +       on functions with arguments.
    +
    +    """
    +    if timeout is None:
    +        timeout = get_async_test_timeout()
    +
    +    def wrap(f: Callable[..., Union[Generator, "Coroutine"]]) -> Callable[..., None]:
    +        # Stack up several decorators to allow us to access the generator
    +        # object itself.  In the innermost wrapper, we capture the generator
    +        # and save it in an attribute of self.  Next, we run the wrapped
    +        # function through @gen.coroutine.  Finally, the coroutine is
    +        # wrapped again to make it synchronous with run_sync.
    +        #
    +        # This is a good case study arguing for either some sort of
    +        # extensibility in the gen decorators or cancellation support.
    +        @functools.wraps(f)
    +        def pre_coroutine(self, *args, **kwargs):
    +            # type: (AsyncTestCase, *Any, **Any) -> Union[Generator, Coroutine]
    +            # Type comments used to avoid pypy3 bug.
    +            result = f(self, *args, **kwargs)
    +            if isinstance(result, Generator) or inspect.iscoroutine(result):
    +                self._test_generator = result
    +            else:
    +                self._test_generator = None
    +            return result
    +
    +        if inspect.iscoroutinefunction(f):
    +            coro = pre_coroutine
    +        else:
    +            coro = gen.coroutine(pre_coroutine)
    +
    +        @functools.wraps(coro)
    +        def post_coroutine(self, *args, **kwargs):
    +            # type: (AsyncTestCase, *Any, **Any) -> None
    +            try:
    +                return self.io_loop.run_sync(
    +                    functools.partial(coro, self, *args, **kwargs), timeout=timeout
    +                )
    +            except TimeoutError as e:
    +                # run_sync raises an error with an unhelpful traceback.
    +                # If the underlying generator is still running, we can throw the
    +                # exception back into it so the stack trace is replaced by the
    +                # point where the test is stopped. The only reason the generator
    +                # would not be running would be if it were cancelled, which means
    +                # a native coroutine, so we can rely on the cr_running attribute.
    +                if self._test_generator is not None and getattr(
    +                    self._test_generator, "cr_running", True
    +                ):
    +                    self._test_generator.throw(type(e), e)
    +                    # In case the test contains an overly broad except
    +                    # clause, we may get back here.
    +                # Coroutine was stopped or didn't raise a useful stack trace,
    +                # so re-raise the original exception which is better than nothing.
    +                raise
    +
    +        return post_coroutine
    +
    +    if func is not None:
    +        # Used like:
    +        #     @gen_test
    +        #     def f(self):
    +        #         pass
    +        return wrap(func)
    +    else:
    +        # Used like @gen_test(timeout=10)
    +        return wrap
    +
    +
    +# Without this attribute, nosetests will try to run gen_test as a test
    +# anywhere it is imported.
    +gen_test.__test__ = False  # type: ignore
    +
    +
    +class ExpectLog(logging.Filter):
    +    """Context manager to capture and suppress expected log output.
    +
    +    Useful to make tests of error conditions less noisy, while still
    +    leaving unexpected log entries visible.  *Not thread safe.*
    +
    +    The attribute ``logged_stack`` is set to ``True`` if any exception
    +    stack trace was logged.
    +
    +    Usage::
    +
    +        with ExpectLog('tornado.application', "Uncaught exception"):
    +            error_response = self.fetch("/some_page")
    +
    +    .. versionchanged:: 4.3
    +       Added the ``logged_stack`` attribute.
    +    """
    +
    +    def __init__(
    +        self,
    +        logger: Union[logging.Logger, basestring_type],
    +        regex: str,
    +        required: bool = True,
    +        level: Optional[int] = None,
    +    ) -> None:
    +        """Constructs an ExpectLog context manager.
    +
    +        :param logger: Logger object (or name of logger) to watch.  Pass
    +            an empty string to watch the root logger.
    +        :param regex: Regular expression to match.  Any log entries on
    +            the specified logger that match this regex will be suppressed.
    +        :param required: If true, an exception will be raised if the end of
    +            the ``with`` statement is reached without matching any log entries.
    +        :param level: A constant from the ``logging`` module indicating the
    +            expected log level. If this parameter is provided, only log messages
    +            at this level will be considered to match. Additionally, the
    +            supplied ``logger`` will have its level adjusted if necessary
    +            (for the duration of the ``ExpectLog`` to enable the expected
    +            message.
    +
    +        .. versionchanged:: 6.1
    +           Added the ``level`` parameter.
    +        """
    +        if isinstance(logger, basestring_type):
    +            logger = logging.getLogger(logger)
    +        self.logger = logger
    +        self.regex = re.compile(regex)
    +        self.required = required
    +        self.matched = False
    +        self.logged_stack = False
    +        self.level = level
    +        self.orig_level = None  # type: Optional[int]
    +
    +    def filter(self, record: logging.LogRecord) -> bool:
    +        if record.exc_info:
    +            self.logged_stack = True
    +        message = record.getMessage()
    +        if self.regex.match(message):
    +            if self.level is not None and record.levelno != self.level:
    +                app_log.warning(
    +                    "Got expected log message %r at unexpected level (%s vs %s)"
    +                    % (message, logging.getLevelName(self.level), record.levelname)
    +                )
    +                return True
    +            self.matched = True
    +            return False
    +        return True
    +
    +    def __enter__(self) -> "ExpectLog":
    +        if self.level is not None and self.level < self.logger.getEffectiveLevel():
    +            self.orig_level = self.logger.level
    +            self.logger.setLevel(self.level)
    +        self.logger.addFilter(self)
    +        return self
    +
    +    def __exit__(
    +        self,
    +        typ: "Optional[Type[BaseException]]",
    +        value: Optional[BaseException],
    +        tb: Optional[TracebackType],
    +    ) -> None:
    +        if self.orig_level is not None:
    +            self.logger.setLevel(self.orig_level)
    +        self.logger.removeFilter(self)
    +        if not typ and self.required and not self.matched:
    +            raise Exception("did not get expected log message")
    +
    +
    +def main(**kwargs: Any) -> None:
    +    """A simple test runner.
    +
    +    This test runner is essentially equivalent to `unittest.main` from
    +    the standard library, but adds support for Tornado-style option
    +    parsing and log formatting. It is *not* necessary to use this
    +    `main` function to run tests using `AsyncTestCase`; these tests
    +    are self-contained and can run with any test runner.
    +
    +    The easiest way to run a test is via the command line::
    +
    +        python -m tornado.testing tornado.test.web_test
    +
    +    See the standard library ``unittest`` module for ways in which
    +    tests can be specified.
    +
    +    Projects with many tests may wish to define a test script like
    +    ``tornado/test/runtests.py``.  This script should define a method
    +    ``all()`` which returns a test suite and then call
    +    `tornado.testing.main()`.  Note that even when a test script is
    +    used, the ``all()`` test suite may be overridden by naming a
    +    single test on the command line::
    +
    +        # Runs all tests
    +        python -m tornado.test.runtests
    +        # Runs one test
    +        python -m tornado.test.runtests tornado.test.web_test
    +
    +    Additional keyword arguments passed through to ``unittest.main()``.
    +    For example, use ``tornado.testing.main(verbosity=2)``
    +    to show many test details as they are run.
    +    See http://docs.python.org/library/unittest.html#unittest.main
    +    for full argument list.
    +
    +    .. versionchanged:: 5.0
    +
    +       This function produces no output of its own; only that produced
    +       by the `unittest` module (previously it would add a PASS or FAIL
    +       log message).
    +    """
    +    from tornado.options import define, options, parse_command_line
    +
    +    define(
    +        "exception_on_interrupt",
    +        type=bool,
    +        default=True,
    +        help=(
    +            "If true (default), ctrl-c raises a KeyboardInterrupt "
    +            "exception.  This prints a stack trace but cannot interrupt "
    +            "certain operations.  If false, the process is more reliably "
    +            "killed, but does not print a stack trace."
    +        ),
    +    )
    +
    +    # support the same options as unittest's command-line interface
    +    define("verbose", type=bool)
    +    define("quiet", type=bool)
    +    define("failfast", type=bool)
    +    define("catch", type=bool)
    +    define("buffer", type=bool)
    +
    +    argv = [sys.argv[0]] + parse_command_line(sys.argv)
    +
    +    if not options.exception_on_interrupt:
    +        signal.signal(signal.SIGINT, signal.SIG_DFL)
    +
    +    if options.verbose is not None:
    +        kwargs["verbosity"] = 2
    +    if options.quiet is not None:
    +        kwargs["verbosity"] = 0
    +    if options.failfast is not None:
    +        kwargs["failfast"] = True
    +    if options.catch is not None:
    +        kwargs["catchbreak"] = True
    +    if options.buffer is not None:
    +        kwargs["buffer"] = True
    +
    +    if __name__ == "__main__" and len(argv) == 1:
    +        print("No tests specified", file=sys.stderr)
    +        sys.exit(1)
    +    # In order to be able to run tests by their fully-qualified name
    +    # on the command line without importing all tests here,
    +    # module must be set to None.  Python 3.2's unittest.main ignores
    +    # defaultTest if no module is given (it tries to do its own
    +    # test discovery, which is incompatible with auto2to3), so don't
    +    # set module if we're not asking for a specific test.
    +    if len(argv) > 1:
    +        unittest.main(module=None, argv=argv, **kwargs)  # type: ignore
    +    else:
    +        unittest.main(defaultTest="all", argv=argv, **kwargs)
    +
    +
    +if __name__ == "__main__":
    +    main()
    diff --git a/telegramer/include/tornado/util.py b/telegramer/include/tornado/util.py
    new file mode 100644
    index 0000000..77c5f94
    --- /dev/null
    +++ b/telegramer/include/tornado/util.py
    @@ -0,0 +1,474 @@
    +"""Miscellaneous utility functions and classes.
    +
    +This module is used internally by Tornado.  It is not necessarily expected
    +that the functions and classes defined here will be useful to other
    +applications, but they are documented here in case they are.
    +
    +The one public-facing part of this module is the `Configurable` class
    +and its `~Configurable.configure` method, which becomes a part of the
    +interface of its subclasses, including `.AsyncHTTPClient`, `.IOLoop`,
    +and `.Resolver`.
    +"""
    +
    +import array
    +import atexit
    +from inspect import getfullargspec
    +import os
    +import re
    +import typing
    +import zlib
    +
    +from typing import (
    +    Any,
    +    Optional,
    +    Dict,
    +    Mapping,
    +    List,
    +    Tuple,
    +    Match,
    +    Callable,
    +    Type,
    +    Sequence,
    +)
    +
    +if typing.TYPE_CHECKING:
    +    # Additional imports only used in type comments.
    +    # This lets us make these imports lazy.
    +    import datetime  # noqa: F401
    +    from types import TracebackType  # noqa: F401
    +    from typing import Union  # noqa: F401
    +    import unittest  # noqa: F401
    +
    +# Aliases for types that are spelled differently in different Python
    +# versions. bytes_type is deprecated and no longer used in Tornado
    +# itself but is left in case anyone outside Tornado is using it.
    +bytes_type = bytes
    +unicode_type = str
    +basestring_type = str
    +
    +try:
    +    from sys import is_finalizing
    +except ImportError:
    +    # Emulate it
    +    def _get_emulated_is_finalizing() -> Callable[[], bool]:
    +        L = []  # type: List[None]
    +        atexit.register(lambda: L.append(None))
    +
    +        def is_finalizing() -> bool:
    +            # Not referencing any globals here
    +            return L != []
    +
    +        return is_finalizing
    +
    +    is_finalizing = _get_emulated_is_finalizing()
    +
    +
    +class TimeoutError(Exception):
    +    """Exception raised by `.with_timeout` and `.IOLoop.run_sync`.
    +
    +    .. versionchanged:: 5.0:
    +       Unified ``tornado.gen.TimeoutError`` and
    +       ``tornado.ioloop.TimeoutError`` as ``tornado.util.TimeoutError``.
    +       Both former names remain as aliases.
    +    """
    +
    +
    +class ObjectDict(Dict[str, Any]):
    +    """Makes a dictionary behave like an object, with attribute-style access.
    +    """
    +
    +    def __getattr__(self, name: str) -> Any:
    +        try:
    +            return self[name]
    +        except KeyError:
    +            raise AttributeError(name)
    +
    +    def __setattr__(self, name: str, value: Any) -> None:
    +        self[name] = value
    +
    +
    +class GzipDecompressor(object):
    +    """Streaming gzip decompressor.
    +
    +    The interface is like that of `zlib.decompressobj` (without some of the
    +    optional arguments, but it understands gzip headers and checksums.
    +    """
    +
    +    def __init__(self) -> None:
    +        # Magic parameter makes zlib module understand gzip header
    +        # http://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib
    +        # This works on cpython and pypy, but not jython.
    +        self.decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS)
    +
    +    def decompress(self, value: bytes, max_length: int = 0) -> bytes:
    +        """Decompress a chunk, returning newly-available data.
    +
    +        Some data may be buffered for later processing; `flush` must
    +        be called when there is no more input data to ensure that
    +        all data was processed.
    +
    +        If ``max_length`` is given, some input data may be left over
    +        in ``unconsumed_tail``; you must retrieve this value and pass
    +        it back to a future call to `decompress` if it is not empty.
    +        """
    +        return self.decompressobj.decompress(value, max_length)
    +
    +    @property
    +    def unconsumed_tail(self) -> bytes:
    +        """Returns the unconsumed portion left over
    +        """
    +        return self.decompressobj.unconsumed_tail
    +
    +    def flush(self) -> bytes:
    +        """Return any remaining buffered data not yet returned by decompress.
    +
    +        Also checks for errors such as truncated input.
    +        No other methods may be called on this object after `flush`.
    +        """
    +        return self.decompressobj.flush()
    +
    +
    +def import_object(name: str) -> Any:
    +    """Imports an object by name.
    +
    +    ``import_object('x')`` is equivalent to ``import x``.
    +    ``import_object('x.y.z')`` is equivalent to ``from x.y import z``.
    +
    +    >>> import tornado.escape
    +    >>> import_object('tornado.escape') is tornado.escape
    +    True
    +    >>> import_object('tornado.escape.utf8') is tornado.escape.utf8
    +    True
    +    >>> import_object('tornado') is tornado
    +    True
    +    >>> import_object('tornado.missing_module')
    +    Traceback (most recent call last):
    +        ...
    +    ImportError: No module named missing_module
    +    """
    +    if name.count(".") == 0:
    +        return __import__(name)
    +
    +    parts = name.split(".")
    +    obj = __import__(".".join(parts[:-1]), fromlist=[parts[-1]])
    +    try:
    +        return getattr(obj, parts[-1])
    +    except AttributeError:
    +        raise ImportError("No module named %s" % parts[-1])
    +
    +
    +def exec_in(
    +    code: Any, glob: Dict[str, Any], loc: Optional[Optional[Mapping[str, Any]]] = None
    +) -> None:
    +    if isinstance(code, str):
    +        # exec(string) inherits the caller's future imports; compile
    +        # the string first to prevent that.
    +        code = compile(code, "", "exec", dont_inherit=True)
    +    exec(code, glob, loc)
    +
    +
    +def raise_exc_info(
    +    exc_info,  # type: Tuple[Optional[type], Optional[BaseException], Optional[TracebackType]]
    +):
    +    # type: (...) -> typing.NoReturn
    +    #
    +    # This function's type annotation must use comments instead of
    +    # real annotations because typing.NoReturn does not exist in
    +    # python 3.5's typing module. The formatting is funky because this
    +    # is apparently what flake8 wants.
    +    try:
    +        if exc_info[1] is not None:
    +            raise exc_info[1].with_traceback(exc_info[2])
    +        else:
    +            raise TypeError("raise_exc_info called with no exception")
    +    finally:
    +        # Clear the traceback reference from our stack frame to
    +        # minimize circular references that slow down GC.
    +        exc_info = (None, None, None)
    +
    +
    +def errno_from_exception(e: BaseException) -> Optional[int]:
    +    """Provides the errno from an Exception object.
    +
    +    There are cases that the errno attribute was not set so we pull
    +    the errno out of the args but if someone instantiates an Exception
    +    without any args you will get a tuple error. So this function
    +    abstracts all that behavior to give you a safe way to get the
    +    errno.
    +    """
    +
    +    if hasattr(e, "errno"):
    +        return e.errno  # type: ignore
    +    elif e.args:
    +        return e.args[0]
    +    else:
    +        return None
    +
    +
    +_alphanum = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
    +
    +
    +def _re_unescape_replacement(match: Match[str]) -> str:
    +    group = match.group(1)
    +    if group[0] in _alphanum:
    +        raise ValueError("cannot unescape '\\\\%s'" % group[0])
    +    return group
    +
    +
    +_re_unescape_pattern = re.compile(r"\\(.)", re.DOTALL)
    +
    +
    +def re_unescape(s: str) -> str:
    +    r"""Unescape a string escaped by `re.escape`.
    +
    +    May raise ``ValueError`` for regular expressions which could not
    +    have been produced by `re.escape` (for example, strings containing
    +    ``\d`` cannot be unescaped).
    +
    +    .. versionadded:: 4.4
    +    """
    +    return _re_unescape_pattern.sub(_re_unescape_replacement, s)
    +
    +
    +class Configurable(object):
    +    """Base class for configurable interfaces.
    +
    +    A configurable interface is an (abstract) class whose constructor
    +    acts as a factory function for one of its implementation subclasses.
    +    The implementation subclass as well as optional keyword arguments to
    +    its initializer can be set globally at runtime with `configure`.
    +
    +    By using the constructor as the factory method, the interface
    +    looks like a normal class, `isinstance` works as usual, etc.  This
    +    pattern is most useful when the choice of implementation is likely
    +    to be a global decision (e.g. when `~select.epoll` is available,
    +    always use it instead of `~select.select`), or when a
    +    previously-monolithic class has been split into specialized
    +    subclasses.
    +
    +    Configurable subclasses must define the class methods
    +    `configurable_base` and `configurable_default`, and use the instance
    +    method `initialize` instead of ``__init__``.
    +
    +    .. versionchanged:: 5.0
    +
    +       It is now possible for configuration to be specified at
    +       multiple levels of a class hierarchy.
    +
    +    """
    +
    +    # Type annotations on this class are mostly done with comments
    +    # because they need to refer to Configurable, which isn't defined
    +    # until after the class definition block. These can use regular
    +    # annotations when our minimum python version is 3.7.
    +    #
    +    # There may be a clever way to use generics here to get more
    +    # precise types (i.e. for a particular Configurable subclass T,
    +    # all the types are subclasses of T, not just Configurable).
    +    __impl_class = None  # type: Optional[Type[Configurable]]
    +    __impl_kwargs = None  # type: Dict[str, Any]
    +
    +    def __new__(cls, *args: Any, **kwargs: Any) -> Any:
    +        base = cls.configurable_base()
    +        init_kwargs = {}  # type: Dict[str, Any]
    +        if cls is base:
    +            impl = cls.configured_class()
    +            if base.__impl_kwargs:
    +                init_kwargs.update(base.__impl_kwargs)
    +        else:
    +            impl = cls
    +        init_kwargs.update(kwargs)
    +        if impl.configurable_base() is not base:
    +            # The impl class is itself configurable, so recurse.
    +            return impl(*args, **init_kwargs)
    +        instance = super(Configurable, cls).__new__(impl)
    +        # initialize vs __init__ chosen for compatibility with AsyncHTTPClient
    +        # singleton magic.  If we get rid of that we can switch to __init__
    +        # here too.
    +        instance.initialize(*args, **init_kwargs)
    +        return instance
    +
    +    @classmethod
    +    def configurable_base(cls):
    +        # type: () -> Type[Configurable]
    +        """Returns the base class of a configurable hierarchy.
    +
    +        This will normally return the class in which it is defined.
    +        (which is *not* necessarily the same as the ``cls`` classmethod
    +        parameter).
    +
    +        """
    +        raise NotImplementedError()
    +
    +    @classmethod
    +    def configurable_default(cls):
    +        # type: () -> Type[Configurable]
    +        """Returns the implementation class to be used if none is configured."""
    +        raise NotImplementedError()
    +
    +    def _initialize(self) -> None:
    +        pass
    +
    +    initialize = _initialize  # type: Callable[..., None]
    +    """Initialize a `Configurable` subclass instance.
    +
    +    Configurable classes should use `initialize` instead of ``__init__``.
    +
    +    .. versionchanged:: 4.2
    +       Now accepts positional arguments in addition to keyword arguments.
    +    """
    +
    +    @classmethod
    +    def configure(cls, impl, **kwargs):
    +        # type: (Union[None, str, Type[Configurable]], Any) -> None
    +        """Sets the class to use when the base class is instantiated.
    +
    +        Keyword arguments will be saved and added to the arguments passed
    +        to the constructor.  This can be used to set global defaults for
    +        some parameters.
    +        """
    +        base = cls.configurable_base()
    +        if isinstance(impl, str):
    +            impl = typing.cast(Type[Configurable], import_object(impl))
    +        if impl is not None and not issubclass(impl, cls):
    +            raise ValueError("Invalid subclass of %s" % cls)
    +        base.__impl_class = impl
    +        base.__impl_kwargs = kwargs
    +
    +    @classmethod
    +    def configured_class(cls):
    +        # type: () -> Type[Configurable]
    +        """Returns the currently configured class."""
    +        base = cls.configurable_base()
    +        # Manually mangle the private name to see whether this base
    +        # has been configured (and not another base higher in the
    +        # hierarchy).
    +        if base.__dict__.get("_Configurable__impl_class") is None:
    +            base.__impl_class = cls.configurable_default()
    +        if base.__impl_class is not None:
    +            return base.__impl_class
    +        else:
    +            # Should be impossible, but mypy wants an explicit check.
    +            raise ValueError("configured class not found")
    +
    +    @classmethod
    +    def _save_configuration(cls):
    +        # type: () -> Tuple[Optional[Type[Configurable]], Dict[str, Any]]
    +        base = cls.configurable_base()
    +        return (base.__impl_class, base.__impl_kwargs)
    +
    +    @classmethod
    +    def _restore_configuration(cls, saved):
    +        # type: (Tuple[Optional[Type[Configurable]], Dict[str, Any]]) -> None
    +        base = cls.configurable_base()
    +        base.__impl_class = saved[0]
    +        base.__impl_kwargs = saved[1]
    +
    +
    +class ArgReplacer(object):
    +    """Replaces one value in an ``args, kwargs`` pair.
    +
    +    Inspects the function signature to find an argument by name
    +    whether it is passed by position or keyword.  For use in decorators
    +    and similar wrappers.
    +    """
    +
    +    def __init__(self, func: Callable, name: str) -> None:
    +        self.name = name
    +        try:
    +            self.arg_pos = self._getargnames(func).index(name)  # type: Optional[int]
    +        except ValueError:
    +            # Not a positional parameter
    +            self.arg_pos = None
    +
    +    def _getargnames(self, func: Callable) -> List[str]:
    +        try:
    +            return getfullargspec(func).args
    +        except TypeError:
    +            if hasattr(func, "func_code"):
    +                # Cython-generated code has all the attributes needed
    +                # by inspect.getfullargspec, but the inspect module only
    +                # works with ordinary functions. Inline the portion of
    +                # getfullargspec that we need here. Note that for static
    +                # functions the @cython.binding(True) decorator must
    +                # be used (for methods it works out of the box).
    +                code = func.func_code  # type: ignore
    +                return code.co_varnames[: code.co_argcount]
    +            raise
    +
    +    def get_old_value(
    +        self, args: Sequence[Any], kwargs: Dict[str, Any], default: Any = None
    +    ) -> Any:
    +        """Returns the old value of the named argument without replacing it.
    +
    +        Returns ``default`` if the argument is not present.
    +        """
    +        if self.arg_pos is not None and len(args) > self.arg_pos:
    +            return args[self.arg_pos]
    +        else:
    +            return kwargs.get(self.name, default)
    +
    +    def replace(
    +        self, new_value: Any, args: Sequence[Any], kwargs: Dict[str, Any]
    +    ) -> Tuple[Any, Sequence[Any], Dict[str, Any]]:
    +        """Replace the named argument in ``args, kwargs`` with ``new_value``.
    +
    +        Returns ``(old_value, args, kwargs)``.  The returned ``args`` and
    +        ``kwargs`` objects may not be the same as the input objects, or
    +        the input objects may be mutated.
    +
    +        If the named argument was not found, ``new_value`` will be added
    +        to ``kwargs`` and None will be returned as ``old_value``.
    +        """
    +        if self.arg_pos is not None and len(args) > self.arg_pos:
    +            # The arg to replace is passed positionally
    +            old_value = args[self.arg_pos]
    +            args = list(args)  # *args is normally a tuple
    +            args[self.arg_pos] = new_value
    +        else:
    +            # The arg to replace is either omitted or passed by keyword.
    +            old_value = kwargs.get(self.name)
    +            kwargs[self.name] = new_value
    +        return old_value, args, kwargs
    +
    +
    +def timedelta_to_seconds(td):
    +    # type: (datetime.timedelta) -> float
    +    """Equivalent to ``td.total_seconds()`` (introduced in Python 2.7)."""
    +    return td.total_seconds()
    +
    +
    +def _websocket_mask_python(mask: bytes, data: bytes) -> bytes:
    +    """Websocket masking function.
    +
    +    `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length.
    +    Returns a `bytes` object of the same length as `data` with the mask applied
    +    as specified in section 5.3 of RFC 6455.
    +
    +    This pure-python implementation may be replaced by an optimized version when available.
    +    """
    +    mask_arr = array.array("B", mask)
    +    unmasked_arr = array.array("B", data)
    +    for i in range(len(data)):
    +        unmasked_arr[i] = unmasked_arr[i] ^ mask_arr[i % 4]
    +    return unmasked_arr.tobytes()
    +
    +
    +if os.environ.get("TORNADO_NO_EXTENSION") or os.environ.get("TORNADO_EXTENSION") == "0":
    +    # These environment variables exist to make it easier to do performance
    +    # comparisons; they are not guaranteed to remain supported in the future.
    +    _websocket_mask = _websocket_mask_python
    +else:
    +    try:
    +        from tornado.speedups import websocket_mask as _websocket_mask
    +    except ImportError:
    +        if os.environ.get("TORNADO_EXTENSION") == "1":
    +            raise
    +        _websocket_mask = _websocket_mask_python
    +
    +
    +def doctests():
    +    # type: () -> unittest.TestSuite
    +    import doctest
    +
    +    return doctest.DocTestSuite()
    diff --git a/telegramer/include/tornado/web.py b/telegramer/include/tornado/web.py
    new file mode 100644
    index 0000000..546e6ec
    --- /dev/null
    +++ b/telegramer/include/tornado/web.py
    @@ -0,0 +1,3588 @@
    +#
    +# Copyright 2009 Facebook
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License"); you may
    +# not use this file except in compliance with the License. You may obtain
    +# a copy of the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    +# License for the specific language governing permissions and limitations
    +# under the License.
    +
    +"""``tornado.web`` provides a simple web framework with asynchronous
    +features that allow it to scale to large numbers of open connections,
    +making it ideal for `long polling
    +`_.
    +
    +Here is a simple "Hello, world" example app:
    +
    +.. testcode::
    +
    +    import tornado.ioloop
    +    import tornado.web
    +
    +    class MainHandler(tornado.web.RequestHandler):
    +        def get(self):
    +            self.write("Hello, world")
    +
    +    if __name__ == "__main__":
    +        application = tornado.web.Application([
    +            (r"/", MainHandler),
    +        ])
    +        application.listen(8888)
    +        tornado.ioloop.IOLoop.current().start()
    +
    +.. testoutput::
    +   :hide:
    +
    +
    +See the :doc:`guide` for additional information.
    +
    +Thread-safety notes
    +-------------------
    +
    +In general, methods on `RequestHandler` and elsewhere in Tornado are
    +not thread-safe. In particular, methods such as
    +`~RequestHandler.write()`, `~RequestHandler.finish()`, and
    +`~RequestHandler.flush()` must only be called from the main thread. If
    +you use multiple threads it is important to use `.IOLoop.add_callback`
    +to transfer control back to the main thread before finishing the
    +request, or to limit your use of other threads to
    +`.IOLoop.run_in_executor` and ensure that your callbacks running in
    +the executor do not refer to Tornado objects.
    +
    +"""
    +
    +import base64
    +import binascii
    +import datetime
    +import email.utils
    +import functools
    +import gzip
    +import hashlib
    +import hmac
    +import http.cookies
    +from inspect import isclass
    +from io import BytesIO
    +import mimetypes
    +import numbers
    +import os.path
    +import re
    +import sys
    +import threading
    +import time
    +import tornado
    +import traceback
    +import types
    +import urllib.parse
    +from urllib.parse import urlencode
    +
    +from tornado.concurrent import Future, future_set_result_unless_cancelled
    +from tornado import escape
    +from tornado import gen
    +from tornado.httpserver import HTTPServer
    +from tornado import httputil
    +from tornado import iostream
    +import tornado.locale
    +from tornado import locale
    +from tornado.log import access_log, app_log, gen_log
    +from tornado import template
    +from tornado.escape import utf8, _unicode
    +from tornado.routing import (
    +    AnyMatches,
    +    DefaultHostMatches,
    +    HostMatches,
    +    ReversibleRouter,
    +    Rule,
    +    ReversibleRuleRouter,
    +    URLSpec,
    +    _RuleList,
    +)
    +from tornado.util import ObjectDict, unicode_type, _websocket_mask
    +
    +url = URLSpec
    +
    +from typing import (
    +    Dict,
    +    Any,
    +    Union,
    +    Optional,
    +    Awaitable,
    +    Tuple,
    +    List,
    +    Callable,
    +    Iterable,
    +    Generator,
    +    Type,
    +    cast,
    +    overload,
    +)
    +from types import TracebackType
    +import typing
    +
    +if typing.TYPE_CHECKING:
    +    from typing import Set  # noqa: F401
    +
    +
    +# The following types are accepted by RequestHandler.set_header
    +# and related methods.
    +_HeaderTypes = Union[bytes, unicode_type, int, numbers.Integral, datetime.datetime]
    +
    +_CookieSecretTypes = Union[str, bytes, Dict[int, str], Dict[int, bytes]]
    +
    +
    +MIN_SUPPORTED_SIGNED_VALUE_VERSION = 1
    +"""The oldest signed value version supported by this version of Tornado.
    +
    +Signed values older than this version cannot be decoded.
    +
    +.. versionadded:: 3.2.1
    +"""
    +
    +MAX_SUPPORTED_SIGNED_VALUE_VERSION = 2
    +"""The newest signed value version supported by this version of Tornado.
    +
    +Signed values newer than this version cannot be decoded.
    +
    +.. versionadded:: 3.2.1
    +"""
    +
    +DEFAULT_SIGNED_VALUE_VERSION = 2
    +"""The signed value version produced by `.RequestHandler.create_signed_value`.
    +
    +May be overridden by passing a ``version`` keyword argument.
    +
    +.. versionadded:: 3.2.1
    +"""
    +
    +DEFAULT_SIGNED_VALUE_MIN_VERSION = 1
    +"""The oldest signed value accepted by `.RequestHandler.get_secure_cookie`.
    +
    +May be overridden by passing a ``min_version`` keyword argument.
    +
    +.. versionadded:: 3.2.1
    +"""
    +
    +
    +class _ArgDefaultMarker:
    +    pass
    +
    +
    +_ARG_DEFAULT = _ArgDefaultMarker()
    +
    +
    +class RequestHandler(object):
    +    """Base class for HTTP request handlers.
    +
    +    Subclasses must define at least one of the methods defined in the
    +    "Entry points" section below.
    +
    +    Applications should not construct `RequestHandler` objects
    +    directly and subclasses should not override ``__init__`` (override
    +    `~RequestHandler.initialize` instead).
    +
    +    """
    +
    +    SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PATCH", "PUT", "OPTIONS")
    +
    +    _template_loaders = {}  # type: Dict[str, template.BaseLoader]
    +    _template_loader_lock = threading.Lock()
    +    _remove_control_chars_regex = re.compile(r"[\x00-\x08\x0e-\x1f]")
    +
    +    _stream_request_body = False
    +
    +    # Will be set in _execute.
    +    _transforms = None  # type: List[OutputTransform]
    +    path_args = None  # type: List[str]
    +    path_kwargs = None  # type: Dict[str, str]
    +
    +    def __init__(
    +        self,
    +        application: "Application",
    +        request: httputil.HTTPServerRequest,
    +        **kwargs: Any
    +    ) -> None:
    +        super().__init__()
    +
    +        self.application = application
    +        self.request = request
    +        self._headers_written = False
    +        self._finished = False
    +        self._auto_finish = True
    +        self._prepared_future = None
    +        self.ui = ObjectDict(
    +            (n, self._ui_method(m)) for n, m in application.ui_methods.items()
    +        )
    +        # UIModules are available as both `modules` and `_tt_modules` in the
    +        # template namespace.  Historically only `modules` was available
    +        # but could be clobbered by user additions to the namespace.
    +        # The template {% module %} directive looks in `_tt_modules` to avoid
    +        # possible conflicts.
    +        self.ui["_tt_modules"] = _UIModuleNamespace(self, application.ui_modules)
    +        self.ui["modules"] = self.ui["_tt_modules"]
    +        self.clear()
    +        assert self.request.connection is not None
    +        # TODO: need to add set_close_callback to HTTPConnection interface
    +        self.request.connection.set_close_callback(  # type: ignore
    +            self.on_connection_close
    +        )
    +        self.initialize(**kwargs)  # type: ignore
    +
    +    def _initialize(self) -> None:
    +        pass
    +
    +    initialize = _initialize  # type: Callable[..., None]
    +    """Hook for subclass initialization. Called for each request.
    +
    +    A dictionary passed as the third argument of a ``URLSpec`` will be
    +    supplied as keyword arguments to ``initialize()``.
    +
    +    Example::
    +
    +        class ProfileHandler(RequestHandler):
    +            def initialize(self, database):
    +                self.database = database
    +
    +            def get(self, username):
    +                ...
    +
    +        app = Application([
    +            (r'/user/(.*)', ProfileHandler, dict(database=database)),
    +            ])
    +    """
    +
    +    @property
    +    def settings(self) -> Dict[str, Any]:
    +        """An alias for `self.application.settings `."""
    +        return self.application.settings
    +
    +    def _unimplemented_method(self, *args: str, **kwargs: str) -> None:
    +        raise HTTPError(405)
    +
    +    head = _unimplemented_method  # type: Callable[..., Optional[Awaitable[None]]]
    +    get = _unimplemented_method  # type: Callable[..., Optional[Awaitable[None]]]
    +    post = _unimplemented_method  # type: Callable[..., Optional[Awaitable[None]]]
    +    delete = _unimplemented_method  # type: Callable[..., Optional[Awaitable[None]]]
    +    patch = _unimplemented_method  # type: Callable[..., Optional[Awaitable[None]]]
    +    put = _unimplemented_method  # type: Callable[..., Optional[Awaitable[None]]]
    +    options = _unimplemented_method  # type: Callable[..., Optional[Awaitable[None]]]
    +
    +    def prepare(self) -> Optional[Awaitable[None]]:
    +        """Called at the beginning of a request before  `get`/`post`/etc.
    +
    +        Override this method to perform common initialization regardless
    +        of the request method.
    +
    +        Asynchronous support: Use ``async def`` or decorate this method with
    +        `.gen.coroutine` to make it asynchronous.
    +        If this method returns an  ``Awaitable`` execution will not proceed
    +        until the ``Awaitable`` is done.
    +
    +        .. versionadded:: 3.1
    +           Asynchronous support.
    +        """
    +        pass
    +
    +    def on_finish(self) -> None:
    +        """Called after the end of a request.
    +
    +        Override this method to perform cleanup, logging, etc.
    +        This method is a counterpart to `prepare`.  ``on_finish`` may
    +        not produce any output, as it is called after the response
    +        has been sent to the client.
    +        """
    +        pass
    +
    +    def on_connection_close(self) -> None:
    +        """Called in async handlers if the client closed the connection.
    +
    +        Override this to clean up resources associated with
    +        long-lived connections.  Note that this method is called only if
    +        the connection was closed during asynchronous processing; if you
    +        need to do cleanup after every request override `on_finish`
    +        instead.
    +
    +        Proxies may keep a connection open for a time (perhaps
    +        indefinitely) after the client has gone away, so this method
    +        may not be called promptly after the end user closes their
    +        connection.
    +        """
    +        if _has_stream_request_body(self.__class__):
    +            if not self.request._body_future.done():
    +                self.request._body_future.set_exception(iostream.StreamClosedError())
    +                self.request._body_future.exception()
    +
    +    def clear(self) -> None:
    +        """Resets all headers and content for this response."""
    +        self._headers = httputil.HTTPHeaders(
    +            {
    +                "Server": "TornadoServer/%s" % tornado.version,
    +                "Content-Type": "text/html; charset=UTF-8",
    +                "Date": httputil.format_timestamp(time.time()),
    +            }
    +        )
    +        self.set_default_headers()
    +        self._write_buffer = []  # type: List[bytes]
    +        self._status_code = 200
    +        self._reason = httputil.responses[200]
    +
    +    def set_default_headers(self) -> None:
    +        """Override this to set HTTP headers at the beginning of the request.
    +
    +        For example, this is the place to set a custom ``Server`` header.
    +        Note that setting such headers in the normal flow of request
    +        processing may not do what you want, since headers may be reset
    +        during error handling.
    +        """
    +        pass
    +
    +    def set_status(self, status_code: int, reason: Optional[str] = None) -> None:
    +        """Sets the status code for our response.
    +
    +        :arg int status_code: Response status code.
    +        :arg str reason: Human-readable reason phrase describing the status
    +            code. If ``None``, it will be filled in from
    +            `http.client.responses` or "Unknown".
    +
    +        .. versionchanged:: 5.0
    +
    +           No longer validates that the response code is in
    +           `http.client.responses`.
    +        """
    +        self._status_code = status_code
    +        if reason is not None:
    +            self._reason = escape.native_str(reason)
    +        else:
    +            self._reason = httputil.responses.get(status_code, "Unknown")
    +
    +    def get_status(self) -> int:
    +        """Returns the status code for our response."""
    +        return self._status_code
    +
    +    def set_header(self, name: str, value: _HeaderTypes) -> None:
    +        """Sets the given response header name and value.
    +
    +        All header values are converted to strings (`datetime` objects
    +        are formatted according to the HTTP specification for the
    +        ``Date`` header).
    +
    +        """
    +        self._headers[name] = self._convert_header_value(value)
    +
    +    def add_header(self, name: str, value: _HeaderTypes) -> None:
    +        """Adds the given response header and value.
    +
    +        Unlike `set_header`, `add_header` may be called multiple times
    +        to return multiple values for the same header.
    +        """
    +        self._headers.add(name, self._convert_header_value(value))
    +
    +    def clear_header(self, name: str) -> None:
    +        """Clears an outgoing header, undoing a previous `set_header` call.
    +
    +        Note that this method does not apply to multi-valued headers
    +        set by `add_header`.
    +        """
    +        if name in self._headers:
    +            del self._headers[name]
    +
    +    _INVALID_HEADER_CHAR_RE = re.compile(r"[\x00-\x1f]")
    +
    +    def _convert_header_value(self, value: _HeaderTypes) -> str:
    +        # Convert the input value to a str. This type check is a bit
    +        # subtle: The bytes case only executes on python 3, and the
    +        # unicode case only executes on python 2, because the other
    +        # cases are covered by the first match for str.
    +        if isinstance(value, str):
    +            retval = value
    +        elif isinstance(value, bytes):  # py3
    +            # Non-ascii characters in headers are not well supported,
    +            # but if you pass bytes, use latin1 so they pass through as-is.
    +            retval = value.decode("latin1")
    +        elif isinstance(value, unicode_type):  # py2
    +            # TODO: This is inconsistent with the use of latin1 above,
    +            # but it's been that way for a long time. Should it change?
    +            retval = escape.utf8(value)
    +        elif isinstance(value, numbers.Integral):
    +            # return immediately since we know the converted value will be safe
    +            return str(value)
    +        elif isinstance(value, datetime.datetime):
    +            return httputil.format_timestamp(value)
    +        else:
    +            raise TypeError("Unsupported header value %r" % value)
    +        # If \n is allowed into the header, it is possible to inject
    +        # additional headers or split the request.
    +        if RequestHandler._INVALID_HEADER_CHAR_RE.search(retval):
    +            raise ValueError("Unsafe header value %r", retval)
    +        return retval
    +
    +    @overload
    +    def get_argument(self, name: str, default: str, strip: bool = True) -> str:
    +        pass
    +
    +    @overload
    +    def get_argument(  # noqa: F811
    +        self, name: str, default: _ArgDefaultMarker = _ARG_DEFAULT, strip: bool = True
    +    ) -> str:
    +        pass
    +
    +    @overload
    +    def get_argument(  # noqa: F811
    +        self, name: str, default: None, strip: bool = True
    +    ) -> Optional[str]:
    +        pass
    +
    +    def get_argument(  # noqa: F811
    +        self,
    +        name: str,
    +        default: Union[None, str, _ArgDefaultMarker] = _ARG_DEFAULT,
    +        strip: bool = True,
    +    ) -> Optional[str]:
    +        """Returns the value of the argument with the given name.
    +
    +        If default is not provided, the argument is considered to be
    +        required, and we raise a `MissingArgumentError` if it is missing.
    +
    +        If the argument appears in the request more than once, we return the
    +        last value.
    +
    +        This method searches both the query and body arguments.
    +        """
    +        return self._get_argument(name, default, self.request.arguments, strip)
    +
    +    def get_arguments(self, name: str, strip: bool = True) -> List[str]:
    +        """Returns a list of the arguments with the given name.
    +
    +        If the argument is not present, returns an empty list.
    +
    +        This method searches both the query and body arguments.
    +        """
    +
    +        # Make sure `get_arguments` isn't accidentally being called with a
    +        # positional argument that's assumed to be a default (like in
    +        # `get_argument`.)
    +        assert isinstance(strip, bool)
    +
    +        return self._get_arguments(name, self.request.arguments, strip)
    +
    +    def get_body_argument(
    +        self,
    +        name: str,
    +        default: Union[None, str, _ArgDefaultMarker] = _ARG_DEFAULT,
    +        strip: bool = True,
    +    ) -> Optional[str]:
    +        """Returns the value of the argument with the given name
    +        from the request body.
    +
    +        If default is not provided, the argument is considered to be
    +        required, and we raise a `MissingArgumentError` if it is missing.
    +
    +        If the argument appears in the url more than once, we return the
    +        last value.
    +
    +        .. versionadded:: 3.2
    +        """
    +        return self._get_argument(name, default, self.request.body_arguments, strip)
    +
    +    def get_body_arguments(self, name: str, strip: bool = True) -> List[str]:
    +        """Returns a list of the body arguments with the given name.
    +
    +        If the argument is not present, returns an empty list.
    +
    +        .. versionadded:: 3.2
    +        """
    +        return self._get_arguments(name, self.request.body_arguments, strip)
    +
    +    def get_query_argument(
    +        self,
    +        name: str,
    +        default: Union[None, str, _ArgDefaultMarker] = _ARG_DEFAULT,
    +        strip: bool = True,
    +    ) -> Optional[str]:
    +        """Returns the value of the argument with the given name
    +        from the request query string.
    +
    +        If default is not provided, the argument is considered to be
    +        required, and we raise a `MissingArgumentError` if it is missing.
    +
    +        If the argument appears in the url more than once, we return the
    +        last value.
    +
    +        .. versionadded:: 3.2
    +        """
    +        return self._get_argument(name, default, self.request.query_arguments, strip)
    +
    +    def get_query_arguments(self, name: str, strip: bool = True) -> List[str]:
    +        """Returns a list of the query arguments with the given name.
    +
    +        If the argument is not present, returns an empty list.
    +
    +        .. versionadded:: 3.2
    +        """
    +        return self._get_arguments(name, self.request.query_arguments, strip)
    +
    +    def _get_argument(
    +        self,
    +        name: str,
    +        default: Union[None, str, _ArgDefaultMarker],
    +        source: Dict[str, List[bytes]],
    +        strip: bool = True,
    +    ) -> Optional[str]:
    +        args = self._get_arguments(name, source, strip=strip)
    +        if not args:
    +            if isinstance(default, _ArgDefaultMarker):
    +                raise MissingArgumentError(name)
    +            return default
    +        return args[-1]
    +
    +    def _get_arguments(
    +        self, name: str, source: Dict[str, List[bytes]], strip: bool = True
    +    ) -> List[str]:
    +        values = []
    +        for v in source.get(name, []):
    +            s = self.decode_argument(v, name=name)
    +            if isinstance(s, unicode_type):
    +                # Get rid of any weird control chars (unless decoding gave
    +                # us bytes, in which case leave it alone)
    +                s = RequestHandler._remove_control_chars_regex.sub(" ", s)
    +            if strip:
    +                s = s.strip()
    +            values.append(s)
    +        return values
    +
    +    def decode_argument(self, value: bytes, name: Optional[str] = None) -> str:
    +        """Decodes an argument from the request.
    +
    +        The argument has been percent-decoded and is now a byte string.
    +        By default, this method decodes the argument as utf-8 and returns
    +        a unicode string, but this may be overridden in subclasses.
    +
    +        This method is used as a filter for both `get_argument()` and for
    +        values extracted from the url and passed to `get()`/`post()`/etc.
    +
    +        The name of the argument is provided if known, but may be None
    +        (e.g. for unnamed groups in the url regex).
    +        """
    +        try:
    +            return _unicode(value)
    +        except UnicodeDecodeError:
    +            raise HTTPError(
    +                400, "Invalid unicode in %s: %r" % (name or "url", value[:40])
    +            )
    +
    +    @property
    +    def cookies(self) -> Dict[str, http.cookies.Morsel]:
    +        """An alias for
    +        `self.request.cookies <.httputil.HTTPServerRequest.cookies>`."""
    +        return self.request.cookies
    +
    +    def get_cookie(self, name: str, default: Optional[str] = None) -> Optional[str]:
    +        """Returns the value of the request cookie with the given name.
    +
    +        If the named cookie is not present, returns ``default``.
    +
    +        This method only returns cookies that were present in the request.
    +        It does not see the outgoing cookies set by `set_cookie` in this
    +        handler.
    +        """
    +        if self.request.cookies is not None and name in self.request.cookies:
    +            return self.request.cookies[name].value
    +        return default
    +
    +    def set_cookie(
    +        self,
    +        name: str,
    +        value: Union[str, bytes],
    +        domain: Optional[str] = None,
    +        expires: Optional[Union[float, Tuple, datetime.datetime]] = None,
    +        path: str = "/",
    +        expires_days: Optional[float] = None,
    +        **kwargs: Any
    +    ) -> None:
    +        """Sets an outgoing cookie name/value with the given options.
    +
    +        Newly-set cookies are not immediately visible via `get_cookie`;
    +        they are not present until the next request.
    +
    +        expires may be a numeric timestamp as returned by `time.time`,
    +        a time tuple as returned by `time.gmtime`, or a
    +        `datetime.datetime` object.
    +
    +        Additional keyword arguments are set on the cookies.Morsel
    +        directly.
    +        See https://docs.python.org/3/library/http.cookies.html#http.cookies.Morsel
    +        for available attributes.
    +        """
    +        # The cookie library only accepts type str, in both python 2 and 3
    +        name = escape.native_str(name)
    +        value = escape.native_str(value)
    +        if re.search(r"[\x00-\x20]", name + value):
    +            # Don't let us accidentally inject bad stuff
    +            raise ValueError("Invalid cookie %r: %r" % (name, value))
    +        if not hasattr(self, "_new_cookie"):
    +            self._new_cookie = (
    +                http.cookies.SimpleCookie()
    +            )  # type: http.cookies.SimpleCookie
    +        if name in self._new_cookie:
    +            del self._new_cookie[name]
    +        self._new_cookie[name] = value
    +        morsel = self._new_cookie[name]
    +        if domain:
    +            morsel["domain"] = domain
    +        if expires_days is not None and not expires:
    +            expires = datetime.datetime.utcnow() + datetime.timedelta(days=expires_days)
    +        if expires:
    +            morsel["expires"] = httputil.format_timestamp(expires)
    +        if path:
    +            morsel["path"] = path
    +        for k, v in kwargs.items():
    +            if k == "max_age":
    +                k = "max-age"
    +
    +            # skip falsy values for httponly and secure flags because
    +            # SimpleCookie sets them regardless
    +            if k in ["httponly", "secure"] and not v:
    +                continue
    +
    +            morsel[k] = v
    +
    +    def clear_cookie(
    +        self, name: str, path: str = "/", domain: Optional[str] = None
    +    ) -> None:
    +        """Deletes the cookie with the given name.
    +
    +        Due to limitations of the cookie protocol, you must pass the same
    +        path and domain to clear a cookie as were used when that cookie
    +        was set (but there is no way to find out on the server side
    +        which values were used for a given cookie).
    +
    +        Similar to `set_cookie`, the effect of this method will not be
    +        seen until the following request.
    +        """
    +        expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
    +        self.set_cookie(name, value="", path=path, expires=expires, domain=domain)
    +
    +    def clear_all_cookies(self, path: str = "/", domain: Optional[str] = None) -> None:
    +        """Deletes all the cookies the user sent with this request.
    +
    +        See `clear_cookie` for more information on the path and domain
    +        parameters.
    +
    +        Similar to `set_cookie`, the effect of this method will not be
    +        seen until the following request.
    +
    +        .. versionchanged:: 3.2
    +
    +           Added the ``path`` and ``domain`` parameters.
    +        """
    +        for name in self.request.cookies:
    +            self.clear_cookie(name, path=path, domain=domain)
    +
    +    def set_secure_cookie(
    +        self,
    +        name: str,
    +        value: Union[str, bytes],
    +        expires_days: Optional[float] = 30,
    +        version: Optional[int] = None,
    +        **kwargs: Any
    +    ) -> None:
    +        """Signs and timestamps a cookie so it cannot be forged.
    +
    +        You must specify the ``cookie_secret`` setting in your Application
    +        to use this method. It should be a long, random sequence of bytes
    +        to be used as the HMAC secret for the signature.
    +
    +        To read a cookie set with this method, use `get_secure_cookie()`.
    +
    +        Note that the ``expires_days`` parameter sets the lifetime of the
    +        cookie in the browser, but is independent of the ``max_age_days``
    +        parameter to `get_secure_cookie`.
    +        A value of None limits the lifetime to the current browser session.
    +
    +        Secure cookies may contain arbitrary byte values, not just unicode
    +        strings (unlike regular cookies)
    +
    +        Similar to `set_cookie`, the effect of this method will not be
    +        seen until the following request.
    +
    +        .. versionchanged:: 3.2.1
    +
    +           Added the ``version`` argument.  Introduced cookie version 2
    +           and made it the default.
    +        """
    +        self.set_cookie(
    +            name,
    +            self.create_signed_value(name, value, version=version),
    +            expires_days=expires_days,
    +            **kwargs
    +        )
    +
    +    def create_signed_value(
    +        self, name: str, value: Union[str, bytes], version: Optional[int] = None
    +    ) -> bytes:
    +        """Signs and timestamps a string so it cannot be forged.
    +
    +        Normally used via set_secure_cookie, but provided as a separate
    +        method for non-cookie uses.  To decode a value not stored
    +        as a cookie use the optional value argument to get_secure_cookie.
    +
    +        .. versionchanged:: 3.2.1
    +
    +           Added the ``version`` argument.  Introduced cookie version 2
    +           and made it the default.
    +        """
    +        self.require_setting("cookie_secret", "secure cookies")
    +        secret = self.application.settings["cookie_secret"]
    +        key_version = None
    +        if isinstance(secret, dict):
    +            if self.application.settings.get("key_version") is None:
    +                raise Exception("key_version setting must be used for secret_key dicts")
    +            key_version = self.application.settings["key_version"]
    +
    +        return create_signed_value(
    +            secret, name, value, version=version, key_version=key_version
    +        )
    +
    +    def get_secure_cookie(
    +        self,
    +        name: str,
    +        value: Optional[str] = None,
    +        max_age_days: float = 31,
    +        min_version: Optional[int] = None,
    +    ) -> Optional[bytes]:
    +        """Returns the given signed cookie if it validates, or None.
    +
    +        The decoded cookie value is returned as a byte string (unlike
    +        `get_cookie`).
    +
    +        Similar to `get_cookie`, this method only returns cookies that
    +        were present in the request. It does not see outgoing cookies set by
    +        `set_secure_cookie` in this handler.
    +
    +        .. versionchanged:: 3.2.1
    +
    +           Added the ``min_version`` argument.  Introduced cookie version 2;
    +           both versions 1 and 2 are accepted by default.
    +        """
    +        self.require_setting("cookie_secret", "secure cookies")
    +        if value is None:
    +            value = self.get_cookie(name)
    +        return decode_signed_value(
    +            self.application.settings["cookie_secret"],
    +            name,
    +            value,
    +            max_age_days=max_age_days,
    +            min_version=min_version,
    +        )
    +
    +    def get_secure_cookie_key_version(
    +        self, name: str, value: Optional[str] = None
    +    ) -> Optional[int]:
    +        """Returns the signing key version of the secure cookie.
    +
    +        The version is returned as int.
    +        """
    +        self.require_setting("cookie_secret", "secure cookies")
    +        if value is None:
    +            value = self.get_cookie(name)
    +        if value is None:
    +            return None
    +        return get_signature_key_version(value)
    +
    +    def redirect(
    +        self, url: str, permanent: bool = False, status: Optional[int] = None
    +    ) -> None:
    +        """Sends a redirect to the given (optionally relative) URL.
    +
    +        If the ``status`` argument is specified, that value is used as the
    +        HTTP status code; otherwise either 301 (permanent) or 302
    +        (temporary) is chosen based on the ``permanent`` argument.
    +        The default is 302 (temporary).
    +        """
    +        if self._headers_written:
    +            raise Exception("Cannot redirect after headers have been written")
    +        if status is None:
    +            status = 301 if permanent else 302
    +        else:
    +            assert isinstance(status, int) and 300 <= status <= 399
    +        self.set_status(status)
    +        self.set_header("Location", utf8(url))
    +        self.finish()
    +
    +    def write(self, chunk: Union[str, bytes, dict]) -> None:
    +        """Writes the given chunk to the output buffer.
    +
    +        To write the output to the network, use the `flush()` method below.
    +
    +        If the given chunk is a dictionary, we write it as JSON and set
    +        the Content-Type of the response to be ``application/json``.
    +        (if you want to send JSON as a different ``Content-Type``, call
    +        ``set_header`` *after* calling ``write()``).
    +
    +        Note that lists are not converted to JSON because of a potential
    +        cross-site security vulnerability.  All JSON output should be
    +        wrapped in a dictionary.  More details at
    +        http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ and
    +        https://github.com/facebook/tornado/issues/1009
    +        """
    +        if self._finished:
    +            raise RuntimeError("Cannot write() after finish()")
    +        if not isinstance(chunk, (bytes, unicode_type, dict)):
    +            message = "write() only accepts bytes, unicode, and dict objects"
    +            if isinstance(chunk, list):
    +                message += (
    +                    ". Lists not accepted for security reasons; see "
    +                    + "http://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.write"  # noqa: E501
    +                )
    +            raise TypeError(message)
    +        if isinstance(chunk, dict):
    +            chunk = escape.json_encode(chunk)
    +            self.set_header("Content-Type", "application/json; charset=UTF-8")
    +        chunk = utf8(chunk)
    +        self._write_buffer.append(chunk)
    +
    +    def render(self, template_name: str, **kwargs: Any) -> "Future[None]":
    +        """Renders the template with the given arguments as the response.
    +
    +        ``render()`` calls ``finish()``, so no other output methods can be called
    +        after it.
    +
    +        Returns a `.Future` with the same semantics as the one returned by `finish`.
    +        Awaiting this `.Future` is optional.
    +
    +        .. versionchanged:: 5.1
    +
    +           Now returns a `.Future` instead of ``None``.
    +        """
    +        if self._finished:
    +            raise RuntimeError("Cannot render() after finish()")
    +        html = self.render_string(template_name, **kwargs)
    +
    +        # Insert the additional JS and CSS added by the modules on the page
    +        js_embed = []
    +        js_files = []
    +        css_embed = []
    +        css_files = []
    +        html_heads = []
    +        html_bodies = []
    +        for module in getattr(self, "_active_modules", {}).values():
    +            embed_part = module.embedded_javascript()
    +            if embed_part:
    +                js_embed.append(utf8(embed_part))
    +            file_part = module.javascript_files()
    +            if file_part:
    +                if isinstance(file_part, (unicode_type, bytes)):
    +                    js_files.append(_unicode(file_part))
    +                else:
    +                    js_files.extend(file_part)
    +            embed_part = module.embedded_css()
    +            if embed_part:
    +                css_embed.append(utf8(embed_part))
    +            file_part = module.css_files()
    +            if file_part:
    +                if isinstance(file_part, (unicode_type, bytes)):
    +                    css_files.append(_unicode(file_part))
    +                else:
    +                    css_files.extend(file_part)
    +            head_part = module.html_head()
    +            if head_part:
    +                html_heads.append(utf8(head_part))
    +            body_part = module.html_body()
    +            if body_part:
    +                html_bodies.append(utf8(body_part))
    +
    +        if js_files:
    +            # Maintain order of JavaScript files given by modules
    +            js = self.render_linked_js(js_files)
    +            sloc = html.rindex(b"")
    +            html = html[:sloc] + utf8(js) + b"\n" + html[sloc:]
    +        if js_embed:
    +            js_bytes = self.render_embed_js(js_embed)
    +            sloc = html.rindex(b"")
    +            html = html[:sloc] + js_bytes + b"\n" + html[sloc:]
    +        if css_files:
    +            css = self.render_linked_css(css_files)
    +            hloc = html.index(b"")
    +            html = html[:hloc] + utf8(css) + b"\n" + html[hloc:]
    +        if css_embed:
    +            css_bytes = self.render_embed_css(css_embed)
    +            hloc = html.index(b"")
    +            html = html[:hloc] + css_bytes + b"\n" + html[hloc:]
    +        if html_heads:
    +            hloc = html.index(b"")
    +            html = html[:hloc] + b"".join(html_heads) + b"\n" + html[hloc:]
    +        if html_bodies:
    +            hloc = html.index(b"")
    +            html = html[:hloc] + b"".join(html_bodies) + b"\n" + html[hloc:]
    +        return self.finish(html)
    +
    +    def render_linked_js(self, js_files: Iterable[str]) -> str:
    +        """Default method used to render the final js links for the
    +        rendered webpage.
    +
    +        Override this method in a sub-classed controller to change the output.
    +        """
    +        paths = []
    +        unique_paths = set()  # type: Set[str]
    +
    +        for path in js_files:
    +            if not is_absolute(path):
    +                path = self.static_url(path)
    +            if path not in unique_paths:
    +                paths.append(path)
    +                unique_paths.add(path)
    +
    +        return "".join(
    +            ''
    +            for p in paths
    +        )
    +
    +    def render_embed_js(self, js_embed: Iterable[bytes]) -> bytes:
    +        """Default method used to render the final embedded js for the
    +        rendered webpage.
    +
    +        Override this method in a sub-classed controller to change the output.
    +        """
    +        return (
    +            b'"
    +        )
    +
    +    def render_linked_css(self, css_files: Iterable[str]) -> str:
    +        """Default method used to render the final css links for the
    +        rendered webpage.
    +
    +        Override this method in a sub-classed controller to change the output.
    +        """
    +        paths = []
    +        unique_paths = set()  # type: Set[str]
    +
    +        for path in css_files:
    +            if not is_absolute(path):
    +                path = self.static_url(path)
    +            if path not in unique_paths:
    +                paths.append(path)
    +                unique_paths.add(path)
    +
    +        return "".join(
    +            ''
    +            for p in paths
    +        )
    +
    +    def render_embed_css(self, css_embed: Iterable[bytes]) -> bytes:
    +        """Default method used to render the final embedded css for the
    +        rendered webpage.
    +
    +        Override this method in a sub-classed controller to change the output.
    +        """
    +        return b'"
    +
    +    def render_string(self, template_name: str, **kwargs: Any) -> bytes:
    +        """Generate the given template with the given arguments.
    +
    +        We return the generated byte string (in utf8). To generate and
    +        write a template as a response, use render() above.
    +        """
    +        # If no template_path is specified, use the path of the calling file
    +        template_path = self.get_template_path()
    +        if not template_path:
    +            frame = sys._getframe(0)
    +            web_file = frame.f_code.co_filename
    +            while frame.f_code.co_filename == web_file:
    +                frame = frame.f_back
    +            assert frame.f_code.co_filename is not None
    +            template_path = os.path.dirname(frame.f_code.co_filename)
    +        with RequestHandler._template_loader_lock:
    +            if template_path not in RequestHandler._template_loaders:
    +                loader = self.create_template_loader(template_path)
    +                RequestHandler._template_loaders[template_path] = loader
    +            else:
    +                loader = RequestHandler._template_loaders[template_path]
    +        t = loader.load(template_name)
    +        namespace = self.get_template_namespace()
    +        namespace.update(kwargs)
    +        return t.generate(**namespace)
    +
    +    def get_template_namespace(self) -> Dict[str, Any]:
    +        """Returns a dictionary to be used as the default template namespace.
    +
    +        May be overridden by subclasses to add or modify values.
    +
    +        The results of this method will be combined with additional
    +        defaults in the `tornado.template` module and keyword arguments
    +        to `render` or `render_string`.
    +        """
    +        namespace = dict(
    +            handler=self,
    +            request=self.request,
    +            current_user=self.current_user,
    +            locale=self.locale,
    +            _=self.locale.translate,
    +            pgettext=self.locale.pgettext,
    +            static_url=self.static_url,
    +            xsrf_form_html=self.xsrf_form_html,
    +            reverse_url=self.reverse_url,
    +        )
    +        namespace.update(self.ui)
    +        return namespace
    +
    +    def create_template_loader(self, template_path: str) -> template.BaseLoader:
    +        """Returns a new template loader for the given path.
    +
    +        May be overridden by subclasses.  By default returns a
    +        directory-based loader on the given path, using the
    +        ``autoescape`` and ``template_whitespace`` application
    +        settings.  If a ``template_loader`` application setting is
    +        supplied, uses that instead.
    +        """
    +        settings = self.application.settings
    +        if "template_loader" in settings:
    +            return settings["template_loader"]
    +        kwargs = {}
    +        if "autoescape" in settings:
    +            # autoescape=None means "no escaping", so we have to be sure
    +            # to only pass this kwarg if the user asked for it.
    +            kwargs["autoescape"] = settings["autoescape"]
    +        if "template_whitespace" in settings:
    +            kwargs["whitespace"] = settings["template_whitespace"]
    +        return template.Loader(template_path, **kwargs)
    +
    +    def flush(self, include_footers: bool = False) -> "Future[None]":
    +        """Flushes the current output buffer to the network.
    +
    +        .. versionchanged:: 4.0
    +           Now returns a `.Future` if no callback is given.
    +
    +        .. versionchanged:: 6.0
    +
    +           The ``callback`` argument was removed.
    +        """
    +        assert self.request.connection is not None
    +        chunk = b"".join(self._write_buffer)
    +        self._write_buffer = []
    +        if not self._headers_written:
    +            self._headers_written = True
    +            for transform in self._transforms:
    +                assert chunk is not None
    +                (
    +                    self._status_code,
    +                    self._headers,
    +                    chunk,
    +                ) = transform.transform_first_chunk(
    +                    self._status_code, self._headers, chunk, include_footers
    +                )
    +            # Ignore the chunk and only write the headers for HEAD requests
    +            if self.request.method == "HEAD":
    +                chunk = b""
    +
    +            # Finalize the cookie headers (which have been stored in a side
    +            # object so an outgoing cookie could be overwritten before it
    +            # is sent).
    +            if hasattr(self, "_new_cookie"):
    +                for cookie in self._new_cookie.values():
    +                    self.add_header("Set-Cookie", cookie.OutputString(None))
    +
    +            start_line = httputil.ResponseStartLine("", self._status_code, self._reason)
    +            return self.request.connection.write_headers(
    +                start_line, self._headers, chunk
    +            )
    +        else:
    +            for transform in self._transforms:
    +                chunk = transform.transform_chunk(chunk, include_footers)
    +            # Ignore the chunk and only write the headers for HEAD requests
    +            if self.request.method != "HEAD":
    +                return self.request.connection.write(chunk)
    +            else:
    +                future = Future()  # type: Future[None]
    +                future.set_result(None)
    +                return future
    +
    +    def finish(self, chunk: Optional[Union[str, bytes, dict]] = None) -> "Future[None]":
    +        """Finishes this response, ending the HTTP request.
    +
    +        Passing a ``chunk`` to ``finish()`` is equivalent to passing that
    +        chunk to ``write()`` and then calling ``finish()`` with no arguments.
    +
    +        Returns a `.Future` which may optionally be awaited to track the sending
    +        of the response to the client. This `.Future` resolves when all the response
    +        data has been sent, and raises an error if the connection is closed before all
    +        data can be sent.
    +
    +        .. versionchanged:: 5.1
    +
    +           Now returns a `.Future` instead of ``None``.
    +        """
    +        if self._finished:
    +            raise RuntimeError("finish() called twice")
    +
    +        if chunk is not None:
    +            self.write(chunk)
    +
    +        # Automatically support ETags and add the Content-Length header if
    +        # we have not flushed any content yet.
    +        if not self._headers_written:
    +            if (
    +                self._status_code == 200
    +                and self.request.method in ("GET", "HEAD")
    +                and "Etag" not in self._headers
    +            ):
    +                self.set_etag_header()
    +                if self.check_etag_header():
    +                    self._write_buffer = []
    +                    self.set_status(304)
    +            if self._status_code in (204, 304) or (100 <= self._status_code < 200):
    +                assert not self._write_buffer, (
    +                    "Cannot send body with %s" % self._status_code
    +                )
    +                self._clear_representation_headers()
    +            elif "Content-Length" not in self._headers:
    +                content_length = sum(len(part) for part in self._write_buffer)
    +                self.set_header("Content-Length", content_length)
    +
    +        assert self.request.connection is not None
    +        # Now that the request is finished, clear the callback we
    +        # set on the HTTPConnection (which would otherwise prevent the
    +        # garbage collection of the RequestHandler when there
    +        # are keepalive connections)
    +        self.request.connection.set_close_callback(None)  # type: ignore
    +
    +        future = self.flush(include_footers=True)
    +        self.request.connection.finish()
    +        self._log()
    +        self._finished = True
    +        self.on_finish()
    +        self._break_cycles()
    +        return future
    +
    +    def detach(self) -> iostream.IOStream:
    +        """Take control of the underlying stream.
    +
    +        Returns the underlying `.IOStream` object and stops all
    +        further HTTP processing. Intended for implementing protocols
    +        like websockets that tunnel over an HTTP handshake.
    +
    +        This method is only supported when HTTP/1.1 is used.
    +
    +        .. versionadded:: 5.1
    +        """
    +        self._finished = True
    +        # TODO: add detach to HTTPConnection?
    +        return self.request.connection.detach()  # type: ignore
    +
    +    def _break_cycles(self) -> None:
    +        # Break up a reference cycle between this handler and the
    +        # _ui_module closures to allow for faster GC on CPython.
    +        self.ui = None  # type: ignore
    +
    +    def send_error(self, status_code: int = 500, **kwargs: Any) -> None:
    +        """Sends the given HTTP error code to the browser.
    +
    +        If `flush()` has already been called, it is not possible to send
    +        an error, so this method will simply terminate the response.
    +        If output has been written but not yet flushed, it will be discarded
    +        and replaced with the error page.
    +
    +        Override `write_error()` to customize the error page that is returned.
    +        Additional keyword arguments are passed through to `write_error`.
    +        """
    +        if self._headers_written:
    +            gen_log.error("Cannot send error response after headers written")
    +            if not self._finished:
    +                # If we get an error between writing headers and finishing,
    +                # we are unlikely to be able to finish due to a
    +                # Content-Length mismatch. Try anyway to release the
    +                # socket.
    +                try:
    +                    self.finish()
    +                except Exception:
    +                    gen_log.error("Failed to flush partial response", exc_info=True)
    +            return
    +        self.clear()
    +
    +        reason = kwargs.get("reason")
    +        if "exc_info" in kwargs:
    +            exception = kwargs["exc_info"][1]
    +            if isinstance(exception, HTTPError) and exception.reason:
    +                reason = exception.reason
    +        self.set_status(status_code, reason=reason)
    +        try:
    +            self.write_error(status_code, **kwargs)
    +        except Exception:
    +            app_log.error("Uncaught exception in write_error", exc_info=True)
    +        if not self._finished:
    +            self.finish()
    +
    +    def write_error(self, status_code: int, **kwargs: Any) -> None:
    +        """Override to implement custom error pages.
    +
    +        ``write_error`` may call `write`, `render`, `set_header`, etc
    +        to produce output as usual.
    +
    +        If this error was caused by an uncaught exception (including
    +        HTTPError), an ``exc_info`` triple will be available as
    +        ``kwargs["exc_info"]``.  Note that this exception may not be
    +        the "current" exception for purposes of methods like
    +        ``sys.exc_info()`` or ``traceback.format_exc``.
    +        """
    +        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
    +            # in debug mode, try to send a traceback
    +            self.set_header("Content-Type", "text/plain")
    +            for line in traceback.format_exception(*kwargs["exc_info"]):
    +                self.write(line)
    +            self.finish()
    +        else:
    +            self.finish(
    +                "%(code)d: %(message)s"
    +                "%(code)d: %(message)s"
    +                % {"code": status_code, "message": self._reason}
    +            )
    +
    +    @property
    +    def locale(self) -> tornado.locale.Locale:
    +        """The locale for the current session.
    +
    +        Determined by either `get_user_locale`, which you can override to
    +        set the locale based on, e.g., a user preference stored in a
    +        database, or `get_browser_locale`, which uses the ``Accept-Language``
    +        header.
    +
    +        .. versionchanged: 4.1
    +           Added a property setter.
    +        """
    +        if not hasattr(self, "_locale"):
    +            loc = self.get_user_locale()
    +            if loc is not None:
    +                self._locale = loc
    +            else:
    +                self._locale = self.get_browser_locale()
    +                assert self._locale
    +        return self._locale
    +
    +    @locale.setter
    +    def locale(self, value: tornado.locale.Locale) -> None:
    +        self._locale = value
    +
    +    def get_user_locale(self) -> Optional[tornado.locale.Locale]:
    +        """Override to determine the locale from the authenticated user.
    +
    +        If None is returned, we fall back to `get_browser_locale()`.
    +
    +        This method should return a `tornado.locale.Locale` object,
    +        most likely obtained via a call like ``tornado.locale.get("en")``
    +        """
    +        return None
    +
    +    def get_browser_locale(self, default: str = "en_US") -> tornado.locale.Locale:
    +        """Determines the user's locale from ``Accept-Language`` header.
    +
    +        See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
    +        """
    +        if "Accept-Language" in self.request.headers:
    +            languages = self.request.headers["Accept-Language"].split(",")
    +            locales = []
    +            for language in languages:
    +                parts = language.strip().split(";")
    +                if len(parts) > 1 and parts[1].startswith("q="):
    +                    try:
    +                        score = float(parts[1][2:])
    +                    except (ValueError, TypeError):
    +                        score = 0.0
    +                else:
    +                    score = 1.0
    +                locales.append((parts[0], score))
    +            if locales:
    +                locales.sort(key=lambda pair: pair[1], reverse=True)
    +                codes = [loc[0] for loc in locales]
    +                return locale.get(*codes)
    +        return locale.get(default)
    +
    +    @property
    +    def current_user(self) -> Any:
    +        """The authenticated user for this request.
    +
    +        This is set in one of two ways:
    +
    +        * A subclass may override `get_current_user()`, which will be called
    +          automatically the first time ``self.current_user`` is accessed.
    +          `get_current_user()` will only be called once per request,
    +          and is cached for future access::
    +
    +              def get_current_user(self):
    +                  user_cookie = self.get_secure_cookie("user")
    +                  if user_cookie:
    +                      return json.loads(user_cookie)
    +                  return None
    +
    +        * It may be set as a normal variable, typically from an overridden
    +          `prepare()`::
    +
    +              @gen.coroutine
    +              def prepare(self):
    +                  user_id_cookie = self.get_secure_cookie("user_id")
    +                  if user_id_cookie:
    +                      self.current_user = yield load_user(user_id_cookie)
    +
    +        Note that `prepare()` may be a coroutine while `get_current_user()`
    +        may not, so the latter form is necessary if loading the user requires
    +        asynchronous operations.
    +
    +        The user object may be any type of the application's choosing.
    +        """
    +        if not hasattr(self, "_current_user"):
    +            self._current_user = self.get_current_user()
    +        return self._current_user
    +
    +    @current_user.setter
    +    def current_user(self, value: Any) -> None:
    +        self._current_user = value
    +
    +    def get_current_user(self) -> Any:
    +        """Override to determine the current user from, e.g., a cookie.
    +
    +        This method may not be a coroutine.
    +        """
    +        return None
    +
    +    def get_login_url(self) -> str:
    +        """Override to customize the login URL based on the request.
    +
    +        By default, we use the ``login_url`` application setting.
    +        """
    +        self.require_setting("login_url", "@tornado.web.authenticated")
    +        return self.application.settings["login_url"]
    +
    +    def get_template_path(self) -> Optional[str]:
    +        """Override to customize template path for each handler.
    +
    +        By default, we use the ``template_path`` application setting.
    +        Return None to load templates relative to the calling file.
    +        """
    +        return self.application.settings.get("template_path")
    +
    +    @property
    +    def xsrf_token(self) -> bytes:
    +        """The XSRF-prevention token for the current user/session.
    +
    +        To prevent cross-site request forgery, we set an '_xsrf' cookie
    +        and include the same '_xsrf' value as an argument with all POST
    +        requests. If the two do not match, we reject the form submission
    +        as a potential forgery.
    +
    +        See http://en.wikipedia.org/wiki/Cross-site_request_forgery
    +
    +        This property is of type `bytes`, but it contains only ASCII
    +        characters. If a character string is required, there is no
    +        need to base64-encode it; just decode the byte string as
    +        UTF-8.
    +
    +        .. versionchanged:: 3.2.2
    +           The xsrf token will now be have a random mask applied in every
    +           request, which makes it safe to include the token in pages
    +           that are compressed.  See http://breachattack.com for more
    +           information on the issue fixed by this change.  Old (version 1)
    +           cookies will be converted to version 2 when this method is called
    +           unless the ``xsrf_cookie_version`` `Application` setting is
    +           set to 1.
    +
    +        .. versionchanged:: 4.3
    +           The ``xsrf_cookie_kwargs`` `Application` setting may be
    +           used to supply additional cookie options (which will be
    +           passed directly to `set_cookie`). For example,
    +           ``xsrf_cookie_kwargs=dict(httponly=True, secure=True)``
    +           will set the ``secure`` and ``httponly`` flags on the
    +           ``_xsrf`` cookie.
    +        """
    +        if not hasattr(self, "_xsrf_token"):
    +            version, token, timestamp = self._get_raw_xsrf_token()
    +            output_version = self.settings.get("xsrf_cookie_version", 2)
    +            cookie_kwargs = self.settings.get("xsrf_cookie_kwargs", {})
    +            if output_version == 1:
    +                self._xsrf_token = binascii.b2a_hex(token)
    +            elif output_version == 2:
    +                mask = os.urandom(4)
    +                self._xsrf_token = b"|".join(
    +                    [
    +                        b"2",
    +                        binascii.b2a_hex(mask),
    +                        binascii.b2a_hex(_websocket_mask(mask, token)),
    +                        utf8(str(int(timestamp))),
    +                    ]
    +                )
    +            else:
    +                raise ValueError("unknown xsrf cookie version %d", output_version)
    +            if version is None:
    +                if self.current_user and "expires_days" not in cookie_kwargs:
    +                    cookie_kwargs["expires_days"] = 30
    +                self.set_cookie("_xsrf", self._xsrf_token, **cookie_kwargs)
    +        return self._xsrf_token
    +
    +    def _get_raw_xsrf_token(self) -> Tuple[Optional[int], bytes, float]:
    +        """Read or generate the xsrf token in its raw form.
    +
    +        The raw_xsrf_token is a tuple containing:
    +
    +        * version: the version of the cookie from which this token was read,
    +          or None if we generated a new token in this request.
    +        * token: the raw token data; random (non-ascii) bytes.
    +        * timestamp: the time this token was generated (will not be accurate
    +          for version 1 cookies)
    +        """
    +        if not hasattr(self, "_raw_xsrf_token"):
    +            cookie = self.get_cookie("_xsrf")
    +            if cookie:
    +                version, token, timestamp = self._decode_xsrf_token(cookie)
    +            else:
    +                version, token, timestamp = None, None, None
    +            if token is None:
    +                version = None
    +                token = os.urandom(16)
    +                timestamp = time.time()
    +            assert token is not None
    +            assert timestamp is not None
    +            self._raw_xsrf_token = (version, token, timestamp)
    +        return self._raw_xsrf_token
    +
    +    def _decode_xsrf_token(
    +        self, cookie: str
    +    ) -> Tuple[Optional[int], Optional[bytes], Optional[float]]:
    +        """Convert a cookie string into a the tuple form returned by
    +        _get_raw_xsrf_token.
    +        """
    +
    +        try:
    +            m = _signed_value_version_re.match(utf8(cookie))
    +
    +            if m:
    +                version = int(m.group(1))
    +                if version == 2:
    +                    _, mask_str, masked_token, timestamp_str = cookie.split("|")
    +
    +                    mask = binascii.a2b_hex(utf8(mask_str))
    +                    token = _websocket_mask(mask, binascii.a2b_hex(utf8(masked_token)))
    +                    timestamp = int(timestamp_str)
    +                    return version, token, timestamp
    +                else:
    +                    # Treat unknown versions as not present instead of failing.
    +                    raise Exception("Unknown xsrf cookie version")
    +            else:
    +                version = 1
    +                try:
    +                    token = binascii.a2b_hex(utf8(cookie))
    +                except (binascii.Error, TypeError):
    +                    token = utf8(cookie)
    +                # We don't have a usable timestamp in older versions.
    +                timestamp = int(time.time())
    +                return (version, token, timestamp)
    +        except Exception:
    +            # Catch exceptions and return nothing instead of failing.
    +            gen_log.debug("Uncaught exception in _decode_xsrf_token", exc_info=True)
    +            return None, None, None
    +
    +    def check_xsrf_cookie(self) -> None:
    +        """Verifies that the ``_xsrf`` cookie matches the ``_xsrf`` argument.
    +
    +        To prevent cross-site request forgery, we set an ``_xsrf``
    +        cookie and include the same value as a non-cookie
    +        field with all ``POST`` requests. If the two do not match, we
    +        reject the form submission as a potential forgery.
    +
    +        The ``_xsrf`` value may be set as either a form field named ``_xsrf``
    +        or in a custom HTTP header named ``X-XSRFToken`` or ``X-CSRFToken``
    +        (the latter is accepted for compatibility with Django).
    +
    +        See http://en.wikipedia.org/wiki/Cross-site_request_forgery
    +
    +        .. versionchanged:: 3.2.2
    +           Added support for cookie version 2.  Both versions 1 and 2 are
    +           supported.
    +        """
    +        # Prior to release 1.1.1, this check was ignored if the HTTP header
    +        # ``X-Requested-With: XMLHTTPRequest`` was present.  This exception
    +        # has been shown to be insecure and has been removed.  For more
    +        # information please see
    +        # http://www.djangoproject.com/weblog/2011/feb/08/security/
    +        # http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails
    +        token = (
    +            self.get_argument("_xsrf", None)
    +            or self.request.headers.get("X-Xsrftoken")
    +            or self.request.headers.get("X-Csrftoken")
    +        )
    +        if not token:
    +            raise HTTPError(403, "'_xsrf' argument missing from POST")
    +        _, token, _ = self._decode_xsrf_token(token)
    +        _, expected_token, _ = self._get_raw_xsrf_token()
    +        if not token:
    +            raise HTTPError(403, "'_xsrf' argument has invalid format")
    +        if not hmac.compare_digest(utf8(token), utf8(expected_token)):
    +            raise HTTPError(403, "XSRF cookie does not match POST argument")
    +
    +    def xsrf_form_html(self) -> str:
    +        """An HTML ```` element to be included with all POST forms.
    +
    +        It defines the ``_xsrf`` input value, which we check on all POST
    +        requests to prevent cross-site request forgery. If you have set
    +        the ``xsrf_cookies`` application setting, you must include this
    +        HTML within all of your HTML forms.
    +
    +        In a template, this method should be called with ``{% module
    +        xsrf_form_html() %}``
    +
    +        See `check_xsrf_cookie()` above for more information.
    +        """
    +        return (
    +            ''
    +        )
    +
    +    def static_url(
    +        self, path: str, include_host: Optional[bool] = None, **kwargs: Any
    +    ) -> str:
    +        """Returns a static URL for the given relative static file path.
    +
    +        This method requires you set the ``static_path`` setting in your
    +        application (which specifies the root directory of your static
    +        files).
    +
    +        This method returns a versioned url (by default appending
    +        ``?v=``), which allows the static files to be
    +        cached indefinitely.  This can be disabled by passing
    +        ``include_version=False`` (in the default implementation;
    +        other static file implementations are not required to support
    +        this, but they may support other options).
    +
    +        By default this method returns URLs relative to the current
    +        host, but if ``include_host`` is true the URL returned will be
    +        absolute.  If this handler has an ``include_host`` attribute,
    +        that value will be used as the default for all `static_url`
    +        calls that do not pass ``include_host`` as a keyword argument.
    +
    +        """
    +        self.require_setting("static_path", "static_url")
    +        get_url = self.settings.get(
    +            "static_handler_class", StaticFileHandler
    +        ).make_static_url
    +
    +        if include_host is None:
    +            include_host = getattr(self, "include_host", False)
    +
    +        if include_host:
    +            base = self.request.protocol + "://" + self.request.host
    +        else:
    +            base = ""
    +
    +        return base + get_url(self.settings, path, **kwargs)
    +
    +    def require_setting(self, name: str, feature: str = "this feature") -> None:
    +        """Raises an exception if the given app setting is not defined."""
    +        if not self.application.settings.get(name):
    +            raise Exception(
    +                "You must define the '%s' setting in your "
    +                "application to use %s" % (name, feature)
    +            )
    +
    +    def reverse_url(self, name: str, *args: Any) -> str:
    +        """Alias for `Application.reverse_url`."""
    +        return self.application.reverse_url(name, *args)
    +
    +    def compute_etag(self) -> Optional[str]:
    +        """Computes the etag header to be used for this request.
    +
    +        By default uses a hash of the content written so far.
    +
    +        May be overridden to provide custom etag implementations,
    +        or may return None to disable tornado's default etag support.
    +        """
    +        hasher = hashlib.sha1()
    +        for part in self._write_buffer:
    +            hasher.update(part)
    +        return '"%s"' % hasher.hexdigest()
    +
    +    def set_etag_header(self) -> None:
    +        """Sets the response's Etag header using ``self.compute_etag()``.
    +
    +        Note: no header will be set if ``compute_etag()`` returns ``None``.
    +
    +        This method is called automatically when the request is finished.
    +        """
    +        etag = self.compute_etag()
    +        if etag is not None:
    +            self.set_header("Etag", etag)
    +
    +    def check_etag_header(self) -> bool:
    +        """Checks the ``Etag`` header against requests's ``If-None-Match``.
    +
    +        Returns ``True`` if the request's Etag matches and a 304 should be
    +        returned. For example::
    +
    +            self.set_etag_header()
    +            if self.check_etag_header():
    +                self.set_status(304)
    +                return
    +
    +        This method is called automatically when the request is finished,
    +        but may be called earlier for applications that override
    +        `compute_etag` and want to do an early check for ``If-None-Match``
    +        before completing the request.  The ``Etag`` header should be set
    +        (perhaps with `set_etag_header`) before calling this method.
    +        """
    +        computed_etag = utf8(self._headers.get("Etag", ""))
    +        # Find all weak and strong etag values from If-None-Match header
    +        # because RFC 7232 allows multiple etag values in a single header.
    +        etags = re.findall(
    +            br'\*|(?:W/)?"[^"]*"', utf8(self.request.headers.get("If-None-Match", ""))
    +        )
    +        if not computed_etag or not etags:
    +            return False
    +
    +        match = False
    +        if etags[0] == b"*":
    +            match = True
    +        else:
    +            # Use a weak comparison when comparing entity-tags.
    +            def val(x: bytes) -> bytes:
    +                return x[2:] if x.startswith(b"W/") else x
    +
    +            for etag in etags:
    +                if val(etag) == val(computed_etag):
    +                    match = True
    +                    break
    +        return match
    +
    +    async def _execute(
    +        self, transforms: List["OutputTransform"], *args: bytes, **kwargs: bytes
    +    ) -> None:
    +        """Executes this request with the given output transforms."""
    +        self._transforms = transforms
    +        try:
    +            if self.request.method not in self.SUPPORTED_METHODS:
    +                raise HTTPError(405)
    +            self.path_args = [self.decode_argument(arg) for arg in args]
    +            self.path_kwargs = dict(
    +                (k, self.decode_argument(v, name=k)) for (k, v) in kwargs.items()
    +            )
    +            # If XSRF cookies are turned on, reject form submissions without
    +            # the proper cookie
    +            if self.request.method not in (
    +                "GET",
    +                "HEAD",
    +                "OPTIONS",
    +            ) and self.application.settings.get("xsrf_cookies"):
    +                self.check_xsrf_cookie()
    +
    +            result = self.prepare()
    +            if result is not None:
    +                result = await result
    +            if self._prepared_future is not None:
    +                # Tell the Application we've finished with prepare()
    +                # and are ready for the body to arrive.
    +                future_set_result_unless_cancelled(self._prepared_future, None)
    +            if self._finished:
    +                return
    +
    +            if _has_stream_request_body(self.__class__):
    +                # In streaming mode request.body is a Future that signals
    +                # the body has been completely received.  The Future has no
    +                # result; the data has been passed to self.data_received
    +                # instead.
    +                try:
    +                    await self.request._body_future
    +                except iostream.StreamClosedError:
    +                    return
    +
    +            method = getattr(self, self.request.method.lower())
    +            result = method(*self.path_args, **self.path_kwargs)
    +            if result is not None:
    +                result = await result
    +            if self._auto_finish and not self._finished:
    +                self.finish()
    +        except Exception as e:
    +            try:
    +                self._handle_request_exception(e)
    +            except Exception:
    +                app_log.error("Exception in exception handler", exc_info=True)
    +            finally:
    +                # Unset result to avoid circular references
    +                result = None
    +            if self._prepared_future is not None and not self._prepared_future.done():
    +                # In case we failed before setting _prepared_future, do it
    +                # now (to unblock the HTTP server).  Note that this is not
    +                # in a finally block to avoid GC issues prior to Python 3.4.
    +                self._prepared_future.set_result(None)
    +
    +    def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
    +        """Implement this method to handle streamed request data.
    +
    +        Requires the `.stream_request_body` decorator.
    +
    +        May be a coroutine for flow control.
    +        """
    +        raise NotImplementedError()
    +
    +    def _log(self) -> None:
    +        """Logs the current request.
    +
    +        Sort of deprecated since this functionality was moved to the
    +        Application, but left in place for the benefit of existing apps
    +        that have overridden this method.
    +        """
    +        self.application.log_request(self)
    +
    +    def _request_summary(self) -> str:
    +        return "%s %s (%s)" % (
    +            self.request.method,
    +            self.request.uri,
    +            self.request.remote_ip,
    +        )
    +
    +    def _handle_request_exception(self, e: BaseException) -> None:
    +        if isinstance(e, Finish):
    +            # Not an error; just finish the request without logging.
    +            if not self._finished:
    +                self.finish(*e.args)
    +            return
    +        try:
    +            self.log_exception(*sys.exc_info())
    +        except Exception:
    +            # An error here should still get a best-effort send_error()
    +            # to avoid leaking the connection.
    +            app_log.error("Error in exception logger", exc_info=True)
    +        if self._finished:
    +            # Extra errors after the request has been finished should
    +            # be logged, but there is no reason to continue to try and
    +            # send a response.
    +            return
    +        if isinstance(e, HTTPError):
    +            self.send_error(e.status_code, exc_info=sys.exc_info())
    +        else:
    +            self.send_error(500, exc_info=sys.exc_info())
    +
    +    def log_exception(
    +        self,
    +        typ: "Optional[Type[BaseException]]",
    +        value: Optional[BaseException],
    +        tb: Optional[TracebackType],
    +    ) -> None:
    +        """Override to customize logging of uncaught exceptions.
    +
    +        By default logs instances of `HTTPError` as warnings without
    +        stack traces (on the ``tornado.general`` logger), and all
    +        other exceptions as errors with stack traces (on the
    +        ``tornado.application`` logger).
    +
    +        .. versionadded:: 3.1
    +        """
    +        if isinstance(value, HTTPError):
    +            if value.log_message:
    +                format = "%d %s: " + value.log_message
    +                args = [value.status_code, self._request_summary()] + list(value.args)
    +                gen_log.warning(format, *args)
    +        else:
    +            app_log.error(
    +                "Uncaught exception %s\n%r",
    +                self._request_summary(),
    +                self.request,
    +                exc_info=(typ, value, tb),  # type: ignore
    +            )
    +
    +    def _ui_module(self, name: str, module: Type["UIModule"]) -> Callable[..., str]:
    +        def render(*args, **kwargs) -> str:  # type: ignore
    +            if not hasattr(self, "_active_modules"):
    +                self._active_modules = {}  # type: Dict[str, UIModule]
    +            if name not in self._active_modules:
    +                self._active_modules[name] = module(self)
    +            rendered = self._active_modules[name].render(*args, **kwargs)
    +            return rendered
    +
    +        return render
    +
    +    def _ui_method(self, method: Callable[..., str]) -> Callable[..., str]:
    +        return lambda *args, **kwargs: method(self, *args, **kwargs)
    +
    +    def _clear_representation_headers(self) -> None:
    +        # 304 responses should not contain representation metadata
    +        # headers (defined in
    +        # https://tools.ietf.org/html/rfc7231#section-3.1)
    +        # not explicitly allowed by
    +        # https://tools.ietf.org/html/rfc7232#section-4.1
    +        headers = ["Content-Encoding", "Content-Language", "Content-Type"]
    +        for h in headers:
    +            self.clear_header(h)
    +
    +
    +def stream_request_body(cls: Type[RequestHandler]) -> Type[RequestHandler]:
    +    """Apply to `RequestHandler` subclasses to enable streaming body support.
    +
    +    This decorator implies the following changes:
    +
    +    * `.HTTPServerRequest.body` is undefined, and body arguments will not
    +      be included in `RequestHandler.get_argument`.
    +    * `RequestHandler.prepare` is called when the request headers have been
    +      read instead of after the entire body has been read.
    +    * The subclass must define a method ``data_received(self, data):``, which
    +      will be called zero or more times as data is available.  Note that
    +      if the request has an empty body, ``data_received`` may not be called.
    +    * ``prepare`` and ``data_received`` may return Futures (such as via
    +      ``@gen.coroutine``, in which case the next method will not be called
    +      until those futures have completed.
    +    * The regular HTTP method (``post``, ``put``, etc) will be called after
    +      the entire body has been read.
    +
    +    See the `file receiver demo `_
    +    for example usage.
    +    """  # noqa: E501
    +    if not issubclass(cls, RequestHandler):
    +        raise TypeError("expected subclass of RequestHandler, got %r", cls)
    +    cls._stream_request_body = True
    +    return cls
    +
    +
    +def _has_stream_request_body(cls: Type[RequestHandler]) -> bool:
    +    if not issubclass(cls, RequestHandler):
    +        raise TypeError("expected subclass of RequestHandler, got %r", cls)
    +    return cls._stream_request_body
    +
    +
    +def removeslash(
    +    method: Callable[..., Optional[Awaitable[None]]]
    +) -> Callable[..., Optional[Awaitable[None]]]:
    +    """Use this decorator to remove trailing slashes from the request path.
    +
    +    For example, a request to ``/foo/`` would redirect to ``/foo`` with this
    +    decorator. Your request handler mapping should use a regular expression
    +    like ``r'/foo/*'`` in conjunction with using the decorator.
    +    """
    +
    +    @functools.wraps(method)
    +    def wrapper(  # type: ignore
    +        self: RequestHandler, *args, **kwargs
    +    ) -> Optional[Awaitable[None]]:
    +        if self.request.path.endswith("/"):
    +            if self.request.method in ("GET", "HEAD"):
    +                uri = self.request.path.rstrip("/")
    +                if uri:  # don't try to redirect '/' to ''
    +                    if self.request.query:
    +                        uri += "?" + self.request.query
    +                    self.redirect(uri, permanent=True)
    +                    return None
    +            else:
    +                raise HTTPError(404)
    +        return method(self, *args, **kwargs)
    +
    +    return wrapper
    +
    +
    +def addslash(
    +    method: Callable[..., Optional[Awaitable[None]]]
    +) -> Callable[..., Optional[Awaitable[None]]]:
    +    """Use this decorator to add a missing trailing slash to the request path.
    +
    +    For example, a request to ``/foo`` would redirect to ``/foo/`` with this
    +    decorator. Your request handler mapping should use a regular expression
    +    like ``r'/foo/?'`` in conjunction with using the decorator.
    +    """
    +
    +    @functools.wraps(method)
    +    def wrapper(  # type: ignore
    +        self: RequestHandler, *args, **kwargs
    +    ) -> Optional[Awaitable[None]]:
    +        if not self.request.path.endswith("/"):
    +            if self.request.method in ("GET", "HEAD"):
    +                uri = self.request.path + "/"
    +                if self.request.query:
    +                    uri += "?" + self.request.query
    +                self.redirect(uri, permanent=True)
    +                return None
    +            raise HTTPError(404)
    +        return method(self, *args, **kwargs)
    +
    +    return wrapper
    +
    +
    +class _ApplicationRouter(ReversibleRuleRouter):
    +    """Routing implementation used internally by `Application`.
    +
    +    Provides a binding between `Application` and `RequestHandler`.
    +    This implementation extends `~.routing.ReversibleRuleRouter` in a couple of ways:
    +        * it allows to use `RequestHandler` subclasses as `~.routing.Rule` target and
    +        * it allows to use a list/tuple of rules as `~.routing.Rule` target.
    +        ``process_rule`` implementation will substitute this list with an appropriate
    +        `_ApplicationRouter` instance.
    +    """
    +
    +    def __init__(
    +        self, application: "Application", rules: Optional[_RuleList] = None
    +    ) -> None:
    +        assert isinstance(application, Application)
    +        self.application = application
    +        super().__init__(rules)
    +
    +    def process_rule(self, rule: Rule) -> Rule:
    +        rule = super().process_rule(rule)
    +
    +        if isinstance(rule.target, (list, tuple)):
    +            rule.target = _ApplicationRouter(
    +                self.application, rule.target  # type: ignore
    +            )
    +
    +        return rule
    +
    +    def get_target_delegate(
    +        self, target: Any, request: httputil.HTTPServerRequest, **target_params: Any
    +    ) -> Optional[httputil.HTTPMessageDelegate]:
    +        if isclass(target) and issubclass(target, RequestHandler):
    +            return self.application.get_handler_delegate(
    +                request, target, **target_params
    +            )
    +
    +        return super().get_target_delegate(target, request, **target_params)
    +
    +
    +class Application(ReversibleRouter):
    +    r"""A collection of request handlers that make up a web application.
    +
    +    Instances of this class are callable and can be passed directly to
    +    HTTPServer to serve the application::
    +
    +        application = web.Application([
    +            (r"/", MainPageHandler),
    +        ])
    +        http_server = httpserver.HTTPServer(application)
    +        http_server.listen(8080)
    +        ioloop.IOLoop.current().start()
    +
    +    The constructor for this class takes in a list of `~.routing.Rule`
    +    objects or tuples of values corresponding to the arguments of
    +    `~.routing.Rule` constructor: ``(matcher, target, [target_kwargs], [name])``,
    +    the values in square brackets being optional. The default matcher is
    +    `~.routing.PathMatches`, so ``(regexp, target)`` tuples can also be used
    +    instead of ``(PathMatches(regexp), target)``.
    +
    +    A common routing target is a `RequestHandler` subclass, but you can also
    +    use lists of rules as a target, which create a nested routing configuration::
    +
    +        application = web.Application([
    +            (HostMatches("example.com"), [
    +                (r"/", MainPageHandler),
    +                (r"/feed", FeedHandler),
    +            ]),
    +        ])
    +
    +    In addition to this you can use nested `~.routing.Router` instances,
    +    `~.httputil.HTTPMessageDelegate` subclasses and callables as routing targets
    +    (see `~.routing` module docs for more information).
    +
    +    When we receive requests, we iterate over the list in order and
    +    instantiate an instance of the first request class whose regexp
    +    matches the request path. The request class can be specified as
    +    either a class object or a (fully-qualified) name.
    +
    +    A dictionary may be passed as the third element (``target_kwargs``)
    +    of the tuple, which will be used as keyword arguments to the handler's
    +    constructor and `~RequestHandler.initialize` method. This pattern
    +    is used for the `StaticFileHandler` in this example (note that a
    +    `StaticFileHandler` can be installed automatically with the
    +    static_path setting described below)::
    +
    +        application = web.Application([
    +            (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
    +        ])
    +
    +    We support virtual hosts with the `add_handlers` method, which takes in
    +    a host regular expression as the first argument::
    +
    +        application.add_handlers(r"www\.myhost\.com", [
    +            (r"/article/([0-9]+)", ArticleHandler),
    +        ])
    +
    +    If there's no match for the current request's host, then ``default_host``
    +    parameter value is matched against host regular expressions.
    +
    +
    +    .. warning::
    +
    +       Applications that do not use TLS may be vulnerable to :ref:`DNS
    +       rebinding ` attacks. This attack is especially
    +       relevant to applications that only listen on ``127.0.0.1`` or
    +       other private networks. Appropriate host patterns must be used
    +       (instead of the default of ``r'.*'``) to prevent this risk. The
    +       ``default_host`` argument must not be used in applications that
    +       may be vulnerable to DNS rebinding.
    +
    +    You can serve static files by sending the ``static_path`` setting
    +    as a keyword argument. We will serve those files from the
    +    ``/static/`` URI (this is configurable with the
    +    ``static_url_prefix`` setting), and we will serve ``/favicon.ico``
    +    and ``/robots.txt`` from the same directory.  A custom subclass of
    +    `StaticFileHandler` can be specified with the
    +    ``static_handler_class`` setting.
    +
    +    .. versionchanged:: 4.5
    +       Integration with the new `tornado.routing` module.
    +
    +    """
    +
    +    def __init__(
    +        self,
    +        handlers: Optional[_RuleList] = None,
    +        default_host: Optional[str] = None,
    +        transforms: Optional[List[Type["OutputTransform"]]] = None,
    +        **settings: Any
    +    ) -> None:
    +        if transforms is None:
    +            self.transforms = []  # type: List[Type[OutputTransform]]
    +            if settings.get("compress_response") or settings.get("gzip"):
    +                self.transforms.append(GZipContentEncoding)
    +        else:
    +            self.transforms = transforms
    +        self.default_host = default_host
    +        self.settings = settings
    +        self.ui_modules = {
    +            "linkify": _linkify,
    +            "xsrf_form_html": _xsrf_form_html,
    +            "Template": TemplateModule,
    +        }
    +        self.ui_methods = {}  # type: Dict[str, Callable[..., str]]
    +        self._load_ui_modules(settings.get("ui_modules", {}))
    +        self._load_ui_methods(settings.get("ui_methods", {}))
    +        if self.settings.get("static_path"):
    +            path = self.settings["static_path"]
    +            handlers = list(handlers or [])
    +            static_url_prefix = settings.get("static_url_prefix", "/static/")
    +            static_handler_class = settings.get(
    +                "static_handler_class", StaticFileHandler
    +            )
    +            static_handler_args = settings.get("static_handler_args", {})
    +            static_handler_args["path"] = path
    +            for pattern in [
    +                re.escape(static_url_prefix) + r"(.*)",
    +                r"/(favicon\.ico)",
    +                r"/(robots\.txt)",
    +            ]:
    +                handlers.insert(0, (pattern, static_handler_class, static_handler_args))
    +
    +        if self.settings.get("debug"):
    +            self.settings.setdefault("autoreload", True)
    +            self.settings.setdefault("compiled_template_cache", False)
    +            self.settings.setdefault("static_hash_cache", False)
    +            self.settings.setdefault("serve_traceback", True)
    +
    +        self.wildcard_router = _ApplicationRouter(self, handlers)
    +        self.default_router = _ApplicationRouter(
    +            self, [Rule(AnyMatches(), self.wildcard_router)]
    +        )
    +
    +        # Automatically reload modified modules
    +        if self.settings.get("autoreload"):
    +            from tornado import autoreload
    +
    +            autoreload.start()
    +
    +    def listen(self, port: int, address: str = "", **kwargs: Any) -> HTTPServer:
    +        """Starts an HTTP server for this application on the given port.
    +
    +        This is a convenience alias for creating an `.HTTPServer`
    +        object and calling its listen method.  Keyword arguments not
    +        supported by `HTTPServer.listen <.TCPServer.listen>` are passed to the
    +        `.HTTPServer` constructor.  For advanced uses
    +        (e.g. multi-process mode), do not use this method; create an
    +        `.HTTPServer` and call its
    +        `.TCPServer.bind`/`.TCPServer.start` methods directly.
    +
    +        Note that after calling this method you still need to call
    +        ``IOLoop.current().start()`` to start the server.
    +
    +        Returns the `.HTTPServer` object.
    +
    +        .. versionchanged:: 4.3
    +           Now returns the `.HTTPServer` object.
    +        """
    +        server = HTTPServer(self, **kwargs)
    +        server.listen(port, address)
    +        return server
    +
    +    def add_handlers(self, host_pattern: str, host_handlers: _RuleList) -> None:
    +        """Appends the given handlers to our handler list.
    +
    +        Host patterns are processed sequentially in the order they were
    +        added. All matching patterns will be considered.
    +        """
    +        host_matcher = HostMatches(host_pattern)
    +        rule = Rule(host_matcher, _ApplicationRouter(self, host_handlers))
    +
    +        self.default_router.rules.insert(-1, rule)
    +
    +        if self.default_host is not None:
    +            self.wildcard_router.add_rules(
    +                [(DefaultHostMatches(self, host_matcher.host_pattern), host_handlers)]
    +            )
    +
    +    def add_transform(self, transform_class: Type["OutputTransform"]) -> None:
    +        self.transforms.append(transform_class)
    +
    +    def _load_ui_methods(self, methods: Any) -> None:
    +        if isinstance(methods, types.ModuleType):
    +            self._load_ui_methods(dict((n, getattr(methods, n)) for n in dir(methods)))
    +        elif isinstance(methods, list):
    +            for m in methods:
    +                self._load_ui_methods(m)
    +        else:
    +            for name, fn in methods.items():
    +                if (
    +                    not name.startswith("_")
    +                    and hasattr(fn, "__call__")
    +                    and name[0].lower() == name[0]
    +                ):
    +                    self.ui_methods[name] = fn
    +
    +    def _load_ui_modules(self, modules: Any) -> None:
    +        if isinstance(modules, types.ModuleType):
    +            self._load_ui_modules(dict((n, getattr(modules, n)) for n in dir(modules)))
    +        elif isinstance(modules, list):
    +            for m in modules:
    +                self._load_ui_modules(m)
    +        else:
    +            assert isinstance(modules, dict)
    +            for name, cls in modules.items():
    +                try:
    +                    if issubclass(cls, UIModule):
    +                        self.ui_modules[name] = cls
    +                except TypeError:
    +                    pass
    +
    +    def __call__(
    +        self, request: httputil.HTTPServerRequest
    +    ) -> Optional[Awaitable[None]]:
    +        # Legacy HTTPServer interface
    +        dispatcher = self.find_handler(request)
    +        return dispatcher.execute()
    +
    +    def find_handler(
    +        self, request: httputil.HTTPServerRequest, **kwargs: Any
    +    ) -> "_HandlerDelegate":
    +        route = self.default_router.find_handler(request)
    +        if route is not None:
    +            return cast("_HandlerDelegate", route)
    +
    +        if self.settings.get("default_handler_class"):
    +            return self.get_handler_delegate(
    +                request,
    +                self.settings["default_handler_class"],
    +                self.settings.get("default_handler_args", {}),
    +            )
    +
    +        return self.get_handler_delegate(request, ErrorHandler, {"status_code": 404})
    +
    +    def get_handler_delegate(
    +        self,
    +        request: httputil.HTTPServerRequest,
    +        target_class: Type[RequestHandler],
    +        target_kwargs: Optional[Dict[str, Any]] = None,
    +        path_args: Optional[List[bytes]] = None,
    +        path_kwargs: Optional[Dict[str, bytes]] = None,
    +    ) -> "_HandlerDelegate":
    +        """Returns `~.httputil.HTTPMessageDelegate` that can serve a request
    +        for application and `RequestHandler` subclass.
    +
    +        :arg httputil.HTTPServerRequest request: current HTTP request.
    +        :arg RequestHandler target_class: a `RequestHandler` class.
    +        :arg dict target_kwargs: keyword arguments for ``target_class`` constructor.
    +        :arg list path_args: positional arguments for ``target_class`` HTTP method that
    +            will be executed while handling a request (``get``, ``post`` or any other).
    +        :arg dict path_kwargs: keyword arguments for ``target_class`` HTTP method.
    +        """
    +        return _HandlerDelegate(
    +            self, request, target_class, target_kwargs, path_args, path_kwargs
    +        )
    +
    +    def reverse_url(self, name: str, *args: Any) -> str:
    +        """Returns a URL path for handler named ``name``
    +
    +        The handler must be added to the application as a named `URLSpec`.
    +
    +        Args will be substituted for capturing groups in the `URLSpec` regex.
    +        They will be converted to strings if necessary, encoded as utf8,
    +        and url-escaped.
    +        """
    +        reversed_url = self.default_router.reverse_url(name, *args)
    +        if reversed_url is not None:
    +            return reversed_url
    +
    +        raise KeyError("%s not found in named urls" % name)
    +
    +    def log_request(self, handler: RequestHandler) -> None:
    +        """Writes a completed HTTP request to the logs.
    +
    +        By default writes to the python root logger.  To change
    +        this behavior either subclass Application and override this method,
    +        or pass a function in the application settings dictionary as
    +        ``log_function``.
    +        """
    +        if "log_function" in self.settings:
    +            self.settings["log_function"](handler)
    +            return
    +        if handler.get_status() < 400:
    +            log_method = access_log.info
    +        elif handler.get_status() < 500:
    +            log_method = access_log.warning
    +        else:
    +            log_method = access_log.error
    +        request_time = 1000.0 * handler.request.request_time()
    +        log_method(
    +            "%d %s %.2fms",
    +            handler.get_status(),
    +            handler._request_summary(),
    +            request_time,
    +        )
    +
    +
    +class _HandlerDelegate(httputil.HTTPMessageDelegate):
    +    def __init__(
    +        self,
    +        application: Application,
    +        request: httputil.HTTPServerRequest,
    +        handler_class: Type[RequestHandler],
    +        handler_kwargs: Optional[Dict[str, Any]],
    +        path_args: Optional[List[bytes]],
    +        path_kwargs: Optional[Dict[str, bytes]],
    +    ) -> None:
    +        self.application = application
    +        self.connection = request.connection
    +        self.request = request
    +        self.handler_class = handler_class
    +        self.handler_kwargs = handler_kwargs or {}
    +        self.path_args = path_args or []
    +        self.path_kwargs = path_kwargs or {}
    +        self.chunks = []  # type: List[bytes]
    +        self.stream_request_body = _has_stream_request_body(self.handler_class)
    +
    +    def headers_received(
    +        self,
    +        start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine],
    +        headers: httputil.HTTPHeaders,
    +    ) -> Optional[Awaitable[None]]:
    +        if self.stream_request_body:
    +            self.request._body_future = Future()
    +            return self.execute()
    +        return None
    +
    +    def data_received(self, data: bytes) -> Optional[Awaitable[None]]:
    +        if self.stream_request_body:
    +            return self.handler.data_received(data)
    +        else:
    +            self.chunks.append(data)
    +            return None
    +
    +    def finish(self) -> None:
    +        if self.stream_request_body:
    +            future_set_result_unless_cancelled(self.request._body_future, None)
    +        else:
    +            self.request.body = b"".join(self.chunks)
    +            self.request._parse_body()
    +            self.execute()
    +
    +    def on_connection_close(self) -> None:
    +        if self.stream_request_body:
    +            self.handler.on_connection_close()
    +        else:
    +            self.chunks = None  # type: ignore
    +
    +    def execute(self) -> Optional[Awaitable[None]]:
    +        # If template cache is disabled (usually in the debug mode),
    +        # re-compile templates and reload static files on every
    +        # request so you don't need to restart to see changes
    +        if not self.application.settings.get("compiled_template_cache", True):
    +            with RequestHandler._template_loader_lock:
    +                for loader in RequestHandler._template_loaders.values():
    +                    loader.reset()
    +        if not self.application.settings.get("static_hash_cache", True):
    +            StaticFileHandler.reset()
    +
    +        self.handler = self.handler_class(
    +            self.application, self.request, **self.handler_kwargs
    +        )
    +        transforms = [t(self.request) for t in self.application.transforms]
    +
    +        if self.stream_request_body:
    +            self.handler._prepared_future = Future()
    +        # Note that if an exception escapes handler._execute it will be
    +        # trapped in the Future it returns (which we are ignoring here,
    +        # leaving it to be logged when the Future is GC'd).
    +        # However, that shouldn't happen because _execute has a blanket
    +        # except handler, and we cannot easily access the IOLoop here to
    +        # call add_future (because of the requirement to remain compatible
    +        # with WSGI)
    +        fut = gen.convert_yielded(
    +            self.handler._execute(transforms, *self.path_args, **self.path_kwargs)
    +        )
    +        fut.add_done_callback(lambda f: f.result())
    +        # If we are streaming the request body, then execute() is finished
    +        # when the handler has prepared to receive the body.  If not,
    +        # it doesn't matter when execute() finishes (so we return None)
    +        return self.handler._prepared_future
    +
    +
    +class HTTPError(Exception):
    +    """An exception that will turn into an HTTP error response.
    +
    +    Raising an `HTTPError` is a convenient alternative to calling
    +    `RequestHandler.send_error` since it automatically ends the
    +    current function.
    +
    +    To customize the response sent with an `HTTPError`, override
    +    `RequestHandler.write_error`.
    +
    +    :arg int status_code: HTTP status code.  Must be listed in
    +        `httplib.responses ` unless the ``reason``
    +        keyword argument is given.
    +    :arg str log_message: Message to be written to the log for this error
    +        (will not be shown to the user unless the `Application` is in debug
    +        mode).  May contain ``%s``-style placeholders, which will be filled
    +        in with remaining positional parameters.
    +    :arg str reason: Keyword-only argument.  The HTTP "reason" phrase
    +        to pass in the status line along with ``status_code``.  Normally
    +        determined automatically from ``status_code``, but can be used
    +        to use a non-standard numeric code.
    +    """
    +
    +    def __init__(
    +        self,
    +        status_code: int = 500,
    +        log_message: Optional[str] = None,
    +        *args: Any,
    +        **kwargs: Any
    +    ) -> None:
    +        self.status_code = status_code
    +        self.log_message = log_message
    +        self.args = args
    +        self.reason = kwargs.get("reason", None)
    +        if log_message and not args:
    +            self.log_message = log_message.replace("%", "%%")
    +
    +    def __str__(self) -> str:
    +        message = "HTTP %d: %s" % (
    +            self.status_code,
    +            self.reason or httputil.responses.get(self.status_code, "Unknown"),
    +        )
    +        if self.log_message:
    +            return message + " (" + (self.log_message % self.args) + ")"
    +        else:
    +            return message
    +
    +
    +class Finish(Exception):
    +    """An exception that ends the request without producing an error response.
    +
    +    When `Finish` is raised in a `RequestHandler`, the request will
    +    end (calling `RequestHandler.finish` if it hasn't already been
    +    called), but the error-handling methods (including
    +    `RequestHandler.write_error`) will not be called.
    +
    +    If `Finish()` was created with no arguments, the pending response
    +    will be sent as-is. If `Finish()` was given an argument, that
    +    argument will be passed to `RequestHandler.finish()`.
    +
    +    This can be a more convenient way to implement custom error pages
    +    than overriding ``write_error`` (especially in library code)::
    +
    +        if self.current_user is None:
    +            self.set_status(401)
    +            self.set_header('WWW-Authenticate', 'Basic realm="something"')
    +            raise Finish()
    +
    +    .. versionchanged:: 4.3
    +       Arguments passed to ``Finish()`` will be passed on to
    +       `RequestHandler.finish`.
    +    """
    +
    +    pass
    +
    +
    +class MissingArgumentError(HTTPError):
    +    """Exception raised by `RequestHandler.get_argument`.
    +
    +    This is a subclass of `HTTPError`, so if it is uncaught a 400 response
    +    code will be used instead of 500 (and a stack trace will not be logged).
    +
    +    .. versionadded:: 3.1
    +    """
    +
    +    def __init__(self, arg_name: str) -> None:
    +        super().__init__(400, "Missing argument %s" % arg_name)
    +        self.arg_name = arg_name
    +
    +
    +class ErrorHandler(RequestHandler):
    +    """Generates an error response with ``status_code`` for all requests."""
    +
    +    def initialize(self, status_code: int) -> None:
    +        self.set_status(status_code)
    +
    +    def prepare(self) -> None:
    +        raise HTTPError(self._status_code)
    +
    +    def check_xsrf_cookie(self) -> None:
    +        # POSTs to an ErrorHandler don't actually have side effects,
    +        # so we don't need to check the xsrf token.  This allows POSTs
    +        # to the wrong url to return a 404 instead of 403.
    +        pass
    +
    +
    +class RedirectHandler(RequestHandler):
    +    """Redirects the client to the given URL for all GET requests.
    +
    +    You should provide the keyword argument ``url`` to the handler, e.g.::
    +
    +        application = web.Application([
    +            (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
    +        ])
    +
    +    `RedirectHandler` supports regular expression substitutions. E.g., to
    +    swap the first and second parts of a path while preserving the remainder::
    +
    +        application = web.Application([
    +            (r"/(.*?)/(.*?)/(.*)", web.RedirectHandler, {"url": "/{1}/{0}/{2}"}),
    +        ])
    +
    +    The final URL is formatted with `str.format` and the substrings that match
    +    the capturing groups. In the above example, a request to "/a/b/c" would be
    +    formatted like::
    +
    +        str.format("/{1}/{0}/{2}", "a", "b", "c")  # -> "/b/a/c"
    +
    +    Use Python's :ref:`format string syntax ` to customize how
    +    values are substituted.
    +
    +    .. versionchanged:: 4.5
    +       Added support for substitutions into the destination URL.
    +
    +    .. versionchanged:: 5.0
    +       If any query arguments are present, they will be copied to the
    +       destination URL.
    +    """
    +
    +    def initialize(self, url: str, permanent: bool = True) -> None:
    +        self._url = url
    +        self._permanent = permanent
    +
    +    def get(self, *args: Any, **kwargs: Any) -> None:
    +        to_url = self._url.format(*args, **kwargs)
    +        if self.request.query_arguments:
    +            # TODO: figure out typing for the next line.
    +            to_url = httputil.url_concat(
    +                to_url,
    +                list(httputil.qs_to_qsl(self.request.query_arguments)),  # type: ignore
    +            )
    +        self.redirect(to_url, permanent=self._permanent)
    +
    +
    +class StaticFileHandler(RequestHandler):
    +    """A simple handler that can serve static content from a directory.
    +
    +    A `StaticFileHandler` is configured automatically if you pass the
    +    ``static_path`` keyword argument to `Application`.  This handler
    +    can be customized with the ``static_url_prefix``, ``static_handler_class``,
    +    and ``static_handler_args`` settings.
    +
    +    To map an additional path to this handler for a static data directory
    +    you would add a line to your application like::
    +
    +        application = web.Application([
    +            (r"/content/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
    +        ])
    +
    +    The handler constructor requires a ``path`` argument, which specifies the
    +    local root directory of the content to be served.
    +
    +    Note that a capture group in the regex is required to parse the value for
    +    the ``path`` argument to the get() method (different than the constructor
    +    argument above); see `URLSpec` for details.
    +
    +    To serve a file like ``index.html`` automatically when a directory is
    +    requested, set ``static_handler_args=dict(default_filename="index.html")``
    +    in your application settings, or add ``default_filename`` as an initializer
    +    argument for your ``StaticFileHandler``.
    +
    +    To maximize the effectiveness of browser caching, this class supports
    +    versioned urls (by default using the argument ``?v=``).  If a version
    +    is given, we instruct the browser to cache this file indefinitely.
    +    `make_static_url` (also available as `RequestHandler.static_url`) can
    +    be used to construct a versioned url.
    +
    +    This handler is intended primarily for use in development and light-duty
    +    file serving; for heavy traffic it will be more efficient to use
    +    a dedicated static file server (such as nginx or Apache).  We support
    +    the HTTP ``Accept-Ranges`` mechanism to return partial content (because
    +    some browsers require this functionality to be present to seek in
    +    HTML5 audio or video).
    +
    +    **Subclassing notes**
    +
    +    This class is designed to be extensible by subclassing, but because
    +    of the way static urls are generated with class methods rather than
    +    instance methods, the inheritance patterns are somewhat unusual.
    +    Be sure to use the ``@classmethod`` decorator when overriding a
    +    class method.  Instance methods may use the attributes ``self.path``
    +    ``self.absolute_path``, and ``self.modified``.
    +
    +    Subclasses should only override methods discussed in this section;
    +    overriding other methods is error-prone.  Overriding
    +    ``StaticFileHandler.get`` is particularly problematic due to the
    +    tight coupling with ``compute_etag`` and other methods.
    +
    +    To change the way static urls are generated (e.g. to match the behavior
    +    of another server or CDN), override `make_static_url`, `parse_url_path`,
    +    `get_cache_time`, and/or `get_version`.
    +
    +    To replace all interaction with the filesystem (e.g. to serve
    +    static content from a database), override `get_content`,
    +    `get_content_size`, `get_modified_time`, `get_absolute_path`, and
    +    `validate_absolute_path`.
    +
    +    .. versionchanged:: 3.1
    +       Many of the methods for subclasses were added in Tornado 3.1.
    +    """
    +
    +    CACHE_MAX_AGE = 86400 * 365 * 10  # 10 years
    +
    +    _static_hashes = {}  # type: Dict[str, Optional[str]]
    +    _lock = threading.Lock()  # protects _static_hashes
    +
    +    def initialize(self, path: str, default_filename: Optional[str] = None) -> None:
    +        self.root = path
    +        self.default_filename = default_filename
    +
    +    @classmethod
    +    def reset(cls) -> None:
    +        with cls._lock:
    +            cls._static_hashes = {}
    +
    +    def head(self, path: str) -> Awaitable[None]:
    +        return self.get(path, include_body=False)
    +
    +    async def get(self, path: str, include_body: bool = True) -> None:
    +        # Set up our path instance variables.
    +        self.path = self.parse_url_path(path)
    +        del path  # make sure we don't refer to path instead of self.path again
    +        absolute_path = self.get_absolute_path(self.root, self.path)
    +        self.absolute_path = self.validate_absolute_path(self.root, absolute_path)
    +        if self.absolute_path is None:
    +            return
    +
    +        self.modified = self.get_modified_time()
    +        self.set_headers()
    +
    +        if self.should_return_304():
    +            self.set_status(304)
    +            return
    +
    +        request_range = None
    +        range_header = self.request.headers.get("Range")
    +        if range_header:
    +            # As per RFC 2616 14.16, if an invalid Range header is specified,
    +            # the request will be treated as if the header didn't exist.
    +            request_range = httputil._parse_request_range(range_header)
    +
    +        size = self.get_content_size()
    +        if request_range:
    +            start, end = request_range
    +            if start is not None and start < 0:
    +                start += size
    +                if start < 0:
    +                    start = 0
    +            if (
    +                start is not None
    +                and (start >= size or (end is not None and start >= end))
    +            ) or end == 0:
    +                # As per RFC 2616 14.35.1, a range is not satisfiable only: if
    +                # the first requested byte is equal to or greater than the
    +                # content, or when a suffix with length 0 is specified.
    +                # https://tools.ietf.org/html/rfc7233#section-2.1
    +                # A byte-range-spec is invalid if the last-byte-pos value is present
    +                # and less than the first-byte-pos.
    +                self.set_status(416)  # Range Not Satisfiable
    +                self.set_header("Content-Type", "text/plain")
    +                self.set_header("Content-Range", "bytes */%s" % (size,))
    +                return
    +            if end is not None and end > size:
    +                # Clients sometimes blindly use a large range to limit their
    +                # download size; cap the endpoint at the actual file size.
    +                end = size
    +            # Note: only return HTTP 206 if less than the entire range has been
    +            # requested. Not only is this semantically correct, but Chrome
    +            # refuses to play audio if it gets an HTTP 206 in response to
    +            # ``Range: bytes=0-``.
    +            if size != (end or size) - (start or 0):
    +                self.set_status(206)  # Partial Content
    +                self.set_header(
    +                    "Content-Range", httputil._get_content_range(start, end, size)
    +                )
    +        else:
    +            start = end = None
    +
    +        if start is not None and end is not None:
    +            content_length = end - start
    +        elif end is not None:
    +            content_length = end
    +        elif start is not None:
    +            content_length = size - start
    +        else:
    +            content_length = size
    +        self.set_header("Content-Length", content_length)
    +
    +        if include_body:
    +            content = self.get_content(self.absolute_path, start, end)
    +            if isinstance(content, bytes):
    +                content = [content]
    +            for chunk in content:
    +                try:
    +                    self.write(chunk)
    +                    await self.flush()
    +                except iostream.StreamClosedError:
    +                    return
    +        else:
    +            assert self.request.method == "HEAD"
    +
    +    def compute_etag(self) -> Optional[str]:
    +        """Sets the ``Etag`` header based on static url version.
    +
    +        This allows efficient ``If-None-Match`` checks against cached
    +        versions, and sends the correct ``Etag`` for a partial response
    +        (i.e. the same ``Etag`` as the full file).
    +
    +        .. versionadded:: 3.1
    +        """
    +        assert self.absolute_path is not None
    +        version_hash = self._get_cached_version(self.absolute_path)
    +        if not version_hash:
    +            return None
    +        return '"%s"' % (version_hash,)
    +
    +    def set_headers(self) -> None:
    +        """Sets the content and caching headers on the response.
    +
    +        .. versionadded:: 3.1
    +        """
    +        self.set_header("Accept-Ranges", "bytes")
    +        self.set_etag_header()
    +
    +        if self.modified is not None:
    +            self.set_header("Last-Modified", self.modified)
    +
    +        content_type = self.get_content_type()
    +        if content_type:
    +            self.set_header("Content-Type", content_type)
    +
    +        cache_time = self.get_cache_time(self.path, self.modified, content_type)
    +        if cache_time > 0:
    +            self.set_header(
    +                "Expires",
    +                datetime.datetime.utcnow() + datetime.timedelta(seconds=cache_time),
    +            )
    +            self.set_header("Cache-Control", "max-age=" + str(cache_time))
    +
    +        self.set_extra_headers(self.path)
    +
    +    def should_return_304(self) -> bool:
    +        """Returns True if the headers indicate that we should return 304.
    +
    +        .. versionadded:: 3.1
    +        """
    +        # If client sent If-None-Match, use it, ignore If-Modified-Since
    +        if self.request.headers.get("If-None-Match"):
    +            return self.check_etag_header()
    +
    +        # Check the If-Modified-Since, and don't send the result if the
    +        # content has not been modified
    +        ims_value = self.request.headers.get("If-Modified-Since")
    +        if ims_value is not None:
    +            date_tuple = email.utils.parsedate(ims_value)
    +            if date_tuple is not None:
    +                if_since = datetime.datetime(*date_tuple[:6])
    +                assert self.modified is not None
    +                if if_since >= self.modified:
    +                    return True
    +
    +        return False
    +
    +    @classmethod
    +    def get_absolute_path(cls, root: str, path: str) -> str:
    +        """Returns the absolute location of ``path`` relative to ``root``.
    +
    +        ``root`` is the path configured for this `StaticFileHandler`
    +        (in most cases the ``static_path`` `Application` setting).
    +
    +        This class method may be overridden in subclasses.  By default
    +        it returns a filesystem path, but other strings may be used
    +        as long as they are unique and understood by the subclass's
    +        overridden `get_content`.
    +
    +        .. versionadded:: 3.1
    +        """
    +        abspath = os.path.abspath(os.path.join(root, path))
    +        return abspath
    +
    +    def validate_absolute_path(self, root: str, absolute_path: str) -> Optional[str]:
    +        """Validate and return the absolute path.
    +
    +        ``root`` is the configured path for the `StaticFileHandler`,
    +        and ``path`` is the result of `get_absolute_path`
    +
    +        This is an instance method called during request processing,
    +        so it may raise `HTTPError` or use methods like
    +        `RequestHandler.redirect` (return None after redirecting to
    +        halt further processing).  This is where 404 errors for missing files
    +        are generated.
    +
    +        This method may modify the path before returning it, but note that
    +        any such modifications will not be understood by `make_static_url`.
    +
    +        In instance methods, this method's result is available as
    +        ``self.absolute_path``.
    +
    +        .. versionadded:: 3.1
    +        """
    +        # os.path.abspath strips a trailing /.
    +        # We must add it back to `root` so that we only match files
    +        # in a directory named `root` instead of files starting with
    +        # that prefix.
    +        root = os.path.abspath(root)
    +        if not root.endswith(os.path.sep):
    +            # abspath always removes a trailing slash, except when
    +            # root is '/'. This is an unusual case, but several projects
    +            # have independently discovered this technique to disable
    +            # Tornado's path validation and (hopefully) do their own,
    +            # so we need to support it.
    +            root += os.path.sep
    +        # The trailing slash also needs to be temporarily added back
    +        # the requested path so a request to root/ will match.
    +        if not (absolute_path + os.path.sep).startswith(root):
    +            raise HTTPError(403, "%s is not in root static directory", self.path)
    +        if os.path.isdir(absolute_path) and self.default_filename is not None:
    +            # need to look at the request.path here for when path is empty
    +            # but there is some prefix to the path that was already
    +            # trimmed by the routing
    +            if not self.request.path.endswith("/"):
    +                self.redirect(self.request.path + "/", permanent=True)
    +                return None
    +            absolute_path = os.path.join(absolute_path, self.default_filename)
    +        if not os.path.exists(absolute_path):
    +            raise HTTPError(404)
    +        if not os.path.isfile(absolute_path):
    +            raise HTTPError(403, "%s is not a file", self.path)
    +        return absolute_path
    +
    +    @classmethod
    +    def get_content(
    +        cls, abspath: str, start: Optional[int] = None, end: Optional[int] = None
    +    ) -> Generator[bytes, None, None]:
    +        """Retrieve the content of the requested resource which is located
    +        at the given absolute path.
    +
    +        This class method may be overridden by subclasses.  Note that its
    +        signature is different from other overridable class methods
    +        (no ``settings`` argument); this is deliberate to ensure that
    +        ``abspath`` is able to stand on its own as a cache key.
    +
    +        This method should either return a byte string or an iterator
    +        of byte strings.  The latter is preferred for large files
    +        as it helps reduce memory fragmentation.
    +
    +        .. versionadded:: 3.1
    +        """
    +        with open(abspath, "rb") as file:
    +            if start is not None:
    +                file.seek(start)
    +            if end is not None:
    +                remaining = end - (start or 0)  # type: Optional[int]
    +            else:
    +                remaining = None
    +            while True:
    +                chunk_size = 64 * 1024
    +                if remaining is not None and remaining < chunk_size:
    +                    chunk_size = remaining
    +                chunk = file.read(chunk_size)
    +                if chunk:
    +                    if remaining is not None:
    +                        remaining -= len(chunk)
    +                    yield chunk
    +                else:
    +                    if remaining is not None:
    +                        assert remaining == 0
    +                    return
    +
    +    @classmethod
    +    def get_content_version(cls, abspath: str) -> str:
    +        """Returns a version string for the resource at the given path.
    +
    +        This class method may be overridden by subclasses.  The
    +        default implementation is a SHA-512 hash of the file's contents.
    +
    +        .. versionadded:: 3.1
    +        """
    +        data = cls.get_content(abspath)
    +        hasher = hashlib.sha512()
    +        if isinstance(data, bytes):
    +            hasher.update(data)
    +        else:
    +            for chunk in data:
    +                hasher.update(chunk)
    +        return hasher.hexdigest()
    +
    +    def _stat(self) -> os.stat_result:
    +        assert self.absolute_path is not None
    +        if not hasattr(self, "_stat_result"):
    +            self._stat_result = os.stat(self.absolute_path)
    +        return self._stat_result
    +
    +    def get_content_size(self) -> int:
    +        """Retrieve the total size of the resource at the given path.
    +
    +        This method may be overridden by subclasses.
    +
    +        .. versionadded:: 3.1
    +
    +        .. versionchanged:: 4.0
    +           This method is now always called, instead of only when
    +           partial results are requested.
    +        """
    +        stat_result = self._stat()
    +        return stat_result.st_size
    +
    +    def get_modified_time(self) -> Optional[datetime.datetime]:
    +        """Returns the time that ``self.absolute_path`` was last modified.
    +
    +        May be overridden in subclasses.  Should return a `~datetime.datetime`
    +        object or None.
    +
    +        .. versionadded:: 3.1
    +        """
    +        stat_result = self._stat()
    +        # NOTE: Historically, this used stat_result[stat.ST_MTIME],
    +        # which truncates the fractional portion of the timestamp. It
    +        # was changed from that form to stat_result.st_mtime to
    +        # satisfy mypy (which disallows the bracket operator), but the
    +        # latter form returns a float instead of an int. For
    +        # consistency with the past (and because we have a unit test
    +        # that relies on this), we truncate the float here, although
    +        # I'm not sure that's the right thing to do.
    +        modified = datetime.datetime.utcfromtimestamp(int(stat_result.st_mtime))
    +        return modified
    +
    +    def get_content_type(self) -> str:
    +        """Returns the ``Content-Type`` header to be used for this request.
    +
    +        .. versionadded:: 3.1
    +        """
    +        assert self.absolute_path is not None
    +        mime_type, encoding = mimetypes.guess_type(self.absolute_path)
    +        # per RFC 6713, use the appropriate type for a gzip compressed file
    +        if encoding == "gzip":
    +            return "application/gzip"
    +        # As of 2015-07-21 there is no bzip2 encoding defined at
    +        # http://www.iana.org/assignments/media-types/media-types.xhtml
    +        # So for that (and any other encoding), use octet-stream.
    +        elif encoding is not None:
    +            return "application/octet-stream"
    +        elif mime_type is not None:
    +            return mime_type
    +        # if mime_type not detected, use application/octet-stream
    +        else:
    +            return "application/octet-stream"
    +
    +    def set_extra_headers(self, path: str) -> None:
    +        """For subclass to add extra headers to the response"""
    +        pass
    +
    +    def get_cache_time(
    +        self, path: str, modified: Optional[datetime.datetime], mime_type: str
    +    ) -> int:
    +        """Override to customize cache control behavior.
    +
    +        Return a positive number of seconds to make the result
    +        cacheable for that amount of time or 0 to mark resource as
    +        cacheable for an unspecified amount of time (subject to
    +        browser heuristics).
    +
    +        By default returns cache expiry of 10 years for resources requested
    +        with ``v`` argument.
    +        """
    +        return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0
    +
    +    @classmethod
    +    def make_static_url(
    +        cls, settings: Dict[str, Any], path: str, include_version: bool = True
    +    ) -> str:
    +        """Constructs a versioned url for the given path.
    +
    +        This method may be overridden in subclasses (but note that it
    +        is a class method rather than an instance method).  Subclasses
    +        are only required to implement the signature
    +        ``make_static_url(cls, settings, path)``; other keyword
    +        arguments may be passed through `~RequestHandler.static_url`
    +        but are not standard.
    +
    +        ``settings`` is the `Application.settings` dictionary.  ``path``
    +        is the static path being requested.  The url returned should be
    +        relative to the current host.
    +
    +        ``include_version`` determines whether the generated URL should
    +        include the query string containing the version hash of the
    +        file corresponding to the given ``path``.
    +
    +        """
    +        url = settings.get("static_url_prefix", "/static/") + path
    +        if not include_version:
    +            return url
    +
    +        version_hash = cls.get_version(settings, path)
    +        if not version_hash:
    +            return url
    +
    +        return "%s?v=%s" % (url, version_hash)
    +
    +    def parse_url_path(self, url_path: str) -> str:
    +        """Converts a static URL path into a filesystem path.
    +
    +        ``url_path`` is the path component of the URL with
    +        ``static_url_prefix`` removed.  The return value should be
    +        filesystem path relative to ``static_path``.
    +
    +        This is the inverse of `make_static_url`.
    +        """
    +        if os.path.sep != "/":
    +            url_path = url_path.replace("/", os.path.sep)
    +        return url_path
    +
    +    @classmethod
    +    def get_version(cls, settings: Dict[str, Any], path: str) -> Optional[str]:
    +        """Generate the version string to be used in static URLs.
    +
    +        ``settings`` is the `Application.settings` dictionary and ``path``
    +        is the relative location of the requested asset on the filesystem.
    +        The returned value should be a string, or ``None`` if no version
    +        could be determined.
    +
    +        .. versionchanged:: 3.1
    +           This method was previously recommended for subclasses to override;
    +           `get_content_version` is now preferred as it allows the base
    +           class to handle caching of the result.
    +        """
    +        abs_path = cls.get_absolute_path(settings["static_path"], path)
    +        return cls._get_cached_version(abs_path)
    +
    +    @classmethod
    +    def _get_cached_version(cls, abs_path: str) -> Optional[str]:
    +        with cls._lock:
    +            hashes = cls._static_hashes
    +            if abs_path not in hashes:
    +                try:
    +                    hashes[abs_path] = cls.get_content_version(abs_path)
    +                except Exception:
    +                    gen_log.error("Could not open static file %r", abs_path)
    +                    hashes[abs_path] = None
    +            hsh = hashes.get(abs_path)
    +            if hsh:
    +                return hsh
    +        return None
    +
    +
    +class FallbackHandler(RequestHandler):
    +    """A `RequestHandler` that wraps another HTTP server callback.
    +
    +    The fallback is a callable object that accepts an
    +    `~.httputil.HTTPServerRequest`, such as an `Application` or
    +    `tornado.wsgi.WSGIContainer`.  This is most useful to use both
    +    Tornado ``RequestHandlers`` and WSGI in the same server.  Typical
    +    usage::
    +
    +        wsgi_app = tornado.wsgi.WSGIContainer(
    +            django.core.handlers.wsgi.WSGIHandler())
    +        application = tornado.web.Application([
    +            (r"/foo", FooHandler),
    +            (r".*", FallbackHandler, dict(fallback=wsgi_app),
    +        ])
    +    """
    +
    +    def initialize(
    +        self, fallback: Callable[[httputil.HTTPServerRequest], None]
    +    ) -> None:
    +        self.fallback = fallback
    +
    +    def prepare(self) -> None:
    +        self.fallback(self.request)
    +        self._finished = True
    +        self.on_finish()
    +
    +
    +class OutputTransform(object):
    +    """A transform modifies the result of an HTTP request (e.g., GZip encoding)
    +
    +    Applications are not expected to create their own OutputTransforms
    +    or interact with them directly; the framework chooses which transforms
    +    (if any) to apply.
    +    """
    +
    +    def __init__(self, request: httputil.HTTPServerRequest) -> None:
    +        pass
    +
    +    def transform_first_chunk(
    +        self,
    +        status_code: int,
    +        headers: httputil.HTTPHeaders,
    +        chunk: bytes,
    +        finishing: bool,
    +    ) -> Tuple[int, httputil.HTTPHeaders, bytes]:
    +        return status_code, headers, chunk
    +
    +    def transform_chunk(self, chunk: bytes, finishing: bool) -> bytes:
    +        return chunk
    +
    +
    +class GZipContentEncoding(OutputTransform):
    +    """Applies the gzip content encoding to the response.
    +
    +    See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
    +
    +    .. versionchanged:: 4.0
    +        Now compresses all mime types beginning with ``text/``, instead
    +        of just a whitelist. (the whitelist is still used for certain
    +        non-text mime types).
    +    """
    +
    +    # Whitelist of compressible mime types (in addition to any types
    +    # beginning with "text/").
    +    CONTENT_TYPES = set(
    +        [
    +            "application/javascript",
    +            "application/x-javascript",
    +            "application/xml",
    +            "application/atom+xml",
    +            "application/json",
    +            "application/xhtml+xml",
    +            "image/svg+xml",
    +        ]
    +    )
    +    # Python's GzipFile defaults to level 9, while most other gzip
    +    # tools (including gzip itself) default to 6, which is probably a
    +    # better CPU/size tradeoff.
    +    GZIP_LEVEL = 6
    +    # Responses that are too short are unlikely to benefit from gzipping
    +    # after considering the "Content-Encoding: gzip" header and the header
    +    # inside the gzip encoding.
    +    # Note that responses written in multiple chunks will be compressed
    +    # regardless of size.
    +    MIN_LENGTH = 1024
    +
    +    def __init__(self, request: httputil.HTTPServerRequest) -> None:
    +        self._gzipping = "gzip" in request.headers.get("Accept-Encoding", "")
    +
    +    def _compressible_type(self, ctype: str) -> bool:
    +        return ctype.startswith("text/") or ctype in self.CONTENT_TYPES
    +
    +    def transform_first_chunk(
    +        self,
    +        status_code: int,
    +        headers: httputil.HTTPHeaders,
    +        chunk: bytes,
    +        finishing: bool,
    +    ) -> Tuple[int, httputil.HTTPHeaders, bytes]:
    +        # TODO: can/should this type be inherited from the superclass?
    +        if "Vary" in headers:
    +            headers["Vary"] += ", Accept-Encoding"
    +        else:
    +            headers["Vary"] = "Accept-Encoding"
    +        if self._gzipping:
    +            ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
    +            self._gzipping = (
    +                self._compressible_type(ctype)
    +                and (not finishing or len(chunk) >= self.MIN_LENGTH)
    +                and ("Content-Encoding" not in headers)
    +            )
    +        if self._gzipping:
    +            headers["Content-Encoding"] = "gzip"
    +            self._gzip_value = BytesIO()
    +            self._gzip_file = gzip.GzipFile(
    +                mode="w", fileobj=self._gzip_value, compresslevel=self.GZIP_LEVEL
    +            )
    +            chunk = self.transform_chunk(chunk, finishing)
    +            if "Content-Length" in headers:
    +                # The original content length is no longer correct.
    +                # If this is the last (and only) chunk, we can set the new
    +                # content-length; otherwise we remove it and fall back to
    +                # chunked encoding.
    +                if finishing:
    +                    headers["Content-Length"] = str(len(chunk))
    +                else:
    +                    del headers["Content-Length"]
    +        return status_code, headers, chunk
    +
    +    def transform_chunk(self, chunk: bytes, finishing: bool) -> bytes:
    +        if self._gzipping:
    +            self._gzip_file.write(chunk)
    +            if finishing:
    +                self._gzip_file.close()
    +            else:
    +                self._gzip_file.flush()
    +            chunk = self._gzip_value.getvalue()
    +            self._gzip_value.truncate(0)
    +            self._gzip_value.seek(0)
    +        return chunk
    +
    +
    +def authenticated(
    +    method: Callable[..., Optional[Awaitable[None]]]
    +) -> Callable[..., Optional[Awaitable[None]]]:
    +    """Decorate methods with this to require that the user be logged in.
    +
    +    If the user is not logged in, they will be redirected to the configured
    +    `login url `.
    +
    +    If you configure a login url with a query parameter, Tornado will
    +    assume you know what you're doing and use it as-is.  If not, it
    +    will add a `next` parameter so the login page knows where to send
    +    you once you're logged in.
    +    """
    +
    +    @functools.wraps(method)
    +    def wrapper(  # type: ignore
    +        self: RequestHandler, *args, **kwargs
    +    ) -> Optional[Awaitable[None]]:
    +        if not self.current_user:
    +            if self.request.method in ("GET", "HEAD"):
    +                url = self.get_login_url()
    +                if "?" not in url:
    +                    if urllib.parse.urlsplit(url).scheme:
    +                        # if login url is absolute, make next absolute too
    +                        next_url = self.request.full_url()
    +                    else:
    +                        assert self.request.uri is not None
    +                        next_url = self.request.uri
    +                    url += "?" + urlencode(dict(next=next_url))
    +                self.redirect(url)
    +                return None
    +            raise HTTPError(403)
    +        return method(self, *args, **kwargs)
    +
    +    return wrapper
    +
    +
    +class UIModule(object):
    +    """A re-usable, modular UI unit on a page.
    +
    +    UI modules often execute additional queries, and they can include
    +    additional CSS and JavaScript that will be included in the output
    +    page, which is automatically inserted on page render.
    +
    +    Subclasses of UIModule must override the `render` method.
    +    """
    +
    +    def __init__(self, handler: RequestHandler) -> None:
    +        self.handler = handler
    +        self.request = handler.request
    +        self.ui = handler.ui
    +        self.locale = handler.locale
    +
    +    @property
    +    def current_user(self) -> Any:
    +        return self.handler.current_user
    +
    +    def render(self, *args: Any, **kwargs: Any) -> str:
    +        """Override in subclasses to return this module's output."""
    +        raise NotImplementedError()
    +
    +    def embedded_javascript(self) -> Optional[str]:
    +        """Override to return a JavaScript string
    +        to be embedded in the page."""
    +        return None
    +
    +    def javascript_files(self) -> Optional[Iterable[str]]:
    +        """Override to return a list of JavaScript files needed by this module.
    +
    +        If the return values are relative paths, they will be passed to
    +        `RequestHandler.static_url`; otherwise they will be used as-is.
    +        """
    +        return None
    +
    +    def embedded_css(self) -> Optional[str]:
    +        """Override to return a CSS string
    +        that will be embedded in the page."""
    +        return None
    +
    +    def css_files(self) -> Optional[Iterable[str]]:
    +        """Override to returns a list of CSS files required by this module.
    +
    +        If the return values are relative paths, they will be passed to
    +        `RequestHandler.static_url`; otherwise they will be used as-is.
    +        """
    +        return None
    +
    +    def html_head(self) -> Optional[str]:
    +        """Override to return an HTML string that will be put in the 
    +        element.
    +        """
    +        return None
    +
    +    def html_body(self) -> Optional[str]:
    +        """Override to return an HTML string that will be put at the end of
    +        the  element.
    +        """
    +        return None
    +
    +    def render_string(self, path: str, **kwargs: Any) -> bytes:
    +        """Renders a template and returns it as a string."""
    +        return self.handler.render_string(path, **kwargs)
    +
    +
    +class _linkify(UIModule):
    +    def render(self, text: str, **kwargs: Any) -> str:  # type: ignore
    +        return escape.linkify(text, **kwargs)
    +
    +
    +class _xsrf_form_html(UIModule):
    +    def render(self) -> str:  # type: ignore
    +        return self.handler.xsrf_form_html()
    +
    +
    +class TemplateModule(UIModule):
    +    """UIModule that simply renders the given template.
    +
    +    {% module Template("foo.html") %} is similar to {% include "foo.html" %},
    +    but the module version gets its own namespace (with kwargs passed to
    +    Template()) instead of inheriting the outer template's namespace.
    +
    +    Templates rendered through this module also get access to UIModule's
    +    automatic JavaScript/CSS features.  Simply call set_resources
    +    inside the template and give it keyword arguments corresponding to
    +    the methods on UIModule: {{ set_resources(js_files=static_url("my.js")) }}
    +    Note that these resources are output once per template file, not once
    +    per instantiation of the template, so they must not depend on
    +    any arguments to the template.
    +    """
    +
    +    def __init__(self, handler: RequestHandler) -> None:
    +        super().__init__(handler)
    +        # keep resources in both a list and a dict to preserve order
    +        self._resource_list = []  # type: List[Dict[str, Any]]
    +        self._resource_dict = {}  # type: Dict[str, Dict[str, Any]]
    +
    +    def render(self, path: str, **kwargs: Any) -> bytes:  # type: ignore
    +        def set_resources(**kwargs) -> str:  # type: ignore
    +            if path not in self._resource_dict:
    +                self._resource_list.append(kwargs)
    +                self._resource_dict[path] = kwargs
    +            else:
    +                if self._resource_dict[path] != kwargs:
    +                    raise ValueError(
    +                        "set_resources called with different "
    +                        "resources for the same template"
    +                    )
    +            return ""
    +
    +        return self.render_string(path, set_resources=set_resources, **kwargs)
    +
    +    def _get_resources(self, key: str) -> Iterable[str]:
    +        return (r[key] for r in self._resource_list if key in r)
    +
    +    def embedded_javascript(self) -> str:
    +        return "\n".join(self._get_resources("embedded_javascript"))
    +
    +    def javascript_files(self) -> Iterable[str]:
    +        result = []
    +        for f in self._get_resources("javascript_files"):
    +            if isinstance(f, (unicode_type, bytes)):
    +                result.append(f)
    +            else:
    +                result.extend(f)
    +        return result
    +
    +    def embedded_css(self) -> str:
    +        return "\n".join(self._get_resources("embedded_css"))
    +
    +    def css_files(self) -> Iterable[str]:
    +        result = []
    +        for f in self._get_resources("css_files"):
    +            if isinstance(f, (unicode_type, bytes)):
    +                result.append(f)
    +            else:
    +                result.extend(f)
    +        return result
    +
    +    def html_head(self) -> str:
    +        return "".join(self._get_resources("html_head"))
    +
    +    def html_body(self) -> str:
    +        return "".join(self._get_resources("html_body"))
    +
    +
    +class _UIModuleNamespace(object):
    +    """Lazy namespace which creates UIModule proxies bound to a handler."""
    +
    +    def __init__(
    +        self, handler: RequestHandler, ui_modules: Dict[str, Type[UIModule]]
    +    ) -> None:
    +        self.handler = handler
    +        self.ui_modules = ui_modules
    +
    +    def __getitem__(self, key: str) -> Callable[..., str]:
    +        return self.handler._ui_module(key, self.ui_modules[key])
    +
    +    def __getattr__(self, key: str) -> Callable[..., str]:
    +        try:
    +            return self[key]
    +        except KeyError as e:
    +            raise AttributeError(str(e))
    +
    +
    +def create_signed_value(
    +    secret: _CookieSecretTypes,
    +    name: str,
    +    value: Union[str, bytes],
    +    version: Optional[int] = None,
    +    clock: Optional[Callable[[], float]] = None,
    +    key_version: Optional[int] = None,
    +) -> bytes:
    +    if version is None:
    +        version = DEFAULT_SIGNED_VALUE_VERSION
    +    if clock is None:
    +        clock = time.time
    +
    +    timestamp = utf8(str(int(clock())))
    +    value = base64.b64encode(utf8(value))
    +    if version == 1:
    +        assert not isinstance(secret, dict)
    +        signature = _create_signature_v1(secret, name, value, timestamp)
    +        value = b"|".join([value, timestamp, signature])
    +        return value
    +    elif version == 2:
    +        # The v2 format consists of a version number and a series of
    +        # length-prefixed fields "%d:%s", the last of which is a
    +        # signature, all separated by pipes.  All numbers are in
    +        # decimal format with no leading zeros.  The signature is an
    +        # HMAC-SHA256 of the whole string up to that point, including
    +        # the final pipe.
    +        #
    +        # The fields are:
    +        # - format version (i.e. 2; no length prefix)
    +        # - key version (integer, default is 0)
    +        # - timestamp (integer seconds since epoch)
    +        # - name (not encoded; assumed to be ~alphanumeric)
    +        # - value (base64-encoded)
    +        # - signature (hex-encoded; no length prefix)
    +        def format_field(s: Union[str, bytes]) -> bytes:
    +            return utf8("%d:" % len(s)) + utf8(s)
    +
    +        to_sign = b"|".join(
    +            [
    +                b"2",
    +                format_field(str(key_version or 0)),
    +                format_field(timestamp),
    +                format_field(name),
    +                format_field(value),
    +                b"",
    +            ]
    +        )
    +
    +        if isinstance(secret, dict):
    +            assert (
    +                key_version is not None
    +            ), "Key version must be set when sign key dict is used"
    +            assert version >= 2, "Version must be at least 2 for key version support"
    +            secret = secret[key_version]
    +
    +        signature = _create_signature_v2(secret, to_sign)
    +        return to_sign + signature
    +    else:
    +        raise ValueError("Unsupported version %d" % version)
    +
    +
    +# A leading version number in decimal
    +# with no leading zeros, followed by a pipe.
    +_signed_value_version_re = re.compile(br"^([1-9][0-9]*)\|(.*)$")
    +
    +
    +def _get_version(value: bytes) -> int:
    +    # Figures out what version value is.  Version 1 did not include an
    +    # explicit version field and started with arbitrary base64 data,
    +    # which makes this tricky.
    +    m = _signed_value_version_re.match(value)
    +    if m is None:
    +        version = 1
    +    else:
    +        try:
    +            version = int(m.group(1))
    +            if version > 999:
    +                # Certain payloads from the version-less v1 format may
    +                # be parsed as valid integers.  Due to base64 padding
    +                # restrictions, this can only happen for numbers whose
    +                # length is a multiple of 4, so we can treat all
    +                # numbers up to 999 as versions, and for the rest we
    +                # fall back to v1 format.
    +                version = 1
    +        except ValueError:
    +            version = 1
    +    return version
    +
    +
    +def decode_signed_value(
    +    secret: _CookieSecretTypes,
    +    name: str,
    +    value: Union[None, str, bytes],
    +    max_age_days: float = 31,
    +    clock: Optional[Callable[[], float]] = None,
    +    min_version: Optional[int] = None,
    +) -> Optional[bytes]:
    +    if clock is None:
    +        clock = time.time
    +    if min_version is None:
    +        min_version = DEFAULT_SIGNED_VALUE_MIN_VERSION
    +    if min_version > 2:
    +        raise ValueError("Unsupported min_version %d" % min_version)
    +    if not value:
    +        return None
    +
    +    value = utf8(value)
    +    version = _get_version(value)
    +
    +    if version < min_version:
    +        return None
    +    if version == 1:
    +        assert not isinstance(secret, dict)
    +        return _decode_signed_value_v1(secret, name, value, max_age_days, clock)
    +    elif version == 2:
    +        return _decode_signed_value_v2(secret, name, value, max_age_days, clock)
    +    else:
    +        return None
    +
    +
    +def _decode_signed_value_v1(
    +    secret: Union[str, bytes],
    +    name: str,
    +    value: bytes,
    +    max_age_days: float,
    +    clock: Callable[[], float],
    +) -> Optional[bytes]:
    +    parts = utf8(value).split(b"|")
    +    if len(parts) != 3:
    +        return None
    +    signature = _create_signature_v1(secret, name, parts[0], parts[1])
    +    if not hmac.compare_digest(parts[2], signature):
    +        gen_log.warning("Invalid cookie signature %r", value)
    +        return None
    +    timestamp = int(parts[1])
    +    if timestamp < clock() - max_age_days * 86400:
    +        gen_log.warning("Expired cookie %r", value)
    +        return None
    +    if timestamp > clock() + 31 * 86400:
    +        # _cookie_signature does not hash a delimiter between the
    +        # parts of the cookie, so an attacker could transfer trailing
    +        # digits from the payload to the timestamp without altering the
    +        # signature.  For backwards compatibility, sanity-check timestamp
    +        # here instead of modifying _cookie_signature.
    +        gen_log.warning("Cookie timestamp in future; possible tampering %r", value)
    +        return None
    +    if parts[1].startswith(b"0"):
    +        gen_log.warning("Tampered cookie %r", value)
    +        return None
    +    try:
    +        return base64.b64decode(parts[0])
    +    except Exception:
    +        return None
    +
    +
    +def _decode_fields_v2(value: bytes) -> Tuple[int, bytes, bytes, bytes, bytes]:
    +    def _consume_field(s: bytes) -> Tuple[bytes, bytes]:
    +        length, _, rest = s.partition(b":")
    +        n = int(length)
    +        field_value = rest[:n]
    +        # In python 3, indexing bytes returns small integers; we must
    +        # use a slice to get a byte string as in python 2.
    +        if rest[n : n + 1] != b"|":
    +            raise ValueError("malformed v2 signed value field")
    +        rest = rest[n + 1 :]
    +        return field_value, rest
    +
    +    rest = value[2:]  # remove version number
    +    key_version, rest = _consume_field(rest)
    +    timestamp, rest = _consume_field(rest)
    +    name_field, rest = _consume_field(rest)
    +    value_field, passed_sig = _consume_field(rest)
    +    return int(key_version), timestamp, name_field, value_field, passed_sig
    +
    +
    +def _decode_signed_value_v2(
    +    secret: _CookieSecretTypes,
    +    name: str,
    +    value: bytes,
    +    max_age_days: float,
    +    clock: Callable[[], float],
    +) -> Optional[bytes]:
    +    try:
    +        (
    +            key_version,
    +            timestamp_bytes,
    +            name_field,
    +            value_field,
    +            passed_sig,
    +        ) = _decode_fields_v2(value)
    +    except ValueError:
    +        return None
    +    signed_string = value[: -len(passed_sig)]
    +
    +    if isinstance(secret, dict):
    +        try:
    +            secret = secret[key_version]
    +        except KeyError:
    +            return None
    +
    +    expected_sig = _create_signature_v2(secret, signed_string)
    +    if not hmac.compare_digest(passed_sig, expected_sig):
    +        return None
    +    if name_field != utf8(name):
    +        return None
    +    timestamp = int(timestamp_bytes)
    +    if timestamp < clock() - max_age_days * 86400:
    +        # The signature has expired.
    +        return None
    +    try:
    +        return base64.b64decode(value_field)
    +    except Exception:
    +        return None
    +
    +
    +def get_signature_key_version(value: Union[str, bytes]) -> Optional[int]:
    +    value = utf8(value)
    +    version = _get_version(value)
    +    if version < 2:
    +        return None
    +    try:
    +        key_version, _, _, _, _ = _decode_fields_v2(value)
    +    except ValueError:
    +        return None
    +
    +    return key_version
    +
    +
    +def _create_signature_v1(secret: Union[str, bytes], *parts: Union[str, bytes]) -> bytes:
    +    hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
    +    for part in parts:
    +        hash.update(utf8(part))
    +    return utf8(hash.hexdigest())
    +
    +
    +def _create_signature_v2(secret: Union[str, bytes], s: bytes) -> bytes:
    +    hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
    +    hash.update(utf8(s))
    +    return utf8(hash.hexdigest())
    +
    +
    +def is_absolute(path: str) -> bool:
    +    return any(path.startswith(x) for x in ["/", "http:", "https:"])
    diff --git a/telegramer/include/tornado/websocket.py b/telegramer/include/tornado/websocket.py
    new file mode 100644
    index 0000000..eef49e7
    --- /dev/null
    +++ b/telegramer/include/tornado/websocket.py
    @@ -0,0 +1,1666 @@
    +"""Implementation of the WebSocket protocol.
    +
    +`WebSockets `_ allow for bidirectional
    +communication between the browser and server.
    +
    +WebSockets are supported in the current versions of all major browsers,
    +although older versions that do not support WebSockets are still in use
    +(refer to http://caniuse.com/websockets for details).
    +
    +This module implements the final version of the WebSocket protocol as
    +defined in `RFC 6455 `_.  Certain
    +browser versions (notably Safari 5.x) implemented an earlier draft of
    +the protocol (known as "draft 76") and are not compatible with this module.
    +
    +.. versionchanged:: 4.0
    +   Removed support for the draft 76 protocol version.
    +"""
    +
    +import abc
    +import asyncio
    +import base64
    +import hashlib
    +import os
    +import sys
    +import struct
    +import tornado.escape
    +import tornado.web
    +from urllib.parse import urlparse
    +import zlib
    +
    +from tornado.concurrent import Future, future_set_result_unless_cancelled
    +from tornado.escape import utf8, native_str, to_unicode
    +from tornado import gen, httpclient, httputil
    +from tornado.ioloop import IOLoop, PeriodicCallback
    +from tornado.iostream import StreamClosedError, IOStream
    +from tornado.log import gen_log, app_log
    +from tornado import simple_httpclient
    +from tornado.queues import Queue
    +from tornado.tcpclient import TCPClient
    +from tornado.util import _websocket_mask
    +
    +from typing import (
    +    TYPE_CHECKING,
    +    cast,
    +    Any,
    +    Optional,
    +    Dict,
    +    Union,
    +    List,
    +    Awaitable,
    +    Callable,
    +    Tuple,
    +    Type,
    +)
    +from types import TracebackType
    +
    +if TYPE_CHECKING:
    +    from typing_extensions import Protocol
    +
    +    # The zlib compressor types aren't actually exposed anywhere
    +    # publicly, so declare protocols for the portions we use.
    +    class _Compressor(Protocol):
    +        def compress(self, data: bytes) -> bytes:
    +            pass
    +
    +        def flush(self, mode: int) -> bytes:
    +            pass
    +
    +    class _Decompressor(Protocol):
    +        unconsumed_tail = b""  # type: bytes
    +
    +        def decompress(self, data: bytes, max_length: int) -> bytes:
    +            pass
    +
    +    class _WebSocketDelegate(Protocol):
    +        # The common base interface implemented by WebSocketHandler on
    +        # the server side and WebSocketClientConnection on the client
    +        # side.
    +        def on_ws_connection_close(
    +            self, close_code: Optional[int] = None, close_reason: Optional[str] = None
    +        ) -> None:
    +            pass
    +
    +        def on_message(self, message: Union[str, bytes]) -> Optional["Awaitable[None]"]:
    +            pass
    +
    +        def on_ping(self, data: bytes) -> None:
    +            pass
    +
    +        def on_pong(self, data: bytes) -> None:
    +            pass
    +
    +        def log_exception(
    +            self,
    +            typ: Optional[Type[BaseException]],
    +            value: Optional[BaseException],
    +            tb: Optional[TracebackType],
    +        ) -> None:
    +            pass
    +
    +
    +_default_max_message_size = 10 * 1024 * 1024
    +
    +
    +class WebSocketError(Exception):
    +    pass
    +
    +
    +class WebSocketClosedError(WebSocketError):
    +    """Raised by operations on a closed connection.
    +
    +    .. versionadded:: 3.2
    +    """
    +
    +    pass
    +
    +
    +class _DecompressTooLargeError(Exception):
    +    pass
    +
    +
    +class _WebSocketParams(object):
    +    def __init__(
    +        self,
    +        ping_interval: Optional[float] = None,
    +        ping_timeout: Optional[float] = None,
    +        max_message_size: int = _default_max_message_size,
    +        compression_options: Optional[Dict[str, Any]] = None,
    +    ) -> None:
    +        self.ping_interval = ping_interval
    +        self.ping_timeout = ping_timeout
    +        self.max_message_size = max_message_size
    +        self.compression_options = compression_options
    +
    +
    +class WebSocketHandler(tornado.web.RequestHandler):
    +    """Subclass this class to create a basic WebSocket handler.
    +
    +    Override `on_message` to handle incoming messages, and use
    +    `write_message` to send messages to the client. You can also
    +    override `open` and `on_close` to handle opened and closed
    +    connections.
    +
    +    Custom upgrade response headers can be sent by overriding
    +    `~tornado.web.RequestHandler.set_default_headers` or
    +    `~tornado.web.RequestHandler.prepare`.
    +
    +    See http://dev.w3.org/html5/websockets/ for details on the
    +    JavaScript interface.  The protocol is specified at
    +    http://tools.ietf.org/html/rfc6455.
    +
    +    Here is an example WebSocket handler that echos back all received messages
    +    back to the client:
    +
    +    .. testcode::
    +
    +      class EchoWebSocket(tornado.websocket.WebSocketHandler):
    +          def open(self):
    +              print("WebSocket opened")
    +
    +          def on_message(self, message):
    +              self.write_message(u"You said: " + message)
    +
    +          def on_close(self):
    +              print("WebSocket closed")
    +
    +    .. testoutput::
    +       :hide:
    +
    +    WebSockets are not standard HTTP connections. The "handshake" is
    +    HTTP, but after the handshake, the protocol is
    +    message-based. Consequently, most of the Tornado HTTP facilities
    +    are not available in handlers of this type. The only communication
    +    methods available to you are `write_message()`, `ping()`, and
    +    `close()`. Likewise, your request handler class should implement
    +    `open()` method rather than ``get()`` or ``post()``.
    +
    +    If you map the handler above to ``/websocket`` in your application, you can
    +    invoke it in JavaScript with::
    +
    +      var ws = new WebSocket("ws://localhost:8888/websocket");
    +      ws.onopen = function() {
    +         ws.send("Hello, world");
    +      };
    +      ws.onmessage = function (evt) {
    +         alert(evt.data);
    +      };
    +
    +    This script pops up an alert box that says "You said: Hello, world".
    +
    +    Web browsers allow any site to open a websocket connection to any other,
    +    instead of using the same-origin policy that governs other network
    +    access from JavaScript.  This can be surprising and is a potential
    +    security hole, so since Tornado 4.0 `WebSocketHandler` requires
    +    applications that wish to receive cross-origin websockets to opt in
    +    by overriding the `~WebSocketHandler.check_origin` method (see that
    +    method's docs for details).  Failure to do so is the most likely
    +    cause of 403 errors when making a websocket connection.
    +
    +    When using a secure websocket connection (``wss://``) with a self-signed
    +    certificate, the connection from a browser may fail because it wants
    +    to show the "accept this certificate" dialog but has nowhere to show it.
    +    You must first visit a regular HTML page using the same certificate
    +    to accept it before the websocket connection will succeed.
    +
    +    If the application setting ``websocket_ping_interval`` has a non-zero
    +    value, a ping will be sent periodically, and the connection will be
    +    closed if a response is not received before the ``websocket_ping_timeout``.
    +
    +    Messages larger than the ``websocket_max_message_size`` application setting
    +    (default 10MiB) will not be accepted.
    +
    +    .. versionchanged:: 4.5
    +       Added ``websocket_ping_interval``, ``websocket_ping_timeout``, and
    +       ``websocket_max_message_size``.
    +    """
    +
    +    def __init__(
    +        self,
    +        application: tornado.web.Application,
    +        request: httputil.HTTPServerRequest,
    +        **kwargs: Any
    +    ) -> None:
    +        super().__init__(application, request, **kwargs)
    +        self.ws_connection = None  # type: Optional[WebSocketProtocol]
    +        self.close_code = None  # type: Optional[int]
    +        self.close_reason = None  # type: Optional[str]
    +        self.stream = None  # type: Optional[IOStream]
    +        self._on_close_called = False
    +
    +    async def get(self, *args: Any, **kwargs: Any) -> None:
    +        self.open_args = args
    +        self.open_kwargs = kwargs
    +
    +        # Upgrade header should be present and should be equal to WebSocket
    +        if self.request.headers.get("Upgrade", "").lower() != "websocket":
    +            self.set_status(400)
    +            log_msg = 'Can "Upgrade" only to "WebSocket".'
    +            self.finish(log_msg)
    +            gen_log.debug(log_msg)
    +            return
    +
    +        # Connection header should be upgrade.
    +        # Some proxy servers/load balancers
    +        # might mess with it.
    +        headers = self.request.headers
    +        connection = map(
    +            lambda s: s.strip().lower(), headers.get("Connection", "").split(",")
    +        )
    +        if "upgrade" not in connection:
    +            self.set_status(400)
    +            log_msg = '"Connection" must be "Upgrade".'
    +            self.finish(log_msg)
    +            gen_log.debug(log_msg)
    +            return
    +
    +        # Handle WebSocket Origin naming convention differences
    +        # The difference between version 8 and 13 is that in 8 the
    +        # client sends a "Sec-Websocket-Origin" header and in 13 it's
    +        # simply "Origin".
    +        if "Origin" in self.request.headers:
    +            origin = self.request.headers.get("Origin")
    +        else:
    +            origin = self.request.headers.get("Sec-Websocket-Origin", None)
    +
    +        # If there was an origin header, check to make sure it matches
    +        # according to check_origin. When the origin is None, we assume it
    +        # did not come from a browser and that it can be passed on.
    +        if origin is not None and not self.check_origin(origin):
    +            self.set_status(403)
    +            log_msg = "Cross origin websockets not allowed"
    +            self.finish(log_msg)
    +            gen_log.debug(log_msg)
    +            return
    +
    +        self.ws_connection = self.get_websocket_protocol()
    +        if self.ws_connection:
    +            await self.ws_connection.accept_connection(self)
    +        else:
    +            self.set_status(426, "Upgrade Required")
    +            self.set_header("Sec-WebSocket-Version", "7, 8, 13")
    +
    +    @property
    +    def ping_interval(self) -> Optional[float]:
    +        """The interval for websocket keep-alive pings.
    +
    +        Set websocket_ping_interval = 0 to disable pings.
    +        """
    +        return self.settings.get("websocket_ping_interval", None)
    +
    +    @property
    +    def ping_timeout(self) -> Optional[float]:
    +        """If no ping is received in this many seconds,
    +        close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
    +        Default is max of 3 pings or 30 seconds.
    +        """
    +        return self.settings.get("websocket_ping_timeout", None)
    +
    +    @property
    +    def max_message_size(self) -> int:
    +        """Maximum allowed message size.
    +
    +        If the remote peer sends a message larger than this, the connection
    +        will be closed.
    +
    +        Default is 10MiB.
    +        """
    +        return self.settings.get(
    +            "websocket_max_message_size", _default_max_message_size
    +        )
    +
    +    def write_message(
    +        self, message: Union[bytes, str, Dict[str, Any]], binary: bool = False
    +    ) -> "Future[None]":
    +        """Sends the given message to the client of this Web Socket.
    +
    +        The message may be either a string or a dict (which will be
    +        encoded as json).  If the ``binary`` argument is false, the
    +        message will be sent as utf8; in binary mode any byte string
    +        is allowed.
    +
    +        If the connection is already closed, raises `WebSocketClosedError`.
    +        Returns a `.Future` which can be used for flow control.
    +
    +        .. versionchanged:: 3.2
    +           `WebSocketClosedError` was added (previously a closed connection
    +           would raise an `AttributeError`)
    +
    +        .. versionchanged:: 4.3
    +           Returns a `.Future` which can be used for flow control.
    +
    +        .. versionchanged:: 5.0
    +           Consistently raises `WebSocketClosedError`. Previously could
    +           sometimes raise `.StreamClosedError`.
    +        """
    +        if self.ws_connection is None or self.ws_connection.is_closing():
    +            raise WebSocketClosedError()
    +        if isinstance(message, dict):
    +            message = tornado.escape.json_encode(message)
    +        return self.ws_connection.write_message(message, binary=binary)
    +
    +    def select_subprotocol(self, subprotocols: List[str]) -> Optional[str]:
    +        """Override to implement subprotocol negotiation.
    +
    +        ``subprotocols`` is a list of strings identifying the
    +        subprotocols proposed by the client.  This method may be
    +        overridden to return one of those strings to select it, or
    +        ``None`` to not select a subprotocol.
    +
    +        Failure to select a subprotocol does not automatically abort
    +        the connection, although clients may close the connection if
    +        none of their proposed subprotocols was selected.
    +
    +        The list may be empty, in which case this method must return
    +        None. This method is always called exactly once even if no
    +        subprotocols were proposed so that the handler can be advised
    +        of this fact.
    +
    +        .. versionchanged:: 5.1
    +
    +           Previously, this method was called with a list containing
    +           an empty string instead of an empty list if no subprotocols
    +           were proposed by the client.
    +        """
    +        return None
    +
    +    @property
    +    def selected_subprotocol(self) -> Optional[str]:
    +        """The subprotocol returned by `select_subprotocol`.
    +
    +        .. versionadded:: 5.1
    +        """
    +        assert self.ws_connection is not None
    +        return self.ws_connection.selected_subprotocol
    +
    +    def get_compression_options(self) -> Optional[Dict[str, Any]]:
    +        """Override to return compression options for the connection.
    +
    +        If this method returns None (the default), compression will
    +        be disabled.  If it returns a dict (even an empty one), it
    +        will be enabled.  The contents of the dict may be used to
    +        control the following compression options:
    +
    +        ``compression_level`` specifies the compression level.
    +
    +        ``mem_level`` specifies the amount of memory used for the internal compression state.
    +
    +         These parameters are documented in details here:
    +         https://docs.python.org/3.6/library/zlib.html#zlib.compressobj
    +
    +        .. versionadded:: 4.1
    +
    +        .. versionchanged:: 4.5
    +
    +           Added ``compression_level`` and ``mem_level``.
    +        """
    +        # TODO: Add wbits option.
    +        return None
    +
    +    def open(self, *args: str, **kwargs: str) -> Optional[Awaitable[None]]:
    +        """Invoked when a new WebSocket is opened.
    +
    +        The arguments to `open` are extracted from the `tornado.web.URLSpec`
    +        regular expression, just like the arguments to
    +        `tornado.web.RequestHandler.get`.
    +
    +        `open` may be a coroutine. `on_message` will not be called until
    +        `open` has returned.
    +
    +        .. versionchanged:: 5.1
    +
    +           ``open`` may be a coroutine.
    +        """
    +        pass
    +
    +    def on_message(self, message: Union[str, bytes]) -> Optional[Awaitable[None]]:
    +        """Handle incoming messages on the WebSocket
    +
    +        This method must be overridden.
    +
    +        .. versionchanged:: 4.5
    +
    +           ``on_message`` can be a coroutine.
    +        """
    +        raise NotImplementedError
    +
    +    def ping(self, data: Union[str, bytes] = b"") -> None:
    +        """Send ping frame to the remote end.
    +
    +        The data argument allows a small amount of data (up to 125
    +        bytes) to be sent as a part of the ping message. Note that not
    +        all websocket implementations expose this data to
    +        applications.
    +
    +        Consider using the ``websocket_ping_interval`` application
    +        setting instead of sending pings manually.
    +
    +        .. versionchanged:: 5.1
    +
    +           The data argument is now optional.
    +
    +        """
    +        data = utf8(data)
    +        if self.ws_connection is None or self.ws_connection.is_closing():
    +            raise WebSocketClosedError()
    +        self.ws_connection.write_ping(data)
    +
    +    def on_pong(self, data: bytes) -> None:
    +        """Invoked when the response to a ping frame is received."""
    +        pass
    +
    +    def on_ping(self, data: bytes) -> None:
    +        """Invoked when the a ping frame is received."""
    +        pass
    +
    +    def on_close(self) -> None:
    +        """Invoked when the WebSocket is closed.
    +
    +        If the connection was closed cleanly and a status code or reason
    +        phrase was supplied, these values will be available as the attributes
    +        ``self.close_code`` and ``self.close_reason``.
    +
    +        .. versionchanged:: 4.0
    +
    +           Added ``close_code`` and ``close_reason`` attributes.
    +        """
    +        pass
    +
    +    def close(self, code: Optional[int] = None, reason: Optional[str] = None) -> None:
    +        """Closes this Web Socket.
    +
    +        Once the close handshake is successful the socket will be closed.
    +
    +        ``code`` may be a numeric status code, taken from the values
    +        defined in `RFC 6455 section 7.4.1
    +        `_.
    +        ``reason`` may be a textual message about why the connection is
    +        closing.  These values are made available to the client, but are
    +        not otherwise interpreted by the websocket protocol.
    +
    +        .. versionchanged:: 4.0
    +
    +           Added the ``code`` and ``reason`` arguments.
    +        """
    +        if self.ws_connection:
    +            self.ws_connection.close(code, reason)
    +            self.ws_connection = None
    +
    +    def check_origin(self, origin: str) -> bool:
    +        """Override to enable support for allowing alternate origins.
    +
    +        The ``origin`` argument is the value of the ``Origin`` HTTP
    +        header, the url responsible for initiating this request.  This
    +        method is not called for clients that do not send this header;
    +        such requests are always allowed (because all browsers that
    +        implement WebSockets support this header, and non-browser
    +        clients do not have the same cross-site security concerns).
    +
    +        Should return ``True`` to accept the request or ``False`` to
    +        reject it. By default, rejects all requests with an origin on
    +        a host other than this one.
    +
    +        This is a security protection against cross site scripting attacks on
    +        browsers, since WebSockets are allowed to bypass the usual same-origin
    +        policies and don't use CORS headers.
    +
    +        .. warning::
    +
    +           This is an important security measure; don't disable it
    +           without understanding the security implications. In
    +           particular, if your authentication is cookie-based, you
    +           must either restrict the origins allowed by
    +           ``check_origin()`` or implement your own XSRF-like
    +           protection for websocket connections. See `these
    +           `_
    +           `articles
    +           `_
    +           for more.
    +
    +        To accept all cross-origin traffic (which was the default prior to
    +        Tornado 4.0), simply override this method to always return ``True``::
    +
    +            def check_origin(self, origin):
    +                return True
    +
    +        To allow connections from any subdomain of your site, you might
    +        do something like::
    +
    +            def check_origin(self, origin):
    +                parsed_origin = urllib.parse.urlparse(origin)
    +                return parsed_origin.netloc.endswith(".mydomain.com")
    +
    +        .. versionadded:: 4.0
    +
    +        """
    +        parsed_origin = urlparse(origin)
    +        origin = parsed_origin.netloc
    +        origin = origin.lower()
    +
    +        host = self.request.headers.get("Host")
    +
    +        # Check to see that origin matches host directly, including ports
    +        return origin == host
    +
    +    def set_nodelay(self, value: bool) -> None:
    +        """Set the no-delay flag for this stream.
    +
    +        By default, small messages may be delayed and/or combined to minimize
    +        the number of packets sent.  This can sometimes cause 200-500ms delays
    +        due to the interaction between Nagle's algorithm and TCP delayed
    +        ACKs.  To reduce this delay (at the expense of possibly increasing
    +        bandwidth usage), call ``self.set_nodelay(True)`` once the websocket
    +        connection is established.
    +
    +        See `.BaseIOStream.set_nodelay` for additional details.
    +
    +        .. versionadded:: 3.1
    +        """
    +        assert self.ws_connection is not None
    +        self.ws_connection.set_nodelay(value)
    +
    +    def on_connection_close(self) -> None:
    +        if self.ws_connection:
    +            self.ws_connection.on_connection_close()
    +            self.ws_connection = None
    +        if not self._on_close_called:
    +            self._on_close_called = True
    +            self.on_close()
    +            self._break_cycles()
    +
    +    def on_ws_connection_close(
    +        self, close_code: Optional[int] = None, close_reason: Optional[str] = None
    +    ) -> None:
    +        self.close_code = close_code
    +        self.close_reason = close_reason
    +        self.on_connection_close()
    +
    +    def _break_cycles(self) -> None:
    +        # WebSocketHandlers call finish() early, but we don't want to
    +        # break up reference cycles (which makes it impossible to call
    +        # self.render_string) until after we've really closed the
    +        # connection (if it was established in the first place,
    +        # indicated by status code 101).
    +        if self.get_status() != 101 or self._on_close_called:
    +            super()._break_cycles()
    +
    +    def send_error(self, *args: Any, **kwargs: Any) -> None:
    +        if self.stream is None:
    +            super().send_error(*args, **kwargs)
    +        else:
    +            # If we get an uncaught exception during the handshake,
    +            # we have no choice but to abruptly close the connection.
    +            # TODO: for uncaught exceptions after the handshake,
    +            # we can close the connection more gracefully.
    +            self.stream.close()
    +
    +    def get_websocket_protocol(self) -> Optional["WebSocketProtocol"]:
    +        websocket_version = self.request.headers.get("Sec-WebSocket-Version")
    +        if websocket_version in ("7", "8", "13"):
    +            params = _WebSocketParams(
    +                ping_interval=self.ping_interval,
    +                ping_timeout=self.ping_timeout,
    +                max_message_size=self.max_message_size,
    +                compression_options=self.get_compression_options(),
    +            )
    +            return WebSocketProtocol13(self, False, params)
    +        return None
    +
    +    def _detach_stream(self) -> IOStream:
    +        # disable non-WS methods
    +        for method in [
    +            "write",
    +            "redirect",
    +            "set_header",
    +            "set_cookie",
    +            "set_status",
    +            "flush",
    +            "finish",
    +        ]:
    +            setattr(self, method, _raise_not_supported_for_websockets)
    +        return self.detach()
    +
    +
    +def _raise_not_supported_for_websockets(*args: Any, **kwargs: Any) -> None:
    +    raise RuntimeError("Method not supported for Web Sockets")
    +
    +
    +class WebSocketProtocol(abc.ABC):
    +    """Base class for WebSocket protocol versions.
    +    """
    +
    +    def __init__(self, handler: "_WebSocketDelegate") -> None:
    +        self.handler = handler
    +        self.stream = None  # type: Optional[IOStream]
    +        self.client_terminated = False
    +        self.server_terminated = False
    +
    +    def _run_callback(
    +        self, callback: Callable, *args: Any, **kwargs: Any
    +    ) -> "Optional[Future[Any]]":
    +        """Runs the given callback with exception handling.
    +
    +        If the callback is a coroutine, returns its Future. On error, aborts the
    +        websocket connection and returns None.
    +        """
    +        try:
    +            result = callback(*args, **kwargs)
    +        except Exception:
    +            self.handler.log_exception(*sys.exc_info())
    +            self._abort()
    +            return None
    +        else:
    +            if result is not None:
    +                result = gen.convert_yielded(result)
    +                assert self.stream is not None
    +                self.stream.io_loop.add_future(result, lambda f: f.result())
    +            return result
    +
    +    def on_connection_close(self) -> None:
    +        self._abort()
    +
    +    def _abort(self) -> None:
    +        """Instantly aborts the WebSocket connection by closing the socket"""
    +        self.client_terminated = True
    +        self.server_terminated = True
    +        if self.stream is not None:
    +            self.stream.close()  # forcibly tear down the connection
    +        self.close()  # let the subclass cleanup
    +
    +    @abc.abstractmethod
    +    def close(self, code: Optional[int] = None, reason: Optional[str] = None) -> None:
    +        raise NotImplementedError()
    +
    +    @abc.abstractmethod
    +    def is_closing(self) -> bool:
    +        raise NotImplementedError()
    +
    +    @abc.abstractmethod
    +    async def accept_connection(self, handler: WebSocketHandler) -> None:
    +        raise NotImplementedError()
    +
    +    @abc.abstractmethod
    +    def write_message(
    +        self, message: Union[str, bytes], binary: bool = False
    +    ) -> "Future[None]":
    +        raise NotImplementedError()
    +
    +    @property
    +    @abc.abstractmethod
    +    def selected_subprotocol(self) -> Optional[str]:
    +        raise NotImplementedError()
    +
    +    @abc.abstractmethod
    +    def write_ping(self, data: bytes) -> None:
    +        raise NotImplementedError()
    +
    +    # The entry points below are used by WebSocketClientConnection,
    +    # which was introduced after we only supported a single version of
    +    # WebSocketProtocol. The WebSocketProtocol/WebSocketProtocol13
    +    # boundary is currently pretty ad-hoc.
    +    @abc.abstractmethod
    +    def _process_server_headers(
    +        self, key: Union[str, bytes], headers: httputil.HTTPHeaders
    +    ) -> None:
    +        raise NotImplementedError()
    +
    +    @abc.abstractmethod
    +    def start_pinging(self) -> None:
    +        raise NotImplementedError()
    +
    +    @abc.abstractmethod
    +    async def _receive_frame_loop(self) -> None:
    +        raise NotImplementedError()
    +
    +    @abc.abstractmethod
    +    def set_nodelay(self, x: bool) -> None:
    +        raise NotImplementedError()
    +
    +
    +class _PerMessageDeflateCompressor(object):
    +    def __init__(
    +        self,
    +        persistent: bool,
    +        max_wbits: Optional[int],
    +        compression_options: Optional[Dict[str, Any]] = None,
    +    ) -> None:
    +        if max_wbits is None:
    +            max_wbits = zlib.MAX_WBITS
    +        # There is no symbolic constant for the minimum wbits value.
    +        if not (8 <= max_wbits <= zlib.MAX_WBITS):
    +            raise ValueError(
    +                "Invalid max_wbits value %r; allowed range 8-%d",
    +                max_wbits,
    +                zlib.MAX_WBITS,
    +            )
    +        self._max_wbits = max_wbits
    +
    +        if (
    +            compression_options is None
    +            or "compression_level" not in compression_options
    +        ):
    +            self._compression_level = tornado.web.GZipContentEncoding.GZIP_LEVEL
    +        else:
    +            self._compression_level = compression_options["compression_level"]
    +
    +        if compression_options is None or "mem_level" not in compression_options:
    +            self._mem_level = 8
    +        else:
    +            self._mem_level = compression_options["mem_level"]
    +
    +        if persistent:
    +            self._compressor = self._create_compressor()  # type: Optional[_Compressor]
    +        else:
    +            self._compressor = None
    +
    +    def _create_compressor(self) -> "_Compressor":
    +        return zlib.compressobj(
    +            self._compression_level, zlib.DEFLATED, -self._max_wbits, self._mem_level
    +        )
    +
    +    def compress(self, data: bytes) -> bytes:
    +        compressor = self._compressor or self._create_compressor()
    +        data = compressor.compress(data) + compressor.flush(zlib.Z_SYNC_FLUSH)
    +        assert data.endswith(b"\x00\x00\xff\xff")
    +        return data[:-4]
    +
    +
    +class _PerMessageDeflateDecompressor(object):
    +    def __init__(
    +        self,
    +        persistent: bool,
    +        max_wbits: Optional[int],
    +        max_message_size: int,
    +        compression_options: Optional[Dict[str, Any]] = None,
    +    ) -> None:
    +        self._max_message_size = max_message_size
    +        if max_wbits is None:
    +            max_wbits = zlib.MAX_WBITS
    +        if not (8 <= max_wbits <= zlib.MAX_WBITS):
    +            raise ValueError(
    +                "Invalid max_wbits value %r; allowed range 8-%d",
    +                max_wbits,
    +                zlib.MAX_WBITS,
    +            )
    +        self._max_wbits = max_wbits
    +        if persistent:
    +            self._decompressor = (
    +                self._create_decompressor()
    +            )  # type: Optional[_Decompressor]
    +        else:
    +            self._decompressor = None
    +
    +    def _create_decompressor(self) -> "_Decompressor":
    +        return zlib.decompressobj(-self._max_wbits)
    +
    +    def decompress(self, data: bytes) -> bytes:
    +        decompressor = self._decompressor or self._create_decompressor()
    +        result = decompressor.decompress(
    +            data + b"\x00\x00\xff\xff", self._max_message_size
    +        )
    +        if decompressor.unconsumed_tail:
    +            raise _DecompressTooLargeError()
    +        return result
    +
    +
    +class WebSocketProtocol13(WebSocketProtocol):
    +    """Implementation of the WebSocket protocol from RFC 6455.
    +
    +    This class supports versions 7 and 8 of the protocol in addition to the
    +    final version 13.
    +    """
    +
    +    # Bit masks for the first byte of a frame.
    +    FIN = 0x80
    +    RSV1 = 0x40
    +    RSV2 = 0x20
    +    RSV3 = 0x10
    +    RSV_MASK = RSV1 | RSV2 | RSV3
    +    OPCODE_MASK = 0x0F
    +
    +    stream = None  # type: IOStream
    +
    +    def __init__(
    +        self,
    +        handler: "_WebSocketDelegate",
    +        mask_outgoing: bool,
    +        params: _WebSocketParams,
    +    ) -> None:
    +        WebSocketProtocol.__init__(self, handler)
    +        self.mask_outgoing = mask_outgoing
    +        self.params = params
    +        self._final_frame = False
    +        self._frame_opcode = None
    +        self._masked_frame = None
    +        self._frame_mask = None  # type: Optional[bytes]
    +        self._frame_length = None
    +        self._fragmented_message_buffer = None  # type: Optional[bytes]
    +        self._fragmented_message_opcode = None
    +        self._waiting = None  # type: object
    +        self._compression_options = params.compression_options
    +        self._decompressor = None  # type: Optional[_PerMessageDeflateDecompressor]
    +        self._compressor = None  # type: Optional[_PerMessageDeflateCompressor]
    +        self._frame_compressed = None  # type: Optional[bool]
    +        # The total uncompressed size of all messages received or sent.
    +        # Unicode messages are encoded to utf8.
    +        # Only for testing; subject to change.
    +        self._message_bytes_in = 0
    +        self._message_bytes_out = 0
    +        # The total size of all packets received or sent.  Includes
    +        # the effect of compression, frame overhead, and control frames.
    +        self._wire_bytes_in = 0
    +        self._wire_bytes_out = 0
    +        self.ping_callback = None  # type: Optional[PeriodicCallback]
    +        self.last_ping = 0.0
    +        self.last_pong = 0.0
    +        self.close_code = None  # type: Optional[int]
    +        self.close_reason = None  # type: Optional[str]
    +
    +    # Use a property for this to satisfy the abc.
    +    @property
    +    def selected_subprotocol(self) -> Optional[str]:
    +        return self._selected_subprotocol
    +
    +    @selected_subprotocol.setter
    +    def selected_subprotocol(self, value: Optional[str]) -> None:
    +        self._selected_subprotocol = value
    +
    +    async def accept_connection(self, handler: WebSocketHandler) -> None:
    +        try:
    +            self._handle_websocket_headers(handler)
    +        except ValueError:
    +            handler.set_status(400)
    +            log_msg = "Missing/Invalid WebSocket headers"
    +            handler.finish(log_msg)
    +            gen_log.debug(log_msg)
    +            return
    +
    +        try:
    +            await self._accept_connection(handler)
    +        except asyncio.CancelledError:
    +            self._abort()
    +            return
    +        except ValueError:
    +            gen_log.debug("Malformed WebSocket request received", exc_info=True)
    +            self._abort()
    +            return
    +
    +    def _handle_websocket_headers(self, handler: WebSocketHandler) -> None:
    +        """Verifies all invariant- and required headers
    +
    +        If a header is missing or have an incorrect value ValueError will be
    +        raised
    +        """
    +        fields = ("Host", "Sec-Websocket-Key", "Sec-Websocket-Version")
    +        if not all(map(lambda f: handler.request.headers.get(f), fields)):
    +            raise ValueError("Missing/Invalid WebSocket headers")
    +
    +    @staticmethod
    +    def compute_accept_value(key: Union[str, bytes]) -> str:
    +        """Computes the value for the Sec-WebSocket-Accept header,
    +        given the value for Sec-WebSocket-Key.
    +        """
    +        sha1 = hashlib.sha1()
    +        sha1.update(utf8(key))
    +        sha1.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")  # Magic value
    +        return native_str(base64.b64encode(sha1.digest()))
    +
    +    def _challenge_response(self, handler: WebSocketHandler) -> str:
    +        return WebSocketProtocol13.compute_accept_value(
    +            cast(str, handler.request.headers.get("Sec-Websocket-Key"))
    +        )
    +
    +    async def _accept_connection(self, handler: WebSocketHandler) -> None:
    +        subprotocol_header = handler.request.headers.get("Sec-WebSocket-Protocol")
    +        if subprotocol_header:
    +            subprotocols = [s.strip() for s in subprotocol_header.split(",")]
    +        else:
    +            subprotocols = []
    +        self.selected_subprotocol = handler.select_subprotocol(subprotocols)
    +        if self.selected_subprotocol:
    +            assert self.selected_subprotocol in subprotocols
    +            handler.set_header("Sec-WebSocket-Protocol", self.selected_subprotocol)
    +
    +        extensions = self._parse_extensions_header(handler.request.headers)
    +        for ext in extensions:
    +            if ext[0] == "permessage-deflate" and self._compression_options is not None:
    +                # TODO: negotiate parameters if compression_options
    +                # specifies limits.
    +                self._create_compressors("server", ext[1], self._compression_options)
    +                if (
    +                    "client_max_window_bits" in ext[1]
    +                    and ext[1]["client_max_window_bits"] is None
    +                ):
    +                    # Don't echo an offered client_max_window_bits
    +                    # parameter with no value.
    +                    del ext[1]["client_max_window_bits"]
    +                handler.set_header(
    +                    "Sec-WebSocket-Extensions",
    +                    httputil._encode_header("permessage-deflate", ext[1]),
    +                )
    +                break
    +
    +        handler.clear_header("Content-Type")
    +        handler.set_status(101)
    +        handler.set_header("Upgrade", "websocket")
    +        handler.set_header("Connection", "Upgrade")
    +        handler.set_header("Sec-WebSocket-Accept", self._challenge_response(handler))
    +        handler.finish()
    +
    +        self.stream = handler._detach_stream()
    +
    +        self.start_pinging()
    +        try:
    +            open_result = handler.open(*handler.open_args, **handler.open_kwargs)
    +            if open_result is not None:
    +                await open_result
    +        except Exception:
    +            handler.log_exception(*sys.exc_info())
    +            self._abort()
    +            return
    +
    +        await self._receive_frame_loop()
    +
    +    def _parse_extensions_header(
    +        self, headers: httputil.HTTPHeaders
    +    ) -> List[Tuple[str, Dict[str, str]]]:
    +        extensions = headers.get("Sec-WebSocket-Extensions", "")
    +        if extensions:
    +            return [httputil._parse_header(e.strip()) for e in extensions.split(",")]
    +        return []
    +
    +    def _process_server_headers(
    +        self, key: Union[str, bytes], headers: httputil.HTTPHeaders
    +    ) -> None:
    +        """Process the headers sent by the server to this client connection.
    +
    +        'key' is the websocket handshake challenge/response key.
    +        """
    +        assert headers["Upgrade"].lower() == "websocket"
    +        assert headers["Connection"].lower() == "upgrade"
    +        accept = self.compute_accept_value(key)
    +        assert headers["Sec-Websocket-Accept"] == accept
    +
    +        extensions = self._parse_extensions_header(headers)
    +        for ext in extensions:
    +            if ext[0] == "permessage-deflate" and self._compression_options is not None:
    +                self._create_compressors("client", ext[1])
    +            else:
    +                raise ValueError("unsupported extension %r", ext)
    +
    +        self.selected_subprotocol = headers.get("Sec-WebSocket-Protocol", None)
    +
    +    def _get_compressor_options(
    +        self,
    +        side: str,
    +        agreed_parameters: Dict[str, Any],
    +        compression_options: Optional[Dict[str, Any]] = None,
    +    ) -> Dict[str, Any]:
    +        """Converts a websocket agreed_parameters set to keyword arguments
    +        for our compressor objects.
    +        """
    +        options = dict(
    +            persistent=(side + "_no_context_takeover") not in agreed_parameters
    +        )  # type: Dict[str, Any]
    +        wbits_header = agreed_parameters.get(side + "_max_window_bits", None)
    +        if wbits_header is None:
    +            options["max_wbits"] = zlib.MAX_WBITS
    +        else:
    +            options["max_wbits"] = int(wbits_header)
    +        options["compression_options"] = compression_options
    +        return options
    +
    +    def _create_compressors(
    +        self,
    +        side: str,
    +        agreed_parameters: Dict[str, Any],
    +        compression_options: Optional[Dict[str, Any]] = None,
    +    ) -> None:
    +        # TODO: handle invalid parameters gracefully
    +        allowed_keys = set(
    +            [
    +                "server_no_context_takeover",
    +                "client_no_context_takeover",
    +                "server_max_window_bits",
    +                "client_max_window_bits",
    +            ]
    +        )
    +        for key in agreed_parameters:
    +            if key not in allowed_keys:
    +                raise ValueError("unsupported compression parameter %r" % key)
    +        other_side = "client" if (side == "server") else "server"
    +        self._compressor = _PerMessageDeflateCompressor(
    +            **self._get_compressor_options(side, agreed_parameters, compression_options)
    +        )
    +        self._decompressor = _PerMessageDeflateDecompressor(
    +            max_message_size=self.params.max_message_size,
    +            **self._get_compressor_options(
    +                other_side, agreed_parameters, compression_options
    +            )
    +        )
    +
    +    def _write_frame(
    +        self, fin: bool, opcode: int, data: bytes, flags: int = 0
    +    ) -> "Future[None]":
    +        data_len = len(data)
    +        if opcode & 0x8:
    +            # All control frames MUST have a payload length of 125
    +            # bytes or less and MUST NOT be fragmented.
    +            if not fin:
    +                raise ValueError("control frames may not be fragmented")
    +            if data_len > 125:
    +                raise ValueError("control frame payloads may not exceed 125 bytes")
    +        if fin:
    +            finbit = self.FIN
    +        else:
    +            finbit = 0
    +        frame = struct.pack("B", finbit | opcode | flags)
    +        if self.mask_outgoing:
    +            mask_bit = 0x80
    +        else:
    +            mask_bit = 0
    +        if data_len < 126:
    +            frame += struct.pack("B", data_len | mask_bit)
    +        elif data_len <= 0xFFFF:
    +            frame += struct.pack("!BH", 126 | mask_bit, data_len)
    +        else:
    +            frame += struct.pack("!BQ", 127 | mask_bit, data_len)
    +        if self.mask_outgoing:
    +            mask = os.urandom(4)
    +            data = mask + _websocket_mask(mask, data)
    +        frame += data
    +        self._wire_bytes_out += len(frame)
    +        return self.stream.write(frame)
    +
    +    def write_message(
    +        self, message: Union[str, bytes], binary: bool = False
    +    ) -> "Future[None]":
    +        """Sends the given message to the client of this Web Socket."""
    +        if binary:
    +            opcode = 0x2
    +        else:
    +            opcode = 0x1
    +        message = tornado.escape.utf8(message)
    +        assert isinstance(message, bytes)
    +        self._message_bytes_out += len(message)
    +        flags = 0
    +        if self._compressor:
    +            message = self._compressor.compress(message)
    +            flags |= self.RSV1
    +        # For historical reasons, write methods in Tornado operate in a semi-synchronous
    +        # mode in which awaiting the Future they return is optional (But errors can
    +        # still be raised). This requires us to go through an awkward dance here
    +        # to transform the errors that may be returned while presenting the same
    +        # semi-synchronous interface.
    +        try:
    +            fut = self._write_frame(True, opcode, message, flags=flags)
    +        except StreamClosedError:
    +            raise WebSocketClosedError()
    +
    +        async def wrapper() -> None:
    +            try:
    +                await fut
    +            except StreamClosedError:
    +                raise WebSocketClosedError()
    +
    +        return asyncio.ensure_future(wrapper())
    +
    +    def write_ping(self, data: bytes) -> None:
    +        """Send ping frame."""
    +        assert isinstance(data, bytes)
    +        self._write_frame(True, 0x9, data)
    +
    +    async def _receive_frame_loop(self) -> None:
    +        try:
    +            while not self.client_terminated:
    +                await self._receive_frame()
    +        except StreamClosedError:
    +            self._abort()
    +        self.handler.on_ws_connection_close(self.close_code, self.close_reason)
    +
    +    async def _read_bytes(self, n: int) -> bytes:
    +        data = await self.stream.read_bytes(n)
    +        self._wire_bytes_in += n
    +        return data
    +
    +    async def _receive_frame(self) -> None:
    +        # Read the frame header.
    +        data = await self._read_bytes(2)
    +        header, mask_payloadlen = struct.unpack("BB", data)
    +        is_final_frame = header & self.FIN
    +        reserved_bits = header & self.RSV_MASK
    +        opcode = header & self.OPCODE_MASK
    +        opcode_is_control = opcode & 0x8
    +        if self._decompressor is not None and opcode != 0:
    +            # Compression flag is present in the first frame's header,
    +            # but we can't decompress until we have all the frames of
    +            # the message.
    +            self._frame_compressed = bool(reserved_bits & self.RSV1)
    +            reserved_bits &= ~self.RSV1
    +        if reserved_bits:
    +            # client is using as-yet-undefined extensions; abort
    +            self._abort()
    +            return
    +        is_masked = bool(mask_payloadlen & 0x80)
    +        payloadlen = mask_payloadlen & 0x7F
    +
    +        # Parse and validate the length.
    +        if opcode_is_control and payloadlen >= 126:
    +            # control frames must have payload < 126
    +            self._abort()
    +            return
    +        if payloadlen < 126:
    +            self._frame_length = payloadlen
    +        elif payloadlen == 126:
    +            data = await self._read_bytes(2)
    +            payloadlen = struct.unpack("!H", data)[0]
    +        elif payloadlen == 127:
    +            data = await self._read_bytes(8)
    +            payloadlen = struct.unpack("!Q", data)[0]
    +        new_len = payloadlen
    +        if self._fragmented_message_buffer is not None:
    +            new_len += len(self._fragmented_message_buffer)
    +        if new_len > self.params.max_message_size:
    +            self.close(1009, "message too big")
    +            self._abort()
    +            return
    +
    +        # Read the payload, unmasking if necessary.
    +        if is_masked:
    +            self._frame_mask = await self._read_bytes(4)
    +        data = await self._read_bytes(payloadlen)
    +        if is_masked:
    +            assert self._frame_mask is not None
    +            data = _websocket_mask(self._frame_mask, data)
    +
    +        # Decide what to do with this frame.
    +        if opcode_is_control:
    +            # control frames may be interleaved with a series of fragmented
    +            # data frames, so control frames must not interact with
    +            # self._fragmented_*
    +            if not is_final_frame:
    +                # control frames must not be fragmented
    +                self._abort()
    +                return
    +        elif opcode == 0:  # continuation frame
    +            if self._fragmented_message_buffer is None:
    +                # nothing to continue
    +                self._abort()
    +                return
    +            self._fragmented_message_buffer += data
    +            if is_final_frame:
    +                opcode = self._fragmented_message_opcode
    +                data = self._fragmented_message_buffer
    +                self._fragmented_message_buffer = None
    +        else:  # start of new data message
    +            if self._fragmented_message_buffer is not None:
    +                # can't start new message until the old one is finished
    +                self._abort()
    +                return
    +            if not is_final_frame:
    +                self._fragmented_message_opcode = opcode
    +                self._fragmented_message_buffer = data
    +
    +        if is_final_frame:
    +            handled_future = self._handle_message(opcode, data)
    +            if handled_future is not None:
    +                await handled_future
    +
    +    def _handle_message(self, opcode: int, data: bytes) -> "Optional[Future[None]]":
    +        """Execute on_message, returning its Future if it is a coroutine."""
    +        if self.client_terminated:
    +            return None
    +
    +        if self._frame_compressed:
    +            assert self._decompressor is not None
    +            try:
    +                data = self._decompressor.decompress(data)
    +            except _DecompressTooLargeError:
    +                self.close(1009, "message too big after decompression")
    +                self._abort()
    +                return None
    +
    +        if opcode == 0x1:
    +            # UTF-8 data
    +            self._message_bytes_in += len(data)
    +            try:
    +                decoded = data.decode("utf-8")
    +            except UnicodeDecodeError:
    +                self._abort()
    +                return None
    +            return self._run_callback(self.handler.on_message, decoded)
    +        elif opcode == 0x2:
    +            # Binary data
    +            self._message_bytes_in += len(data)
    +            return self._run_callback(self.handler.on_message, data)
    +        elif opcode == 0x8:
    +            # Close
    +            self.client_terminated = True
    +            if len(data) >= 2:
    +                self.close_code = struct.unpack(">H", data[:2])[0]
    +            if len(data) > 2:
    +                self.close_reason = to_unicode(data[2:])
    +            # Echo the received close code, if any (RFC 6455 section 5.5.1).
    +            self.close(self.close_code)
    +        elif opcode == 0x9:
    +            # Ping
    +            try:
    +                self._write_frame(True, 0xA, data)
    +            except StreamClosedError:
    +                self._abort()
    +            self._run_callback(self.handler.on_ping, data)
    +        elif opcode == 0xA:
    +            # Pong
    +            self.last_pong = IOLoop.current().time()
    +            return self._run_callback(self.handler.on_pong, data)
    +        else:
    +            self._abort()
    +        return None
    +
    +    def close(self, code: Optional[int] = None, reason: Optional[str] = None) -> None:
    +        """Closes the WebSocket connection."""
    +        if not self.server_terminated:
    +            if not self.stream.closed():
    +                if code is None and reason is not None:
    +                    code = 1000  # "normal closure" status code
    +                if code is None:
    +                    close_data = b""
    +                else:
    +                    close_data = struct.pack(">H", code)
    +                if reason is not None:
    +                    close_data += utf8(reason)
    +                try:
    +                    self._write_frame(True, 0x8, close_data)
    +                except StreamClosedError:
    +                    self._abort()
    +            self.server_terminated = True
    +        if self.client_terminated:
    +            if self._waiting is not None:
    +                self.stream.io_loop.remove_timeout(self._waiting)
    +                self._waiting = None
    +            self.stream.close()
    +        elif self._waiting is None:
    +            # Give the client a few seconds to complete a clean shutdown,
    +            # otherwise just close the connection.
    +            self._waiting = self.stream.io_loop.add_timeout(
    +                self.stream.io_loop.time() + 5, self._abort
    +            )
    +        if self.ping_callback:
    +            self.ping_callback.stop()
    +            self.ping_callback = None
    +
    +    def is_closing(self) -> bool:
    +        """Return ``True`` if this connection is closing.
    +
    +        The connection is considered closing if either side has
    +        initiated its closing handshake or if the stream has been
    +        shut down uncleanly.
    +        """
    +        return self.stream.closed() or self.client_terminated or self.server_terminated
    +
    +    @property
    +    def ping_interval(self) -> Optional[float]:
    +        interval = self.params.ping_interval
    +        if interval is not None:
    +            return interval
    +        return 0
    +
    +    @property
    +    def ping_timeout(self) -> Optional[float]:
    +        timeout = self.params.ping_timeout
    +        if timeout is not None:
    +            return timeout
    +        assert self.ping_interval is not None
    +        return max(3 * self.ping_interval, 30)
    +
    +    def start_pinging(self) -> None:
    +        """Start sending periodic pings to keep the connection alive"""
    +        assert self.ping_interval is not None
    +        if self.ping_interval > 0:
    +            self.last_ping = self.last_pong = IOLoop.current().time()
    +            self.ping_callback = PeriodicCallback(
    +                self.periodic_ping, self.ping_interval * 1000
    +            )
    +            self.ping_callback.start()
    +
    +    def periodic_ping(self) -> None:
    +        """Send a ping to keep the websocket alive
    +
    +        Called periodically if the websocket_ping_interval is set and non-zero.
    +        """
    +        if self.is_closing() and self.ping_callback is not None:
    +            self.ping_callback.stop()
    +            return
    +
    +        # Check for timeout on pong. Make sure that we really have
    +        # sent a recent ping in case the machine with both server and
    +        # client has been suspended since the last ping.
    +        now = IOLoop.current().time()
    +        since_last_pong = now - self.last_pong
    +        since_last_ping = now - self.last_ping
    +        assert self.ping_interval is not None
    +        assert self.ping_timeout is not None
    +        if (
    +            since_last_ping < 2 * self.ping_interval
    +            and since_last_pong > self.ping_timeout
    +        ):
    +            self.close()
    +            return
    +
    +        self.write_ping(b"")
    +        self.last_ping = now
    +
    +    def set_nodelay(self, x: bool) -> None:
    +        self.stream.set_nodelay(x)
    +
    +
    +class WebSocketClientConnection(simple_httpclient._HTTPConnection):
    +    """WebSocket client connection.
    +
    +    This class should not be instantiated directly; use the
    +    `websocket_connect` function instead.
    +    """
    +
    +    protocol = None  # type: WebSocketProtocol
    +
    +    def __init__(
    +        self,
    +        request: httpclient.HTTPRequest,
    +        on_message_callback: Optional[Callable[[Union[None, str, bytes]], None]] = None,
    +        compression_options: Optional[Dict[str, Any]] = None,
    +        ping_interval: Optional[float] = None,
    +        ping_timeout: Optional[float] = None,
    +        max_message_size: int = _default_max_message_size,
    +        subprotocols: Optional[List[str]] = [],
    +    ) -> None:
    +        self.connect_future = Future()  # type: Future[WebSocketClientConnection]
    +        self.read_queue = Queue(1)  # type: Queue[Union[None, str, bytes]]
    +        self.key = base64.b64encode(os.urandom(16))
    +        self._on_message_callback = on_message_callback
    +        self.close_code = None  # type: Optional[int]
    +        self.close_reason = None  # type: Optional[str]
    +        self.params = _WebSocketParams(
    +            ping_interval=ping_interval,
    +            ping_timeout=ping_timeout,
    +            max_message_size=max_message_size,
    +            compression_options=compression_options,
    +        )
    +
    +        scheme, sep, rest = request.url.partition(":")
    +        scheme = {"ws": "http", "wss": "https"}[scheme]
    +        request.url = scheme + sep + rest
    +        request.headers.update(
    +            {
    +                "Upgrade": "websocket",
    +                "Connection": "Upgrade",
    +                "Sec-WebSocket-Key": self.key,
    +                "Sec-WebSocket-Version": "13",
    +            }
    +        )
    +        if subprotocols is not None:
    +            request.headers["Sec-WebSocket-Protocol"] = ",".join(subprotocols)
    +        if compression_options is not None:
    +            # Always offer to let the server set our max_wbits (and even though
    +            # we don't offer it, we will accept a client_no_context_takeover
    +            # from the server).
    +            # TODO: set server parameters for deflate extension
    +            # if requested in self.compression_options.
    +            request.headers[
    +                "Sec-WebSocket-Extensions"
    +            ] = "permessage-deflate; client_max_window_bits"
    +
    +        # Websocket connection is currently unable to follow redirects
    +        request.follow_redirects = False
    +
    +        self.tcp_client = TCPClient()
    +        super().__init__(
    +            None,
    +            request,
    +            lambda: None,
    +            self._on_http_response,
    +            104857600,
    +            self.tcp_client,
    +            65536,
    +            104857600,
    +        )
    +
    +    def close(self, code: Optional[int] = None, reason: Optional[str] = None) -> None:
    +        """Closes the websocket connection.
    +
    +        ``code`` and ``reason`` are documented under
    +        `WebSocketHandler.close`.
    +
    +        .. versionadded:: 3.2
    +
    +        .. versionchanged:: 4.0
    +
    +           Added the ``code`` and ``reason`` arguments.
    +        """
    +        if self.protocol is not None:
    +            self.protocol.close(code, reason)
    +            self.protocol = None  # type: ignore
    +
    +    def on_connection_close(self) -> None:
    +        if not self.connect_future.done():
    +            self.connect_future.set_exception(StreamClosedError())
    +        self._on_message(None)
    +        self.tcp_client.close()
    +        super().on_connection_close()
    +
    +    def on_ws_connection_close(
    +        self, close_code: Optional[int] = None, close_reason: Optional[str] = None
    +    ) -> None:
    +        self.close_code = close_code
    +        self.close_reason = close_reason
    +        self.on_connection_close()
    +
    +    def _on_http_response(self, response: httpclient.HTTPResponse) -> None:
    +        if not self.connect_future.done():
    +            if response.error:
    +                self.connect_future.set_exception(response.error)
    +            else:
    +                self.connect_future.set_exception(
    +                    WebSocketError("Non-websocket response")
    +                )
    +
    +    async def headers_received(
    +        self,
    +        start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine],
    +        headers: httputil.HTTPHeaders,
    +    ) -> None:
    +        assert isinstance(start_line, httputil.ResponseStartLine)
    +        if start_line.code != 101:
    +            await super().headers_received(start_line, headers)
    +            return
    +
    +        if self._timeout is not None:
    +            self.io_loop.remove_timeout(self._timeout)
    +            self._timeout = None
    +
    +        self.headers = headers
    +        self.protocol = self.get_websocket_protocol()
    +        self.protocol._process_server_headers(self.key, self.headers)
    +        self.protocol.stream = self.connection.detach()
    +
    +        IOLoop.current().add_callback(self.protocol._receive_frame_loop)
    +        self.protocol.start_pinging()
    +
    +        # Once we've taken over the connection, clear the final callback
    +        # we set on the http request.  This deactivates the error handling
    +        # in simple_httpclient that would otherwise interfere with our
    +        # ability to see exceptions.
    +        self.final_callback = None  # type: ignore
    +
    +        future_set_result_unless_cancelled(self.connect_future, self)
    +
    +    def write_message(
    +        self, message: Union[str, bytes], binary: bool = False
    +    ) -> "Future[None]":
    +        """Sends a message to the WebSocket server.
    +
    +        If the stream is closed, raises `WebSocketClosedError`.
    +        Returns a `.Future` which can be used for flow control.
    +
    +        .. versionchanged:: 5.0
    +           Exception raised on a closed stream changed from `.StreamClosedError`
    +           to `WebSocketClosedError`.
    +        """
    +        return self.protocol.write_message(message, binary=binary)
    +
    +    def read_message(
    +        self,
    +        callback: Optional[Callable[["Future[Union[None, str, bytes]]"], None]] = None,
    +    ) -> Awaitable[Union[None, str, bytes]]:
    +        """Reads a message from the WebSocket server.
    +
    +        If on_message_callback was specified at WebSocket
    +        initialization, this function will never return messages
    +
    +        Returns a future whose result is the message, or None
    +        if the connection is closed.  If a callback argument
    +        is given it will be called with the future when it is
    +        ready.
    +        """
    +
    +        awaitable = self.read_queue.get()
    +        if callback is not None:
    +            self.io_loop.add_future(asyncio.ensure_future(awaitable), callback)
    +        return awaitable
    +
    +    def on_message(self, message: Union[str, bytes]) -> Optional[Awaitable[None]]:
    +        return self._on_message(message)
    +
    +    def _on_message(
    +        self, message: Union[None, str, bytes]
    +    ) -> Optional[Awaitable[None]]:
    +        if self._on_message_callback:
    +            self._on_message_callback(message)
    +            return None
    +        else:
    +            return self.read_queue.put(message)
    +
    +    def ping(self, data: bytes = b"") -> None:
    +        """Send ping frame to the remote end.
    +
    +        The data argument allows a small amount of data (up to 125
    +        bytes) to be sent as a part of the ping message. Note that not
    +        all websocket implementations expose this data to
    +        applications.
    +
    +        Consider using the ``ping_interval`` argument to
    +        `websocket_connect` instead of sending pings manually.
    +
    +        .. versionadded:: 5.1
    +
    +        """
    +        data = utf8(data)
    +        if self.protocol is None:
    +            raise WebSocketClosedError()
    +        self.protocol.write_ping(data)
    +
    +    def on_pong(self, data: bytes) -> None:
    +        pass
    +
    +    def on_ping(self, data: bytes) -> None:
    +        pass
    +
    +    def get_websocket_protocol(self) -> WebSocketProtocol:
    +        return WebSocketProtocol13(self, mask_outgoing=True, params=self.params)
    +
    +    @property
    +    def selected_subprotocol(self) -> Optional[str]:
    +        """The subprotocol selected by the server.
    +
    +        .. versionadded:: 5.1
    +        """
    +        return self.protocol.selected_subprotocol
    +
    +    def log_exception(
    +        self,
    +        typ: "Optional[Type[BaseException]]",
    +        value: Optional[BaseException],
    +        tb: Optional[TracebackType],
    +    ) -> None:
    +        assert typ is not None
    +        assert value is not None
    +        app_log.error("Uncaught exception %s", value, exc_info=(typ, value, tb))
    +
    +
    +def websocket_connect(
    +    url: Union[str, httpclient.HTTPRequest],
    +    callback: Optional[Callable[["Future[WebSocketClientConnection]"], None]] = None,
    +    connect_timeout: Optional[float] = None,
    +    on_message_callback: Optional[Callable[[Union[None, str, bytes]], None]] = None,
    +    compression_options: Optional[Dict[str, Any]] = None,
    +    ping_interval: Optional[float] = None,
    +    ping_timeout: Optional[float] = None,
    +    max_message_size: int = _default_max_message_size,
    +    subprotocols: Optional[List[str]] = None,
    +) -> "Awaitable[WebSocketClientConnection]":
    +    """Client-side websocket support.
    +
    +    Takes a url and returns a Future whose result is a
    +    `WebSocketClientConnection`.
    +
    +    ``compression_options`` is interpreted in the same way as the
    +    return value of `.WebSocketHandler.get_compression_options`.
    +
    +    The connection supports two styles of operation. In the coroutine
    +    style, the application typically calls
    +    `~.WebSocketClientConnection.read_message` in a loop::
    +
    +        conn = yield websocket_connect(url)
    +        while True:
    +            msg = yield conn.read_message()
    +            if msg is None: break
    +            # Do something with msg
    +
    +    In the callback style, pass an ``on_message_callback`` to
    +    ``websocket_connect``. In both styles, a message of ``None``
    +    indicates that the connection has been closed.
    +
    +    ``subprotocols`` may be a list of strings specifying proposed
    +    subprotocols. The selected protocol may be found on the
    +    ``selected_subprotocol`` attribute of the connection object
    +    when the connection is complete.
    +
    +    .. versionchanged:: 3.2
    +       Also accepts ``HTTPRequest`` objects in place of urls.
    +
    +    .. versionchanged:: 4.1
    +       Added ``compression_options`` and ``on_message_callback``.
    +
    +    .. versionchanged:: 4.5
    +       Added the ``ping_interval``, ``ping_timeout``, and ``max_message_size``
    +       arguments, which have the same meaning as in `WebSocketHandler`.
    +
    +    .. versionchanged:: 5.0
    +       The ``io_loop`` argument (deprecated since version 4.1) has been removed.
    +
    +    .. versionchanged:: 5.1
    +       Added the ``subprotocols`` argument.
    +    """
    +    if isinstance(url, httpclient.HTTPRequest):
    +        assert connect_timeout is None
    +        request = url
    +        # Copy and convert the headers dict/object (see comments in
    +        # AsyncHTTPClient.fetch)
    +        request.headers = httputil.HTTPHeaders(request.headers)
    +    else:
    +        request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout)
    +    request = cast(
    +        httpclient.HTTPRequest,
    +        httpclient._RequestProxy(request, httpclient.HTTPRequest._DEFAULTS),
    +    )
    +    conn = WebSocketClientConnection(
    +        request,
    +        on_message_callback=on_message_callback,
    +        compression_options=compression_options,
    +        ping_interval=ping_interval,
    +        ping_timeout=ping_timeout,
    +        max_message_size=max_message_size,
    +        subprotocols=subprotocols,
    +    )
    +    if callback is not None:
    +        IOLoop.current().add_future(conn.connect_future, callback)
    +    return conn.connect_future
    diff --git a/telegramer/include/tornado/wsgi.py b/telegramer/include/tornado/wsgi.py
    new file mode 100644
    index 0000000..77124aa
    --- /dev/null
    +++ b/telegramer/include/tornado/wsgi.py
    @@ -0,0 +1,199 @@
    +#
    +# Copyright 2009 Facebook
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License"); you may
    +# not use this file except in compliance with the License. You may obtain
    +# a copy of the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    +# License for the specific language governing permissions and limitations
    +# under the License.
    +
    +"""WSGI support for the Tornado web framework.
    +
    +WSGI is the Python standard for web servers, and allows for interoperability
    +between Tornado and other Python web frameworks and servers.
    +
    +This module provides WSGI support via the `WSGIContainer` class, which
    +makes it possible to run applications using other WSGI frameworks on
    +the Tornado HTTP server. The reverse is not supported; the Tornado
    +`.Application` and `.RequestHandler` classes are designed for use with
    +the Tornado `.HTTPServer` and cannot be used in a generic WSGI
    +container.
    +
    +"""
    +
    +import sys
    +from io import BytesIO
    +import tornado
    +
    +from tornado import escape
    +from tornado import httputil
    +from tornado.log import access_log
    +
    +from typing import List, Tuple, Optional, Callable, Any, Dict, Text
    +from types import TracebackType
    +import typing
    +
    +if typing.TYPE_CHECKING:
    +    from typing import Type  # noqa: F401
    +    from wsgiref.types import WSGIApplication as WSGIAppType  # noqa: F401
    +
    +
    +# PEP 3333 specifies that WSGI on python 3 generally deals with byte strings
    +# that are smuggled inside objects of type unicode (via the latin1 encoding).
    +# This function is like those in the tornado.escape module, but defined
    +# here to minimize the temptation to use it in non-wsgi contexts.
    +def to_wsgi_str(s: bytes) -> str:
    +    assert isinstance(s, bytes)
    +    return s.decode("latin1")
    +
    +
    +class WSGIContainer(object):
    +    r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
    +
    +    .. warning::
    +
    +       WSGI is a *synchronous* interface, while Tornado's concurrency model
    +       is based on single-threaded asynchronous execution.  This means that
    +       running a WSGI app with Tornado's `WSGIContainer` is *less scalable*
    +       than running the same app in a multi-threaded WSGI server like
    +       ``gunicorn`` or ``uwsgi``.  Use `WSGIContainer` only when there are
    +       benefits to combining Tornado and WSGI in the same process that
    +       outweigh the reduced scalability.
    +
    +    Wrap a WSGI function in a `WSGIContainer` and pass it to `.HTTPServer` to
    +    run it. For example::
    +
    +        def simple_app(environ, start_response):
    +            status = "200 OK"
    +            response_headers = [("Content-type", "text/plain")]
    +            start_response(status, response_headers)
    +            return ["Hello world!\n"]
    +
    +        container = tornado.wsgi.WSGIContainer(simple_app)
    +        http_server = tornado.httpserver.HTTPServer(container)
    +        http_server.listen(8888)
    +        tornado.ioloop.IOLoop.current().start()
    +
    +    This class is intended to let other frameworks (Django, web.py, etc)
    +    run on the Tornado HTTP server and I/O loop.
    +
    +    The `tornado.web.FallbackHandler` class is often useful for mixing
    +    Tornado and WSGI apps in the same server.  See
    +    https://github.com/bdarnell/django-tornado-demo for a complete example.
    +    """
    +
    +    def __init__(self, wsgi_application: "WSGIAppType") -> None:
    +        self.wsgi_application = wsgi_application
    +
    +    def __call__(self, request: httputil.HTTPServerRequest) -> None:
    +        data = {}  # type: Dict[str, Any]
    +        response = []  # type: List[bytes]
    +
    +        def start_response(
    +            status: str,
    +            headers: List[Tuple[str, str]],
    +            exc_info: Optional[
    +                Tuple[
    +                    "Optional[Type[BaseException]]",
    +                    Optional[BaseException],
    +                    Optional[TracebackType],
    +                ]
    +            ] = None,
    +        ) -> Callable[[bytes], Any]:
    +            data["status"] = status
    +            data["headers"] = headers
    +            return response.append
    +
    +        app_response = self.wsgi_application(
    +            WSGIContainer.environ(request), start_response
    +        )
    +        try:
    +            response.extend(app_response)
    +            body = b"".join(response)
    +        finally:
    +            if hasattr(app_response, "close"):
    +                app_response.close()  # type: ignore
    +        if not data:
    +            raise Exception("WSGI app did not call start_response")
    +
    +        status_code_str, reason = data["status"].split(" ", 1)
    +        status_code = int(status_code_str)
    +        headers = data["headers"]  # type: List[Tuple[str, str]]
    +        header_set = set(k.lower() for (k, v) in headers)
    +        body = escape.utf8(body)
    +        if status_code != 304:
    +            if "content-length" not in header_set:
    +                headers.append(("Content-Length", str(len(body))))
    +            if "content-type" not in header_set:
    +                headers.append(("Content-Type", "text/html; charset=UTF-8"))
    +        if "server" not in header_set:
    +            headers.append(("Server", "TornadoServer/%s" % tornado.version))
    +
    +        start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
    +        header_obj = httputil.HTTPHeaders()
    +        for key, value in headers:
    +            header_obj.add(key, value)
    +        assert request.connection is not None
    +        request.connection.write_headers(start_line, header_obj, chunk=body)
    +        request.connection.finish()
    +        self._log(status_code, request)
    +
    +    @staticmethod
    +    def environ(request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
    +        """Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment.
    +        """
    +        hostport = request.host.split(":")
    +        if len(hostport) == 2:
    +            host = hostport[0]
    +            port = int(hostport[1])
    +        else:
    +            host = request.host
    +            port = 443 if request.protocol == "https" else 80
    +        environ = {
    +            "REQUEST_METHOD": request.method,
    +            "SCRIPT_NAME": "",
    +            "PATH_INFO": to_wsgi_str(
    +                escape.url_unescape(request.path, encoding=None, plus=False)
    +            ),
    +            "QUERY_STRING": request.query,
    +            "REMOTE_ADDR": request.remote_ip,
    +            "SERVER_NAME": host,
    +            "SERVER_PORT": str(port),
    +            "SERVER_PROTOCOL": request.version,
    +            "wsgi.version": (1, 0),
    +            "wsgi.url_scheme": request.protocol,
    +            "wsgi.input": BytesIO(escape.utf8(request.body)),
    +            "wsgi.errors": sys.stderr,
    +            "wsgi.multithread": False,
    +            "wsgi.multiprocess": True,
    +            "wsgi.run_once": False,
    +        }
    +        if "Content-Type" in request.headers:
    +            environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
    +        if "Content-Length" in request.headers:
    +            environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
    +        for key, value in request.headers.items():
    +            environ["HTTP_" + key.replace("-", "_").upper()] = value
    +        return environ
    +
    +    def _log(self, status_code: int, request: httputil.HTTPServerRequest) -> None:
    +        if status_code < 400:
    +            log_method = access_log.info
    +        elif status_code < 500:
    +            log_method = access_log.warning
    +        else:
    +            log_method = access_log.error
    +        request_time = 1000.0 * request.request_time()
    +        assert request.method is not None
    +        assert request.uri is not None
    +        summary = request.method + " " + request.uri + " (" + request.remote_ip + ")"
    +        log_method("%d %s %.2fms", status_code, summary, request_time)
    +
    +
    +HTTPRequest = httputil.HTTPServerRequest
    diff --git a/telegramer/include/tzlocal/__init__.py b/telegramer/include/tzlocal/__init__.py
    new file mode 100644
    index 0000000..98ed04f
    --- /dev/null
    +++ b/telegramer/include/tzlocal/__init__.py
    @@ -0,0 +1,13 @@
    +import sys
    +
    +if sys.platform == "win32":
    +    from tzlocal.win32 import (
    +        get_localzone,
    +        get_localzone_name,
    +        reload_localzone,
    +    )  # pragma: no cover
    +else:
    +    from tzlocal.unix import get_localzone, get_localzone_name, reload_localzone
    +
    +
    +__all__ = ["get_localzone", "get_localzone_name", "reload_localzone"]
    diff --git a/telegramer/include/tzlocal/unix.py b/telegramer/include/tzlocal/unix.py
    new file mode 100644
    index 0000000..eaf96d9
    --- /dev/null
    +++ b/telegramer/include/tzlocal/unix.py
    @@ -0,0 +1,215 @@
    +import os
    +import re
    +import sys
    +import warnings
    +from datetime import timezone
    +import pytz_deprecation_shim as pds
    +
    +from tzlocal import utils
    +
    +if sys.version_info >= (3, 9):
    +    from zoneinfo import ZoneInfo  # pragma: no cover
    +else:
    +    from backports.zoneinfo import ZoneInfo  # pragma: no cover
    +
    +_cache_tz = None
    +_cache_tz_name = None
    +
    +
    +def _get_localzone_name(_root="/"):
    +    """Tries to find the local timezone configuration.
    +
    +    This method finds the timezone name, if it can, or it returns None.
    +
    +    The parameter _root makes the function look for files like /etc/localtime
    +    beneath the _root directory. This is primarily used by the tests.
    +    In normal usage you call the function without parameters."""
    +
    +    # First try the ENV setting.
    +    tzenv = utils._tz_name_from_env()
    +    if tzenv:
    +        return tzenv
    +
    +    # Are we under Termux on Android?
    +    if os.path.exists(os.path.join(_root, "system/bin/getprop")):
    +        import subprocess
    +
    +        androidtz = (
    +            subprocess.check_output(["getprop", "persist.sys.timezone"])
    +            .strip()
    +            .decode()
    +        )
    +        return androidtz
    +
    +    # Now look for distribution specific configuration files
    +    # that contain the timezone name.
    +
    +    # Stick all of them in a dict, to compare later.
    +    found_configs = {}
    +
    +    for configfile in ("etc/timezone", "var/db/zoneinfo"):
    +        tzpath = os.path.join(_root, configfile)
    +        try:
    +            with open(tzpath, "rt") as tzfile:
    +                data = tzfile.read()
    +
    +                etctz = data.strip('/ \t\r\n')
    +                if not etctz:
    +                    # Empty file, skip
    +                    continue
    +                for etctz in etctz.splitlines():
    +                    # Get rid of host definitions and comments:
    +                    if " " in etctz:
    +                        etctz, dummy = etctz.split(" ", 1)
    +                    if "#" in etctz:
    +                        etctz, dummy = etctz.split("#", 1)
    +                    if not etctz:
    +                        continue
    +
    +                    found_configs[tzpath] = etctz.replace(" ", "_")
    +
    +        except (IOError, UnicodeDecodeError):
    +            # File doesn't exist or is a directory, or it's a binary file.
    +            continue
    +
    +    # CentOS has a ZONE setting in /etc/sysconfig/clock,
    +    # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
    +    # Gentoo has a TIMEZONE setting in /etc/conf.d/clock
    +    # We look through these files for a timezone:
    +
    +    zone_re = re.compile(r"\s*ZONE\s*=\s*\"")
    +    timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"")
    +    end_re = re.compile('"')
    +
    +    for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
    +        tzpath = os.path.join(_root, filename)
    +        try:
    +            with open(tzpath, "rt") as tzfile:
    +                data = tzfile.readlines()
    +
    +            for line in data:
    +                # Look for the ZONE= setting.
    +                match = zone_re.match(line)
    +                if match is None:
    +                    # No ZONE= setting. Look for the TIMEZONE= setting.
    +                    match = timezone_re.match(line)
    +                if match is not None:
    +                    # Some setting existed
    +                    line = line[match.end():]
    +                    etctz = line[: end_re.search(line).start()]
    +
    +                    # We found a timezone
    +                    found_configs[tzpath] = etctz.replace(" ", "_")
    +
    +        except (IOError, UnicodeDecodeError):
    +            # UnicodeDecode handles when clock is symlink to /etc/localtime
    +            continue
    +
    +    # systemd distributions use symlinks that include the zone name,
    +    # see manpage of localtime(5) and timedatectl(1)
    +    tzpath = os.path.join(_root, "etc/localtime")
    +    if os.path.exists(tzpath) and os.path.islink(tzpath):
    +        etctz = realtzpath = os.path.realpath(tzpath)
    +        start = etctz.find("/") + 1
    +        while start != 0:
    +            etctz = etctz[start:]
    +            try:
    +                pds.timezone(etctz)
    +                tzinfo = f"{tzpath} is a symlink to"
    +                found_configs[tzinfo] = etctz.replace(" ", "_")
    +            except pds.UnknownTimeZoneError:
    +                pass
    +            start = etctz.find("/") + 1
    +
    +    if len(found_configs) > 0:
    +        # We found some explicit config of some sort!
    +        if len(found_configs) > 1:
    +            # Uh-oh, multiple configs. See if they match:
    +            unique_tzs = set()
    +            zoneinfo = os.path.join(_root, "usr", "share", "zoneinfo")
    +            directory_depth = len(zoneinfo.split(os.path.sep))
    +
    +            for tzname in found_configs.values():
    +                # Look them up in /usr/share/zoneinfo, and find what they
    +                # really point to:
    +                path = os.path.realpath(os.path.join(zoneinfo, *tzname.split("/")))
    +                real_zone_name = "/".join(path.split(os.path.sep)[directory_depth:])
    +                unique_tzs.add(real_zone_name)
    +
    +            if len(unique_tzs) != 1:
    +                message = "Multiple conflicting time zone configurations found:\n"
    +                for key, value in found_configs.items():
    +                    message += f"{key}: {value}\n"
    +                message += "Fix the configuration, or set the time zone in a TZ environment variable.\n"
    +                raise utils.ZoneInfoNotFoundError(message)
    +
    +        # We found exactly one config! Use it.
    +        return list(found_configs.values())[0]
    +
    +
    +def _get_localzone(_root="/"):
    +    """Creates a timezone object from the timezone name.
    +
    +    If there is no timezone config, it will try to create a file from the
    +    localtime timezone, and if there isn't one, it will default to UTC.
    +
    +    The parameter _root makes the function look for files like /etc/localtime
    +    beneath the _root directory. This is primarily used by the tests.
    +    In normal usage you call the function without parameters."""
    +
    +    # First try the ENV setting.
    +    tzenv = utils._tz_from_env()
    +    if tzenv:
    +        return tzenv
    +
    +    tzname = _get_localzone_name(_root)
    +    if tzname is None:
    +        # No explicit setting existed. Use localtime
    +        for filename in ("etc/localtime", "usr/local/etc/localtime"):
    +            tzpath = os.path.join(_root, filename)
    +
    +            if not os.path.exists(tzpath):
    +                continue
    +            with open(tzpath, "rb") as tzfile:
    +                tz = pds.wrap_zone(ZoneInfo.from_file(tzfile, key="local"))
    +                break
    +        else:
    +            warnings.warn("Can not find any timezone configuration, defaulting to UTC.")
    +            tz = timezone.utc
    +    else:
    +        tz = pds.timezone(tzname)
    +
    +    if _root == "/":
    +        # We are using a file in etc to name the timezone.
    +        # Verify that the timezone specified there is actually used:
    +        utils.assert_tz_offset(tz)
    +    return tz
    +
    +
    +def get_localzone_name():
    +    """Get the computers configured local timezone name, if any."""
    +    global _cache_tz_name
    +    if _cache_tz_name is None:
    +        _cache_tz_name = _get_localzone_name()
    +
    +    return _cache_tz_name
    +
    +
    +def get_localzone():
    +    """Get the computers configured local timezone, if any."""
    +
    +    global _cache_tz
    +    if _cache_tz is None:
    +        _cache_tz = _get_localzone()
    +
    +    return _cache_tz
    +
    +
    +def reload_localzone():
    +    """Reload the cached localzone. You need to call this if the timezone has changed."""
    +    global _cache_tz_name
    +    global _cache_tz
    +    _cache_tz_name = _get_localzone_name()
    +    _cache_tz = _get_localzone()
    +
    +    return _cache_tz
    diff --git a/telegramer/include/tzlocal/utils.py b/telegramer/include/tzlocal/utils.py
    new file mode 100644
    index 0000000..d3f9242
    --- /dev/null
    +++ b/telegramer/include/tzlocal/utils.py
    @@ -0,0 +1,125 @@
    +# -*- coding: utf-8 -*-
    +import os
    +import time
    +import datetime
    +import calendar
    +import pytz_deprecation_shim as pds
    +
    +try:
    +    import zoneinfo  # pragma: no cover
    +except ImportError:
    +    from backports import zoneinfo  # pragma: no cover
    +
    +from tzlocal import windows_tz
    +
    +
    +class ZoneInfoNotFoundError(pds.UnknownTimeZoneError, zoneinfo.ZoneInfoNotFoundError):
    +    """An exception derived from both pytz and zoneinfo
    +
    +    This exception will be trappable both by pytz expecting clients and
    +    zoneinfo expecting clients.
    +    """
    +
    +
    +def get_system_offset():
    +    """Get system's timezone offset using built-in library time.
    +
    +    For the Timezone constants (altzone, daylight, timezone, and tzname), the
    +    value is determined by the timezone rules in effect at module load time or
    +    the last time tzset() is called and may be incorrect for times in the past.
    +
    +    To keep compatibility with Windows, we're always importing time module here.
    +    """
    +
    +    localtime = calendar.timegm(time.localtime())
    +    gmtime = calendar.timegm(time.gmtime())
    +    offset = gmtime - localtime
    +    # We could get the localtime and gmtime on either side of a second switch
    +    # so we check that the difference is less than one minute, because nobody
    +    # has that small DST differences.
    +    if abs(offset - time.altzone) < 60:
    +        return -time.altzone  # pragma: no cover
    +    else:
    +        return -time.timezone  # pragma: no cover
    +
    +
    +def get_tz_offset(tz):
    +    """Get timezone's offset using built-in function datetime.utcoffset()."""
    +    return int(datetime.datetime.now(tz).utcoffset().total_seconds())
    +
    +
    +def assert_tz_offset(tz):
    +    """Assert that system's timezone offset equals to the timezone offset found.
    +
    +    If they don't match, we probably have a misconfiguration, for example, an
    +    incorrect timezone set in /etc/timezone file in systemd distributions."""
    +    tz_offset = get_tz_offset(tz)
    +    system_offset = get_system_offset()
    +    if tz_offset != system_offset:
    +        msg = (
    +            "Timezone offset does not match system offset: {} != {}. "
    +            "Please, check your config files."
    +        ).format(tz_offset, system_offset)
    +        raise ValueError(msg)
    +
    +
    +def _tz_name_from_env(tzenv=None):
    +    if tzenv is None:
    +        tzenv = os.environ.get("TZ")
    +
    +    if not tzenv:
    +        return None
    +
    +    if tzenv in windows_tz.tz_win:
    +        # Yup, it's a timezone
    +        return tzenv
    +
    +    if os.path.isabs(tzenv) and os.path.exists(tzenv):
    +        # It's a file specification
    +        parts = tzenv.split(os.sep)
    +
    +        # Is it a zone info zone?
    +        possible_tz = "/".join(parts[-2:])
    +        if possible_tz in windows_tz.tz_win:
    +            # Yup, it is
    +            return possible_tz
    +
    +        # Maybe it's a short one, like UTC?
    +        if parts[-1] in windows_tz.tz_win:
    +            # Indeed
    +            return parts[-1]
    +
    +
    +def _tz_from_env(tzenv=None):
    +    if tzenv is None:
    +        tzenv = os.environ.get("TZ")
    +
    +    if not tzenv:
    +        return None
    +
    +    # Some weird format that exists:
    +    if tzenv[0] == ":":
    +        tzenv = tzenv[1:]
    +
    +    # TZ specifies a file
    +    if os.path.isabs(tzenv) and os.path.exists(tzenv):
    +        # Try to see if we can figure out the name
    +        tzname = _tz_name_from_env(tzenv)
    +        if not tzname:
    +            # Nope, not a standard timezone name, just take the filename
    +            tzname = tzenv.split(os.sep)[-1]
    +        with open(tzenv, "rb") as tzfile:
    +            zone = zoneinfo.ZoneInfo.from_file(tzfile, key=tzname)
    +            return pds.wrap_zone(zone)
    +
    +    # TZ must specify a zoneinfo zone.
    +    try:
    +        tz = pds.timezone(tzenv)
    +        # That worked, so we return this:
    +        return tz
    +    except pds.UnknownTimeZoneError:
    +        # Nope, it's something like "PST4DST" etc, we can't handle that.
    +        raise ZoneInfoNotFoundError(
    +            "tzlocal() does not support non-zoneinfo timezones like %s. \n"
    +            "Please use a timezone in the form of Continent/City"
    +        ) from None
    diff --git a/telegramer/include/tzlocal/win32.py b/telegramer/include/tzlocal/win32.py
    new file mode 100644
    index 0000000..720ab2b
    --- /dev/null
    +++ b/telegramer/include/tzlocal/win32.py
    @@ -0,0 +1,137 @@
    +from datetime import datetime
    +import pytz_deprecation_shim as pds
    +
    +try:
    +    import _winreg as winreg
    +except ImportError:
    +    import winreg
    +
    +from tzlocal.windows_tz import win_tz
    +from tzlocal import utils
    +
    +_cache_tz = None
    +_cache_tz_name = None
    +
    +
    +def valuestodict(key):
    +    """Convert a registry key's values to a dictionary."""
    +    result = {}
    +    size = winreg.QueryInfoKey(key)[1]
    +    for i in range(size):
    +        data = winreg.EnumValue(key, i)
    +        result[data[0]] = data[1]
    +    return result
    +
    +
    +def _get_dst_info(tz):
    +    # Find the offset for when it doesn't have DST:
    +    dst_offset = std_offset = None
    +    has_dst = False
    +    year = datetime.now().year
    +    for dt in (datetime(year, 1, 1), datetime(year, 6, 1)):
    +        if tz.dst(dt).total_seconds() == 0.0:
    +            # OK, no DST during winter, get this offset
    +            std_offset = tz.utcoffset(dt).total_seconds()
    +        else:
    +            has_dst = True
    +
    +    return has_dst, std_offset, dst_offset
    +
    +
    +def _get_localzone_name():
    +    # Windows is special. It has unique time zone names (in several
    +    # meanings of the word) available, but unfortunately, they can be
    +    # translated to the language of the operating system, so we need to
    +    # do a backwards lookup, by going through all time zones and see which
    +    # one matches.
    +    tzenv = utils._tz_name_from_env()
    +    if tzenv:
    +        return tzenv
    +
    +    handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
    +
    +    TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
    +    localtz = winreg.OpenKey(handle, TZLOCALKEYNAME)
    +    keyvalues = valuestodict(localtz)
    +    localtz.Close()
    +
    +    if "TimeZoneKeyName" in keyvalues:
    +        # Windows 7 and later
    +
    +        # For some reason this returns a string with loads of NUL bytes at
    +        # least on some systems. I don't know if this is a bug somewhere, I
    +        # just work around it.
    +        tzkeyname = keyvalues["TimeZoneKeyName"].split("\x00", 1)[0]
    +    else:
    +        # Don't support XP any longer
    +        raise LookupError("Can not find Windows timezone configuration")
    +
    +    timezone = win_tz.get(tzkeyname)
    +    if timezone is None:
    +        # Nope, that didn't work. Try adding "Standard Time",
    +        # it seems to work a lot of times:
    +        timezone = win_tz.get(tzkeyname + " Standard Time")
    +
    +    # Return what we have.
    +    if timezone is None:
    +        raise utils.ZoneInfoNotFoundError(tzkeyname)
    +
    +    if keyvalues.get("DynamicDaylightTimeDisabled", 0) == 1:
    +        # DST is disabled, so don't return the timezone name,
    +        # instead return Etc/GMT+offset
    +
    +        tz = pds.timezone(timezone)
    +        has_dst, std_offset, dst_offset = _get_dst_info(tz)
    +        if not has_dst:
    +            # The DST is turned off in the windows configuration,
    +            # but this timezone doesn't have DST so it doesn't matter
    +            return timezone
    +
    +        if std_offset is None:
    +            raise utils.ZoneInfoNotFoundError(
    +                f"{tzkeyname} claims to not have a non-DST time!?")
    +
    +        if std_offset % 3600:
    +            # I can't convert this to an hourly offset
    +            raise utils.ZoneInfoNotFoundError(
    +                f"tzlocal can't support disabling DST in the {timezone} zone.")
    +
    +        # This has whole hours as offset, return it as Etc/GMT
    +        return f"Etc/GMT{-std_offset//3600:+.0f}"
    +
    +    return timezone
    +
    +
    +def get_localzone_name():
    +    """Get the zoneinfo timezone name that matches the Windows-configured timezone."""
    +    global _cache_tz_name
    +    if _cache_tz_name is None:
    +        _cache_tz_name = _get_localzone_name()
    +
    +    return _cache_tz_name
    +
    +
    +def get_localzone():
    +    """Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone."""
    +
    +    global _cache_tz
    +    if _cache_tz is None:
    +        _cache_tz = pds.timezone(get_localzone_name())
    +
    +    if not utils._tz_name_from_env():
    +        # If the timezone does NOT come from a TZ environment variable,
    +        # verify that it's correct. If it's from the environment,
    +        # we accept it, this is so you can run tests with different timezones.
    +        utils.assert_tz_offset(_cache_tz)
    +
    +    return _cache_tz
    +
    +
    +def reload_localzone():
    +    """Reload the cached localzone. You need to call this if the timezone has changed."""
    +    global _cache_tz
    +    global _cache_tz_name
    +    _cache_tz_name = _get_localzone_name()
    +    _cache_tz = pds.timezone(_cache_tz_name)
    +    utils.assert_tz_offset(_cache_tz)
    +    return _cache_tz
    diff --git a/telegramer/include/tzlocal/windows_tz.py b/telegramer/include/tzlocal/windows_tz.py
    new file mode 100644
    index 0000000..0d28503
    --- /dev/null
    +++ b/telegramer/include/tzlocal/windows_tz.py
    @@ -0,0 +1,699 @@
    +# This file is autogenerated by the update_windows_mapping.py script
    +# Do not edit.
    +win_tz = {'AUS Central Standard Time': 'Australia/Darwin',
    + 'AUS Eastern Standard Time': 'Australia/Sydney',
    + 'Afghanistan Standard Time': 'Asia/Kabul',
    + 'Alaskan Standard Time': 'America/Anchorage',
    + 'Aleutian Standard Time': 'America/Adak',
    + 'Altai Standard Time': 'Asia/Barnaul',
    + 'Arab Standard Time': 'Asia/Riyadh',
    + 'Arabian Standard Time': 'Asia/Dubai',
    + 'Arabic Standard Time': 'Asia/Baghdad',
    + 'Argentina Standard Time': 'America/Buenos_Aires',
    + 'Astrakhan Standard Time': 'Europe/Astrakhan',
    + 'Atlantic Standard Time': 'America/Halifax',
    + 'Aus Central W. Standard Time': 'Australia/Eucla',
    + 'Azerbaijan Standard Time': 'Asia/Baku',
    + 'Azores Standard Time': 'Atlantic/Azores',
    + 'Bahia Standard Time': 'America/Bahia',
    + 'Bangladesh Standard Time': 'Asia/Dhaka',
    + 'Belarus Standard Time': 'Europe/Minsk',
    + 'Bougainville Standard Time': 'Pacific/Bougainville',
    + 'Canada Central Standard Time': 'America/Regina',
    + 'Cape Verde Standard Time': 'Atlantic/Cape_Verde',
    + 'Caucasus Standard Time': 'Asia/Yerevan',
    + 'Cen. Australia Standard Time': 'Australia/Adelaide',
    + 'Central America Standard Time': 'America/Guatemala',
    + 'Central Asia Standard Time': 'Asia/Almaty',
    + 'Central Brazilian Standard Time': 'America/Cuiaba',
    + 'Central Europe Standard Time': 'Europe/Budapest',
    + 'Central European Standard Time': 'Europe/Warsaw',
    + 'Central Pacific Standard Time': 'Pacific/Guadalcanal',
    + 'Central Standard Time': 'America/Chicago',
    + 'Central Standard Time (Mexico)': 'America/Mexico_City',
    + 'Chatham Islands Standard Time': 'Pacific/Chatham',
    + 'China Standard Time': 'Asia/Shanghai',
    + 'Cuba Standard Time': 'America/Havana',
    + 'Dateline Standard Time': 'Etc/GMT+12',
    + 'E. Africa Standard Time': 'Africa/Nairobi',
    + 'E. Australia Standard Time': 'Australia/Brisbane',
    + 'E. Europe Standard Time': 'Europe/Chisinau',
    + 'E. South America Standard Time': 'America/Sao_Paulo',
    + 'Easter Island Standard Time': 'Pacific/Easter',
    + 'Eastern Standard Time': 'America/New_York',
    + 'Eastern Standard Time (Mexico)': 'America/Cancun',
    + 'Egypt Standard Time': 'Africa/Cairo',
    + 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg',
    + 'FLE Standard Time': 'Europe/Kiev',
    + 'Fiji Standard Time': 'Pacific/Fiji',
    + 'GMT Standard Time': 'Europe/London',
    + 'GTB Standard Time': 'Europe/Bucharest',
    + 'Georgian Standard Time': 'Asia/Tbilisi',
    + 'Greenland Standard Time': 'America/Godthab',
    + 'Greenwich Standard Time': 'Atlantic/Reykjavik',
    + 'Haiti Standard Time': 'America/Port-au-Prince',
    + 'Hawaiian Standard Time': 'Pacific/Honolulu',
    + 'India Standard Time': 'Asia/Calcutta',
    + 'Iran Standard Time': 'Asia/Tehran',
    + 'Israel Standard Time': 'Asia/Jerusalem',
    + 'Jordan Standard Time': 'Asia/Amman',
    + 'Kaliningrad Standard Time': 'Europe/Kaliningrad',
    + 'Korea Standard Time': 'Asia/Seoul',
    + 'Libya Standard Time': 'Africa/Tripoli',
    + 'Line Islands Standard Time': 'Pacific/Kiritimati',
    + 'Lord Howe Standard Time': 'Australia/Lord_Howe',
    + 'Magadan Standard Time': 'Asia/Magadan',
    + 'Magallanes Standard Time': 'America/Punta_Arenas',
    + 'Marquesas Standard Time': 'Pacific/Marquesas',
    + 'Mauritius Standard Time': 'Indian/Mauritius',
    + 'Middle East Standard Time': 'Asia/Beirut',
    + 'Montevideo Standard Time': 'America/Montevideo',
    + 'Morocco Standard Time': 'Africa/Casablanca',
    + 'Mountain Standard Time': 'America/Denver',
    + 'Mountain Standard Time (Mexico)': 'America/Chihuahua',
    + 'Myanmar Standard Time': 'Asia/Rangoon',
    + 'N. Central Asia Standard Time': 'Asia/Novosibirsk',
    + 'Namibia Standard Time': 'Africa/Windhoek',
    + 'Nepal Standard Time': 'Asia/Katmandu',
    + 'New Zealand Standard Time': 'Pacific/Auckland',
    + 'Newfoundland Standard Time': 'America/St_Johns',
    + 'Norfolk Standard Time': 'Pacific/Norfolk',
    + 'North Asia East Standard Time': 'Asia/Irkutsk',
    + 'North Asia Standard Time': 'Asia/Krasnoyarsk',
    + 'North Korea Standard Time': 'Asia/Pyongyang',
    + 'Omsk Standard Time': 'Asia/Omsk',
    + 'Pacific SA Standard Time': 'America/Santiago',
    + 'Pacific Standard Time': 'America/Los_Angeles',
    + 'Pacific Standard Time (Mexico)': 'America/Tijuana',
    + 'Pakistan Standard Time': 'Asia/Karachi',
    + 'Paraguay Standard Time': 'America/Asuncion',
    + 'Qyzylorda Standard Time': 'Asia/Qyzylorda',
    + 'Romance Standard Time': 'Europe/Paris',
    + 'Russia Time Zone 10': 'Asia/Srednekolymsk',
    + 'Russia Time Zone 11': 'Asia/Kamchatka',
    + 'Russia Time Zone 3': 'Europe/Samara',
    + 'Russian Standard Time': 'Europe/Moscow',
    + 'SA Eastern Standard Time': 'America/Cayenne',
    + 'SA Pacific Standard Time': 'America/Bogota',
    + 'SA Western Standard Time': 'America/La_Paz',
    + 'SE Asia Standard Time': 'Asia/Bangkok',
    + 'Saint Pierre Standard Time': 'America/Miquelon',
    + 'Sakhalin Standard Time': 'Asia/Sakhalin',
    + 'Samoa Standard Time': 'Pacific/Apia',
    + 'Sao Tome Standard Time': 'Africa/Sao_Tome',
    + 'Saratov Standard Time': 'Europe/Saratov',
    + 'Singapore Standard Time': 'Asia/Singapore',
    + 'South Africa Standard Time': 'Africa/Johannesburg',
    + 'South Sudan Standard Time': 'Africa/Juba',
    + 'Sri Lanka Standard Time': 'Asia/Colombo',
    + 'Sudan Standard Time': 'Africa/Khartoum',
    + 'Syria Standard Time': 'Asia/Damascus',
    + 'Taipei Standard Time': 'Asia/Taipei',
    + 'Tasmania Standard Time': 'Australia/Hobart',
    + 'Tocantins Standard Time': 'America/Araguaina',
    + 'Tokyo Standard Time': 'Asia/Tokyo',
    + 'Tomsk Standard Time': 'Asia/Tomsk',
    + 'Tonga Standard Time': 'Pacific/Tongatapu',
    + 'Transbaikal Standard Time': 'Asia/Chita',
    + 'Turkey Standard Time': 'Europe/Istanbul',
    + 'Turks And Caicos Standard Time': 'America/Grand_Turk',
    + 'US Eastern Standard Time': 'America/Indianapolis',
    + 'US Mountain Standard Time': 'America/Phoenix',
    + 'UTC': 'Etc/UTC',
    + 'UTC+12': 'Etc/GMT-12',
    + 'UTC+13': 'Etc/GMT-13',
    + 'UTC-02': 'Etc/GMT+2',
    + 'UTC-08': 'Etc/GMT+8',
    + 'UTC-09': 'Etc/GMT+9',
    + 'UTC-11': 'Etc/GMT+11',
    + 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar',
    + 'Venezuela Standard Time': 'America/Caracas',
    + 'Vladivostok Standard Time': 'Asia/Vladivostok',
    + 'Volgograd Standard Time': 'Europe/Volgograd',
    + 'W. Australia Standard Time': 'Australia/Perth',
    + 'W. Central Africa Standard Time': 'Africa/Lagos',
    + 'W. Europe Standard Time': 'Europe/Berlin',
    + 'W. Mongolia Standard Time': 'Asia/Hovd',
    + 'West Asia Standard Time': 'Asia/Tashkent',
    + 'West Bank Standard Time': 'Asia/Hebron',
    + 'West Pacific Standard Time': 'Pacific/Port_Moresby',
    + 'Yakutsk Standard Time': 'Asia/Yakutsk',
    + 'Yukon Standard Time': 'America/Whitehorse'}
    +
    +# Old name for the win_tz variable:
    +tz_names = win_tz
    +
    +tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
    + 'Africa/Accra': 'Greenwich Standard Time',
    + 'Africa/Addis_Ababa': 'E. Africa Standard Time',
    + 'Africa/Algiers': 'W. Central Africa Standard Time',
    + 'Africa/Asmera': 'E. Africa Standard Time',
    + 'Africa/Bamako': 'Greenwich Standard Time',
    + 'Africa/Bangui': 'W. Central Africa Standard Time',
    + 'Africa/Banjul': 'Greenwich Standard Time',
    + 'Africa/Bissau': 'Greenwich Standard Time',
    + 'Africa/Blantyre': 'South Africa Standard Time',
    + 'Africa/Brazzaville': 'W. Central Africa Standard Time',
    + 'Africa/Bujumbura': 'South Africa Standard Time',
    + 'Africa/Cairo': 'Egypt Standard Time',
    + 'Africa/Casablanca': 'Morocco Standard Time',
    + 'Africa/Ceuta': 'Romance Standard Time',
    + 'Africa/Conakry': 'Greenwich Standard Time',
    + 'Africa/Dakar': 'Greenwich Standard Time',
    + 'Africa/Dar_es_Salaam': 'E. Africa Standard Time',
    + 'Africa/Djibouti': 'E. Africa Standard Time',
    + 'Africa/Douala': 'W. Central Africa Standard Time',
    + 'Africa/El_Aaiun': 'Morocco Standard Time',
    + 'Africa/Freetown': 'Greenwich Standard Time',
    + 'Africa/Gaborone': 'South Africa Standard Time',
    + 'Africa/Harare': 'South Africa Standard Time',
    + 'Africa/Johannesburg': 'South Africa Standard Time',
    + 'Africa/Juba': 'South Sudan Standard Time',
    + 'Africa/Kampala': 'E. Africa Standard Time',
    + 'Africa/Khartoum': 'Sudan Standard Time',
    + 'Africa/Kigali': 'South Africa Standard Time',
    + 'Africa/Kinshasa': 'W. Central Africa Standard Time',
    + 'Africa/Lagos': 'W. Central Africa Standard Time',
    + 'Africa/Libreville': 'W. Central Africa Standard Time',
    + 'Africa/Lome': 'Greenwich Standard Time',
    + 'Africa/Luanda': 'W. Central Africa Standard Time',
    + 'Africa/Lubumbashi': 'South Africa Standard Time',
    + 'Africa/Lusaka': 'South Africa Standard Time',
    + 'Africa/Malabo': 'W. Central Africa Standard Time',
    + 'Africa/Maputo': 'South Africa Standard Time',
    + 'Africa/Maseru': 'South Africa Standard Time',
    + 'Africa/Mbabane': 'South Africa Standard Time',
    + 'Africa/Mogadishu': 'E. Africa Standard Time',
    + 'Africa/Monrovia': 'Greenwich Standard Time',
    + 'Africa/Nairobi': 'E. Africa Standard Time',
    + 'Africa/Ndjamena': 'W. Central Africa Standard Time',
    + 'Africa/Niamey': 'W. Central Africa Standard Time',
    + 'Africa/Nouakchott': 'Greenwich Standard Time',
    + 'Africa/Ouagadougou': 'Greenwich Standard Time',
    + 'Africa/Porto-Novo': 'W. Central Africa Standard Time',
    + 'Africa/Sao_Tome': 'Sao Tome Standard Time',
    + 'Africa/Timbuktu': 'Greenwich Standard Time',
    + 'Africa/Tripoli': 'Libya Standard Time',
    + 'Africa/Tunis': 'W. Central Africa Standard Time',
    + 'Africa/Windhoek': 'Namibia Standard Time',
    + 'America/Adak': 'Aleutian Standard Time',
    + 'America/Anchorage': 'Alaskan Standard Time',
    + 'America/Anguilla': 'SA Western Standard Time',
    + 'America/Antigua': 'SA Western Standard Time',
    + 'America/Araguaina': 'Tocantins Standard Time',
    + 'America/Argentina/La_Rioja': 'Argentina Standard Time',
    + 'America/Argentina/Rio_Gallegos': 'Argentina Standard Time',
    + 'America/Argentina/Salta': 'Argentina Standard Time',
    + 'America/Argentina/San_Juan': 'Argentina Standard Time',
    + 'America/Argentina/San_Luis': 'Argentina Standard Time',
    + 'America/Argentina/Tucuman': 'Argentina Standard Time',
    + 'America/Argentina/Ushuaia': 'Argentina Standard Time',
    + 'America/Aruba': 'SA Western Standard Time',
    + 'America/Asuncion': 'Paraguay Standard Time',
    + 'America/Atka': 'Aleutian Standard Time',
    + 'America/Bahia': 'Bahia Standard Time',
    + 'America/Bahia_Banderas': 'Central Standard Time (Mexico)',
    + 'America/Barbados': 'SA Western Standard Time',
    + 'America/Belem': 'SA Eastern Standard Time',
    + 'America/Belize': 'Central America Standard Time',
    + 'America/Blanc-Sablon': 'SA Western Standard Time',
    + 'America/Boa_Vista': 'SA Western Standard Time',
    + 'America/Bogota': 'SA Pacific Standard Time',
    + 'America/Boise': 'Mountain Standard Time',
    + 'America/Buenos_Aires': 'Argentina Standard Time',
    + 'America/Cambridge_Bay': 'Mountain Standard Time',
    + 'America/Campo_Grande': 'Central Brazilian Standard Time',
    + 'America/Cancun': 'Eastern Standard Time (Mexico)',
    + 'America/Caracas': 'Venezuela Standard Time',
    + 'America/Catamarca': 'Argentina Standard Time',
    + 'America/Cayenne': 'SA Eastern Standard Time',
    + 'America/Cayman': 'SA Pacific Standard Time',
    + 'America/Chicago': 'Central Standard Time',
    + 'America/Chihuahua': 'Mountain Standard Time (Mexico)',
    + 'America/Coral_Harbour': 'SA Pacific Standard Time',
    + 'America/Cordoba': 'Argentina Standard Time',
    + 'America/Costa_Rica': 'Central America Standard Time',
    + 'America/Creston': 'US Mountain Standard Time',
    + 'America/Cuiaba': 'Central Brazilian Standard Time',
    + 'America/Curacao': 'SA Western Standard Time',
    + 'America/Danmarkshavn': 'Greenwich Standard Time',
    + 'America/Dawson': 'Yukon Standard Time',
    + 'America/Dawson_Creek': 'US Mountain Standard Time',
    + 'America/Denver': 'Mountain Standard Time',
    + 'America/Detroit': 'Eastern Standard Time',
    + 'America/Dominica': 'SA Western Standard Time',
    + 'America/Edmonton': 'Mountain Standard Time',
    + 'America/Eirunepe': 'SA Pacific Standard Time',
    + 'America/El_Salvador': 'Central America Standard Time',
    + 'America/Ensenada': 'Pacific Standard Time (Mexico)',
    + 'America/Fort_Nelson': 'US Mountain Standard Time',
    + 'America/Fortaleza': 'SA Eastern Standard Time',
    + 'America/Glace_Bay': 'Atlantic Standard Time',
    + 'America/Godthab': 'Greenland Standard Time',
    + 'America/Goose_Bay': 'Atlantic Standard Time',
    + 'America/Grand_Turk': 'Turks And Caicos Standard Time',
    + 'America/Grenada': 'SA Western Standard Time',
    + 'America/Guadeloupe': 'SA Western Standard Time',
    + 'America/Guatemala': 'Central America Standard Time',
    + 'America/Guayaquil': 'SA Pacific Standard Time',
    + 'America/Guyana': 'SA Western Standard Time',
    + 'America/Halifax': 'Atlantic Standard Time',
    + 'America/Havana': 'Cuba Standard Time',
    + 'America/Hermosillo': 'US Mountain Standard Time',
    + 'America/Indiana/Knox': 'Central Standard Time',
    + 'America/Indiana/Marengo': 'US Eastern Standard Time',
    + 'America/Indiana/Petersburg': 'Eastern Standard Time',
    + 'America/Indiana/Tell_City': 'Central Standard Time',
    + 'America/Indiana/Vevay': 'US Eastern Standard Time',
    + 'America/Indiana/Vincennes': 'Eastern Standard Time',
    + 'America/Indiana/Winamac': 'Eastern Standard Time',
    + 'America/Indianapolis': 'US Eastern Standard Time',
    + 'America/Inuvik': 'Mountain Standard Time',
    + 'America/Iqaluit': 'Eastern Standard Time',
    + 'America/Jamaica': 'SA Pacific Standard Time',
    + 'America/Jujuy': 'Argentina Standard Time',
    + 'America/Juneau': 'Alaskan Standard Time',
    + 'America/Kentucky/Monticello': 'Eastern Standard Time',
    + 'America/Knox_IN': 'Central Standard Time',
    + 'America/Kralendijk': 'SA Western Standard Time',
    + 'America/La_Paz': 'SA Western Standard Time',
    + 'America/Lima': 'SA Pacific Standard Time',
    + 'America/Los_Angeles': 'Pacific Standard Time',
    + 'America/Louisville': 'Eastern Standard Time',
    + 'America/Lower_Princes': 'SA Western Standard Time',
    + 'America/Maceio': 'SA Eastern Standard Time',
    + 'America/Managua': 'Central America Standard Time',
    + 'America/Manaus': 'SA Western Standard Time',
    + 'America/Marigot': 'SA Western Standard Time',
    + 'America/Martinique': 'SA Western Standard Time',
    + 'America/Matamoros': 'Central Standard Time',
    + 'America/Mazatlan': 'Mountain Standard Time (Mexico)',
    + 'America/Mendoza': 'Argentina Standard Time',
    + 'America/Menominee': 'Central Standard Time',
    + 'America/Merida': 'Central Standard Time (Mexico)',
    + 'America/Metlakatla': 'Alaskan Standard Time',
    + 'America/Mexico_City': 'Central Standard Time (Mexico)',
    + 'America/Miquelon': 'Saint Pierre Standard Time',
    + 'America/Moncton': 'Atlantic Standard Time',
    + 'America/Monterrey': 'Central Standard Time (Mexico)',
    + 'America/Montevideo': 'Montevideo Standard Time',
    + 'America/Montreal': 'Eastern Standard Time',
    + 'America/Montserrat': 'SA Western Standard Time',
    + 'America/Nassau': 'Eastern Standard Time',
    + 'America/New_York': 'Eastern Standard Time',
    + 'America/Nipigon': 'Eastern Standard Time',
    + 'America/Nome': 'Alaskan Standard Time',
    + 'America/Noronha': 'UTC-02',
    + 'America/North_Dakota/Beulah': 'Central Standard Time',
    + 'America/North_Dakota/Center': 'Central Standard Time',
    + 'America/North_Dakota/New_Salem': 'Central Standard Time',
    + 'America/Ojinaga': 'Mountain Standard Time',
    + 'America/Panama': 'SA Pacific Standard Time',
    + 'America/Pangnirtung': 'Eastern Standard Time',
    + 'America/Paramaribo': 'SA Eastern Standard Time',
    + 'America/Phoenix': 'US Mountain Standard Time',
    + 'America/Port-au-Prince': 'Haiti Standard Time',
    + 'America/Port_of_Spain': 'SA Western Standard Time',
    + 'America/Porto_Acre': 'SA Pacific Standard Time',
    + 'America/Porto_Velho': 'SA Western Standard Time',
    + 'America/Puerto_Rico': 'SA Western Standard Time',
    + 'America/Punta_Arenas': 'Magallanes Standard Time',
    + 'America/Rainy_River': 'Central Standard Time',
    + 'America/Rankin_Inlet': 'Central Standard Time',
    + 'America/Recife': 'SA Eastern Standard Time',
    + 'America/Regina': 'Canada Central Standard Time',
    + 'America/Resolute': 'Central Standard Time',
    + 'America/Rio_Branco': 'SA Pacific Standard Time',
    + 'America/Santa_Isabel': 'Pacific Standard Time (Mexico)',
    + 'America/Santarem': 'SA Eastern Standard Time',
    + 'America/Santiago': 'Pacific SA Standard Time',
    + 'America/Santo_Domingo': 'SA Western Standard Time',
    + 'America/Sao_Paulo': 'E. South America Standard Time',
    + 'America/Scoresbysund': 'Azores Standard Time',
    + 'America/Shiprock': 'Mountain Standard Time',
    + 'America/Sitka': 'Alaskan Standard Time',
    + 'America/St_Barthelemy': 'SA Western Standard Time',
    + 'America/St_Johns': 'Newfoundland Standard Time',
    + 'America/St_Kitts': 'SA Western Standard Time',
    + 'America/St_Lucia': 'SA Western Standard Time',
    + 'America/St_Thomas': 'SA Western Standard Time',
    + 'America/St_Vincent': 'SA Western Standard Time',
    + 'America/Swift_Current': 'Canada Central Standard Time',
    + 'America/Tegucigalpa': 'Central America Standard Time',
    + 'America/Thule': 'Atlantic Standard Time',
    + 'America/Thunder_Bay': 'Eastern Standard Time',
    + 'America/Tijuana': 'Pacific Standard Time (Mexico)',
    + 'America/Toronto': 'Eastern Standard Time',
    + 'America/Tortola': 'SA Western Standard Time',
    + 'America/Vancouver': 'Pacific Standard Time',
    + 'America/Virgin': 'SA Western Standard Time',
    + 'America/Whitehorse': 'Yukon Standard Time',
    + 'America/Winnipeg': 'Central Standard Time',
    + 'America/Yakutat': 'Alaskan Standard Time',
    + 'America/Yellowknife': 'Mountain Standard Time',
    + 'Antarctica/Casey': 'Central Pacific Standard Time',
    + 'Antarctica/Davis': 'SE Asia Standard Time',
    + 'Antarctica/DumontDUrville': 'West Pacific Standard Time',
    + 'Antarctica/Macquarie': 'Tasmania Standard Time',
    + 'Antarctica/Mawson': 'West Asia Standard Time',
    + 'Antarctica/McMurdo': 'New Zealand Standard Time',
    + 'Antarctica/Palmer': 'SA Eastern Standard Time',
    + 'Antarctica/Rothera': 'SA Eastern Standard Time',
    + 'Antarctica/South_Pole': 'New Zealand Standard Time',
    + 'Antarctica/Syowa': 'E. Africa Standard Time',
    + 'Antarctica/Vostok': 'Central Asia Standard Time',
    + 'Arctic/Longyearbyen': 'W. Europe Standard Time',
    + 'Asia/Aden': 'Arab Standard Time',
    + 'Asia/Almaty': 'Central Asia Standard Time',
    + 'Asia/Amman': 'Jordan Standard Time',
    + 'Asia/Anadyr': 'Russia Time Zone 11',
    + 'Asia/Aqtau': 'West Asia Standard Time',
    + 'Asia/Aqtobe': 'West Asia Standard Time',
    + 'Asia/Ashgabat': 'West Asia Standard Time',
    + 'Asia/Ashkhabad': 'West Asia Standard Time',
    + 'Asia/Atyrau': 'West Asia Standard Time',
    + 'Asia/Baghdad': 'Arabic Standard Time',
    + 'Asia/Bahrain': 'Arab Standard Time',
    + 'Asia/Baku': 'Azerbaijan Standard Time',
    + 'Asia/Bangkok': 'SE Asia Standard Time',
    + 'Asia/Barnaul': 'Altai Standard Time',
    + 'Asia/Beirut': 'Middle East Standard Time',
    + 'Asia/Bishkek': 'Central Asia Standard Time',
    + 'Asia/Brunei': 'Singapore Standard Time',
    + 'Asia/Calcutta': 'India Standard Time',
    + 'Asia/Chita': 'Transbaikal Standard Time',
    + 'Asia/Choibalsan': 'Ulaanbaatar Standard Time',
    + 'Asia/Chongqing': 'China Standard Time',
    + 'Asia/Chungking': 'China Standard Time',
    + 'Asia/Colombo': 'Sri Lanka Standard Time',
    + 'Asia/Dacca': 'Bangladesh Standard Time',
    + 'Asia/Damascus': 'Syria Standard Time',
    + 'Asia/Dhaka': 'Bangladesh Standard Time',
    + 'Asia/Dili': 'Tokyo Standard Time',
    + 'Asia/Dubai': 'Arabian Standard Time',
    + 'Asia/Dushanbe': 'West Asia Standard Time',
    + 'Asia/Famagusta': 'GTB Standard Time',
    + 'Asia/Gaza': 'West Bank Standard Time',
    + 'Asia/Harbin': 'China Standard Time',
    + 'Asia/Hebron': 'West Bank Standard Time',
    + 'Asia/Hong_Kong': 'China Standard Time',
    + 'Asia/Hovd': 'W. Mongolia Standard Time',
    + 'Asia/Irkutsk': 'North Asia East Standard Time',
    + 'Asia/Jakarta': 'SE Asia Standard Time',
    + 'Asia/Jayapura': 'Tokyo Standard Time',
    + 'Asia/Jerusalem': 'Israel Standard Time',
    + 'Asia/Kabul': 'Afghanistan Standard Time',
    + 'Asia/Kamchatka': 'Russia Time Zone 11',
    + 'Asia/Karachi': 'Pakistan Standard Time',
    + 'Asia/Kashgar': 'Central Asia Standard Time',
    + 'Asia/Katmandu': 'Nepal Standard Time',
    + 'Asia/Khandyga': 'Yakutsk Standard Time',
    + 'Asia/Krasnoyarsk': 'North Asia Standard Time',
    + 'Asia/Kuala_Lumpur': 'Singapore Standard Time',
    + 'Asia/Kuching': 'Singapore Standard Time',
    + 'Asia/Kuwait': 'Arab Standard Time',
    + 'Asia/Macao': 'China Standard Time',
    + 'Asia/Macau': 'China Standard Time',
    + 'Asia/Magadan': 'Magadan Standard Time',
    + 'Asia/Makassar': 'Singapore Standard Time',
    + 'Asia/Manila': 'Singapore Standard Time',
    + 'Asia/Muscat': 'Arabian Standard Time',
    + 'Asia/Nicosia': 'GTB Standard Time',
    + 'Asia/Novokuznetsk': 'North Asia Standard Time',
    + 'Asia/Novosibirsk': 'N. Central Asia Standard Time',
    + 'Asia/Omsk': 'Omsk Standard Time',
    + 'Asia/Oral': 'West Asia Standard Time',
    + 'Asia/Phnom_Penh': 'SE Asia Standard Time',
    + 'Asia/Pontianak': 'SE Asia Standard Time',
    + 'Asia/Pyongyang': 'North Korea Standard Time',
    + 'Asia/Qatar': 'Arab Standard Time',
    + 'Asia/Qostanay': 'Central Asia Standard Time',
    + 'Asia/Qyzylorda': 'Qyzylorda Standard Time',
    + 'Asia/Rangoon': 'Myanmar Standard Time',
    + 'Asia/Riyadh': 'Arab Standard Time',
    + 'Asia/Saigon': 'SE Asia Standard Time',
    + 'Asia/Sakhalin': 'Sakhalin Standard Time',
    + 'Asia/Samarkand': 'West Asia Standard Time',
    + 'Asia/Seoul': 'Korea Standard Time',
    + 'Asia/Shanghai': 'China Standard Time',
    + 'Asia/Singapore': 'Singapore Standard Time',
    + 'Asia/Srednekolymsk': 'Russia Time Zone 10',
    + 'Asia/Taipei': 'Taipei Standard Time',
    + 'Asia/Tashkent': 'West Asia Standard Time',
    + 'Asia/Tbilisi': 'Georgian Standard Time',
    + 'Asia/Tehran': 'Iran Standard Time',
    + 'Asia/Tel_Aviv': 'Israel Standard Time',
    + 'Asia/Thimbu': 'Bangladesh Standard Time',
    + 'Asia/Thimphu': 'Bangladesh Standard Time',
    + 'Asia/Tokyo': 'Tokyo Standard Time',
    + 'Asia/Tomsk': 'Tomsk Standard Time',
    + 'Asia/Ujung_Pandang': 'Singapore Standard Time',
    + 'Asia/Ulaanbaatar': 'Ulaanbaatar Standard Time',
    + 'Asia/Ulan_Bator': 'Ulaanbaatar Standard Time',
    + 'Asia/Urumqi': 'Central Asia Standard Time',
    + 'Asia/Ust-Nera': 'Vladivostok Standard Time',
    + 'Asia/Vientiane': 'SE Asia Standard Time',
    + 'Asia/Vladivostok': 'Vladivostok Standard Time',
    + 'Asia/Yakutsk': 'Yakutsk Standard Time',
    + 'Asia/Yekaterinburg': 'Ekaterinburg Standard Time',
    + 'Asia/Yerevan': 'Caucasus Standard Time',
    + 'Atlantic/Azores': 'Azores Standard Time',
    + 'Atlantic/Bermuda': 'Atlantic Standard Time',
    + 'Atlantic/Canary': 'GMT Standard Time',
    + 'Atlantic/Cape_Verde': 'Cape Verde Standard Time',
    + 'Atlantic/Faeroe': 'GMT Standard Time',
    + 'Atlantic/Jan_Mayen': 'W. Europe Standard Time',
    + 'Atlantic/Madeira': 'GMT Standard Time',
    + 'Atlantic/Reykjavik': 'Greenwich Standard Time',
    + 'Atlantic/South_Georgia': 'UTC-02',
    + 'Atlantic/St_Helena': 'Greenwich Standard Time',
    + 'Atlantic/Stanley': 'SA Eastern Standard Time',
    + 'Australia/ACT': 'AUS Eastern Standard Time',
    + 'Australia/Adelaide': 'Cen. Australia Standard Time',
    + 'Australia/Brisbane': 'E. Australia Standard Time',
    + 'Australia/Broken_Hill': 'Cen. Australia Standard Time',
    + 'Australia/Canberra': 'AUS Eastern Standard Time',
    + 'Australia/Currie': 'Tasmania Standard Time',
    + 'Australia/Darwin': 'AUS Central Standard Time',
    + 'Australia/Eucla': 'Aus Central W. Standard Time',
    + 'Australia/Hobart': 'Tasmania Standard Time',
    + 'Australia/LHI': 'Lord Howe Standard Time',
    + 'Australia/Lindeman': 'E. Australia Standard Time',
    + 'Australia/Lord_Howe': 'Lord Howe Standard Time',
    + 'Australia/Melbourne': 'AUS Eastern Standard Time',
    + 'Australia/NSW': 'AUS Eastern Standard Time',
    + 'Australia/North': 'AUS Central Standard Time',
    + 'Australia/Perth': 'W. Australia Standard Time',
    + 'Australia/Queensland': 'E. Australia Standard Time',
    + 'Australia/South': 'Cen. Australia Standard Time',
    + 'Australia/Sydney': 'AUS Eastern Standard Time',
    + 'Australia/Tasmania': 'Tasmania Standard Time',
    + 'Australia/Victoria': 'AUS Eastern Standard Time',
    + 'Australia/West': 'W. Australia Standard Time',
    + 'Australia/Yancowinna': 'Cen. Australia Standard Time',
    + 'Brazil/Acre': 'SA Pacific Standard Time',
    + 'Brazil/DeNoronha': 'UTC-02',
    + 'Brazil/East': 'E. South America Standard Time',
    + 'Brazil/West': 'SA Western Standard Time',
    + 'CST6CDT': 'Central Standard Time',
    + 'Canada/Atlantic': 'Atlantic Standard Time',
    + 'Canada/Central': 'Central Standard Time',
    + 'Canada/Eastern': 'Eastern Standard Time',
    + 'Canada/Mountain': 'Mountain Standard Time',
    + 'Canada/Newfoundland': 'Newfoundland Standard Time',
    + 'Canada/Pacific': 'Pacific Standard Time',
    + 'Canada/Saskatchewan': 'Canada Central Standard Time',
    + 'Canada/Yukon': 'Yukon Standard Time',
    + 'Chile/Continental': 'Pacific SA Standard Time',
    + 'Chile/EasterIsland': 'Easter Island Standard Time',
    + 'Cuba': 'Cuba Standard Time',
    + 'EST5EDT': 'Eastern Standard Time',
    + 'Egypt': 'Egypt Standard Time',
    + 'Eire': 'GMT Standard Time',
    + 'Etc/GMT': 'UTC',
    + 'Etc/GMT+1': 'Cape Verde Standard Time',
    + 'Etc/GMT+10': 'Hawaiian Standard Time',
    + 'Etc/GMT+11': 'UTC-11',
    + 'Etc/GMT+12': 'Dateline Standard Time',
    + 'Etc/GMT+2': 'UTC-02',
    + 'Etc/GMT+3': 'SA Eastern Standard Time',
    + 'Etc/GMT+4': 'SA Western Standard Time',
    + 'Etc/GMT+5': 'SA Pacific Standard Time',
    + 'Etc/GMT+6': 'Central America Standard Time',
    + 'Etc/GMT+7': 'US Mountain Standard Time',
    + 'Etc/GMT+8': 'UTC-08',
    + 'Etc/GMT+9': 'UTC-09',
    + 'Etc/GMT-1': 'W. Central Africa Standard Time',
    + 'Etc/GMT-10': 'West Pacific Standard Time',
    + 'Etc/GMT-11': 'Central Pacific Standard Time',
    + 'Etc/GMT-12': 'UTC+12',
    + 'Etc/GMT-13': 'UTC+13',
    + 'Etc/GMT-14': 'Line Islands Standard Time',
    + 'Etc/GMT-2': 'South Africa Standard Time',
    + 'Etc/GMT-3': 'E. Africa Standard Time',
    + 'Etc/GMT-4': 'Arabian Standard Time',
    + 'Etc/GMT-5': 'West Asia Standard Time',
    + 'Etc/GMT-6': 'Central Asia Standard Time',
    + 'Etc/GMT-7': 'SE Asia Standard Time',
    + 'Etc/GMT-8': 'Singapore Standard Time',
    + 'Etc/GMT-9': 'Tokyo Standard Time',
    + 'Etc/UCT': 'UTC',
    + 'Etc/UTC': 'UTC',
    + 'Europe/Amsterdam': 'W. Europe Standard Time',
    + 'Europe/Andorra': 'W. Europe Standard Time',
    + 'Europe/Astrakhan': 'Astrakhan Standard Time',
    + 'Europe/Athens': 'GTB Standard Time',
    + 'Europe/Belfast': 'GMT Standard Time',
    + 'Europe/Belgrade': 'Central Europe Standard Time',
    + 'Europe/Berlin': 'W. Europe Standard Time',
    + 'Europe/Bratislava': 'Central Europe Standard Time',
    + 'Europe/Brussels': 'Romance Standard Time',
    + 'Europe/Bucharest': 'GTB Standard Time',
    + 'Europe/Budapest': 'Central Europe Standard Time',
    + 'Europe/Busingen': 'W. Europe Standard Time',
    + 'Europe/Chisinau': 'E. Europe Standard Time',
    + 'Europe/Copenhagen': 'Romance Standard Time',
    + 'Europe/Dublin': 'GMT Standard Time',
    + 'Europe/Gibraltar': 'W. Europe Standard Time',
    + 'Europe/Guernsey': 'GMT Standard Time',
    + 'Europe/Helsinki': 'FLE Standard Time',
    + 'Europe/Isle_of_Man': 'GMT Standard Time',
    + 'Europe/Istanbul': 'Turkey Standard Time',
    + 'Europe/Jersey': 'GMT Standard Time',
    + 'Europe/Kaliningrad': 'Kaliningrad Standard Time',
    + 'Europe/Kiev': 'FLE Standard Time',
    + 'Europe/Kirov': 'Russian Standard Time',
    + 'Europe/Lisbon': 'GMT Standard Time',
    + 'Europe/Ljubljana': 'Central Europe Standard Time',
    + 'Europe/London': 'GMT Standard Time',
    + 'Europe/Luxembourg': 'W. Europe Standard Time',
    + 'Europe/Madrid': 'Romance Standard Time',
    + 'Europe/Malta': 'W. Europe Standard Time',
    + 'Europe/Mariehamn': 'FLE Standard Time',
    + 'Europe/Minsk': 'Belarus Standard Time',
    + 'Europe/Monaco': 'W. Europe Standard Time',
    + 'Europe/Moscow': 'Russian Standard Time',
    + 'Europe/Oslo': 'W. Europe Standard Time',
    + 'Europe/Paris': 'Romance Standard Time',
    + 'Europe/Podgorica': 'Central Europe Standard Time',
    + 'Europe/Prague': 'Central Europe Standard Time',
    + 'Europe/Riga': 'FLE Standard Time',
    + 'Europe/Rome': 'W. Europe Standard Time',
    + 'Europe/Samara': 'Russia Time Zone 3',
    + 'Europe/San_Marino': 'W. Europe Standard Time',
    + 'Europe/Sarajevo': 'Central European Standard Time',
    + 'Europe/Saratov': 'Saratov Standard Time',
    + 'Europe/Simferopol': 'Russian Standard Time',
    + 'Europe/Skopje': 'Central European Standard Time',
    + 'Europe/Sofia': 'FLE Standard Time',
    + 'Europe/Stockholm': 'W. Europe Standard Time',
    + 'Europe/Tallinn': 'FLE Standard Time',
    + 'Europe/Tirane': 'Central Europe Standard Time',
    + 'Europe/Tiraspol': 'E. Europe Standard Time',
    + 'Europe/Ulyanovsk': 'Astrakhan Standard Time',
    + 'Europe/Uzhgorod': 'FLE Standard Time',
    + 'Europe/Vaduz': 'W. Europe Standard Time',
    + 'Europe/Vatican': 'W. Europe Standard Time',
    + 'Europe/Vienna': 'W. Europe Standard Time',
    + 'Europe/Vilnius': 'FLE Standard Time',
    + 'Europe/Volgograd': 'Volgograd Standard Time',
    + 'Europe/Warsaw': 'Central European Standard Time',
    + 'Europe/Zagreb': 'Central European Standard Time',
    + 'Europe/Zaporozhye': 'FLE Standard Time',
    + 'Europe/Zurich': 'W. Europe Standard Time',
    + 'GB': 'GMT Standard Time',
    + 'GB-Eire': 'GMT Standard Time',
    + 'GMT+0': 'UTC',
    + 'GMT-0': 'UTC',
    + 'GMT0': 'UTC',
    + 'Greenwich': 'UTC',
    + 'Hongkong': 'China Standard Time',
    + 'Iceland': 'Greenwich Standard Time',
    + 'Indian/Antananarivo': 'E. Africa Standard Time',
    + 'Indian/Chagos': 'Central Asia Standard Time',
    + 'Indian/Christmas': 'SE Asia Standard Time',
    + 'Indian/Cocos': 'Myanmar Standard Time',
    + 'Indian/Comoro': 'E. Africa Standard Time',
    + 'Indian/Kerguelen': 'West Asia Standard Time',
    + 'Indian/Mahe': 'Mauritius Standard Time',
    + 'Indian/Maldives': 'West Asia Standard Time',
    + 'Indian/Mauritius': 'Mauritius Standard Time',
    + 'Indian/Mayotte': 'E. Africa Standard Time',
    + 'Indian/Reunion': 'Mauritius Standard Time',
    + 'Iran': 'Iran Standard Time',
    + 'Israel': 'Israel Standard Time',
    + 'Jamaica': 'SA Pacific Standard Time',
    + 'Japan': 'Tokyo Standard Time',
    + 'Kwajalein': 'UTC+12',
    + 'Libya': 'Libya Standard Time',
    + 'MST7MDT': 'Mountain Standard Time',
    + 'Mexico/BajaNorte': 'Pacific Standard Time (Mexico)',
    + 'Mexico/BajaSur': 'Mountain Standard Time (Mexico)',
    + 'Mexico/General': 'Central Standard Time (Mexico)',
    + 'NZ': 'New Zealand Standard Time',
    + 'NZ-CHAT': 'Chatham Islands Standard Time',
    + 'Navajo': 'Mountain Standard Time',
    + 'PRC': 'China Standard Time',
    + 'PST8PDT': 'Pacific Standard Time',
    + 'Pacific/Apia': 'Samoa Standard Time',
    + 'Pacific/Auckland': 'New Zealand Standard Time',
    + 'Pacific/Bougainville': 'Bougainville Standard Time',
    + 'Pacific/Chatham': 'Chatham Islands Standard Time',
    + 'Pacific/Easter': 'Easter Island Standard Time',
    + 'Pacific/Efate': 'Central Pacific Standard Time',
    + 'Pacific/Enderbury': 'UTC+13',
    + 'Pacific/Fakaofo': 'UTC+13',
    + 'Pacific/Fiji': 'Fiji Standard Time',
    + 'Pacific/Funafuti': 'UTC+12',
    + 'Pacific/Galapagos': 'Central America Standard Time',
    + 'Pacific/Gambier': 'UTC-09',
    + 'Pacific/Guadalcanal': 'Central Pacific Standard Time',
    + 'Pacific/Guam': 'West Pacific Standard Time',
    + 'Pacific/Honolulu': 'Hawaiian Standard Time',
    + 'Pacific/Johnston': 'Hawaiian Standard Time',
    + 'Pacific/Kiritimati': 'Line Islands Standard Time',
    + 'Pacific/Kosrae': 'Central Pacific Standard Time',
    + 'Pacific/Kwajalein': 'UTC+12',
    + 'Pacific/Majuro': 'UTC+12',
    + 'Pacific/Marquesas': 'Marquesas Standard Time',
    + 'Pacific/Midway': 'UTC-11',
    + 'Pacific/Nauru': 'UTC+12',
    + 'Pacific/Niue': 'UTC-11',
    + 'Pacific/Norfolk': 'Norfolk Standard Time',
    + 'Pacific/Noumea': 'Central Pacific Standard Time',
    + 'Pacific/Pago_Pago': 'UTC-11',
    + 'Pacific/Palau': 'Tokyo Standard Time',
    + 'Pacific/Pitcairn': 'UTC-08',
    + 'Pacific/Ponape': 'Central Pacific Standard Time',
    + 'Pacific/Port_Moresby': 'West Pacific Standard Time',
    + 'Pacific/Rarotonga': 'Hawaiian Standard Time',
    + 'Pacific/Saipan': 'West Pacific Standard Time',
    + 'Pacific/Samoa': 'UTC-11',
    + 'Pacific/Tahiti': 'Hawaiian Standard Time',
    + 'Pacific/Tarawa': 'UTC+12',
    + 'Pacific/Tongatapu': 'Tonga Standard Time',
    + 'Pacific/Truk': 'West Pacific Standard Time',
    + 'Pacific/Wake': 'UTC+12',
    + 'Pacific/Wallis': 'UTC+12',
    + 'Poland': 'Central European Standard Time',
    + 'Portugal': 'GMT Standard Time',
    + 'ROC': 'Taipei Standard Time',
    + 'ROK': 'Korea Standard Time',
    + 'Singapore': 'Singapore Standard Time',
    + 'Turkey': 'Turkey Standard Time',
    + 'UCT': 'UTC',
    + 'US/Alaska': 'Alaskan Standard Time',
    + 'US/Aleutian': 'Aleutian Standard Time',
    + 'US/Arizona': 'US Mountain Standard Time',
    + 'US/Central': 'Central Standard Time',
    + 'US/Eastern': 'Eastern Standard Time',
    + 'US/Hawaii': 'Hawaiian Standard Time',
    + 'US/Indiana-Starke': 'Central Standard Time',
    + 'US/Michigan': 'Eastern Standard Time',
    + 'US/Mountain': 'Mountain Standard Time',
    + 'US/Pacific': 'Pacific Standard Time',
    + 'US/Samoa': 'UTC-11',
    + 'UTC': 'UTC',
    + 'Universal': 'UTC',
    + 'W-SU': 'Russian Standard Time',
    + 'Zulu': 'UTC'}
    diff --git a/telegramer/webui.py b/telegramer/webui.py
    index 53c7c59..71ea01a 100755
    --- a/telegramer/webui.py
    +++ b/telegramer/webui.py
    @@ -43,7 +43,7 @@
     from deluge import component
     from deluge.plugins.pluginbase import WebPluginBase
     
    -from common import get_resource
    +from .common import get_resource
     
     
     class WebUI(WebPluginBase):