botapi-py 1.5.3__tar.gz → 1.5.5__tar.gz

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.
@@ -1,10 +1,9 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: botapi-py
3
- Version: 1.5.3
3
+ Version: 1.5.5
4
4
  Summary: A simple and flexible Python library to conveniently build fast Telegram bots using the Bot API.
5
5
  Requires-Dist: httpx>=0.28.1
6
6
  Requires-Dist: pydantic>=2.12.0
7
- Requires-Dist: orjson>=3.11.3 ; extra == 'redis'
8
7
  Requires-Dist: redis>=6.4.0 ; extra == 'redis'
9
8
  Requires-Python: >=3.12
10
9
  Project-URL: Telegram BotAPI Docs, https://core.telegram.org/bots/api
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "botapi-py"
3
- version = "1.5.3"
3
+ version = "1.5.5"
4
4
  description = "A simple and flexible Python library to conveniently build fast Telegram bots using the Bot API."
5
5
  # readme = "README.md"
6
6
  # authors = [
@@ -17,7 +17,6 @@ dependencies = [
17
17
 
18
18
  [project.optional-dependencies]
19
19
  redis = [
20
- "orjson>=3.11.3",
21
20
  "redis>=6.4.0",
22
21
  ]
23
22
 
@@ -28,6 +27,7 @@ build-backend = "uv_build"
28
27
  [dependency-groups]
29
28
  dev = [
30
29
  "bs4>=0.0.2",
30
+ "redis>=6.4.0",
31
31
  ]
32
32
 
33
33
  [tool.uv.build-backend]
@@ -1,11 +1,7 @@
1
1
  from .common import UpdateHandler
2
2
 
3
3
  from .get_updates_handler import GetUpdatesAsyncDispatcher
4
- try:
5
- # Fails at runtime if redis-py is not installed
6
- from .redis_pubsub_handler import RedisPubSubAsyncDispatcher
7
- except ImportError:
8
- pass
4
+ from .redis_pubsub_handler import RedisPubSubAsyncDispatcher
9
5
 
10
6
  __all__ = [
11
7
  'UpdateHandler',
@@ -5,23 +5,27 @@ import inspect
5
5
  import json
6
6
  import logging
7
7
  import signal
8
- from typing import override
8
+ from typing import TYPE_CHECKING, override
9
9
 
10
- import orjson
11
10
  import pydantic
12
11
 
13
- from botapi.types import Update
14
-
15
- from .common import BaseAsyncDispatcher, UpdateHandler
16
-
17
12
  try:
18
13
  import redis.asyncio as redis
19
14
  from redis.asyncio.client import PubSub
20
15
  _has_redis = True
21
16
  except ImportError as e:
22
17
  _has_redis = False
23
- raise e
24
-
18
+ # Fix type checker errors,
19
+ # but do not stop the library (with confusing errors) from being used without redis-py installed.
20
+ # Just raise a MEANINGFUL error when the dev tries to create an instance of `RedisPubSubAsyncDispatcher`.
21
+ if TYPE_CHECKING:
22
+ raise e
23
+
24
+ from botapi.types import Update
25
+
26
+ from .common import BaseAsyncDispatcher, UpdateHandler
27
+
28
+
25
29
  logger = logging.getLogger(__name__)
26
30
 
27
31
 
@@ -29,7 +33,7 @@ class RedisPubSubAsyncDispatcher(BaseAsyncDispatcher):
29
33
  update_handler: UpdateHandler
30
34
  update_handler_argcount: int
31
35
  tasks_shutdown_timeout: float
32
- tasks: set[asyncio.Task] # prevent gc from collecting tasks before they finish
36
+ tasks: set[asyncio.Task[None]] # prevent gc from collecting tasks before they finish
33
37
  redis_client: redis.Redis
34
38
  redis_reconnect_delay: float
35
39
  pubsub: PubSub
@@ -85,7 +89,7 @@ class RedisPubSubAsyncDispatcher(BaseAsyncDispatcher):
85
89
  self.pubsub = self.redis_client.pubsub(ignore_subscribe_messages=True)
86
90
  self.update_handler_argcount = len(inspect.getfullargspec(update_handler).args)
87
91
 
88
- def _task_done_callback(self, task: asyncio.Task) -> None:
92
+ def _task_done_callback(self, task: asyncio.Task[None]) -> None:
89
93
  '''
90
94
  Callback that is called by the event loop when an update_handler task is done.
91
95
 
@@ -106,7 +110,7 @@ class RedisPubSubAsyncDispatcher(BaseAsyncDispatcher):
106
110
  - extracts the bot token from the specified `self.bot_token_json_key` key
107
111
  - validates the update into an `Update` object.
108
112
  '''
109
- data: dict = orjson.loads(message)
113
+ data: dict = json.loads(message)
110
114
 
111
115
  if self.bot_token_json_key not in data:
112
116
  err = f'Missing bot token key "{self.bot_token_json_key}" in the update JSON payload'
@@ -71,6 +71,8 @@ class ServiceMessage(enum.StrEnum):
71
71
  '''
72
72
  NEW_CHAT_MEMBERS = 'new_chat_members'
73
73
  LEFT_CHAT_MEMBER = 'left_chat_member'
74
+ CHAT_OWNER_LEFT = 'chat_owner_left'
75
+ CHAT_OWNER_CHANGED = 'chat_owner_changed'
74
76
  NEW_CHAT_TITLE = 'new_chat_title'
75
77
  NEW_CHAT_PHOTO = 'new_chat_photo'
76
78
  DELETE_CHAT_PHOTO = 'delete_chat_photo'
@@ -119,6 +121,7 @@ class ServiceMessage(enum.StrEnum):
119
121
  WEB_APP_DATA = 'web_app_data'
120
122
  SUGGESTED_POST = 'suggested_post_info'
121
123
 
124
+
122
125
  class OtherMessageType(enum.StrEnum):
123
126
  '''
124
127
  Subset of `MessageType`.
@@ -38,6 +38,7 @@ from .types import (
38
38
  Audio,
39
39
  Document,
40
40
  Story,
41
+ VideoQuality,
41
42
  Video,
42
43
  VideoNote,
43
44
  Voice,
@@ -111,6 +112,7 @@ from .types import (
111
112
  SuggestedPostParameters,
112
113
  DirectMessagesTopic,
113
114
  UserProfilePhotos,
115
+ UserProfileAudios,
114
116
  File,
115
117
  WebAppInfo,
116
118
  ReplyKeyboardMarkup,
@@ -217,6 +219,8 @@ from .types import (
217
219
  ChatBoost,
218
220
  ChatBoostUpdated,
219
221
  ChatBoostRemoved,
222
+ ChatOwnerLeft,
223
+ ChatOwnerChanged,
220
224
  UserChatBoosts,
221
225
  BusinessBotRights,
222
226
  BusinessConnection,
@@ -1541,6 +1545,27 @@ class Methods(core.BaseBot):
1541
1545
  })
1542
1546
  return UserProfilePhotos.model_validate(response)
1543
1547
 
1548
+ async def get_user_profile_audios(
1549
+ self,
1550
+ user_id: int,
1551
+ offset: Optional[int] = None,
1552
+ limit: Optional[int] = None,
1553
+ ) -> UserProfileAudios:
1554
+ '''
1555
+ Use this method to get a list of
1556
+ profile audios for a user. Returns a UserProfileAudios
1557
+ object.
1558
+
1559
+ Reference: https://core.telegram.org/bots/api#getuserprofileaudios
1560
+ '''
1561
+
1562
+ response = await self._send_request('getUserProfileAudios', {
1563
+ 'user_id': user_id,
1564
+ 'offset': offset,
1565
+ 'limit': limit,
1566
+ })
1567
+ return UserProfileAudios.model_validate(response)
1568
+
1544
1569
  async def set_user_emoji_status(
1545
1570
  self,
1546
1571
  user_id: int,
@@ -2326,11 +2351,12 @@ class Methods(core.BaseBot):
2326
2351
  ) -> ForumTopic:
2327
2352
  '''
2328
2353
  Use this method to create a topic in
2329
- a forum supergroup chat. The bot must be
2330
- an administrator in the chat for this to
2331
- work and must have the can_manage_topics administrator rights.
2332
- Returns information about the created topic as a
2333
- ForumTopic object.
2354
+ a forum supergroup chat or a private chat
2355
+ with a user. In the case of a
2356
+ supergroup chat the bot must be an administrator
2357
+ in the chat for this to work and
2358
+ must have the can_manage_topics administrator right. Returns information
2359
+ about the created topic as a ForumTopic object.
2334
2360
 
2335
2361
  Reference: https://core.telegram.org/bots/api#createforumtopic
2336
2362
  '''
@@ -2818,6 +2844,35 @@ class Methods(core.BaseBot):
2818
2844
  })
2819
2845
  return BotShortDescription.model_validate(response)
2820
2846
 
2847
+ async def set_my_profile_photo(
2848
+ self,
2849
+ photo: _InputProfilePhoto,
2850
+ ) -> bool:
2851
+ '''
2852
+ Changes the profile photo of the bot. Returns
2853
+ True on success.
2854
+
2855
+ Reference: https://core.telegram.org/bots/api#setmyprofilephoto
2856
+ '''
2857
+
2858
+ response = await self._send_request('setMyProfilePhoto', {
2859
+ 'photo': photo,
2860
+ })
2861
+ return BoolAdapter.validate_python(response)
2862
+
2863
+ async def remove_my_profile_photo(
2864
+ self,
2865
+ ) -> bool:
2866
+ '''
2867
+ Removes the profile photo of the bot. Requires
2868
+ no parameters. Returns True on success.
2869
+
2870
+ Reference: https://core.telegram.org/bots/api#removemyprofilephoto
2871
+ '''
2872
+
2873
+ response = await self._send_request('removeMyProfilePhoto', {})
2874
+ return BoolAdapter.validate_python(response)
2875
+
2821
2876
  async def set_chat_menu_button(
2822
2877
  self,
2823
2878
  chat_id: Optional[int] = None,
@@ -81,6 +81,8 @@ class Message(MaybeInaccessibleMessage):
81
81
  location: Optional[Location] = Field(default=None)
82
82
  new_chat_members: Optional[List[User]] = Field(default=None)
83
83
  left_chat_member: Optional[User] = Field(default=None)
84
+ chat_owner_left: Optional[ChatOwnerLeft] = Field(default=None)
85
+ chat_owner_changed: Optional[ChatOwnerChanged] = Field(default=None)
84
86
  new_chat_title: Optional[str] = Field(default=None)
85
87
  new_chat_photo: Optional[List[PhotoSize]] = Field(default=None)
86
88
  delete_chat_photo: Optional[bool] = Field(default=None)
@@ -632,6 +634,100 @@ BackgroundTypeAdapter = TypeAdapter[_BackgroundType](_BackgroundType)
632
634
  class ChatMember(BaseModel):
633
635
  status: Literal['creator', 'administrator', 'member', 'restricted', 'left', 'kicked']
634
636
  user: User
637
+
638
+ # @property
639
+ async def is_participant(self) -> bool:
640
+ '''
641
+ Returns a boolean indicating if the user is currently part of of the chat (i.e. not left/banned).
642
+
643
+ This function handles special cases, like `ChatMemberRestricted` with the `is_member` flag.
644
+ '''
645
+ if isinstance(self, ChatMemberRestricted):
646
+ return self.is_member # this `ChatMemberRestricted` flag indicates whether it's a member or it has left
647
+
648
+ if isinstance(self, (
649
+ ChatMemberLeft,
650
+ ChatMemberBanned
651
+ )):
652
+ return False
653
+
654
+ return True
655
+
656
+ async def ban(
657
+ self,
658
+ bot: bot.Bot,
659
+ chat_id: Union[int, str],
660
+ until_date: Optional[int] = None
661
+ ) -> bool:
662
+ '''
663
+ Bans this chat member.
664
+ '''
665
+ return await bot.ban_chat_member(
666
+ chat_id=chat_id,
667
+ user_id=self.user.id,
668
+ until_date=until_date,
669
+ # revoke_messages=revoke_messages
670
+ )
671
+
672
+ async def unban(
673
+ self,
674
+ bot: bot.Bot,
675
+ chat_id: Union[int, str],
676
+ only_if_banned: Optional[bool] = None
677
+ ) -> bool:
678
+ '''
679
+ Unbans this chat member.
680
+ '''
681
+ return await bot.unban_chat_member(
682
+ chat_id=chat_id,
683
+ user_id=self.user.id,
684
+ only_if_banned=only_if_banned
685
+ )
686
+
687
+ async def kick(
688
+ self,
689
+ bot: bot.Bot,
690
+ chat_id: Union[int, str]
691
+ ) -> bool:
692
+ '''
693
+ Kicks this chat member (bans and immediately unbans).
694
+ '''
695
+ return await bot.unban_chat_member(
696
+ chat_id=chat_id,
697
+ user_id=self.user.id,
698
+ only_if_banned=False # ... "guarantees that after the call the user is not a member of the chat, but will be able to join it"
699
+ )
700
+
701
+ async def mute(
702
+ self,
703
+ bot: bot.Bot,
704
+ chat_id: Union[int, str],
705
+ until_date: Optional[int] = None
706
+ ) -> bool:
707
+ '''
708
+ Mutes (restricts) this chat member.
709
+ '''
710
+ return await bot.restrict_chat_member(
711
+ chat_id=chat_id,
712
+ user_id=self.user.id,
713
+ permissions=ChatPermissions(
714
+ can_send_messages=False,
715
+ can_send_audios=False,
716
+ can_send_documents=False,
717
+ can_send_photos=False,
718
+ can_send_videos=False,
719
+ can_send_video_notes=False,
720
+ can_send_voice_notes=False,
721
+ can_send_polls=False,
722
+ can_send_other_messages=False,
723
+ can_add_web_page_previews=False,
724
+ can_change_info=False,
725
+ can_invite_users=False,
726
+ can_pin_messages=False
727
+ ),
728
+ until_date=until_date
729
+ )
730
+
635
731
 
636
732
  class ChatMemberOwner(ChatMember):
637
733
  '''
@@ -2275,7 +2371,8 @@ class User(BaseModel):
2275
2371
  supports_inline_queries: Optional[bool] = Field(default=None)
2276
2372
  can_connect_to_business: Optional[bool] = Field(default=None)
2277
2373
  has_main_web_app: Optional[bool] = Field(default=None)
2278
- has_topics_enabled: Optional[bool] = Field(default=None)
2374
+ has_topics_enabled: Optional[bool] = Field(default=None)
2375
+ allows_users_to_create_topics: Optional[bool] = Field(default=None)
2279
2376
 
2280
2377
  class Chat(BaseModel):
2281
2378
  '''
@@ -2348,6 +2445,7 @@ class ChatFullInfo(BaseModel):
2348
2445
  linked_chat_id: Optional[int] = Field(default=None)
2349
2446
  location: Optional[ChatLocation] = Field(default=None)
2350
2447
  rating: Optional[UserRating] = Field(default=None)
2448
+ first_profile_audio: Optional[Audio] = Field(default=None)
2351
2449
  unique_gift_colors: Optional[UniqueGiftColors] = Field(default=None)
2352
2450
  paid_message_star_count: Optional[int] = Field(default=None)
2353
2451
 
@@ -2536,6 +2634,21 @@ class Story(BaseModel):
2536
2634
  chat: Chat
2537
2635
  id: int
2538
2636
 
2637
+ class VideoQuality(BaseModel):
2638
+ '''
2639
+ This object represents a video file of a
2640
+ specific quality.
2641
+
2642
+ Reference: https://core.telegram.org/bots/api#videoquality
2643
+ '''
2644
+
2645
+ file_id: str
2646
+ file_unique_id: str
2647
+ width: int
2648
+ height: int
2649
+ codec: str
2650
+ file_size: Optional[int] = Field(default=None)
2651
+
2539
2652
  class Video(BaseModel):
2540
2653
  '''
2541
2654
  This object represents a video file.
@@ -2551,6 +2664,7 @@ class Video(BaseModel):
2551
2664
  thumbnail: Optional[PhotoSize] = Field(default=None)
2552
2665
  cover: Optional[List[PhotoSize]] = Field(default=None)
2553
2666
  start_timestamp: Optional[int] = Field(default=None)
2667
+ qualities: Optional[List[VideoQuality]] = Field(default=None)
2554
2668
  file_name: Optional[str] = Field(default=None)
2555
2669
  mime_type: Optional[str] = Field(default=None)
2556
2670
  file_size: Optional[int] = Field(default=None)
@@ -3214,6 +3328,17 @@ class UserProfilePhotos(BaseModel):
3214
3328
  total_count: int
3215
3329
  photos: List[List[PhotoSize]]
3216
3330
 
3331
+ class UserProfileAudios(BaseModel):
3332
+ '''
3333
+ This object represents the audios displayed on a
3334
+ user's profile.
3335
+
3336
+ Reference: https://core.telegram.org/bots/api#userprofileaudios
3337
+ '''
3338
+
3339
+ total_count: int
3340
+ audios: List[Audio]
3341
+
3217
3342
  class File(BaseModel):
3218
3343
  '''
3219
3344
  This object represents a file ready to be
@@ -3304,16 +3429,18 @@ class ReplyKeyboardMarkup(BaseModel):
3304
3429
  class KeyboardButton(BaseModel):
3305
3430
  '''
3306
3431
  This object represents one button of the reply
3307
- keyboard. At most one of the optional fields
3308
- must be used to specify type of the
3309
- button. For simple text buttons, String can be
3310
- used instead of this object to specify the
3311
- button text.
3432
+ keyboard. At most one of the fields other
3433
+ than text, icon_custom_emoji_id, and style must be used
3434
+ to specify the type of the button. For
3435
+ simple text buttons, String can be used instead
3436
+ of this object to specify the button text.
3312
3437
 
3313
3438
  Reference: https://core.telegram.org/bots/api#keyboardbutton
3314
3439
  '''
3315
3440
 
3316
3441
  text: str
3442
+ icon_custom_emoji_id: Optional[str] = Field(default=None)
3443
+ style: Optional[str] = Field(default=None)
3317
3444
  request_users: Optional[KeyboardButtonRequestUsers] = Field(default=None)
3318
3445
  request_chat: Optional[KeyboardButtonRequestChat] = Field(default=None)
3319
3446
  request_contact: Optional[bool] = Field(default=None)
@@ -3420,13 +3547,16 @@ class InlineKeyboardMarkup(BaseModel):
3420
3547
  class InlineKeyboardButton(BaseModel):
3421
3548
  '''
3422
3549
  This object represents one button of an inline
3423
- keyboard. Exactly one of the optional fields must
3424
- be used to specify type of the button.
3550
+ keyboard. Exactly one of the fields other than
3551
+ text, icon_custom_emoji_id, and style must be used to
3552
+ specify the type of the button.
3425
3553
 
3426
3554
  Reference: https://core.telegram.org/bots/api#inlinekeyboardbutton
3427
3555
  '''
3428
3556
 
3429
3557
  text: str
3558
+ icon_custom_emoji_id: Optional[str] = Field(default=None)
3559
+ style: Optional[str] = Field(default=None)
3430
3560
  url: Optional[str] = Field(default=None)
3431
3561
  callback_data: Optional[str] = Field(default=None)
3432
3562
  web_app: Optional[WebAppInfo] = Field(default=None)
@@ -3878,7 +4008,8 @@ class UniqueGiftModel(BaseModel):
3878
4008
 
3879
4009
  name: str
3880
4010
  sticker: Sticker
3881
- rarity_per_mille: int
4011
+ rarity_per_mille: int
4012
+ rarity: Optional[str] = Field(default=None)
3882
4013
 
3883
4014
  class UniqueGiftSymbol(BaseModel):
3884
4015
  '''
@@ -3949,6 +4080,7 @@ class UniqueGift(BaseModel):
3949
4080
  symbol: UniqueGiftSymbol
3950
4081
  backdrop: UniqueGiftBackdrop
3951
4082
  is_premium: Optional[bool] = Field(default=None)
4083
+ is_burned: Optional[bool] = Field(default=None)
3952
4084
  is_from_blockchain: Optional[bool] = Field(default=None)
3953
4085
  colors: Optional[UniqueGiftColors] = Field(default=None)
3954
4086
  publisher_chat: Optional[Chat] = Field(default=None)
@@ -4098,6 +4230,26 @@ class ChatBoostRemoved(BaseModel):
4098
4230
  remove_date: int
4099
4231
  source: _ChatBoostSource
4100
4232
 
4233
+ class ChatOwnerLeft(BaseModel):
4234
+ '''
4235
+ Describes a service message about the chat owner
4236
+ leaving the chat.
4237
+
4238
+ Reference: https://core.telegram.org/bots/api#chatownerleft
4239
+ '''
4240
+
4241
+ new_owner: Optional[User] = Field(default=None)
4242
+
4243
+ class ChatOwnerChanged(BaseModel):
4244
+ '''
4245
+ Describes a service message about an ownership change
4246
+ in the chat.
4247
+
4248
+ Reference: https://core.telegram.org/bots/api#chatownerchanged
4249
+ '''
4250
+
4251
+ new_owner: User
4252
+
4101
4253
  class UserChatBoosts(BaseModel):
4102
4254
  '''
4103
4255
  This object represents a list of boosts added
File without changes
File without changes