sunnygram 1.0.0__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.
- sunnygram-1.0.0/.gitattributes +14 -0
- sunnygram-1.0.0/.github/workflows/ci.yml +55 -0
- sunnygram-1.0.0/.github/workflows/docs.yml +23 -0
- sunnygram-1.0.0/.github/workflows/release.yml +40 -0
- sunnygram-1.0.0/.github/workflows/schema.yml +22 -0
- sunnygram-1.0.0/.gitignore +32 -0
- sunnygram-1.0.0/ARCHITECTURE.md +364 -0
- sunnygram-1.0.0/CHANGELOG.md +962 -0
- sunnygram-1.0.0/CONTRIBUTING.md +154 -0
- sunnygram-1.0.0/LICENSE +373 -0
- sunnygram-1.0.0/PKG-INFO +242 -0
- sunnygram-1.0.0/README.md +202 -0
- sunnygram-1.0.0/SECURITY.md +55 -0
- sunnygram-1.0.0/benchmarks/README.md +39 -0
- sunnygram-1.0.0/benchmarks/delta.py +157 -0
- sunnygram-1.0.0/benchmarks/rules.py +389 -0
- sunnygram-1.0.0/codegen/gen_errors.py +297 -0
- sunnygram-1.0.0/codegen/gen_tl.py +881 -0
- sunnygram-1.0.0/codegen/overrides.py +102 -0
- sunnygram-1.0.0/codegen/parser.py +285 -0
- sunnygram-1.0.0/codegen/refresh.py +247 -0
- sunnygram-1.0.0/codegen/schema/api.tl +3087 -0
- sunnygram-1.0.0/codegen/schema/errors.json +1 -0
- sunnygram-1.0.0/codegen/schema/mtproto.tl +128 -0
- sunnygram-1.0.0/codegen/schema/version.json +23 -0
- sunnygram-1.0.0/docs/account.md +132 -0
- sunnygram-1.0.0/docs/admin.md +126 -0
- sunnygram-1.0.0/docs/api/client.md +35 -0
- sunnygram-1.0.0/docs/api/errors.md +51 -0
- sunnygram-1.0.0/docs/api/filters.md +10 -0
- sunnygram-1.0.0/docs/api/layers.md +74 -0
- sunnygram-1.0.0/docs/api/types.md +74 -0
- sunnygram-1.0.0/docs/attachments.md +115 -0
- sunnygram-1.0.0/docs/bots.md +75 -0
- sunnygram-1.0.0/docs/buttons.md +204 -0
- sunnygram-1.0.0/docs/chats.md +104 -0
- sunnygram-1.0.0/docs/conversations.md +89 -0
- sunnygram-1.0.0/docs/errors.md +104 -0
- sunnygram-1.0.0/docs/files.md +219 -0
- sunnygram-1.0.0/docs/filters.md +130 -0
- sunnygram-1.0.0/docs/importing.md +103 -0
- sunnygram-1.0.0/docs/index.md +102 -0
- sunnygram-1.0.0/docs/inline.md +176 -0
- sunnygram-1.0.0/docs/logging.md +72 -0
- sunnygram-1.0.0/docs/login.md +115 -0
- sunnygram-1.0.0/docs/messages.md +223 -0
- sunnygram-1.0.0/docs/payments.md +118 -0
- sunnygram-1.0.0/docs/peers.md +112 -0
- sunnygram-1.0.0/docs/performance.md +149 -0
- sunnygram-1.0.0/docs/plugins.md +74 -0
- sunnygram-1.0.0/docs/proxies.md +95 -0
- sunnygram-1.0.0/docs/quickstart.md +84 -0
- sunnygram-1.0.0/docs/rate-limits.md +64 -0
- sunnygram-1.0.0/docs/raw-api.md +158 -0
- sunnygram-1.0.0/docs/sessions.md +82 -0
- sunnygram-1.0.0/docs/stories.md +84 -0
- sunnygram-1.0.0/docs/topics.md +104 -0
- sunnygram-1.0.0/docs/updates.md +212 -0
- sunnygram-1.0.0/examples/adopt.py +94 -0
- sunnygram-1.0.0/examples/ask.py +90 -0
- sunnygram-1.0.0/examples/buttons.py +125 -0
- sunnygram-1.0.0/examples/echo.py +75 -0
- sunnygram-1.0.0/examples/events.py +158 -0
- sunnygram-1.0.0/examples/files.py +158 -0
- sunnygram-1.0.0/examples/get_config.py +92 -0
- sunnygram-1.0.0/examples/listen.py +156 -0
- sunnygram-1.0.0/examples/login.py +160 -0
- sunnygram-1.0.0/examples/moderate.py +140 -0
- sunnygram-1.0.0/examples/plugin_bot.py +45 -0
- sunnygram-1.0.0/examples/plugins/__init__.py +6 -0
- sunnygram-1.0.0/examples/plugins/echo.py +22 -0
- sunnygram-1.0.0/examples/plugins/greet.py +23 -0
- sunnygram-1.0.0/examples/shop.py +145 -0
- sunnygram-1.0.0/examples/topics.py +120 -0
- sunnygram-1.0.0/examples/tour.py +123 -0
- sunnygram-1.0.0/mkdocs.yml +110 -0
- sunnygram-1.0.0/pyproject.toml +100 -0
- sunnygram-1.0.0/src/sunnygram/__init__.py +228 -0
- sunnygram-1.0.0/src/sunnygram/auth/__init__.py +37 -0
- sunnygram-1.0.0/src/sunnygram/auth/login.py +425 -0
- sunnygram-1.0.0/src/sunnygram/client.py +2607 -0
- sunnygram-1.0.0/src/sunnygram/conversation.py +246 -0
- sunnygram-1.0.0/src/sunnygram/crypto/__init__.py +97 -0
- sunnygram-1.0.0/src/sunnygram/crypto/accel.py +224 -0
- sunnygram-1.0.0/src/sunnygram/crypto/aes.py +431 -0
- sunnygram-1.0.0/src/sunnygram/crypto/auth_key.py +292 -0
- sunnygram-1.0.0/src/sunnygram/crypto/factorization.py +79 -0
- sunnygram-1.0.0/src/sunnygram/crypto/mtproto.py +90 -0
- sunnygram-1.0.0/src/sunnygram/crypto/rsa.py +206 -0
- sunnygram-1.0.0/src/sunnygram/crypto/srp.py +145 -0
- sunnygram-1.0.0/src/sunnygram/dispatcher.py +668 -0
- sunnygram-1.0.0/src/sunnygram/errors/__init__.py +138 -0
- sunnygram-1.0.0/src/sunnygram/errors/base.py +198 -0
- sunnygram-1.0.0/src/sunnygram/errors/generated.py +4899 -0
- sunnygram-1.0.0/src/sunnygram/errors/rpc.py +234 -0
- sunnygram-1.0.0/src/sunnygram/files/__init__.py +46 -0
- sunnygram-1.0.0/src/sunnygram/files/cdn.py +316 -0
- sunnygram-1.0.0/src/sunnygram/files/download.py +387 -0
- sunnygram-1.0.0/src/sunnygram/files/location.py +184 -0
- sunnygram-1.0.0/src/sunnygram/files/parts.py +105 -0
- sunnygram-1.0.0/src/sunnygram/files/ref.py +324 -0
- sunnygram-1.0.0/src/sunnygram/files/upload.py +215 -0
- sunnygram-1.0.0/src/sunnygram/filters.py +371 -0
- sunnygram-1.0.0/src/sunnygram/loop.py +107 -0
- sunnygram-1.0.0/src/sunnygram/methods/__init__.py +330 -0
- sunnygram-1.0.0/src/sunnygram/methods/account.py +356 -0
- sunnygram-1.0.0/src/sunnygram/methods/admin.py +524 -0
- sunnygram-1.0.0/src/sunnygram/methods/albums.py +191 -0
- sunnygram-1.0.0/src/sunnygram/methods/attachments.py +398 -0
- sunnygram-1.0.0/src/sunnygram/methods/bots.py +428 -0
- sunnygram-1.0.0/src/sunnygram/methods/chats.py +230 -0
- sunnygram-1.0.0/src/sunnygram/methods/folders.py +110 -0
- sunnygram-1.0.0/src/sunnygram/methods/forum.py +237 -0
- sunnygram-1.0.0/src/sunnygram/methods/media.py +171 -0
- sunnygram-1.0.0/src/sunnygram/methods/messages.py +763 -0
- sunnygram-1.0.0/src/sunnygram/methods/payments.py +318 -0
- sunnygram-1.0.0/src/sunnygram/methods/reactions.py +112 -0
- sunnygram-1.0.0/src/sunnygram/methods/stories.py +233 -0
- sunnygram-1.0.0/src/sunnygram/methods/takeout.py +137 -0
- sunnygram-1.0.0/src/sunnygram/methods/users.py +88 -0
- sunnygram-1.0.0/src/sunnygram/migrate.py +787 -0
- sunnygram-1.0.0/src/sunnygram/network/__init__.py +39 -0
- sunnygram-1.0.0/src/sunnygram/network/config.py +82 -0
- sunnygram-1.0.0/src/sunnygram/network/connection.py +864 -0
- sunnygram-1.0.0/src/sunnygram/network/datacenter.py +79 -0
- sunnygram-1.0.0/src/sunnygram/network/handshake.py +204 -0
- sunnygram-1.0.0/src/sunnygram/network/invoker.py +843 -0
- sunnygram-1.0.0/src/sunnygram/network/limiter.py +221 -0
- sunnygram-1.0.0/src/sunnygram/parser/__init__.py +72 -0
- sunnygram-1.0.0/src/sunnygram/parser/entities.py +160 -0
- sunnygram-1.0.0/src/sunnygram/parser/html.py +186 -0
- sunnygram-1.0.0/src/sunnygram/parser/markdown.py +300 -0
- sunnygram-1.0.0/src/sunnygram/peers/__init__.py +49 -0
- sunnygram-1.0.0/src/sunnygram/peers/cache.py +402 -0
- sunnygram-1.0.0/src/sunnygram/peers/resolver.py +354 -0
- sunnygram-1.0.0/src/sunnygram/plugins.py +363 -0
- sunnygram-1.0.0/src/sunnygram/py.typed +0 -0
- sunnygram-1.0.0/src/sunnygram/raw/__init__.py +45 -0
- sunnygram-1.0.0/src/sunnygram/raw/all.py +2527 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/__init__.py +450 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/_root.py +2123 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/account.py +107 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/aicompose.py +23 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/auth.py +65 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/bots.py +30 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/channels.py +35 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/chatlists.py +29 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/communities.py +22 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/contacts.py +47 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/fragment.py +20 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/help.py +93 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/messages.py +232 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/payments.py +99 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/phone.py +34 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/photos.py +25 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/premium.py +24 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/smsjobs.py +22 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/stats.py +30 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/stickers.py +20 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/storage.py +31 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/stories.py +42 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/updates.py +33 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/upload.py +30 -0
- sunnygram-1.0.0/src/sunnygram/raw/base/users.py +30 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/__init__.py +124 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/_root.py +432 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/account.py +3954 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/aicompose.py +279 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/auth.py +932 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/bots.py +1321 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/channels.py +2101 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/chatlists.py +377 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/communities.py +369 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/contacts.py +967 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/ephemeral.py +241 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/folders.py +53 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/fragment.py +53 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/help.py +645 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/langpack.py +191 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/messages.py +10969 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/mtproto.py +335 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/payments.py +2400 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/phone.py +1738 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/photos.py +284 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/premium.py +187 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/smsjobs.py +194 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/stats.py +343 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/stickers.py +441 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/stories.py +1441 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/updates.py +159 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/upload.py +327 -0
- sunnygram-1.0.0/src/sunnygram/raw/functions/users.py +245 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/__init__.py +2714 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/_root.py +52528 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/account.py +1342 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/aicompose.py +79 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/auth.py +934 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/bots.py +220 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/channels.py +257 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/chatlists.py +233 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/communities.py +119 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/contacts.py +428 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/fragment.py +74 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/help.py +1131 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/messages.py +3398 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/mtproto.py +1453 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/payments.py +1757 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/phone.py +314 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/photos.py +124 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/premium.py +199 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/smsjobs.py +122 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/stats.py +403 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/stickers.py +49 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/storage.py +193 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/stories.py +483 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/updates.py +409 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/upload.py +206 -0
- sunnygram-1.0.0/src/sunnygram/raw/types/users.py +176 -0
- sunnygram-1.0.0/src/sunnygram/recent.py +103 -0
- sunnygram-1.0.0/src/sunnygram/session/__init__.py +45 -0
- sunnygram-1.0.0/src/sunnygram/session/msg_id.py +73 -0
- sunnygram-1.0.0/src/sunnygram/session/mtproto.py +236 -0
- sunnygram-1.0.0/src/sunnygram/session/session.py +294 -0
- sunnygram-1.0.0/src/sunnygram/storage/__init__.py +54 -0
- sunnygram-1.0.0/src/sunnygram/storage/base.py +245 -0
- sunnygram-1.0.0/src/sunnygram/storage/memory.py +95 -0
- sunnygram-1.0.0/src/sunnygram/storage/sqlite.py +347 -0
- sunnygram-1.0.0/src/sunnygram/storage/string.py +117 -0
- sunnygram-1.0.0/src/sunnygram/tl/__init__.py +44 -0
- sunnygram-1.0.0/src/sunnygram/tl/core.py +565 -0
- sunnygram-1.0.0/src/sunnygram/transport/__init__.py +33 -0
- sunnygram-1.0.0/src/sunnygram/transport/codec.py +249 -0
- sunnygram-1.0.0/src/sunnygram/transport/obfuscation.py +157 -0
- sunnygram-1.0.0/src/sunnygram/transport/proxy.py +337 -0
- sunnygram-1.0.0/src/sunnygram/transport/tcp.py +176 -0
- sunnygram-1.0.0/src/sunnygram/types/__init__.py +82 -0
- sunnygram-1.0.0/src/sunnygram/types/buttons.py +339 -0
- sunnygram-1.0.0/src/sunnygram/types/callback.py +223 -0
- sunnygram-1.0.0/src/sunnygram/types/chat.py +114 -0
- sunnygram-1.0.0/src/sunnygram/types/dialog.py +115 -0
- sunnygram-1.0.0/src/sunnygram/types/events.py +239 -0
- sunnygram-1.0.0/src/sunnygram/types/folder.py +97 -0
- sunnygram-1.0.0/src/sunnygram/types/inline.py +752 -0
- sunnygram-1.0.0/src/sunnygram/types/join.py +108 -0
- sunnygram-1.0.0/src/sunnygram/types/member.py +307 -0
- sunnygram-1.0.0/src/sunnygram/types/message.py +407 -0
- sunnygram-1.0.0/src/sunnygram/types/payments.py +221 -0
- sunnygram-1.0.0/src/sunnygram/types/poll.py +266 -0
- sunnygram-1.0.0/src/sunnygram/types/reaction.py +188 -0
- sunnygram-1.0.0/src/sunnygram/types/rights.py +205 -0
- sunnygram-1.0.0/src/sunnygram/types/story.py +90 -0
- sunnygram-1.0.0/src/sunnygram/types/topic.py +124 -0
- sunnygram-1.0.0/src/sunnygram/types/user.py +73 -0
- sunnygram-1.0.0/src/sunnygram/updates/__init__.py +19 -0
- sunnygram-1.0.0/src/sunnygram/updates/manager.py +610 -0
- sunnygram-1.0.0/src/sunnygram/updates/state.py +219 -0
- sunnygram-1.0.0/src/sunnygram/utils.py +24 -0
- sunnygram-1.0.0/tests/conftest.py +33 -0
- sunnygram-1.0.0/tests/mtproto_server.py +364 -0
- sunnygram-1.0.0/tests/test_auth.py +598 -0
- sunnygram-1.0.0/tests/test_buttons.py +171 -0
- sunnygram-1.0.0/tests/test_cdn.py +644 -0
- sunnygram-1.0.0/tests/test_client.py +1717 -0
- sunnygram-1.0.0/tests/test_client_surface.py +423 -0
- sunnygram-1.0.0/tests/test_codegen.py +434 -0
- sunnygram-1.0.0/tests/test_connection.py +567 -0
- sunnygram-1.0.0/tests/test_crypto_accel.py +171 -0
- sunnygram-1.0.0/tests/test_crypto_aes.py +164 -0
- sunnygram-1.0.0/tests/test_crypto_auth_key.py +261 -0
- sunnygram-1.0.0/tests/test_crypto_factorization.py +50 -0
- sunnygram-1.0.0/tests/test_crypto_mtproto.py +124 -0
- sunnygram-1.0.0/tests/test_crypto_rsa.py +178 -0
- sunnygram-1.0.0/tests/test_crypto_srp.py +181 -0
- sunnygram-1.0.0/tests/test_dispatcher.py +709 -0
- sunnygram-1.0.0/tests/test_docs.py +179 -0
- sunnygram-1.0.0/tests/test_errors.py +181 -0
- sunnygram-1.0.0/tests/test_events.py +1055 -0
- sunnygram-1.0.0/tests/test_file_ref.py +205 -0
- sunnygram-1.0.0/tests/test_files.py +626 -0
- sunnygram-1.0.0/tests/test_inline.py +350 -0
- sunnygram-1.0.0/tests/test_invoker.py +554 -0
- sunnygram-1.0.0/tests/test_limiter.py +183 -0
- sunnygram-1.0.0/tests/test_loop.py +122 -0
- sunnygram-1.0.0/tests/test_methods_admin.py +629 -0
- sunnygram-1.0.0/tests/test_methods_bots.py +464 -0
- sunnygram-1.0.0/tests/test_methods_chats.py +378 -0
- sunnygram-1.0.0/tests/test_methods_export.py +232 -0
- sunnygram-1.0.0/tests/test_methods_extras.py +400 -0
- sunnygram-1.0.0/tests/test_methods_forum.py +368 -0
- sunnygram-1.0.0/tests/test_methods_media.py +273 -0
- sunnygram-1.0.0/tests/test_methods_messages.py +287 -0
- sunnygram-1.0.0/tests/test_methods_payments.py +301 -0
- sunnygram-1.0.0/tests/test_methods_scheduled.py +241 -0
- sunnygram-1.0.0/tests/test_methods_stories.py +202 -0
- sunnygram-1.0.0/tests/test_migrate.py +471 -0
- sunnygram-1.0.0/tests/test_network.py +364 -0
- sunnygram-1.0.0/tests/test_obfuscation.py +232 -0
- sunnygram-1.0.0/tests/test_package.py +71 -0
- sunnygram-1.0.0/tests/test_parser.py +259 -0
- sunnygram-1.0.0/tests/test_peers.py +658 -0
- sunnygram-1.0.0/tests/test_plugins.py +234 -0
- sunnygram-1.0.0/tests/test_proxy.py +392 -0
- sunnygram-1.0.0/tests/test_recent.py +238 -0
- sunnygram-1.0.0/tests/test_redaction.py +192 -0
- sunnygram-1.0.0/tests/test_session.py +482 -0
- sunnygram-1.0.0/tests/test_storage.py +355 -0
- sunnygram-1.0.0/tests/test_tl_core.py +392 -0
- sunnygram-1.0.0/tests/test_transport.py +332 -0
- sunnygram-1.0.0/tests/test_typing.py +128 -0
- sunnygram-1.0.0/tests/test_updates.py +819 -0
- sunnygram-1.0.0/tests/test_updates_model.py +769 -0
- sunnygram-1.0.0/tests/test_updates_state.py +346 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Normalise line endings on the way into the repository. Everything here is
|
|
2
|
+
# text, and this project is developed on Windows but built, tested and installed
|
|
3
|
+
# on Linux runners, so committing CRLF would put carriage returns into published
|
|
4
|
+
# sdists and make every cross-platform diff noisy. Working copies keep whatever
|
|
5
|
+
# the platform prefers; the committed bytes are LF.
|
|
6
|
+
* text=auto eol=lf
|
|
7
|
+
|
|
8
|
+
# Except the things that are not text. Recording them stops a future addition
|
|
9
|
+
# from being silently corrupted by the rule above.
|
|
10
|
+
*.png binary
|
|
11
|
+
*.jpg binary
|
|
12
|
+
*.gif binary
|
|
13
|
+
*.ico binary
|
|
14
|
+
*.session binary
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: python -m pip install -e ".[dev]"
|
|
22
|
+
- name: Lint
|
|
23
|
+
run: ruff check src tests codegen examples
|
|
24
|
+
- name: Type-check
|
|
25
|
+
run: mypy
|
|
26
|
+
- name: Test
|
|
27
|
+
run: pytest -q --cov --cov-report=term-missing --cov-report=xml
|
|
28
|
+
|
|
29
|
+
# One version reports, so the number is one number rather than three that
|
|
30
|
+
# disagree because the matrix ran them on different interpreters.
|
|
31
|
+
- name: Keep the coverage report
|
|
32
|
+
if: matrix.python-version == '3.13'
|
|
33
|
+
uses: actions/upload-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: coverage
|
|
36
|
+
path: coverage.xml
|
|
37
|
+
|
|
38
|
+
# Both generated trees, rebuilt from the vendored schema and error table and
|
|
39
|
+
# compared against what was committed (rule H7). The suite already compares
|
|
40
|
+
# them in memory; this runs the generators the way a person would and lets
|
|
41
|
+
# the checkout have its say, so a hand-edit or a stale commit fails here
|
|
42
|
+
# whatever it was that produced it.
|
|
43
|
+
codegen:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- uses: actions/setup-python@v5
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.13"
|
|
50
|
+
- name: Regenerate
|
|
51
|
+
run: |
|
|
52
|
+
python codegen/gen_tl.py
|
|
53
|
+
python codegen/gen_errors.py
|
|
54
|
+
- name: Nothing generated should have changed
|
|
55
|
+
run: git diff --exit-code -- src/sunnygram/raw src/sunnygram/errors
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- name: Install
|
|
19
|
+
run: pip install -e ".[docs]"
|
|
20
|
+
# --strict, so a link to a page that moved or a docstring reference that
|
|
21
|
+
# no longer resolves fails the build rather than shipping a broken page.
|
|
22
|
+
- name: Build and deploy
|
|
23
|
+
run: mkdocs gh-deploy --force --strict
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag is pushed. Uses PyPI trusted publishing
|
|
4
|
+
# (OIDC), so no API token is stored anywhere once the project is configured on
|
|
5
|
+
# PyPI; see https://docs.pypi.org/trusted-publishers/. Point the publisher at
|
|
6
|
+
# this repository and the "pypi" environment below.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- name: Build sdist and wheel
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install build
|
|
23
|
+
python -m build
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write # required for trusted publishing
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Schema pin
|
|
2
|
+
|
|
3
|
+
# Whether Telegram has shipped a layer newer than the one we vendored. Kept out
|
|
4
|
+
# of the CI workflow on purpose: this depends on the network and on Telegram's
|
|
5
|
+
# release cadence, so it must never be able to fail a pull request. Moving a
|
|
6
|
+
# layer is a deliberate act (rule S5) and this only reports that one is due.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: "0 7 * * 1"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
check:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
- name: Compare the pin against the newest release
|
|
22
|
+
run: python codegen/refresh.py --check
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
.env
|
|
10
|
+
|
|
11
|
+
# Tooling
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.hypothesis/
|
|
16
|
+
.coverage
|
|
17
|
+
coverage.xml
|
|
18
|
+
|
|
19
|
+
# Docs build
|
|
20
|
+
site/
|
|
21
|
+
|
|
22
|
+
# Editors
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
25
|
+
|
|
26
|
+
# Sessions and downloads from local runs
|
|
27
|
+
*.session
|
|
28
|
+
*.session-journal
|
|
29
|
+
|
|
30
|
+
# OS
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Sunnygram Architecture
|
|
2
|
+
|
|
3
|
+
The contract this codebase is held to: what sits where, what is generated, and the
|
|
4
|
+
numbered rules a review can point at. The README says what the library does and the docs
|
|
5
|
+
say how to use it; neither is repeated here.
|
|
6
|
+
|
|
7
|
+
## The layer stack
|
|
8
|
+
|
|
9
|
+
Bottom to top. Each layer knows only the layer below it. The right column marks
|
|
10
|
+
whether the code is generated or hand-written.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
10 Client + DX client.py, methods/, types/, dispatcher, filters, parser hand
|
|
14
|
+
9 Auth / login auth/ phone, code, 2FA (SRP), QR, bot login hand
|
|
15
|
+
8 File engine files/ chunked up/download, CDN, file_reference hand
|
|
16
|
+
7 Peer cache peers/ access_hash resolve + cache hand
|
|
17
|
+
6 Storage storage/ sqlite / memory / string session hand
|
|
18
|
+
5 Updates updates/ pts/qts/seq state machine, getDifference hand
|
|
19
|
+
4 Network / invoke network/ connection loop, DC migration, RPC routing hand
|
|
20
|
+
3 MTProto session session/ salt, session_id, msg_id, acks, containers hand
|
|
21
|
+
2 Transport transport/ TCP framing, obfuscation, proxies hand
|
|
22
|
+
1 Crypto crypto/ DH auth key, AES-IGE, RSA, PQ, SRP, msg_key hand
|
|
23
|
+
0 TL core + schema tl/ + raw/ binary codec (hand) + all constructors (gen) both
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Layer notes
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| **0. TL core + schema** | `tl/` is the hand-written codec: `TLObject`, the primitive reader and writer, boxed dispatch. `raw/` is generated, split `types` / `functions` / `base`, imported lazily (rule P7). `mtproto.tl`'s service constructors live in `raw/types/mtproto` and `api.tl`'s in `raw/types/_root`, so speaking the protocol does not mean loading the API. |
|
|
31
|
+
| **1. Crypto** | DH handshake and auth-key generation, AES-IGE (the hot path), RSA of the PQ inner data, PQ factorization, MTProto 2.0 `msg_key` derivation, SRP. Security-sensitive and finite. Validated against known vectors. |
|
|
32
|
+
| **2. Transport** | TCP plus four framings: intermediate, full, abridged, padded-intermediate. Obfuscation sits below the framing and above the socket, which nothing above it knows about. Proxies live here too: a tunnel is dealt with before the first frame and then forgotten. |
|
|
33
|
+
| **3. MTProto session** | The encrypted-message envelope: server salt, session id, message id, sequence numbers, acks, containers, gzip, and recovery from `bad_server_salt` and `bad_msg_notification`. |
|
|
34
|
+
| **4. Network / invoke** | Transport and session tied into a live connection with a single receive loop. Routes results to their callers, handles DC config and the `*_MIGRATE` errors, reconnects with backoff, turns `rpc_error` into typed exceptions. |
|
|
35
|
+
| **5. Updates** | `pts` / `qts` / `seq` / `date`, per-channel `pts`, gap detection, and recovery via `updates.getDifference` and `getChannelDifference`. Single source of truth for update state. |
|
|
36
|
+
| **6. Storage** | Auth keys per DC, the update state, and the peer cache. Backends: sqlite, memory, and a portable string session. |
|
|
37
|
+
| **7. Peer cache** | Resolves a username, an id or a phone to an `InputPeer` and caches the access hashes. A known peer must never cost a round trip. |
|
|
38
|
+
| **8. File engine** | Chunked upload with the big-file path and parallel parts; download with CDN redirect handling and `FILE_REFERENCE_EXPIRED` refresh. |
|
|
39
|
+
| **9. Auth / login** | Phone and code, 2FA over SRP, QR, bot token, session listing and termination, logout. |
|
|
40
|
+
| **10. Client + DX** | The product. `Client`, the `methods/` operations, friendly `Message` / `Chat` / `User`, the dispatcher with handler groups, composable filters, markdown and HTML parsing. |
|
|
41
|
+
|
|
42
|
+
Those operations are plain functions taking an invoker, one module per part of
|
|
43
|
+
the API they touch, and `Client` is what turns each into a method and wraps the
|
|
44
|
+
answer in a friendly type. They were written as mixins first and the split is
|
|
45
|
+
better: a function taking an invoker can be tested without a client and called
|
|
46
|
+
by a program that would rather not hold one, where a mixin gave a class its
|
|
47
|
+
methods at the price of every one of them needing the whole client to exist.
|
|
48
|
+
What it costs is a long `client.py`, since that file now holds one thin wrapper
|
|
49
|
+
per operation. Taken deliberately, and the reason the word mixin is gone.
|
|
50
|
+
|
|
51
|
+
## Package tree
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
src/sunnygram/
|
|
56
|
+
__init__.py public API, explicit __all__, everything lazy
|
|
57
|
+
client.py the high-level Client
|
|
58
|
+
utils.py signed(), and the other small shared things
|
|
59
|
+
loop.py which event loop Client.run makes, uvloop when there is one
|
|
60
|
+
py.typed
|
|
61
|
+
|
|
62
|
+
tl/ hand-written TL codec
|
|
63
|
+
core.py TLObject, the reader and writer, boxed dispatch
|
|
64
|
+
raw/ GENERATED (do not edit)
|
|
65
|
+
types/ functions/ base/ all.py
|
|
66
|
+
|
|
67
|
+
errors/
|
|
68
|
+
base.py SunnygramError and what can go wrong locally
|
|
69
|
+
rpc.py RPCError, one class per status code, the valued ones
|
|
70
|
+
generated.py GENERATED every error Telegram documents
|
|
71
|
+
crypto/ aes.py accel.py auth_key.py rsa.py factorization.py
|
|
72
|
+
srp.py mtproto.py
|
|
73
|
+
transport/ tcp.py codec.py obfuscation.py proxy.py
|
|
74
|
+
session/ session.py mtproto.py msg_id.py
|
|
75
|
+
network/ connection.py datacenter.py handshake.py invoker.py
|
|
76
|
+
limiter.py
|
|
77
|
+
updates/ manager.py state.py
|
|
78
|
+
storage/ base.py sqlite.py memory.py string.py
|
|
79
|
+
peers/ resolver.py cache.py
|
|
80
|
+
files/ upload.py download.py location.py parts.py cdn.py
|
|
81
|
+
ref.py
|
|
82
|
+
auth/ login.py
|
|
83
|
+
methods/ messages.py media.py attachments.py albums.py
|
|
84
|
+
reactions.py chats.py admin.py users.py bots.py
|
|
85
|
+
account.py forum.py
|
|
86
|
+
types/ message.py chat.py user.py dialog.py rights.py
|
|
87
|
+
member.py topic.py buttons.py callback.py
|
|
88
|
+
parser/ markdown.py html.py entities.py
|
|
89
|
+
dispatcher.py handlers, groups, StopPropagation, the questions waiting
|
|
90
|
+
conversation.py ask and wait, for code that talks rather than listens
|
|
91
|
+
plugins.py handlers written in their own files, and the loader
|
|
92
|
+
filters.py
|
|
93
|
+
recent.py the messages lately seen, so a reply costs nothing
|
|
94
|
+
migrate.py reading session files and file ids written elsewhere
|
|
95
|
+
|
|
96
|
+
codegen/
|
|
97
|
+
refresh.py vendor the .tl schema at a pinned layer, and the error table
|
|
98
|
+
parser.py read the TL grammar
|
|
99
|
+
gen_tl.py emit raw/ from the schema
|
|
100
|
+
gen_errors.py emit errors/generated.py from the error table
|
|
101
|
+
overrides.py the handful of manual tweaks
|
|
102
|
+
schema/ the vendored copies, with a sha256 and layer record
|
|
103
|
+
|
|
104
|
+
tests/ offline: TL codec, crypto vectors, update-gap fixtures,
|
|
105
|
+
codegen drift, a scripted MTProto server
|
|
106
|
+
docs/ mkdocs-material, docstring-driven
|
|
107
|
+
examples/ the live smoke tests, for a human with a real account
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`migrate.py` is the only module in the package that nothing else in the package needs, which
|
|
111
|
+
is why it is at the top, not inside `storage/` or `files/` next to the formats it is a
|
|
112
|
+
counterpart to. It reads another library's session and another library's file ids, so it has
|
|
113
|
+
to see both the storage layer and the peer layer, and putting it under either would be an
|
|
114
|
+
import going the wrong way for no gain. Its one connection to the rest is that
|
|
115
|
+
`methods/attachments.py` asks it whether a string it does not recognise is a foreign file
|
|
116
|
+
id, which is the whole point: a project moving over should not have to know it moved.
|
|
117
|
+
|
|
118
|
+
Four things sit somewhere other than where their name suggests, all deliberately. `PeerKind`
|
|
119
|
+
is in `storage/base.py` instead of an `enums.py`, because it is part of what a session
|
|
120
|
+
persists and having it there is what keeps `storage` from importing `peers`. `Handler` is in
|
|
121
|
+
`dispatcher.py` rather than a `handlers.py`, because a handler on its own does nothing: it
|
|
122
|
+
is a record the dispatcher reads, and splitting the two would be two files that only ever
|
|
123
|
+
change together. `types/buttons.py` describes what is about to be sent, not what arrived,
|
|
124
|
+
which is the other way round from everything else in `types/`, and it is there anyway
|
|
125
|
+
because it is a shape with no call attached and that is what `types/` is. And `recent.py` is
|
|
126
|
+
at the top, not under `types/` or `peers/`, because it is neither a shape nor a peer: it is
|
|
127
|
+
one bounded cache the client owns, in the same position `filters.py` and `dispatcher.py` are
|
|
128
|
+
in.
|
|
129
|
+
|
|
130
|
+
## Codegen strategy
|
|
131
|
+
|
|
132
|
+
The same shape as Moonlygram, scaled up. The TL schema is a formal grammar, so it
|
|
133
|
+
generates cleanly and carries most of the surface.
|
|
134
|
+
|
|
135
|
+
- `codegen/refresh.py` vendors a pinned copy of the `.tl` schema (a specific layer) and
|
|
136
|
+
Telegram's error table. The schema is taken at a published client release tag, not off
|
|
137
|
+
`dev`, so the layer we announce in `invokeWithLayer` is one Telegram has actually
|
|
138
|
+
deployed. Pinning the layer is what makes releases reproducible and upgrades deliberate;
|
|
139
|
+
`refresh.py --check` says whether a newer layer has shipped without writing anything.
|
|
140
|
+
- `codegen/gen_tl.py` parses the schema and emits `raw/types`, `raw/functions`,
|
|
141
|
+
and `raw/base`. Every generated object serializes and deserializes itself
|
|
142
|
+
through the `tl/` codec.
|
|
143
|
+
- `codegen/gen_errors.py` emits the typed error tree from Telegram's published error table,
|
|
144
|
+
one class per documented error with the official explanation as its docstring. The roots
|
|
145
|
+
stay hand-written in `errors/rpc.py`: one class per status code, plus the few carrying
|
|
146
|
+
behavior a table cannot express, which is a value to wait for or a datacenter to go to.
|
|
147
|
+
The generated classes hang off those, so `except FloodWait` and `except BadRequest` both
|
|
148
|
+
keep working while the eight hundred names underneath come from upstream. The error table
|
|
149
|
+
is vendored on its own terms, not at a release tag, since Telegram regenerates it whenever
|
|
150
|
+
it likes; `refresh.py --errors` takes a fresh copy without moving the layer pin.
|
|
151
|
+
- `codegen/overrides.py` holds the small set of manual adjustments, the same role
|
|
152
|
+
it plays in Moonlygram.
|
|
153
|
+
- CI regenerates and runs `git diff --exit-code` over `raw/` and `errors/`; a
|
|
154
|
+
hand-edit or a stale checkout fails the build. `tests/test_codegen.py` adds the
|
|
155
|
+
drift and round-trip guards.
|
|
156
|
+
|
|
157
|
+
The generated `raw/` layer is the plumbing. The hand-written `types/` wrappers
|
|
158
|
+
and `methods/` functions are the library people actually touch, and they are
|
|
159
|
+
where all the docstrings and ergonomics live. Same split as Moonlygram's generated
|
|
160
|
+
received types vs hand-written `Message` / `Chat` / `User`.
|
|
161
|
+
|
|
162
|
+
## Principles and rules
|
|
163
|
+
|
|
164
|
+
Inherited from Moonlygram, then the new rules for this project. They are numbered
|
|
165
|
+
so we can point at them in review.
|
|
166
|
+
|
|
167
|
+
### Inherited (the family bar)
|
|
168
|
+
|
|
169
|
+
- **H1** src layout, hatchling build, MPL-2.0, published under the Milky family.
|
|
170
|
+
- **H2** `mypy --strict` clean; ships `py.typed`.
|
|
171
|
+
- **H3** ruff over `src tests codegen`; all of ruff + mypy + pytest green before
|
|
172
|
+
anything ships.
|
|
173
|
+
- **H4** Tests are **offline**. No live account, no real credentials in CI. The
|
|
174
|
+
TL codec, crypto, and update-gap logic are tested against vectors and recorded
|
|
175
|
+
fixtures.
|
|
176
|
+
- **H5** Plain-prose docstrings and comments. No Sphinx cross-references, no
|
|
177
|
+
formal Args/Returns blocks. Comment the *why* of a non-obvious choice, and no
|
|
178
|
+
em dashes.
|
|
179
|
+
- **H6** Public API re-exported through `__init__` with an explicit `__all__`.
|
|
180
|
+
- **H7** Generated code is never hand-edited. CI drift guard plus drift tests.
|
|
181
|
+
- **H8** Behavior-bearing and friendly types are hand-written; mechanical data
|
|
182
|
+
types are generated.
|
|
183
|
+
- **H9** Docs are docstring-driven (mkdocs-material + mkdocstrings). Accurate
|
|
184
|
+
docstrings keep the reference accurate.
|
|
185
|
+
- **H10** CI matrix across supported Python versions.
|
|
186
|
+
|
|
187
|
+
### New: safety
|
|
188
|
+
|
|
189
|
+
- **S1** Crypto is validated against official test vectors and is treated as
|
|
190
|
+
frozen: no "creative" refactor lands without re-running the vectors. Use vetted
|
|
191
|
+
primitives (`hashlib`, `hmac`), and `secrets` / `os.urandom` for all
|
|
192
|
+
randomness, never `random`.
|
|
193
|
+
- **S2** Secrets never stringify. Auth keys and session material must never appear in logs,
|
|
194
|
+
`repr`, or exception messages. `tests/test_redaction.py` is the guard, and it works two
|
|
195
|
+
ways: it puts a canary key through every object known to hold one, and it walks the
|
|
196
|
+
hand-written package for any class with a secret-shaped field that would inherit a `repr`
|
|
197
|
+
printing it. The second half is the one that matters, because it fails for a class no one
|
|
198
|
+
has thought about yet, which is the only kind this rule is ever broken by.
|
|
199
|
+
- **S3** Server data is untrusted. The TL deserializer is bounds-checked and
|
|
200
|
+
fails closed on malformed or oversized input. A length prefix never triggers an
|
|
201
|
+
unbounded allocation.
|
|
202
|
+
- **S4** Account-safe by default. Auto-honor `FLOOD_WAIT`, ship sane default rate
|
|
203
|
+
limits, and provide no built-in spam affordances. Same "safe tool" ethos as
|
|
204
|
+
Moonlygram.
|
|
205
|
+
- **S5** Reproducible codegen. The TL layer is pinned; the library never silently
|
|
206
|
+
follows upstream schema drift.
|
|
207
|
+
- **S6** Constant-time comparison (`hmac.compare_digest`) for every
|
|
208
|
+
auth-sensitive equality check (`msg_key`, SRP).
|
|
209
|
+
|
|
210
|
+
### New: speed and performance
|
|
211
|
+
|
|
212
|
+
- **P1** Async-first, one receive loop per connection, never blocked. CPU-bound
|
|
213
|
+
crypto must not stall the loop. The loop itself is part of this: `uvloop` when
|
|
214
|
+
it is installed, chosen in `Client.run` and never installed as a policy at
|
|
215
|
+
import, because the event loop of a program that made its own choice is not
|
|
216
|
+
this library's to replace.
|
|
217
|
+
- **P2** Optional accelerated crypto backend, auto-detected, with a pure-Python
|
|
218
|
+
fallback so the library always works with zero native deps. AES-IGE is the hot
|
|
219
|
+
path.
|
|
220
|
+
- **P3** Allocation-aware TL codec: `bytes` / `bytearray` / `memoryview`, a single growing
|
|
221
|
+
buffer, no per-field object churn. Reading is the hot path and is optimized as one: a
|
|
222
|
+
generated `read` assigns its fields directly instead of calling `__init__`, and the
|
|
223
|
+
fixed-width readers do their own bounds check instead of paying a call for it. That is why
|
|
224
|
+
the generated code has the shape it has. What it is worth depends on how wide the object
|
|
225
|
+
is, because skipping `__init__` saves a fixed amount per object while the inline bounds
|
|
226
|
+
check saves a call per field: `benchmarks/rules.py` measures about 1.3x on a five-field
|
|
227
|
+
type and about 1.65x at fifty fields. Fifty is the end that matters, since `Message` is 49
|
|
228
|
+
fields, `User` 51 and `Channel` 50, and those are most of what a running program reads.
|
|
229
|
+
Quote the range instead of the top of it. The writing side has its own rule, P8.
|
|
230
|
+
- **P4** The peer cache is mandatory and in-memory-fast. Resolving a known peer
|
|
231
|
+
never hits the network. What it holds can go stale, so a call the server
|
|
232
|
+
refuses on the grounds of the peer drops what was cached for it, everywhere
|
|
233
|
+
it was kept: an access hash that has stopped working is worse than none,
|
|
234
|
+
because none resolves again and works while a wrong one fails identically on
|
|
235
|
+
every run of the program.
|
|
236
|
+
- **P5** Parallel file transfers: multiple workers across media-DC connections,
|
|
237
|
+
configurable concurrency with backpressure.
|
|
238
|
+
- **P6** Bounded everything. Caches are bounded LRU, in-flight RPCs are capped,
|
|
239
|
+
queues apply backpressure. Carries over Moonlygram's bounded-in-flight and LRU
|
|
240
|
+
discipline.
|
|
241
|
+
- **P7** Lazy imports for the generated `raw/` package so `import sunnygram` stays cheap and
|
|
242
|
+
does not pull thousands of TL classes eagerly. How the modules are grouped is part of
|
|
243
|
+
this: opening a connection loads the forty MTProto service constructors it speaks in and
|
|
244
|
+
none of the API. This is also why `TLFunction` is generic the way it is. A generated
|
|
245
|
+
function names its result as its type parameter, which lets `invoke` answer with a
|
|
246
|
+
`Config`, not an `Any`, but the parameter is a forward reference into `raw/base` and that
|
|
247
|
+
package has no runtime form. So subscripting is overridden to hand back the class itself
|
|
248
|
+
under `if not TYPE_CHECKING`, and building a generated class allocates no typing
|
|
249
|
+
machinery. Measured by alternating in one process: 18.9 us to create a class plainly, 24.5
|
|
250
|
+
us through the no-op, 37.8 us through a real generic subscript. On
|
|
251
|
+
`raw/functions/messages.py`, the largest module in the tree at 259 functions, that is +0.8
|
|
252
|
+
ms against 111 ms. A real subscript was rejected on those numbers, and separate `.pyi`
|
|
253
|
+
stubs were rejected because a stub shadows the module it describes, so the annotations in
|
|
254
|
+
the generated code would stop being checked at all. The type variable is covariant, which
|
|
255
|
+
is not decorative: a result is only ever something a call gives back, and without
|
|
256
|
+
covariance the ordinary `bool(await invoke(...))` stops compiling.
|
|
257
|
+
- **P8** The packed write body. A constructor whose fields are all fixed-width and none of
|
|
258
|
+
them conditional has a layout that is known when the generator runs, so it is written by
|
|
259
|
+
one `struct` call instead of one method call per field. `benchmarks/rules.py` measures it,
|
|
260
|
+
and what to quote is the range, not the best run in it: across four runs on one machine,
|
|
261
|
+
about 4x to 5x on `inputPeerUser`, 3.9x to 4.8x on `pong`, 3.1x on `updates.State` and
|
|
262
|
+
1.9x to 2x on a two-int constructor. Earlier rounds reported 5.5x and 5.9x on
|
|
263
|
+
`inputPeerUser` and neither was reached again, so both were the top of a range being read
|
|
264
|
+
as the middle of one. Quote the range. It catches only small dense constructors, which is
|
|
265
|
+
the point: those are the ones sent in quantity, and nearly every outgoing call names a
|
|
266
|
+
peer. It is a fast path, not a replacement, because `struct` refuses values this library
|
|
267
|
+
accepts, so the field-by-field writing stays underneath it as the fallback and the
|
|
268
|
+
generated body falls through to it on `struct.error`.
|
|
269
|
+
|
|
270
|
+
### New: correctness and robustness
|
|
271
|
+
|
|
272
|
+
- **C1** The update manager is the single source of truth for `pts`/`qts`. No
|
|
273
|
+
other layer mutates update state.
|
|
274
|
+
- **C2** Reconnect and resume are transparent. A dropped transport never loses
|
|
275
|
+
the session; a re-handshake happens only when genuinely required.
|
|
276
|
+
- **C3** Nothing fails quietly. Anything the library survives on the program's
|
|
277
|
+
behalf, and anything it swallows so that the stream can carry on, is said out
|
|
278
|
+
loud through the `sunnygram` logger: a reconnection, a datacenter move, a
|
|
279
|
+
flood wait, a resynchronization, a handler that raised. A library sets no
|
|
280
|
+
handlers and no levels of its own, so warnings and errors reach stderr on
|
|
281
|
+
Python's own fallback and everything else is the program's to ask for. The
|
|
282
|
+
rule exists because the alternative is the worst kind of fault report there
|
|
283
|
+
is, which is "it stopped working and there was nothing in the output".
|
|
284
|
+
- **C4** A recovery the code performs is a recovery the code proves. Every
|
|
285
|
+
correction this stack makes for itself has a test where the fault is really
|
|
286
|
+
produced: the clock is really wrong, the pong really never comes, the queue
|
|
287
|
+
really overflows, the server really says it has never heard of us. A recovery
|
|
288
|
+
path with no test is a recovery path that has never run, and the ones here
|
|
289
|
+
had not.
|
|
290
|
+
- **C5** The update layer is proved against a model, not a script. The two
|
|
291
|
+
promises in `updates/manager.py` are that every update reaches the program
|
|
292
|
+
exactly once and in the order the server put them in, and a promise of that
|
|
293
|
+
shape cannot be kept by examples: a written test asks the one question its
|
|
294
|
+
author thought of, and a scripted server answers whatever it is asked, so a
|
|
295
|
+
client asking the *wrong* question still gets a plausible answer and the test
|
|
296
|
+
passes. `tests/test_updates_model.py` is the answer. Hypothesis generates the
|
|
297
|
+
sequence of things the server does, and the server is a model that holds the
|
|
298
|
+
real history and derives every answer from it, so a wrong cursor earns the
|
|
299
|
+
empty answer it deserves and an invariant notices what it skipped. The
|
|
300
|
+
invariant that carries the most weight is that a counter is a claim: `pts = N`
|
|
301
|
+
says the first N things have been handled, so everything below our own mark
|
|
302
|
+
has to have reached the program. Both faults this project has actually had in
|
|
303
|
+
that layer are caught by it in under three generated steps, and so is the
|
|
304
|
+
classic form of the fault, where a detected gap is never recovered from and
|
|
305
|
+
the stream silently stops.
|
|
306
|
+
|
|
307
|
+
## Considered and not done
|
|
308
|
+
|
|
309
|
+
Three things were considered and deliberately not done, written down so they are not
|
|
310
|
+
rediscovered as oversights. Outgoing messages are not coalesced into
|
|
311
|
+
`msg_container`, and server salts are not fetched before the current one expires. Both are
|
|
312
|
+
real optimisations and both were measured against: the profile says this stack costs about
|
|
313
|
+
twenty microseconds to code an update and twenty to wrap it, so frames and round trips are
|
|
314
|
+
not where a program's time goes, and the container work in particular lands in the one file
|
|
315
|
+
where `msg_id` and `seq_no` ordering is load-bearing. The trade was not worth it. If a real
|
|
316
|
+
account ever shows otherwise, that is the evidence to reopen them with.
|
|
317
|
+
|
|
318
|
+
The third is `CTR.apply`, which XORs a whole buffer by turning it and the keystream into
|
|
319
|
+
one integer each. For a 512 KiB file part that is a four-megabit bignum, which reads like
|
|
320
|
+
the obvious thing to chunk, and it is not: measured on a 512 KiB part with nothing
|
|
321
|
+
installed, the whole call is 2.59 s, of which the keystream is 2.27 s and the XOR is 6.0 ms,
|
|
322
|
+
or 0.23%. Chunking it at 8 KiB measured 6.2 ms, which is no faster, because
|
|
323
|
+
`int.from_bytes`, `^` and `to_bytes` are each one C loop over the buffer and there is
|
|
324
|
+
nothing quadratic to remove. The cost on that path is the cipher, which is already in the
|
|
325
|
+
four-table formulation, and the honest answer to a slow one is the ladder in accel.py
|
|
326
|
+
rather than a rewrite of the reference. The one real cost is transient memory, 4.3x the
|
|
327
|
+
input at peak against about 2x if it were chunked, which is 2.2 MB for a part that only
|
|
328
|
+
happens when no backend at all is installed. Not worth making the reference implementation
|
|
329
|
+
harder to read for.
|
|
330
|
+
|
|
331
|
+
## Decisions
|
|
332
|
+
|
|
333
|
+
Settled at scaffolding time, 2026-07-28.
|
|
334
|
+
|
|
335
|
+
- **Minimum Python: 3.11.** A step ahead of the family's 3.10. This is a fresh
|
|
336
|
+
codebase and the update and connection layers lean on task groups.
|
|
337
|
+
- **Crypto acceleration: pure Python core, a ladder of faster backends above it.**
|
|
338
|
+
Auto-detected at import in order: `cryptg`, `tgcrypto`, `cryptography`, then our own
|
|
339
|
+
implementation as the always-present fallback. The third rung is the one that matters in
|
|
340
|
+
practice, since most environments already carry `cryptography` for other reasons: it has
|
|
341
|
+
no IGE, so we drive its AES a block at a time, which is enough to turn 210 KiB/s into 5.9
|
|
342
|
+
MiB/s. Its CTR needs no such trick and goes from 0.22 MiB/s to 812 MiB/s in one call.
|
|
343
|
+
Every rung is tested against the pure Python one over random data, because a fast backend
|
|
344
|
+
that is subtly wrong corrupts a session, not failing it.
|
|
345
|
+
- **A datacenter gets several connections, but only for transfers.** Telegram meters a
|
|
346
|
+
connection instead of an account, so parts of a file spread across four sockets arrive
|
|
347
|
+
several times sooner. Ordinary calls stay on one connection per datacenter: they have an
|
|
348
|
+
order, and updates are counted per session, so spreading them would buy throughput no one
|
|
349
|
+
asked for at the cost of both. A call opts in with `bulk=True`, which the file engine sets
|
|
350
|
+
and nothing else does. The pool grows to match how many transfer calls are in hand at once
|
|
351
|
+
and stops at four, and is sized from that count, not from how busy the connections look,
|
|
352
|
+
because a call that has been handed a connection but has not reached the wire yet is still
|
|
353
|
+
demand.
|
|
354
|
+
- **Cipher calls above a threshold leave the event loop.** A 512 KiB file part
|
|
355
|
+
is 3.2 seconds of blocking CPU in pure Python and 127 ms with `cryptography`,
|
|
356
|
+
and neither belongs on the loop. The threshold moves with the backend, since
|
|
357
|
+
the hand-off costs more than the work when the work is quick.
|
|
358
|
+
- **Default storage: sqlite and string sessions together.**
|
|
359
|
+
They share one base, so shipping one without the other saves nothing.
|
|
360
|
+
- **raw/ split: `types` / `functions` / `base`.** The proven shape.
|
|
361
|
+
- **License: MPL-2.0.** The family's other libraries are MIT; this one is not, and
|
|
362
|
+
the difference is deliberate. File-level copyleft keeps improvements to the protocol
|
|
363
|
+
stack itself in the open without asking anything of a program that merely imports it,
|
|
364
|
+
which is the trade MIT does not offer and LGPL overcharges for.
|