Added full support of the Bot API 9.4 (#1761)

* Bump API schema to version 9.4, add new object types, methods, and properties.

* Add tests for `ChatOwnerChanged` and `ChatOwnerLeft` message types

* Add tests for `GetUserProfileAudios`, `RemoveMyProfilePhoto`, and `SetMyProfilePhoto` methods

* Bump version

* Update Makefile variables and refactor `test_get_user_profile_audios.py`

* Document new features and updates from Bot API 9.4 in changelog

* Add `ButtonStyle` enum to represent button styles in the Telegram API

* Fix review issues from PR #1761

- Remove stray '-' artifact from GameHighScore docstring and butcher schema
- Fix Makefile reformat target scope inconsistency (ruff check --fix)
- Fix ButtonStyle enum source URL (#chat -> #inlinekeyboardbutton)
- Add User.get_profile_audios() shortcut method (parallel to get_profile_photos)
- Test ChatOwnerLeft with new_owner=None (edge case)
- Add VideoQuality type and Video.qualities nesting tests
- Add User.get_profile_audios() test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert "Fix review issues from PR #1761"

This reverts commit 2184e98988.

* Update source links for `ButtonStyle` documentation to reflect accurate API references

* Fix review issues from PR #1761 (#1762)

* Fix review issues from PR #1761

- Remove stray '-' artifact from GameHighScore docstring
- Fix Makefile reformat target scope inconsistency (ruff check --fix)
- Add User.get_profile_audios() shortcut method (parallel to get_profile_photos)
- Test ChatOwnerLeft with new_owner=None (edge case)
- Add VideoQuality type and Video.qualities nesting tests
- Add User.get_profile_audios() test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review comments: use fixture and variables in tests, add changelog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review follow-ups for PR #1762

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Reformat code

* Shut up, ruff

---------

Co-authored-by: latand <latand666@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com>
This commit is contained in:
Alex Root Junior 2026-02-10 23:43:52 +02:00 committed by GitHub
parent da7bfdca0c
commit 49d0784e33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 1457 additions and 61 deletions

View file

@ -0,0 +1,19 @@
from aiogram.methods import GetUserProfileAudios
from aiogram.types import Audio, UserProfileAudios
from tests.mocked_bot import MockedBot
class TestGetUserProfileAudios:
async def test_bot_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(
GetUserProfileAudios,
ok=True,
result=UserProfileAudios(
total_count=1,
audios=[Audio(file_id="file_id", file_unique_id="file_unique_id", duration=120)],
),
)
response: UserProfileAudios = await bot.get_user_profile_audios(user_id=42)
bot.get_request()
assert response == prepare_result.result

View file

@ -0,0 +1,11 @@
from aiogram.methods import RemoveMyProfilePhoto
from tests.mocked_bot import MockedBot
class TestRemoveMyProfilePhoto:
async def test_bot_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(RemoveMyProfilePhoto, ok=True, result=True)
response: bool = await bot.remove_my_profile_photo()
bot.get_request()
assert response == prepare_result.result

View file

@ -0,0 +1,14 @@
from aiogram.methods import SetMyProfilePhoto
from aiogram.types import InputProfilePhotoStatic
from tests.mocked_bot import MockedBot
class TestSetMyProfilePhoto:
async def test_bot_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(SetMyProfilePhoto, ok=True, result=True)
response: bool = await bot.set_my_profile_photo(
photo=InputProfilePhotoStatic(photo="file_id")
)
bot.get_request()
assert response == prepare_result.result

View file

@ -46,6 +46,8 @@ from aiogram.types import (
Chat,
ChatBackground,
ChatBoostAdded,
ChatOwnerChanged,
ChatOwnerLeft,
ChatShared,
Checklist,
ChecklistTask,
@ -253,6 +255,31 @@ TEST_MESSAGE_LEFT_CHAT_MEMBER = Message(
chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"),
)
TEST_MESSAGE_CHAT_OWNER_LEFT = Message(
message_id=42,
date=datetime.datetime.now(),
chat_owner_left=ChatOwnerLeft(
new_owner=User(id=43, is_bot=False, first_name="NewOwner"),
),
chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"),
)
TEST_MESSAGE_CHAT_OWNER_LEFT_NO_SUCCESSOR = Message(
message_id=42,
date=datetime.datetime.now(),
chat_owner_left=ChatOwnerLeft(),
chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"),
)
TEST_MESSAGE_CHAT_OWNER_CHANGED = Message(
message_id=42,
date=datetime.datetime.now(),
chat_owner_changed=ChatOwnerChanged(
new_owner=User(id=43, is_bot=False, first_name="NewOwner"),
),
chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"),
)
TEST_MESSAGE_INVOICE = Message(
message_id=42,
date=datetime.datetime.now(),
@ -849,6 +876,8 @@ MESSAGES_AND_CONTENT_TYPES = [
[TEST_MESSAGE_LOCATION, ContentType.LOCATION],
[TEST_MESSAGE_NEW_CHAT_MEMBERS, ContentType.NEW_CHAT_MEMBERS],
[TEST_MESSAGE_LEFT_CHAT_MEMBER, ContentType.LEFT_CHAT_MEMBER],
[TEST_MESSAGE_CHAT_OWNER_LEFT, ContentType.CHAT_OWNER_LEFT],
[TEST_MESSAGE_CHAT_OWNER_CHANGED, ContentType.CHAT_OWNER_CHANGED],
[TEST_MESSAGE_INVOICE, ContentType.INVOICE],
[TEST_MESSAGE_SUCCESSFUL_PAYMENT, ContentType.SUCCESSFUL_PAYMENT],
[TEST_MESSAGE_CONNECTED_WEBSITE, ContentType.CONNECTED_WEBSITE],
@ -930,6 +959,8 @@ MESSAGES_AND_COPY_METHODS = [
[TEST_MESSAGE_STORY, ForwardMessage],
[TEST_MESSAGE_NEW_CHAT_MEMBERS, None],
[TEST_MESSAGE_LEFT_CHAT_MEMBER, None],
[TEST_MESSAGE_CHAT_OWNER_LEFT, None],
[TEST_MESSAGE_CHAT_OWNER_CHANGED, None],
[TEST_MESSAGE_INVOICE, None],
[TEST_MESSAGE_SUCCESSFUL_PAYMENT, None],
[TEST_MESSAGE_CONNECTED_WEBSITE, None],
@ -1023,6 +1054,11 @@ class TestMessage:
def test_content_type(self, message: Message, content_type: str):
assert message.content_type == content_type
def test_chat_owner_left_no_successor(self):
assert (
TEST_MESSAGE_CHAT_OWNER_LEFT_NO_SUCCESSOR.content_type == ContentType.CHAT_OWNER_LEFT
)
def test_as_reply_parameters(self):
message = Message(
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()

View file

@ -56,3 +56,9 @@ class TestUser:
method = user.get_profile_photos(description="test")
assert method.user_id == user.id
def test_get_profile_audios(self):
user = User(id=42, is_bot=False, first_name="Test", last_name="User")
method = user.get_profile_audios(description="test")
assert method.user_id == user.id

View file

@ -0,0 +1,61 @@
import pytest
from aiogram.types import Video, VideoQuality
@pytest.fixture()
def video_quality():
return VideoQuality(
file_id="abc123",
file_unique_id="unique123",
width=1920,
height=1080,
codec="h264",
)
class TestVideoQuality:
def test_instantiation(self, video_quality: VideoQuality):
assert video_quality.file_id == "abc123"
assert video_quality.file_unique_id == "unique123"
assert video_quality.width == 1920
assert video_quality.height == 1080
assert video_quality.codec == "h264"
assert video_quality.file_size is None
def test_instantiation_with_file_size(self):
file_size = 1048576
vq = VideoQuality(
file_id="abc123",
file_unique_id="unique123",
width=1920,
height=1080,
codec="h265",
file_size=file_size,
)
assert vq.file_size == file_size
def test_video_with_qualities(self, video_quality: VideoQuality):
file_size = 524288
video = Video(
file_id="video123",
file_unique_id="unique_video123",
width=1920,
height=1080,
duration=120,
qualities=[
video_quality,
VideoQuality(
file_id="q2",
file_unique_id="uq2",
width=1280,
height=720,
codec="h264",
file_size=file_size,
),
],
)
assert video.qualities is not None
assert len(video.qualities) == 2
assert video.qualities[0].width == 1920
assert video.qualities[1].file_size == file_size