Skip to content

Dev/2.1.3 - #60

Merged
ink-developer merged 9 commits into
mainfrom
dev/2.1.3
Jun 7, 2026
Merged

Dev/2.1.3#60
ink-developer merged 9 commits into
mainfrom
dev/2.1.3

Conversation

@ink-developer

@ink-developer ink-developer commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

Описание

Готовит релиз PyMax 2.1.3 и фиксит стабильность парсинга живых payload-ов, lifecycle соединения и интеграцию logging.

Основное:

  • добавлен fallback UnknownAttachment для неизвестных типов вложений;
  • ослаблены модели audio/video attachments и message elements для полей, которые Max может не присылать;
  • исправлена валидация Photo(url=...) с query string;
  • App.started теперь сбрасывается при потере соединения, ping-task отменяется, pending requests очищаются без unhandled future warnings;
  • configure_logging() больше не перетирает logging host-приложения, но сохраняет pretty-логи из коробки;
  • добавлены release notes для 2.1.3.

Тип изменений

  • Исправление бага
  • Новая функциональность
  • Улучшение документации
  • Рефакторинг

Связанные задачи / Issue

Ссылка на issue, если есть: #
#58
#57
#59

Summary by CodeRabbit

  • New Features

    • Unknown attachment type support prevents crashes while preserving attachment data.
  • Bug Fixes

    • Message no longer crashes on unsupported or unknown attachment types.
    • Audio and video attachments now accept missing duration fields.
    • Element URL and length attributes are now optional.
    • Photo validation correctly handles URLs with query strings.
    • Connection loss properly clears state and pending requests.
    • Reduced reconnect and close log noise.
    • Improved msgpack decoder error logging.
  • Documentation

    • Updated documentation theme and added release notes.
  • Behavior Changes

    • configure_logging() now respects existing logging handlers unless force=True.
    • Pretty logging enabled by default when not configured.

Sysoev86 and others added 9 commits June 3, 2026 23:41
Animoji/sticker elements arrive from the server without a `url` field, so
the strict `url: str` validation raises ValidationError and breaks login
sync. The server returns such elements without `url`, so the model is
stricter than the protocol itself.
Fix photo URL validation with query parameters
fix(types): make ElementAttributes.url optional
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

PyMax 2.1.3 bumps the version and introduces unknown attachment type handling, connection lifecycle callbacks for graceful shutdown, optional fields for attachments and elements, URL query-string filtering in photo validation, and configurable logging with a force-override mode.

Changes

PyMax 2.1.3 Release

Layer / File(s) Summary
Version and Release Documentation
pyproject.toml, src/pymax/__init__.py, docs/index.rst, docs/release-2-1-3.rst
Version incremented to 2.1.3; release notes document feature additions (unknown attachment handling), fixes (payload requirements, query-string parsing, connection-loss cleanup), behavior changes (logging configuration, msgpack decoding), and migration guidance.
Unknown Attachment Type Support
src/pymax/types/domain/attachments/enums.py, unknown.py, __init__.py, src/pymax/types/domain/message.py
AttachmentType.UNKNOWN enum value added; new UnknownAttachment Pydantic model with pre-validation rejects known types; message-level Attachment union expanded to KnownAttachment | UnknownAttachment, enabling graceful fallback for unrecognized payloads.
Field Optionality Updates
src/pymax/types/domain/attachments/audio.py, video.py, src/pymax/types/domain/element.py
AudioAttachment.duration/audio_id, VideoAttachment.duration, Element.length, and ElementAttributes.url changed from required to optional (... | None = None), accepting partial server payloads without validation failure.
Connection Lifecycle Callback and Error Handling
src/pymax/connection/connection.py, src/pymax/app.py, tests/connection/test_connection.py, tests/app/test_app_runtime.py
ConnectionManager accepts optional on_close callback; centralized close handling via _mark_closed() ensures callback fires once; request and receive-loop error paths consistently cancel pending requests and notify callback. App.on_connection_lost() handler stops the app and cancels ping task on connection loss. Tests verify pending-request cleanup and callback invocation.
Photo URL Query String Handling
src/pymax/files/photo.py, tests/files/test_files_and_formatting.py
validate_photo() now parses URL via urlsplit(...).path before extension/MIME-type computation, correctly handling URLs with query strings (e.g., ?quality=...). Test covers query-string case.
Logging Configuration with Force Parameter
src/pymax/logging.py, tests/test_logging.py
configure_logging() gains force: bool = False parameter; when false, respects existing handlers and adapts output routing; when true, replaces all pymax handlers. Handler detection helpers and PYMAX_HANDLER_ATTR marker distinguish PyMax-managed handlers from external ones. Comprehensive tests validate default/force/replacement behavior.
Protocol Codec Simplification
src/pymax/protocol/tcp/payload.py, protocol.py
Msgpack payload codec simplified: encode condenses to single-line; decode returns Any and directly returns e.unpacked instead of complex recovery logic; dict comprehensions converted to single-line form. TCP protocol framing simplified via single-line conditional/constructor expressions.
Integration and Unit Tests
tests/connection/test_connection.py, tests/app/test_app_runtime.py, tests/domain/test_message_models.py, tests/files/test_files_and_formatting.py, tests/test_logging.py
Comprehensive test coverage for unknown attachments (round-trip via UnknownAttachment), optional fields (partial payloads default missing fields to None), connection lifecycle (pending-request cleanup, on_close invocation, app shutdown), photo URL handling, and logging configuration (default/force/replacement modes).
Sphinx Documentation Configuration
docs/conf.py
Theme changed from shibuya to furo; sphinx.ext.autosummary extension added with auto-generation enabled; autodoc_default_options expanded to configure inheritance, member visibility, and exclude additional Pydantic-related symbols.

Sequence Diagram

sequenceDiagram
  participant App
  participant ConnectionManager
  participant ReceiveLoop
  participant Handler

  App->>ConnectionManager: init(on_close=on_connection_lost)
  ConnectionManager->>ConnectionManager: store on_close callback, _close_reported=False
  
  Note over ReceiveLoop: Normal operation
  
  Note over ReceiveLoop: Connection error occurs
  ReceiveLoop->>ConnectionManager: error in _recv_loop
  ConnectionManager->>ConnectionManager: cancel pending requests<br/>set _connection_lost=True
  ConnectionManager->>ConnectionManager: _mark_closed(exc)
  activate ConnectionManager
  ConnectionManager->>ConnectionManager: set _is_open=False
  ConnectionManager->>Handler: on_close(ConnectionError)
  deactivate ConnectionManager
  
  Handler->>App: on_connection_lost triggered
  activate App
  App->>App: set started=False
  App->>App: cancel ping task
  deactivate App
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • MaxApiTeam/PyMax#57: Both PRs modify src/pymax/types/domain/element.py to make ElementAttributes.url optional.
  • MaxApiTeam/PyMax#59: Both PRs modify photo URL validation in src/pymax/files/photo.py to handle query strings via urlsplit().
  • MaxApiTeam/PyMax#50: Both PRs modify connection lifecycle in src/pymax/connection/connection.py for fail/receive-loop teardown and pending-request cancellation.

Poem

🐰 A rabbit hops through PyMax's new warren,

Unknown attachments no longer cause sorrow,

Connections now cleanly say their goodbye,

With logs that are pretty and fields soft and dry,

Version two-one-three hops forth with delight!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Dev/2.1.3' is generic and vague, using a version number with a branch prefix instead of describing the actual changes in this PR. Revise the title to be more specific and descriptive, such as 'Prepare 2.1.3 release: improve attachment parsing, connection stability, and logging integration'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and follows the template structure with clear sections for Описание (description), Тип изменений (change type), and Связанные задачи (related issues), providing detailed information about bug fixes, refactoring, and documentation updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/2.1.3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/pymax/protocol/tcp/payload.py (2)

28-28: 💤 Low value

Optional: Remove unnecessary defensive or b"".

msgpack.packb() always returns bytes, never None or an empty falsy value (e.g., msgpack.packb({}) returns b'\x80'). The or b"" fallback is unnecessary.

♻️ Simplify to direct return
-        return msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True) or b""
+        return msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pymax/protocol/tcp/payload.py` at line 28, Remove the unnecessary
defensive fallback in the payload serialization: change the return that
currently does "msgpack.packb(self._to_msgpack_value(payload),
use_bin_type=True) or b''" to simply return the result of msgpack.packb directly
(i.e., "return msgpack.packb(self._to_msgpack_value(payload),
use_bin_type=True)"); this touches the serialization code that calls
msgpack.packb and uses _to_msgpack_value(payload).

48-56: Rework ExtraData concern: current behavior is intentional and test-covered

  • src/pymax/protocol/tcp/payload.py catches msgpack.exceptions.ExtraData, logs debug details, and returns e.unpacked (trailing msgpack objects in the payload are ignored).
  • tests/protocol/test_protocols.py::test_msgpack_codec_uses_first_dict_when_stream_has_extra_data asserts this exact behavior by decoding concatenated msgpack objects to only the first ({"ok": True}).
  • Any “data loss” here only applies if the protocol allows multiple msgpack objects within a single TCP payload frame; if that’s malformed/unexpected, consider warning or rejecting on ExtraData to avoid silent truncation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pymax/protocol/tcp/payload.py` around lines 48 - 56, The except block
catching msgpack.exceptions.ExtraData (where e is used and e.unpacked is
returned, with payload_bytes logged) is intentional and covered by tests;
preserve the current behavior (return e.unpacked) but add a clear inline comment
explaining that trailing msgpack objects are intentionally ignored (reference
the test that asserts this behavior), and optionally add a TODO or feature flag
comment suggesting rejecting/warning on ExtraData if a stricter protocol is
desired; do not change the return semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pymax/protocol/tcp/payload.py`:
- Line 37: MsgpackPayloadCodec.decode currently returns Any which hides the
implicit dict contract expected by tests and by TcpPayloadDecoder.decode; change
MsgpackPayloadCodec.decode to return dict[str, Any] and implement runtime
handling so when the top-level msgpack value is a map you return a dict with
string keys (decode/convert bytes keys to str), and if the top-level value is
not a map either raise a clear exception or wrap it into a dict with a known key
(so callers like TcpPayloadDecoder.decode and tests/protocol/test_protocols.py
always get dict[str, Any]); ensure key-normalization logic matches
TcpPayloadDecoder.decode expectations.

---

Nitpick comments:
In `@src/pymax/protocol/tcp/payload.py`:
- Line 28: Remove the unnecessary defensive fallback in the payload
serialization: change the return that currently does
"msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True) or b''" to
simply return the result of msgpack.packb directly (i.e., "return
msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True)"); this
touches the serialization code that calls msgpack.packb and uses
_to_msgpack_value(payload).
- Around line 48-56: The except block catching msgpack.exceptions.ExtraData
(where e is used and e.unpacked is returned, with payload_bytes logged) is
intentional and covered by tests; preserve the current behavior (return
e.unpacked) but add a clear inline comment explaining that trailing msgpack
objects are intentionally ignored (reference the test that asserts this
behavior), and optionally add a TODO or feature flag comment suggesting
rejecting/warning on ExtraData if a stricter protocol is desired; do not change
the return semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcc99940-a2d8-4b5f-977b-7cb52569b7cc

📥 Commits

Reviewing files that changed from the base of the PR and between 473218a and b560bdf.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • docs/conf.py
  • docs/index.rst
  • docs/release-2-1-3.rst
  • pyproject.toml
  • src/pymax/__init__.py
  • src/pymax/app.py
  • src/pymax/client.py
  • src/pymax/connection/connection.py
  • src/pymax/files/photo.py
  • src/pymax/logging.py
  • src/pymax/protocol/tcp/payload.py
  • src/pymax/protocol/tcp/protocol.py
  • src/pymax/types/domain/attachments/__init__.py
  • src/pymax/types/domain/attachments/audio.py
  • src/pymax/types/domain/attachments/enums.py
  • src/pymax/types/domain/attachments/unknown.py
  • src/pymax/types/domain/attachments/video.py
  • src/pymax/types/domain/element.py
  • src/pymax/types/domain/message.py
  • tests/app/test_app_runtime.py
  • tests/connection/test_connection.py
  • tests/domain/test_message_models.py
  • tests/files/test_files_and_formatting.py
  • tests/test_logging.py

return list(unpacker)

def decode(self, payload_bytes: bytes) -> dict[Any, Any]:
def decode(self, payload_bytes: bytes) -> Any:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find usages of MsgpackPayloadCodec.decode to verify compatibility.

# Find instantiations and calls
rg -nP 'MsgpackPayloadCodec\(\)\.decode|serializer\.decode' --type=py -C3

Repository: MaxApiTeam/PyMax

Length of output: 460


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the codec class definition
rg -n --hidden --no-ignore -S "class MsgpackPayloadCodec" src

# 2) Find instantiations/usages of MsgpackPayloadCodec
rg -n --hidden --no-ignore -S "MsgpackPayloadCodec\s*\(" src -g'*.py'

# 3) Find call sites of `.decode(` that involve MsgpackPayloadCodec in the same vicinity
rg -n --hidden --no-ignore -S "MsgpackPayloadCodec.*\.decode\(" src -g'*.py' -C3 || true

# 4) Fallback: find all `.decode(` call sites in the codec module(s) where MsgpackPayloadCodec is used
#    (kept narrow to avoid noise)
rg -n --hidden --no-ignore -S "\.decode\(" src/pymax/protocol/tcp -g'*.py' -C2

Repository: MaxApiTeam/PyMax

Length of output: 1276


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show MsgpackPayloadCodec definition + decode signature/implementation
echo "=== payload.py around MsgpackPayloadCodec ==="
nl -ba src/pymax/protocol/tcp/payload.py | sed -n '1,220p' | sed -n '1,220p'

echo "=== protocol.py around MsgpackPayloadCodec instantiation/usage ==="
nl -ba src/pymax/protocol/tcp/protocol.py | sed -n '1,140p'

Repository: MaxApiTeam/PyMax

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== payload.py (top 200 lines) ==="
sed -n '1,200p' src/pymax/protocol/tcp/payload.py

echo "=== payload.py (around lines 60-140) ==="
sed -n '60,160p' src/pymax/protocol/tcp/payload.py

echo "=== protocol.py (top 140 lines) ==="
sed -n '1,140p' src/pymax/protocol/tcp/protocol.py

Repository: MaxApiTeam/PyMax

Length of output: 7618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for all usages/instantiations of MsgpackPayloadCodec
rg -n --hidden --no-ignore -S "MsgpackPayloadCodec" . -g'*.py'

# Look for direct call patterns (new/alt spacing)
rg -n --hidden --no-ignore -S "MsgpackPayloadCodec\s*\(\s*\)\s*\.decode\s*\(" . -g'*.py' || true
rg -n --hidden --no-ignore -S "\.decode\s*\(\s*.*payload_bytes" src/pymax/protocol -g'*.py' || true

# Heuristic: variables named serializer/codec typed/assigned from MsgpackPayloadCodec, then decode called
rg -n --hidden --no-ignore -S "(serializer|codec)\s*=\s*MsgpackPayloadCodec\s*\(\s*\)|MsgpackPayloadCodec\s*\(\s*\)\s*$" . -g'*.py' -C2

# Check if MsgpackPayloadCodec is exported from a public module
rg -n --hidden --no-ignore -S "from .*payload import .*MsgpackPayloadCodec|MsgpackPayloadCodec" src/pymax/protocol -g'*.py' -C2

Repository: MaxApiTeam/PyMax

Length of output: 3284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' tests/protocol/test_protocols.py

Repository: MaxApiTeam/PyMax

Length of output: 4197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find likely local variables assigned from MsgpackPayloadCodec() and where they call .decode(...)
rg -n --hidden --no-ignore -S "(\w+)\s*=\s*MsgpackPayloadCodec\(\)" . -g'*.py'

# Find any "<var>.decode(" where <var> was assigned from MsgpackPayloadCodec()
rg -n --hidden --no-ignore -S "\b(\w+)\.decode\(" tests src -g'*.py'

Repository: MaxApiTeam/PyMax

Length of output: 1170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant part of connection.py where protocol.decode payload is used
sed -n '130,220p' src/pymax/connection/connection.py

# Also search for "payload" usage patterns and inspect nearby lines
rg -n --hidden --no-ignore -S "payload" src/pymax/connection -g'*.py' -C2

Repository: MaxApiTeam/PyMax

Length of output: 4728


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --hidden --no-ignore -S "model_validate\(" src -g'*.py' || true
rg -n --hidden --no-ignore -S "frame\.payload|\.payload\b" src/pymax/connection -g'*.py' -C2 || true
rg -n --hidden --no-ignore -S "payload.*model" src/pymax/connection -g'*.py' -C2 || true

# Show connection handling around where inbound frames are dispatched/resolved
rg -n --hidden --no-ignore -S "def _handle_inbound|_dispatch_event|resolve\(" src/pymax/connection/connection.py -C2
sed -n '1,260p' src/pymax/connection/connection.py

Repository: MaxApiTeam/PyMax

Length of output: 10799


MsgpackPayloadCodec.decode return-type widening to Any is compatible with in-repo callers, but the dict contract is implicit.

  • Direct in-repo callers: tests/protocol/test_protocols.py instantiates MsgpackPayloadCodec and asserts codec.decode(...) returns a dict for msgpack payloads encoded as maps.
  • Protocol decoding path: TcpProtocol consumes payloads only via TcpPayloadDecoder.decode, which normalizes keys and returns dict[str, Any] when the top-level msgpack value is a map.
  • Caveat: MsgpackPayloadCodec.decode can return non-dict top-level msgpack values, and TcpPayloadDecoder.decode is annotated as dict[str, Any], so the dict shape is an unstated runtime contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pymax/protocol/tcp/payload.py` at line 37, MsgpackPayloadCodec.decode
currently returns Any which hides the implicit dict contract expected by tests
and by TcpPayloadDecoder.decode; change MsgpackPayloadCodec.decode to return
dict[str, Any] and implement runtime handling so when the top-level msgpack
value is a map you return a dict with string keys (decode/convert bytes keys to
str), and if the top-level value is not a map either raise a clear exception or
wrap it into a dict with a known key (so callers like TcpPayloadDecoder.decode
and tests/protocol/test_protocols.py always get dict[str, Any]); ensure
key-normalization logic matches TcpPayloadDecoder.decode expectations.

@ink-developer
ink-developer merged commit 6cd0525 into main Jun 7, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Aug 4, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants