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/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
+