botapi-py 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- botapi/__init__.py +4 -0
- botapi/__init__.pyi +4 -0
- botapi/api.py +4532 -0
- botapi/core.py +110 -0
- botapi/enums.py +30 -0
- botapi/errors.py +34 -0
- botapi/types.py +4316 -0
- botapi/utils.py +66 -0
- botapi_py-1.1.0.dist-info/METADATA +8 -0
- botapi_py-1.1.0.dist-info/RECORD +11 -0
- botapi_py-1.1.0.dist-info/WHEEL +4 -0
botapi/core.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from . import types, errors
|
|
4
|
+
|
|
5
|
+
from typing import Any, Generic, Literal, Optional, Dict, TypeVar, TypeAlias
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import httpx
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
MultipartFile: TypeAlias = tuple[
|
|
15
|
+
str | None,
|
|
16
|
+
bytes,
|
|
17
|
+
Optional[str]
|
|
18
|
+
] # (filename, file_content, mime_type)
|
|
19
|
+
|
|
20
|
+
MultipartFiles: TypeAlias = dict[str, MultipartFile]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseBotAPI:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
token: str,
|
|
27
|
+
api_url: str = 'https://api.telegram.org',
|
|
28
|
+
session: Optional[httpx.AsyncClient] = None,
|
|
29
|
+
test_server: bool = False,
|
|
30
|
+
):
|
|
31
|
+
self.token: str = token.removeprefix('bot')
|
|
32
|
+
self.api_url: str = api_url
|
|
33
|
+
self.session: httpx.AsyncClient = session or httpx.AsyncClient(timeout=120)
|
|
34
|
+
self.test_server: bool = test_server
|
|
35
|
+
|
|
36
|
+
if not session:
|
|
37
|
+
log.warning('No HTTP session provided, creating a new one.')
|
|
38
|
+
|
|
39
|
+
def _compose_api_url(self, method: str) -> str:
|
|
40
|
+
url = f'{self.api_url}/bot{self.token}/'
|
|
41
|
+
if self.test_server:
|
|
42
|
+
url += 'test/'
|
|
43
|
+
url += method
|
|
44
|
+
return url
|
|
45
|
+
|
|
46
|
+
def _convert_data(self, data: dict[str, Any]) -> tuple[dict[str, Any], MultipartFiles]:
|
|
47
|
+
new_data: dict[str, Any] = {}
|
|
48
|
+
files: MultipartFiles = {} # (filename, file_content, mime_type)
|
|
49
|
+
|
|
50
|
+
for key, value in data.items():
|
|
51
|
+
|
|
52
|
+
# Multipart file uploading from local file
|
|
53
|
+
'''
|
|
54
|
+
#TODO (is it a good idea?) infer the parameter even if it's always a string (not wrapped inside a InputFile instance):
|
|
55
|
+
- if it's a local file path, send the string as-is if in local botapi mode, otherwise open, read and use multipart
|
|
56
|
+
- if it's a remote file URL or a file_id, send it as-is to the api
|
|
57
|
+
>> but i need to evaluate if it's worth the convenience, because when we pass a local path that points to a non-existing file
|
|
58
|
+
this would consider the string to be a file_id (and yes, it would throw an error, but it would be harder to debug).
|
|
59
|
+
'''
|
|
60
|
+
if isinstance(value, types.InputFile):
|
|
61
|
+
# if value.path:
|
|
62
|
+
# with open(value.path, 'rb') as f:
|
|
63
|
+
# file_content = f.read()
|
|
64
|
+
# elif value.file_content:
|
|
65
|
+
|
|
66
|
+
file_content = value.file_content
|
|
67
|
+
files[key] = (value.file_name, file_content, value.mime_type) # if mime_type is None, httpx will infer it with `mimetypes`
|
|
68
|
+
# continue
|
|
69
|
+
|
|
70
|
+
elif isinstance(value, BaseModel):
|
|
71
|
+
new_data[key] = json.dumps(
|
|
72
|
+
value.model_dump(
|
|
73
|
+
mode='json',
|
|
74
|
+
exclude_none=True,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
elif isinstance(value, list):
|
|
79
|
+
new_data[key] = json.dumps([
|
|
80
|
+
item.model_dump(
|
|
81
|
+
mode='json',
|
|
82
|
+
exclude_none=True,
|
|
83
|
+
)
|
|
84
|
+
if isinstance(item, BaseModel)
|
|
85
|
+
else item for item in value # pyright: ignore[reportUnknownVariableType]
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
new_data[key] = value
|
|
90
|
+
|
|
91
|
+
return new_data, files
|
|
92
|
+
|
|
93
|
+
async def _send_request(self, method: str, data: Dict[str, Any]) -> Any:
|
|
94
|
+
converted_data, files = self._convert_data(data)
|
|
95
|
+
|
|
96
|
+
request = await self.session.post(
|
|
97
|
+
url=self._compose_api_url(method),
|
|
98
|
+
data=converted_data,
|
|
99
|
+
files=files or None
|
|
100
|
+
)
|
|
101
|
+
result: dict[str, Any] = request.json()
|
|
102
|
+
|
|
103
|
+
if not result['ok']:
|
|
104
|
+
raise errors.BotAPIError(
|
|
105
|
+
method=method,
|
|
106
|
+
response=result
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return result['result']
|
|
110
|
+
|
botapi/enums.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
class ChatType(enum.Enum):
|
|
4
|
+
PRIVATE = 'private'
|
|
5
|
+
GROUP = 'group'
|
|
6
|
+
SUPERGROUP = 'supergroup'
|
|
7
|
+
CHANNEL = 'channel'
|
|
8
|
+
|
|
9
|
+
class MessageType(enum.Enum):
|
|
10
|
+
TEXT = 'text'
|
|
11
|
+
ANIMATION = 'animation'
|
|
12
|
+
AUDIO = 'audio'
|
|
13
|
+
DOCUMENT = 'document'
|
|
14
|
+
PAID_MEDIA = 'paid_media'
|
|
15
|
+
PHOTO = 'photo'
|
|
16
|
+
STICKER = 'sticker'
|
|
17
|
+
STORY = 'story'
|
|
18
|
+
VIDEO = 'video'
|
|
19
|
+
VIDEO_NOTE = 'video_note'
|
|
20
|
+
VOICE = 'voice'
|
|
21
|
+
CHECKLIST = 'checklist'
|
|
22
|
+
CONTACT = 'contact'
|
|
23
|
+
DICE = 'dice'
|
|
24
|
+
GAME = 'game'
|
|
25
|
+
POLL = 'poll'
|
|
26
|
+
VENUE = 'venue'
|
|
27
|
+
LOCATION = 'location'
|
|
28
|
+
INVOICE = 'invoice'
|
|
29
|
+
GIVEAWAY = 'giveaway'
|
|
30
|
+
|
botapi/errors.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from . import types
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BotAPIError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
Exception raised for Bot API request failures.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
method (`str`): The method name of the failed API call.
|
|
14
|
+
error_code (`int`, optional): Numeric error code when request fails.
|
|
15
|
+
description (`str`, optional): Human-readable error description.
|
|
16
|
+
parameters (`ResponseParameters`, optional): Additional error handling parameters.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
method: str
|
|
20
|
+
response: dict[str, Any]
|
|
21
|
+
error_code: Optional[int] = None
|
|
22
|
+
description: Optional[str] = None
|
|
23
|
+
parameters: Optional[types.ResponseParameters] = None
|
|
24
|
+
|
|
25
|
+
def __init__(self, method: str, response: dict[str, Any]):
|
|
26
|
+
self.method = method
|
|
27
|
+
self.response = response
|
|
28
|
+
self.error_code = response.get('error_code')
|
|
29
|
+
self.description = response.get('description')
|
|
30
|
+
_parameters = response.get('parameters')
|
|
31
|
+
self.parameters = types.ResponseParameters.model_validate(_parameters) if _parameters else None
|
|
32
|
+
|
|
33
|
+
Exception.__init__(self, f'BotAPI request failed: /{self.method}: {json.dumps(self.response, indent=True)}')
|
|
34
|
+
|