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.
Files changed (312) hide show
  1. sunnygram-1.0.0/.gitattributes +14 -0
  2. sunnygram-1.0.0/.github/workflows/ci.yml +55 -0
  3. sunnygram-1.0.0/.github/workflows/docs.yml +23 -0
  4. sunnygram-1.0.0/.github/workflows/release.yml +40 -0
  5. sunnygram-1.0.0/.github/workflows/schema.yml +22 -0
  6. sunnygram-1.0.0/.gitignore +32 -0
  7. sunnygram-1.0.0/ARCHITECTURE.md +364 -0
  8. sunnygram-1.0.0/CHANGELOG.md +962 -0
  9. sunnygram-1.0.0/CONTRIBUTING.md +154 -0
  10. sunnygram-1.0.0/LICENSE +373 -0
  11. sunnygram-1.0.0/PKG-INFO +242 -0
  12. sunnygram-1.0.0/README.md +202 -0
  13. sunnygram-1.0.0/SECURITY.md +55 -0
  14. sunnygram-1.0.0/benchmarks/README.md +39 -0
  15. sunnygram-1.0.0/benchmarks/delta.py +157 -0
  16. sunnygram-1.0.0/benchmarks/rules.py +389 -0
  17. sunnygram-1.0.0/codegen/gen_errors.py +297 -0
  18. sunnygram-1.0.0/codegen/gen_tl.py +881 -0
  19. sunnygram-1.0.0/codegen/overrides.py +102 -0
  20. sunnygram-1.0.0/codegen/parser.py +285 -0
  21. sunnygram-1.0.0/codegen/refresh.py +247 -0
  22. sunnygram-1.0.0/codegen/schema/api.tl +3087 -0
  23. sunnygram-1.0.0/codegen/schema/errors.json +1 -0
  24. sunnygram-1.0.0/codegen/schema/mtproto.tl +128 -0
  25. sunnygram-1.0.0/codegen/schema/version.json +23 -0
  26. sunnygram-1.0.0/docs/account.md +132 -0
  27. sunnygram-1.0.0/docs/admin.md +126 -0
  28. sunnygram-1.0.0/docs/api/client.md +35 -0
  29. sunnygram-1.0.0/docs/api/errors.md +51 -0
  30. sunnygram-1.0.0/docs/api/filters.md +10 -0
  31. sunnygram-1.0.0/docs/api/layers.md +74 -0
  32. sunnygram-1.0.0/docs/api/types.md +74 -0
  33. sunnygram-1.0.0/docs/attachments.md +115 -0
  34. sunnygram-1.0.0/docs/bots.md +75 -0
  35. sunnygram-1.0.0/docs/buttons.md +204 -0
  36. sunnygram-1.0.0/docs/chats.md +104 -0
  37. sunnygram-1.0.0/docs/conversations.md +89 -0
  38. sunnygram-1.0.0/docs/errors.md +104 -0
  39. sunnygram-1.0.0/docs/files.md +219 -0
  40. sunnygram-1.0.0/docs/filters.md +130 -0
  41. sunnygram-1.0.0/docs/importing.md +103 -0
  42. sunnygram-1.0.0/docs/index.md +102 -0
  43. sunnygram-1.0.0/docs/inline.md +176 -0
  44. sunnygram-1.0.0/docs/logging.md +72 -0
  45. sunnygram-1.0.0/docs/login.md +115 -0
  46. sunnygram-1.0.0/docs/messages.md +223 -0
  47. sunnygram-1.0.0/docs/payments.md +118 -0
  48. sunnygram-1.0.0/docs/peers.md +112 -0
  49. sunnygram-1.0.0/docs/performance.md +149 -0
  50. sunnygram-1.0.0/docs/plugins.md +74 -0
  51. sunnygram-1.0.0/docs/proxies.md +95 -0
  52. sunnygram-1.0.0/docs/quickstart.md +84 -0
  53. sunnygram-1.0.0/docs/rate-limits.md +64 -0
  54. sunnygram-1.0.0/docs/raw-api.md +158 -0
  55. sunnygram-1.0.0/docs/sessions.md +82 -0
  56. sunnygram-1.0.0/docs/stories.md +84 -0
  57. sunnygram-1.0.0/docs/topics.md +104 -0
  58. sunnygram-1.0.0/docs/updates.md +212 -0
  59. sunnygram-1.0.0/examples/adopt.py +94 -0
  60. sunnygram-1.0.0/examples/ask.py +90 -0
  61. sunnygram-1.0.0/examples/buttons.py +125 -0
  62. sunnygram-1.0.0/examples/echo.py +75 -0
  63. sunnygram-1.0.0/examples/events.py +158 -0
  64. sunnygram-1.0.0/examples/files.py +158 -0
  65. sunnygram-1.0.0/examples/get_config.py +92 -0
  66. sunnygram-1.0.0/examples/listen.py +156 -0
  67. sunnygram-1.0.0/examples/login.py +160 -0
  68. sunnygram-1.0.0/examples/moderate.py +140 -0
  69. sunnygram-1.0.0/examples/plugin_bot.py +45 -0
  70. sunnygram-1.0.0/examples/plugins/__init__.py +6 -0
  71. sunnygram-1.0.0/examples/plugins/echo.py +22 -0
  72. sunnygram-1.0.0/examples/plugins/greet.py +23 -0
  73. sunnygram-1.0.0/examples/shop.py +145 -0
  74. sunnygram-1.0.0/examples/topics.py +120 -0
  75. sunnygram-1.0.0/examples/tour.py +123 -0
  76. sunnygram-1.0.0/mkdocs.yml +110 -0
  77. sunnygram-1.0.0/pyproject.toml +100 -0
  78. sunnygram-1.0.0/src/sunnygram/__init__.py +228 -0
  79. sunnygram-1.0.0/src/sunnygram/auth/__init__.py +37 -0
  80. sunnygram-1.0.0/src/sunnygram/auth/login.py +425 -0
  81. sunnygram-1.0.0/src/sunnygram/client.py +2607 -0
  82. sunnygram-1.0.0/src/sunnygram/conversation.py +246 -0
  83. sunnygram-1.0.0/src/sunnygram/crypto/__init__.py +97 -0
  84. sunnygram-1.0.0/src/sunnygram/crypto/accel.py +224 -0
  85. sunnygram-1.0.0/src/sunnygram/crypto/aes.py +431 -0
  86. sunnygram-1.0.0/src/sunnygram/crypto/auth_key.py +292 -0
  87. sunnygram-1.0.0/src/sunnygram/crypto/factorization.py +79 -0
  88. sunnygram-1.0.0/src/sunnygram/crypto/mtproto.py +90 -0
  89. sunnygram-1.0.0/src/sunnygram/crypto/rsa.py +206 -0
  90. sunnygram-1.0.0/src/sunnygram/crypto/srp.py +145 -0
  91. sunnygram-1.0.0/src/sunnygram/dispatcher.py +668 -0
  92. sunnygram-1.0.0/src/sunnygram/errors/__init__.py +138 -0
  93. sunnygram-1.0.0/src/sunnygram/errors/base.py +198 -0
  94. sunnygram-1.0.0/src/sunnygram/errors/generated.py +4899 -0
  95. sunnygram-1.0.0/src/sunnygram/errors/rpc.py +234 -0
  96. sunnygram-1.0.0/src/sunnygram/files/__init__.py +46 -0
  97. sunnygram-1.0.0/src/sunnygram/files/cdn.py +316 -0
  98. sunnygram-1.0.0/src/sunnygram/files/download.py +387 -0
  99. sunnygram-1.0.0/src/sunnygram/files/location.py +184 -0
  100. sunnygram-1.0.0/src/sunnygram/files/parts.py +105 -0
  101. sunnygram-1.0.0/src/sunnygram/files/ref.py +324 -0
  102. sunnygram-1.0.0/src/sunnygram/files/upload.py +215 -0
  103. sunnygram-1.0.0/src/sunnygram/filters.py +371 -0
  104. sunnygram-1.0.0/src/sunnygram/loop.py +107 -0
  105. sunnygram-1.0.0/src/sunnygram/methods/__init__.py +330 -0
  106. sunnygram-1.0.0/src/sunnygram/methods/account.py +356 -0
  107. sunnygram-1.0.0/src/sunnygram/methods/admin.py +524 -0
  108. sunnygram-1.0.0/src/sunnygram/methods/albums.py +191 -0
  109. sunnygram-1.0.0/src/sunnygram/methods/attachments.py +398 -0
  110. sunnygram-1.0.0/src/sunnygram/methods/bots.py +428 -0
  111. sunnygram-1.0.0/src/sunnygram/methods/chats.py +230 -0
  112. sunnygram-1.0.0/src/sunnygram/methods/folders.py +110 -0
  113. sunnygram-1.0.0/src/sunnygram/methods/forum.py +237 -0
  114. sunnygram-1.0.0/src/sunnygram/methods/media.py +171 -0
  115. sunnygram-1.0.0/src/sunnygram/methods/messages.py +763 -0
  116. sunnygram-1.0.0/src/sunnygram/methods/payments.py +318 -0
  117. sunnygram-1.0.0/src/sunnygram/methods/reactions.py +112 -0
  118. sunnygram-1.0.0/src/sunnygram/methods/stories.py +233 -0
  119. sunnygram-1.0.0/src/sunnygram/methods/takeout.py +137 -0
  120. sunnygram-1.0.0/src/sunnygram/methods/users.py +88 -0
  121. sunnygram-1.0.0/src/sunnygram/migrate.py +787 -0
  122. sunnygram-1.0.0/src/sunnygram/network/__init__.py +39 -0
  123. sunnygram-1.0.0/src/sunnygram/network/config.py +82 -0
  124. sunnygram-1.0.0/src/sunnygram/network/connection.py +864 -0
  125. sunnygram-1.0.0/src/sunnygram/network/datacenter.py +79 -0
  126. sunnygram-1.0.0/src/sunnygram/network/handshake.py +204 -0
  127. sunnygram-1.0.0/src/sunnygram/network/invoker.py +843 -0
  128. sunnygram-1.0.0/src/sunnygram/network/limiter.py +221 -0
  129. sunnygram-1.0.0/src/sunnygram/parser/__init__.py +72 -0
  130. sunnygram-1.0.0/src/sunnygram/parser/entities.py +160 -0
  131. sunnygram-1.0.0/src/sunnygram/parser/html.py +186 -0
  132. sunnygram-1.0.0/src/sunnygram/parser/markdown.py +300 -0
  133. sunnygram-1.0.0/src/sunnygram/peers/__init__.py +49 -0
  134. sunnygram-1.0.0/src/sunnygram/peers/cache.py +402 -0
  135. sunnygram-1.0.0/src/sunnygram/peers/resolver.py +354 -0
  136. sunnygram-1.0.0/src/sunnygram/plugins.py +363 -0
  137. sunnygram-1.0.0/src/sunnygram/py.typed +0 -0
  138. sunnygram-1.0.0/src/sunnygram/raw/__init__.py +45 -0
  139. sunnygram-1.0.0/src/sunnygram/raw/all.py +2527 -0
  140. sunnygram-1.0.0/src/sunnygram/raw/base/__init__.py +450 -0
  141. sunnygram-1.0.0/src/sunnygram/raw/base/_root.py +2123 -0
  142. sunnygram-1.0.0/src/sunnygram/raw/base/account.py +107 -0
  143. sunnygram-1.0.0/src/sunnygram/raw/base/aicompose.py +23 -0
  144. sunnygram-1.0.0/src/sunnygram/raw/base/auth.py +65 -0
  145. sunnygram-1.0.0/src/sunnygram/raw/base/bots.py +30 -0
  146. sunnygram-1.0.0/src/sunnygram/raw/base/channels.py +35 -0
  147. sunnygram-1.0.0/src/sunnygram/raw/base/chatlists.py +29 -0
  148. sunnygram-1.0.0/src/sunnygram/raw/base/communities.py +22 -0
  149. sunnygram-1.0.0/src/sunnygram/raw/base/contacts.py +47 -0
  150. sunnygram-1.0.0/src/sunnygram/raw/base/fragment.py +20 -0
  151. sunnygram-1.0.0/src/sunnygram/raw/base/help.py +93 -0
  152. sunnygram-1.0.0/src/sunnygram/raw/base/messages.py +232 -0
  153. sunnygram-1.0.0/src/sunnygram/raw/base/payments.py +99 -0
  154. sunnygram-1.0.0/src/sunnygram/raw/base/phone.py +34 -0
  155. sunnygram-1.0.0/src/sunnygram/raw/base/photos.py +25 -0
  156. sunnygram-1.0.0/src/sunnygram/raw/base/premium.py +24 -0
  157. sunnygram-1.0.0/src/sunnygram/raw/base/smsjobs.py +22 -0
  158. sunnygram-1.0.0/src/sunnygram/raw/base/stats.py +30 -0
  159. sunnygram-1.0.0/src/sunnygram/raw/base/stickers.py +20 -0
  160. sunnygram-1.0.0/src/sunnygram/raw/base/storage.py +31 -0
  161. sunnygram-1.0.0/src/sunnygram/raw/base/stories.py +42 -0
  162. sunnygram-1.0.0/src/sunnygram/raw/base/updates.py +33 -0
  163. sunnygram-1.0.0/src/sunnygram/raw/base/upload.py +30 -0
  164. sunnygram-1.0.0/src/sunnygram/raw/base/users.py +30 -0
  165. sunnygram-1.0.0/src/sunnygram/raw/functions/__init__.py +124 -0
  166. sunnygram-1.0.0/src/sunnygram/raw/functions/_root.py +432 -0
  167. sunnygram-1.0.0/src/sunnygram/raw/functions/account.py +3954 -0
  168. sunnygram-1.0.0/src/sunnygram/raw/functions/aicompose.py +279 -0
  169. sunnygram-1.0.0/src/sunnygram/raw/functions/auth.py +932 -0
  170. sunnygram-1.0.0/src/sunnygram/raw/functions/bots.py +1321 -0
  171. sunnygram-1.0.0/src/sunnygram/raw/functions/channels.py +2101 -0
  172. sunnygram-1.0.0/src/sunnygram/raw/functions/chatlists.py +377 -0
  173. sunnygram-1.0.0/src/sunnygram/raw/functions/communities.py +369 -0
  174. sunnygram-1.0.0/src/sunnygram/raw/functions/contacts.py +967 -0
  175. sunnygram-1.0.0/src/sunnygram/raw/functions/ephemeral.py +241 -0
  176. sunnygram-1.0.0/src/sunnygram/raw/functions/folders.py +53 -0
  177. sunnygram-1.0.0/src/sunnygram/raw/functions/fragment.py +53 -0
  178. sunnygram-1.0.0/src/sunnygram/raw/functions/help.py +645 -0
  179. sunnygram-1.0.0/src/sunnygram/raw/functions/langpack.py +191 -0
  180. sunnygram-1.0.0/src/sunnygram/raw/functions/messages.py +10969 -0
  181. sunnygram-1.0.0/src/sunnygram/raw/functions/mtproto.py +335 -0
  182. sunnygram-1.0.0/src/sunnygram/raw/functions/payments.py +2400 -0
  183. sunnygram-1.0.0/src/sunnygram/raw/functions/phone.py +1738 -0
  184. sunnygram-1.0.0/src/sunnygram/raw/functions/photos.py +284 -0
  185. sunnygram-1.0.0/src/sunnygram/raw/functions/premium.py +187 -0
  186. sunnygram-1.0.0/src/sunnygram/raw/functions/smsjobs.py +194 -0
  187. sunnygram-1.0.0/src/sunnygram/raw/functions/stats.py +343 -0
  188. sunnygram-1.0.0/src/sunnygram/raw/functions/stickers.py +441 -0
  189. sunnygram-1.0.0/src/sunnygram/raw/functions/stories.py +1441 -0
  190. sunnygram-1.0.0/src/sunnygram/raw/functions/updates.py +159 -0
  191. sunnygram-1.0.0/src/sunnygram/raw/functions/upload.py +327 -0
  192. sunnygram-1.0.0/src/sunnygram/raw/functions/users.py +245 -0
  193. sunnygram-1.0.0/src/sunnygram/raw/types/__init__.py +2714 -0
  194. sunnygram-1.0.0/src/sunnygram/raw/types/_root.py +52528 -0
  195. sunnygram-1.0.0/src/sunnygram/raw/types/account.py +1342 -0
  196. sunnygram-1.0.0/src/sunnygram/raw/types/aicompose.py +79 -0
  197. sunnygram-1.0.0/src/sunnygram/raw/types/auth.py +934 -0
  198. sunnygram-1.0.0/src/sunnygram/raw/types/bots.py +220 -0
  199. sunnygram-1.0.0/src/sunnygram/raw/types/channels.py +257 -0
  200. sunnygram-1.0.0/src/sunnygram/raw/types/chatlists.py +233 -0
  201. sunnygram-1.0.0/src/sunnygram/raw/types/communities.py +119 -0
  202. sunnygram-1.0.0/src/sunnygram/raw/types/contacts.py +428 -0
  203. sunnygram-1.0.0/src/sunnygram/raw/types/fragment.py +74 -0
  204. sunnygram-1.0.0/src/sunnygram/raw/types/help.py +1131 -0
  205. sunnygram-1.0.0/src/sunnygram/raw/types/messages.py +3398 -0
  206. sunnygram-1.0.0/src/sunnygram/raw/types/mtproto.py +1453 -0
  207. sunnygram-1.0.0/src/sunnygram/raw/types/payments.py +1757 -0
  208. sunnygram-1.0.0/src/sunnygram/raw/types/phone.py +314 -0
  209. sunnygram-1.0.0/src/sunnygram/raw/types/photos.py +124 -0
  210. sunnygram-1.0.0/src/sunnygram/raw/types/premium.py +199 -0
  211. sunnygram-1.0.0/src/sunnygram/raw/types/smsjobs.py +122 -0
  212. sunnygram-1.0.0/src/sunnygram/raw/types/stats.py +403 -0
  213. sunnygram-1.0.0/src/sunnygram/raw/types/stickers.py +49 -0
  214. sunnygram-1.0.0/src/sunnygram/raw/types/storage.py +193 -0
  215. sunnygram-1.0.0/src/sunnygram/raw/types/stories.py +483 -0
  216. sunnygram-1.0.0/src/sunnygram/raw/types/updates.py +409 -0
  217. sunnygram-1.0.0/src/sunnygram/raw/types/upload.py +206 -0
  218. sunnygram-1.0.0/src/sunnygram/raw/types/users.py +176 -0
  219. sunnygram-1.0.0/src/sunnygram/recent.py +103 -0
  220. sunnygram-1.0.0/src/sunnygram/session/__init__.py +45 -0
  221. sunnygram-1.0.0/src/sunnygram/session/msg_id.py +73 -0
  222. sunnygram-1.0.0/src/sunnygram/session/mtproto.py +236 -0
  223. sunnygram-1.0.0/src/sunnygram/session/session.py +294 -0
  224. sunnygram-1.0.0/src/sunnygram/storage/__init__.py +54 -0
  225. sunnygram-1.0.0/src/sunnygram/storage/base.py +245 -0
  226. sunnygram-1.0.0/src/sunnygram/storage/memory.py +95 -0
  227. sunnygram-1.0.0/src/sunnygram/storage/sqlite.py +347 -0
  228. sunnygram-1.0.0/src/sunnygram/storage/string.py +117 -0
  229. sunnygram-1.0.0/src/sunnygram/tl/__init__.py +44 -0
  230. sunnygram-1.0.0/src/sunnygram/tl/core.py +565 -0
  231. sunnygram-1.0.0/src/sunnygram/transport/__init__.py +33 -0
  232. sunnygram-1.0.0/src/sunnygram/transport/codec.py +249 -0
  233. sunnygram-1.0.0/src/sunnygram/transport/obfuscation.py +157 -0
  234. sunnygram-1.0.0/src/sunnygram/transport/proxy.py +337 -0
  235. sunnygram-1.0.0/src/sunnygram/transport/tcp.py +176 -0
  236. sunnygram-1.0.0/src/sunnygram/types/__init__.py +82 -0
  237. sunnygram-1.0.0/src/sunnygram/types/buttons.py +339 -0
  238. sunnygram-1.0.0/src/sunnygram/types/callback.py +223 -0
  239. sunnygram-1.0.0/src/sunnygram/types/chat.py +114 -0
  240. sunnygram-1.0.0/src/sunnygram/types/dialog.py +115 -0
  241. sunnygram-1.0.0/src/sunnygram/types/events.py +239 -0
  242. sunnygram-1.0.0/src/sunnygram/types/folder.py +97 -0
  243. sunnygram-1.0.0/src/sunnygram/types/inline.py +752 -0
  244. sunnygram-1.0.0/src/sunnygram/types/join.py +108 -0
  245. sunnygram-1.0.0/src/sunnygram/types/member.py +307 -0
  246. sunnygram-1.0.0/src/sunnygram/types/message.py +407 -0
  247. sunnygram-1.0.0/src/sunnygram/types/payments.py +221 -0
  248. sunnygram-1.0.0/src/sunnygram/types/poll.py +266 -0
  249. sunnygram-1.0.0/src/sunnygram/types/reaction.py +188 -0
  250. sunnygram-1.0.0/src/sunnygram/types/rights.py +205 -0
  251. sunnygram-1.0.0/src/sunnygram/types/story.py +90 -0
  252. sunnygram-1.0.0/src/sunnygram/types/topic.py +124 -0
  253. sunnygram-1.0.0/src/sunnygram/types/user.py +73 -0
  254. sunnygram-1.0.0/src/sunnygram/updates/__init__.py +19 -0
  255. sunnygram-1.0.0/src/sunnygram/updates/manager.py +610 -0
  256. sunnygram-1.0.0/src/sunnygram/updates/state.py +219 -0
  257. sunnygram-1.0.0/src/sunnygram/utils.py +24 -0
  258. sunnygram-1.0.0/tests/conftest.py +33 -0
  259. sunnygram-1.0.0/tests/mtproto_server.py +364 -0
  260. sunnygram-1.0.0/tests/test_auth.py +598 -0
  261. sunnygram-1.0.0/tests/test_buttons.py +171 -0
  262. sunnygram-1.0.0/tests/test_cdn.py +644 -0
  263. sunnygram-1.0.0/tests/test_client.py +1717 -0
  264. sunnygram-1.0.0/tests/test_client_surface.py +423 -0
  265. sunnygram-1.0.0/tests/test_codegen.py +434 -0
  266. sunnygram-1.0.0/tests/test_connection.py +567 -0
  267. sunnygram-1.0.0/tests/test_crypto_accel.py +171 -0
  268. sunnygram-1.0.0/tests/test_crypto_aes.py +164 -0
  269. sunnygram-1.0.0/tests/test_crypto_auth_key.py +261 -0
  270. sunnygram-1.0.0/tests/test_crypto_factorization.py +50 -0
  271. sunnygram-1.0.0/tests/test_crypto_mtproto.py +124 -0
  272. sunnygram-1.0.0/tests/test_crypto_rsa.py +178 -0
  273. sunnygram-1.0.0/tests/test_crypto_srp.py +181 -0
  274. sunnygram-1.0.0/tests/test_dispatcher.py +709 -0
  275. sunnygram-1.0.0/tests/test_docs.py +179 -0
  276. sunnygram-1.0.0/tests/test_errors.py +181 -0
  277. sunnygram-1.0.0/tests/test_events.py +1055 -0
  278. sunnygram-1.0.0/tests/test_file_ref.py +205 -0
  279. sunnygram-1.0.0/tests/test_files.py +626 -0
  280. sunnygram-1.0.0/tests/test_inline.py +350 -0
  281. sunnygram-1.0.0/tests/test_invoker.py +554 -0
  282. sunnygram-1.0.0/tests/test_limiter.py +183 -0
  283. sunnygram-1.0.0/tests/test_loop.py +122 -0
  284. sunnygram-1.0.0/tests/test_methods_admin.py +629 -0
  285. sunnygram-1.0.0/tests/test_methods_bots.py +464 -0
  286. sunnygram-1.0.0/tests/test_methods_chats.py +378 -0
  287. sunnygram-1.0.0/tests/test_methods_export.py +232 -0
  288. sunnygram-1.0.0/tests/test_methods_extras.py +400 -0
  289. sunnygram-1.0.0/tests/test_methods_forum.py +368 -0
  290. sunnygram-1.0.0/tests/test_methods_media.py +273 -0
  291. sunnygram-1.0.0/tests/test_methods_messages.py +287 -0
  292. sunnygram-1.0.0/tests/test_methods_payments.py +301 -0
  293. sunnygram-1.0.0/tests/test_methods_scheduled.py +241 -0
  294. sunnygram-1.0.0/tests/test_methods_stories.py +202 -0
  295. sunnygram-1.0.0/tests/test_migrate.py +471 -0
  296. sunnygram-1.0.0/tests/test_network.py +364 -0
  297. sunnygram-1.0.0/tests/test_obfuscation.py +232 -0
  298. sunnygram-1.0.0/tests/test_package.py +71 -0
  299. sunnygram-1.0.0/tests/test_parser.py +259 -0
  300. sunnygram-1.0.0/tests/test_peers.py +658 -0
  301. sunnygram-1.0.0/tests/test_plugins.py +234 -0
  302. sunnygram-1.0.0/tests/test_proxy.py +392 -0
  303. sunnygram-1.0.0/tests/test_recent.py +238 -0
  304. sunnygram-1.0.0/tests/test_redaction.py +192 -0
  305. sunnygram-1.0.0/tests/test_session.py +482 -0
  306. sunnygram-1.0.0/tests/test_storage.py +355 -0
  307. sunnygram-1.0.0/tests/test_tl_core.py +392 -0
  308. sunnygram-1.0.0/tests/test_transport.py +332 -0
  309. sunnygram-1.0.0/tests/test_typing.py +128 -0
  310. sunnygram-1.0.0/tests/test_updates.py +819 -0
  311. sunnygram-1.0.0/tests/test_updates_model.py +769 -0
  312. 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.