python-telegram-bot/telegram/request/_requestparameter.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

176 lines
7.4 KiB
Python
Raw Normal View History

#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
2024-02-19 20:06:25 +01:00
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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 describes a single parameter of a request to the Bot API."""
2022-05-19 12:47:53 +02:00
import json
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, final
from telegram._files.inputfile import InputFile
from telegram._files.inputmedia import InputMedia, InputPaidMedia
from telegram._files.inputsticker import InputSticker
from telegram._telegramobject import TelegramObject
from telegram._utils.datetime import to_timestamp
from telegram._utils.enum import StringEnum
from telegram._utils.types import UploadFileDict
@final
@dataclass(repr=True, eq=False, order=False, frozen=True)
class RequestParameter:
"""Instances of this class represent a single parameter to be sent along with a request to
the Bot API.
.. versionadded:: 20.0
Warning:
This class intended is to be used internally by the library and *not* by the user. Changes
to this class are not considered breaking changes and may not be documented in the
changelog.
Args:
name (:obj:`str`): The name of the parameter.
value (:obj:`object` | :obj:`None`): The value of the parameter. Must be JSON-dumpable.
input_files (list[:class:`telegram.InputFile`], optional): A list of files that should be
uploaded along with this parameter.
Attributes:
name (:obj:`str`): The name of the parameter.
value (:obj:`object` | :obj:`None`): The value of the parameter.
input_files (list[:class:`telegram.InputFile` | :obj:`None`): A list of files that should
be uploaded along with this parameter.
"""
__slots__ = ("input_files", "name", "value")
name: str
value: object
input_files: Optional[list[InputFile]]
@property
def json_value(self) -> Optional[str]:
"""The JSON dumped :attr:`value` or :obj:`None` if :attr:`value` is :obj:`None`.
The latter can currently only happen if :attr:`input_files` has exactly one element that
must not be uploaded via an attach:// URI.
"""
if isinstance(self.value, str):
return self.value
if self.value is None:
return None
return json.dumps(self.value)
@property
def multipart_data(self) -> Optional[UploadFileDict]:
"""A dict with the file data to upload, if any.
2024-09-01 15:25:34 +02:00
.. versionchanged:: 21.5
Content may now be a file handle.
"""
if not self.input_files:
return None
return {
(input_file.attach_name or self.name): input_file.field_tuple
for input_file in self.input_files
}
@staticmethod
def _value_and_input_files_from_input( # pylint: disable=too-many-return-statements
value: object,
) -> tuple[object, list[InputFile]]:
"""Converts `value` into something that we can json-dump. Returns two values:
1. the JSON-dumpable value. May be `None` in case the value is an InputFile which must
not be uploaded via an attach:// URI
2. A list of InputFiles that should be uploaded for this value
Note that we handle files differently depending on whether attaching them via an URI of the
form attach://<name> is documented to be allowed or not.
There was some confusion whether this worked for all files, so that we stick to the
documented ways for now.
See https://github.com/tdlib/telegram-bot-api/issues/167 and
https://github.com/tdlib/telegram-bot-api/issues/259
This method only does some special casing for our own helper class StringEnum, but not
for general enums. This is because:
* tg.constants currently only uses IntEnum as second enum type and json dumping that
is no problem
* if a user passes a custom enum, it's unlikely that we can actually properly handle it
even with some special casing.
"""
if isinstance(value, datetime):
return to_timestamp(value), []
if isinstance(value, StringEnum):
return value.value, []
if isinstance(value, InputFile):
if value.attach_uri:
return value.attach_uri, [value]
return None, [value]
if isinstance(value, (InputMedia, InputPaidMedia)) and isinstance(value.media, InputFile):
# We call to_dict and change the returned dict instead of overriding
# value.media in case the same value is reused for another request
data = value.to_dict()
if value.media.attach_uri:
data["media"] = value.media.attach_uri
else:
data.pop("media", None)
thumbnail = data.get("thumbnail", None)
if isinstance(thumbnail, InputFile):
if thumbnail.attach_uri:
data["thumbnail"] = thumbnail.attach_uri
else:
data.pop("thumbnail", None)
return data, [value.media, thumbnail]
return data, [value.media]
if isinstance(value, InputSticker) and isinstance(value.sticker, InputFile):
# We call to_dict and change the returned dict instead of overriding
# value.sticker in case the same value is reused for another request
data = value.to_dict()
data["sticker"] = value.sticker.attach_uri
return data, [value.sticker]
if isinstance(value, TelegramObject):
# Needs to be last, because InputMedia is a subclass of TelegramObject
return value.to_dict(), []
return value, []
@classmethod
def from_input(cls, key: str, value: object) -> "RequestParameter":
"""Builds an instance of this class for a given key-value pair that represents the raw
input as passed along from a method of :class:`telegram.Bot`.
"""
if not isinstance(value, (str, bytes)) and isinstance(value, Sequence):
param_values = []
input_files = []
for obj in value:
param_value, input_file = cls._value_and_input_files_from_input(obj)
if param_value is not None:
param_values.append(param_value)
input_files.extend(input_file)
return RequestParameter(
name=key, value=param_values, input_files=input_files if input_files else None
)
param_value, input_files = cls._value_and_input_files_from_input(value)
return RequestParameter(
name=key, value=param_value, input_files=input_files if input_files else None
)