simcord 3.2.2__tar.gz → 3.3.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 (419) hide show
  1. {simcord-3.2.2 → simcord-3.3.0}/CHANGELOG.md +16 -0
  2. {simcord-3.2.2 → simcord-3.3.0}/PKG-INFO +1 -1
  3. {simcord-3.2.2 → simcord-3.3.0}/docs/api.md +11 -0
  4. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/components.md +19 -3
  5. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/fixtures.md +15 -0
  6. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/testing-slash-commands.md +7 -1
  7. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/threads-reactions-dms.md +23 -2
  8. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/time-control.md +59 -3
  9. {simcord-3.2.2 → simcord-3.3.0}/docs/stability.md +7 -1
  10. {simcord-3.2.2 → simcord-3.3.0}/pyproject.toml +1 -1
  11. simcord-3.3.0/src/simcord/_clock.py +35 -0
  12. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_dpy_internals.py +40 -3
  13. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_settlement.py +13 -0
  14. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/actors.py +17 -4
  15. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/base.py +7 -4
  16. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/env.py +77 -26
  17. simcord-3.3.0/tests/integration/test_native_typing.py +200 -0
  18. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_settlement_contract.py +50 -0
  19. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_shared_clock.py +128 -3
  20. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_time.py +97 -0
  21. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_dpy_internals.py +3 -3
  22. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_pytest_clock.py +13 -1
  23. {simcord-3.2.2 → simcord-3.3.0}/uv.lock +1 -1
  24. simcord-3.2.2/src/simcord/_clock.py +0 -15
  25. {simcord-3.2.2 → simcord-3.3.0}/.github/ISSUE_TEMPLATE/bug.md +0 -0
  26. {simcord-3.2.2 → simcord-3.3.0}/.github/ISSUE_TEMPLATE/feature.md +0 -0
  27. {simcord-3.2.2 → simcord-3.3.0}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
  28. {simcord-3.2.2 → simcord-3.3.0}/.github/dependabot.yml +0 -0
  29. {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/ci.yml +0 -0
  30. {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/dpy-master.yml +0 -0
  31. {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/release.yml +0 -0
  32. {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/verify.yml +0 -0
  33. {simcord-3.2.2 → simcord-3.3.0}/.gitignore +0 -0
  34. {simcord-3.2.2 → simcord-3.3.0}/.pre-commit-config.yaml +0 -0
  35. {simcord-3.2.2 → simcord-3.3.0}/.python-version +0 -0
  36. {simcord-3.2.2 → simcord-3.3.0}/.readthedocs.yaml +0 -0
  37. {simcord-3.2.2 → simcord-3.3.0}/CODE_OF_CONDUCT.md +0 -0
  38. {simcord-3.2.2 → simcord-3.3.0}/CONTRIBUTING.md +0 -0
  39. {simcord-3.2.2 → simcord-3.3.0}/LICENSE +0 -0
  40. {simcord-3.2.2 → simcord-3.3.0}/README.md +0 -0
  41. {simcord-3.2.2 → simcord-3.3.0}/SECURITY.md +0 -0
  42. {simcord-3.2.2 → simcord-3.3.0}/benchmarks/_helpers.py +0 -0
  43. {simcord-3.2.2 → simcord-3.3.0}/benchmarks/simulation_attribution.py +0 -0
  44. {simcord-3.2.2 → simcord-3.3.0}/benchmarks/test_dispatch_benchmarks.py +0 -0
  45. {simcord-3.2.2 → simcord-3.3.0}/benchmarks/test_perf_guards.py +0 -0
  46. {simcord-3.2.2 → simcord-3.3.0}/benchmarks/test_simulation_benchmarks.py +0 -0
  47. {simcord-3.2.2 → simcord-3.3.0}/changes/README.md +0 -0
  48. {simcord-3.2.2 → simcord-3.3.0}/docs/architecture.md +0 -0
  49. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/adventure/alpine.png +0 -0
  50. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/adventure/canyon.png +0 -0
  51. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/attachments.png +0 -0
  52. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/buttons.png +0 -0
  53. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/embeds.png +0 -0
  54. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/entities.png +0 -0
  55. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/layouts.png +0 -0
  56. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/markdown.png +0 -0
  57. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/modals.png +0 -0
  58. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/polls.png +0 -0
  59. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/replies.png +0 -0
  60. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/selects.png +0 -0
  61. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/slash-commands.png +0 -0
  62. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/simcord-showcase-poll-refresh.gif +0 -0
  63. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/simcord-terminal-demo.gif +0 -0
  64. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/simcord-transparent.webp +0 -0
  65. {simcord-3.2.2 → simcord-3.3.0}/docs/assets/social-card.png +0 -0
  66. {simcord-3.2.2 → simcord-3.3.0}/docs/changelog.md +0 -0
  67. {simcord-3.2.2 → simcord-3.3.0}/docs/concepts.md +0 -0
  68. {simcord-3.2.2 → simcord-3.3.0}/docs/cookbook.md +0 -0
  69. {simcord-3.2.2 → simcord-3.3.0}/docs/faq.md +0 -0
  70. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/ai-coding-agents.md +0 -0
  71. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/diagnostics.md +0 -0
  72. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/discord-visual-calibration.md +0 -0
  73. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/intents.md +0 -0
  74. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/interactions.md +0 -0
  75. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/messages.md +0 -0
  76. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/mocks-vs-simulation.md +0 -0
  77. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/moderation-and-guild.md +0 -0
  78. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/monetization.md +0 -0
  79. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/permissions.md +0 -0
  80. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/preview.md +0 -0
  81. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/test-without-token.md +0 -0
  82. {simcord-3.2.2 → simcord-3.3.0}/docs/guides/testing-discord-py-bots.md +0 -0
  83. {simcord-3.2.2 → simcord-3.3.0}/docs/index.md +0 -0
  84. {simcord-3.2.2 → simcord-3.3.0}/docs/installation.md +0 -0
  85. {simcord-3.2.2 → simcord-3.3.0}/docs/llms.txt +0 -0
  86. {simcord-3.2.2 → simcord-3.3.0}/docs/parity-matrix.md +0 -0
  87. {simcord-3.2.2 → simcord-3.3.0}/docs/performance.md +0 -0
  88. {simcord-3.2.2 → simcord-3.3.0}/docs/preview-attestation.md +0 -0
  89. {simcord-3.2.2 → simcord-3.3.0}/docs/quickstart.md +0 -0
  90. {simcord-3.2.2 → simcord-3.3.0}/docs/robots.txt +0 -0
  91. {simcord-3.2.2 → simcord-3.3.0}/examples/README.md +0 -0
  92. {simcord-3.2.2 → simcord-3.3.0}/examples/__init__.py +0 -0
  93. {simcord-3.2.2 → simcord-3.3.0}/examples/bot.py +0 -0
  94. {simcord-3.2.2 → simcord-3.3.0}/examples/conftest.py +0 -0
  95. {simcord-3.2.2 → simcord-3.3.0}/examples/preview_example.py +0 -0
  96. {simcord-3.2.2 → simcord-3.3.0}/examples/test_bot.py +0 -0
  97. {simcord-3.2.2 → simcord-3.3.0}/jsconfig.preview.json +0 -0
  98. {simcord-3.2.2 → simcord-3.3.0}/mkdocs.yml +0 -0
  99. {simcord-3.2.2 → simcord-3.3.0}/overrides/main.html +0 -0
  100. {simcord-3.2.2 → simcord-3.3.0}/package-lock.json +0 -0
  101. {simcord-3.2.2 → simcord-3.3.0}/package.json +0 -0
  102. {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_discord_reference.py +0 -0
  103. {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_monetization_reference.py +0 -0
  104. {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_remaining_monetization.py +0 -0
  105. {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_visual_reference.py +0 -0
  106. {simcord-3.2.2 → simcord-3.3.0}/scripts/check_discoverability.py +0 -0
  107. {simcord-3.2.2 → simcord-3.3.0}/scripts/check_package.py +0 -0
  108. {simcord-3.2.2 → simcord-3.3.0}/scripts/check_reference_modal_offline.py +0 -0
  109. {simcord-3.2.2 → simcord-3.3.0}/scripts/compare_visual_reference.py +0 -0
  110. {simcord-3.2.2 → simcord-3.3.0}/scripts/discord_reference_bot.py +0 -0
  111. {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_checkout.py +0 -0
  112. {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_config.py +0 -0
  113. {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_expiry.py +0 -0
  114. {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_journal.py +0 -0
  115. {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_lifecycle.py +0 -0
  116. {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_transport.py +0 -0
  117. {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_upgrade.py +0 -0
  118. {simcord-3.2.2 → simcord-3.3.0}/scripts/preview_dogfood.py +0 -0
  119. {simcord-3.2.2 → simcord-3.3.0}/scripts/record_readme_demo.py +0 -0
  120. {simcord-3.2.2 → simcord-3.3.0}/scripts/replay_visual_reference.py +0 -0
  121. {simcord-3.2.2 → simcord-3.3.0}/scripts/validate_discord_reference_pack.py +0 -0
  122. {simcord-3.2.2 → simcord-3.3.0}/scripts/validate_monetization_reference.py +0 -0
  123. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/__init__.py +0 -0
  124. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_modal.py +0 -0
  125. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_payload.py +0 -0
  126. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_runtime.py +0 -0
  127. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/asserts.py +0 -0
  128. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/__init__.py +0 -0
  129. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/_deadlines.py +0 -0
  130. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/access.py +0 -0
  131. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/cdn.py +0 -0
  132. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/command_access.py +0 -0
  133. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/errors.py +0 -0
  134. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/__init__.py +0 -0
  135. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/auditlog.py +0 -0
  136. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/automod.py +0 -0
  137. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/channel.py +0 -0
  138. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/expression.py +0 -0
  139. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/guild.py +0 -0
  140. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/interaction.py +0 -0
  141. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/invite.py +0 -0
  142. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/member.py +0 -0
  143. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/message.py +0 -0
  144. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/monetization.py +0 -0
  145. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/role.py +0 -0
  146. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/scheduled_event.py +0 -0
  147. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/stage_instance.py +0 -0
  148. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/user.py +0 -0
  149. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/voice.py +0 -0
  150. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/webhook.py +0 -0
  151. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/__init__.py +0 -0
  152. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/audit.py +0 -0
  153. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/channels.py +0 -0
  154. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/commands.py +0 -0
  155. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/expressions.py +0 -0
  156. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/guilds.py +0 -0
  157. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/invites.py +0 -0
  158. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/messages.py +0 -0
  159. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/monetization.py +0 -0
  160. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/permissions.py +0 -0
  161. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/polls.py +0 -0
  162. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/reactions.py +0 -0
  163. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/scheduled_events.py +0 -0
  164. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/voice.py +0 -0
  165. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/webhooks.py +0 -0
  166. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/permissions.py +0 -0
  167. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/serializers.py +0 -0
  168. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/state.py +0 -0
  169. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/builders.py +0 -0
  170. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/components.py +0 -0
  171. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/enums.py +0 -0
  172. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/gateway.py +0 -0
  173. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/__init__.py +0 -0
  174. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/_helpers.py +0 -0
  175. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/client.py +0 -0
  176. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/router.py +0 -0
  177. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/__init__.py +0 -0
  178. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/application.py +0 -0
  179. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/auditlogs.py +0 -0
  180. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/automod.py +0 -0
  181. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/channels.py +0 -0
  182. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/commands.py +0 -0
  183. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/expressions.py +0 -0
  184. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/guilds.py +0 -0
  185. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/interactions.py +0 -0
  186. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/invites.py +0 -0
  187. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/messages.py +0 -0
  188. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/monetization.py +0 -0
  189. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/reactions.py +0 -0
  190. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/scheduled_events.py +0 -0
  191. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/stage_instances.py +0 -0
  192. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/intents.py +0 -0
  193. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/interactions.py +0 -0
  194. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/monetization.py +0 -0
  195. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/parity.py +0 -0
  196. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/__init__.py +0 -0
  197. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_action_plans.py +0 -0
  198. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_actions.py +0 -0
  199. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_assets.py +0 -0
  200. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_capture.py +0 -0
  201. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_commands.py +0 -0
  202. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_component_projection.py +0 -0
  203. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_dependencies.py +0 -0
  204. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_diagnostics.py +0 -0
  205. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_identity.py +0 -0
  206. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_markdown.py +0 -0
  207. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_media.py +0 -0
  208. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_media_worker.py +0 -0
  209. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_messages.py +0 -0
  210. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_pages.py +0 -0
  211. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_queries.py +0 -0
  212. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_server.py +0 -0
  213. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_snapshot.py +0 -0
  214. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/protocol.schema.json +0 -0
  215. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/actions.js +0 -0
  216. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/app.js +0 -0
  217. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/assets.js +0 -0
  218. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/backend-diagnostics.json +0 -0
  219. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/command-catalog.js +0 -0
  220. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/command-drafts.js +0 -0
  221. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/commands.js +0 -0
  222. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/components.js +0 -0
  223. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/composer.js +0 -0
  224. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/connection.js +0 -0
  225. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/diagnostics.js +0 -0
  226. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/dom.js +0 -0
  227. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/focus.js +0 -0
  228. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Arabic.txt +0 -0
  229. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-CJK.txt +0 -0
  230. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Devanagari.txt +0 -0
  231. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Emoji.txt +0 -0
  232. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Hebrew.txt +0 -0
  233. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto.txt +0 -0
  234. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/manifest.json +0 -0
  235. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-color-emoji-v2.051.ttf +0 -0
  236. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-arabic-v2.012.ttf +0 -0
  237. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-devanagari-v2.007.ttf +0 -0
  238. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-hebrew-v3.001.ttf +0 -0
  239. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-latin-italic-v2.015.ttf +0 -0
  240. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-latin-v2.015.ttf +0 -0
  241. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-mono-v2.014.ttf +0 -0
  242. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-sc-v2.004.ttf +0 -0
  243. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/geometry.js +0 -0
  244. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/highlight-types.d.ts +0 -0
  245. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/index.html +0 -0
  246. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/listbox.js +0 -0
  247. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/media.js +0 -0
  248. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/messages.js +0 -0
  249. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/modal.js +0 -0
  250. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/presentation.js +0 -0
  251. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/preview-types.d.ts +0 -0
  252. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/preview.css +0 -0
  253. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/select-drafts.js +0 -0
  254. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/selects.js +0 -0
  255. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/session.js +0 -0
  256. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/surfaces.js +0 -0
  257. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/text.js +0 -0
  258. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/timeline.js +0 -0
  259. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/transport.js +0 -0
  260. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/LICENSE +0 -0
  261. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/SHA256SUMS +0 -0
  262. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/core.min.js +0 -0
  263. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/bash.min.js +0 -0
  264. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/cpp.min.js +0 -0
  265. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/csharp.min.js +0 -0
  266. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/css.min.js +0 -0
  267. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/diff.min.js +0 -0
  268. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/dockerfile.min.js +0 -0
  269. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/go.min.js +0 -0
  270. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/ini.min.js +0 -0
  271. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/java.min.js +0 -0
  272. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/javascript.min.js +0 -0
  273. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/json.min.js +0 -0
  274. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/markdown.min.js +0 -0
  275. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/python.min.js +0 -0
  276. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/rust.min.js +0 -0
  277. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/sql.min.js +0 -0
  278. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/typescript.min.js +0 -0
  279. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/xml.min.js +0 -0
  280. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/yaml.min.js +0 -0
  281. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/lottie/5.12.2/lottie_light_canvas.min.js +0 -0
  282. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/lottie/LICENSE +0 -0
  283. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/lottie/SHA256SUMS +0 -0
  284. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/workbench.js +0 -0
  285. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/py.typed +0 -0
  286. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/pytest_plugin.py +0 -0
  287. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/results.py +0 -0
  288. {simcord-3.2.2 → simcord-3.3.0}/src/simcord/types.py +0 -0
  289. {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/_world.py +0 -0
  290. {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_field_honesty.py +0 -0
  291. {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_field_honesty_fuzz.py +0 -0
  292. {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_route_coverage.py +0 -0
  293. {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_serializers.py +0 -0
  294. {simcord-3.2.2 → simcord-3.3.0}/tests/conftest.py +0 -0
  295. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/__init__.py +0 -0
  296. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/commands/option_cases.json +0 -0
  297. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/__init__.py +0 -0
  298. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/catalog.py +0 -0
  299. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/coverage.json +0 -0
  300. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/measurements.json +0 -0
  301. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/moving-square.json +0 -0
  302. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/profiles.json +0 -0
  303. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/states.json +0 -0
  304. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/video.mp4 +0 -0
  305. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/voice.ogg +0 -0
  306. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/__init__.py +0 -0
  307. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/events.py +0 -0
  308. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/general.py +0 -0
  309. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/interactions.py +0 -0
  310. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/moderation.py +0 -0
  311. {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/picker.py +0 -0
  312. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/preview_helpers.py +0 -0
  313. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_access_boundaries.py +0 -0
  314. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_app_commands.py +0 -0
  315. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_application_emojis.py +0 -0
  316. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_asserts.py +0 -0
  317. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_atomic_mutations.py +0 -0
  318. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_audit_logs.py +0 -0
  319. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_automod.py +0 -0
  320. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_bot_users_and_webhooks.py +0 -0
  321. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_callback_thread_safety.py +0 -0
  322. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_channels.py +0 -0
  323. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_command_permissions.py +0 -0
  324. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_command_visibility.py +0 -0
  325. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_common_bot_sweep.py +0 -0
  326. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_components_v2_interactions.py +0 -0
  327. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_components_v2_messages.py +0 -0
  328. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_coverage_sweep.py +0 -0
  329. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_diagnostics.py +0 -0
  330. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_edge_cases.py +0 -0
  331. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_entitlement_journey.py +0 -0
  332. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_error_capture_and_edits.py +0 -0
  333. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_expressions.py +0 -0
  334. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_forum.py +0 -0
  335. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_forwarding.py +0 -0
  336. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
  337. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_guilds.py +0 -0
  338. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_intents.py +0 -0
  339. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_interaction_followups.py +0 -0
  340. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_interactions.py +0 -0
  341. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_invites.py +0 -0
  342. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_lifecycle.py +0 -0
  343. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_members.py +0 -0
  344. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_message_content_rest.py +0 -0
  345. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_message_routes.py +0 -0
  346. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_messages.py +0 -0
  347. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_misc_branches.py +0 -0
  348. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_moderation.py +0 -0
  349. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_observation_contracts.py +0 -0
  350. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_polls.py +0 -0
  351. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview.py +0 -0
  352. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_action_admission.py +0 -0
  353. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_additional.py +0 -0
  354. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_browser_ownership.py +0 -0
  355. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_capture.py +0 -0
  356. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_capture_dx.py +0 -0
  357. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_channel.py +0 -0
  358. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_command_picker.py +0 -0
  359. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_commands.py +0 -0
  360. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_components_v2.py +0 -0
  361. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_coverage.py +0 -0
  362. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_dx.py +0 -0
  363. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_edges.py +0 -0
  364. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_entity_contracts.py +0 -0
  365. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_fixes.py +0 -0
  366. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_fonts.py +0 -0
  367. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_identity.py +0 -0
  368. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_message_actions.py +0 -0
  369. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_modal_contracts.py +0 -0
  370. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_navigation_contracts.py +0 -0
  371. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_observations.py +0 -0
  372. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_provenance.py +0 -0
  373. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_render_contracts.py +0 -0
  374. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_security.py +0 -0
  375. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_support_report.py +0 -0
  376. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_viewport_ownership.py +0 -0
  377. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_workbench_contracts.py +0 -0
  378. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_reactions.py +0 -0
  379. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_reference_replay.py +0 -0
  380. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_roles_and_ordering.py +0 -0
  381. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_runtime_executor_ownership.py +0 -0
  382. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_scheduled_events.py +0 -0
  383. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_sdk_compatibility.py +0 -0
  384. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_setup_hook_suspension.py +0 -0
  385. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_sharding.py +0 -0
  386. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_stage_instances.py +0 -0
  387. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_threads.py +0 -0
  388. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_transport_edges.py +0 -0
  389. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_visual_reference_geometry.py +0 -0
  390. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_voice_state.py +0 -0
  391. {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_world_helpers.py +0 -0
  392. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_assert_helpers.py +0 -0
  393. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_automod_preparation.py +0 -0
  394. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_backend_lookup_errors.py +0 -0
  395. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_backend_performance_contracts.py +0 -0
  396. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_command_access.py +0 -0
  397. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_components_v2_validation.py +0 -0
  398. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_errors.py +0 -0
  399. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_intent_rules.py +0 -0
  400. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_lazy_import.py +0 -0
  401. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_local_eliminations.py +0 -0
  402. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_backend.py +0 -0
  403. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_http.py +0 -0
  404. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_capture.py +0 -0
  405. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_checkout.py +0 -0
  406. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_expiry.py +0 -0
  407. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_lifecycle.py +0 -0
  408. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_upgrade.py +0 -0
  409. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_validation.py +0 -0
  410. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_option_validation.py +0 -0
  411. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_parity.py +0 -0
  412. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_parity_tooling.py +0 -0
  413. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_payload_ownership.py +0 -0
  414. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_permission_validators.py +0 -0
  415. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_permissions.py +0 -0
  416. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_preview_action_specs.py +0 -0
  417. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_pytest_plugin.py +0 -0
  418. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_remaining_monetization_capture.py +0 -0
  419. {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_router.py +0 -0
@@ -4,6 +4,22 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
4
4
 
5
5
  <!-- towncrier release notes start -->
6
6
 
7
+ ## 3.3.0 (2026-11-02)
8
+
9
+ ### Features
10
+
11
+ - Add the aware `start_time` option to `Env`, `simcord.run` and pytest markers, applying a validated UTC origin before fixtures and startup while preserving the deterministic default and elapsed time across bot restarts. Application and SQL clocks still require explicit application clock injection.
12
+
13
+ ### Bug fixes
14
+
15
+ - Recognize discord.py's native typing interval as a virtual-only wait while preserving the initial typing request, every five-second refresh, HTTP work, and normal cancellation and lifecycle cleanup.
16
+ - Deliver already-real-due native asyncio cancellation deadlines through blocked input during automatic clock advancement, preserving chronological resource/timer processing, strict settlement, executor ownership, and undelivered settlement failures.
17
+
18
+ ### Documentation
19
+
20
+ - Clarify the guild-versus-DM handle scopes enforced since 3.2.1, with actionable setup errors and migration examples: use `member.user.send_dm(...)` for DM messages and `member.user` for DM slash commands, autocomplete, components, and modals. The existing `member.send_dm(...)` convenience remains supported; accepted and rejected actions are unchanged.
21
+
22
+
7
23
  ## 3.2.2 (2026-10-31)
8
24
 
9
25
  ### Bug fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simcord
3
- Version: 3.2.2
3
+ Version: 3.3.0
4
4
  Summary: Offline discord.py bot testing with pytest. No network, token, or test server.
5
5
  Project-URL: Homepage, https://github.com/SilentHacks/simcord
6
6
  Project-URL: Documentation, https://simcord.readthedocs.io/
@@ -19,6 +19,17 @@ top-level `simcord` package unless a module-qualified path is shown.
19
19
 
20
20
  ::: simcord.Env
21
21
 
22
+ ### `Env(start_time=None)`
23
+
24
+ Sets the immutable initial virtual UTC instant before backend IDs, fixtures and startup.
25
+ `None` preserves 2026-01-01 UTC. Supply an aware `datetime`; offsets normalize to UTC.
26
+ Invalid values raise `SetupError`. Supported UTC instants are at least
27
+ 2015-01-01 00:00:00 and strictly before 2154-05-15 07:35:11.104.
28
+ Elapsed time advances normally and survives bot restarts. `simcord.run` and
29
+ `@pytest.mark.simcord` accept the same option. This does not synchronize application or
30
+ SQL wall clocks; bind application time seams to `env.now` before startup when needed.
31
+ See [time control](guides/time-control.md#choosing-the-initial-utc-instant).
32
+
22
33
  ### `Env(settle_timeout=5.0)`
23
34
 
24
35
  `settle_timeout` is the default maximum time spent joining runnable bot work. A direct
@@ -26,9 +26,25 @@ inspect remote media dimensions or contents.
26
26
 
27
27
  Nested buttons and selects dispatch through their real discord.py callbacks. The same
28
28
  `click`, `select`, and `submit_modal` methods are available on `UserHandle` for component
29
- flows in DMs. Use `component_text=` and `component_custom_id=` with `assert_message`,
30
- `assert_sent`, or `assert_responded` to inspect layouts without treating TextDisplay text
31
- as legacy message content.
29
+ flows in DMs. Since 3.2.1, use `member.user` for these DM actions rather than the
30
+ guild-scoped `MemberActor`:
31
+
32
+ ```python
33
+ opened = await alice.user.click(dm_message, custom_id="feedback:open")
34
+ submitted = await alice.user.submit_modal(opened, {"comment": "Great bot"})
35
+ # For a DM select menu:
36
+ selected = await alice.user.select(dm_menu, ["green"], custom_id="color")
37
+ ```
38
+
39
+ DM callbacks receive SDK interactions with `interaction.guild is None`. Keep using
40
+ `alice.click(...)`, `alice.select(...)`, and `alice.submit_modal(...)` for interactions in
41
+ `alice`'s guild. A `UserHandle` cannot act on guild messages; use a `MemberActor` belonging
42
+ to the target guild instead. These are SimCord handle scopes, not limitations on what the
43
+ same human can do in Discord.
44
+
45
+ Use `component_text=` and `component_custom_id=` with `assert_message`, `assert_sent`,
46
+ or `assert_responded` to inspect layouts without treating TextDisplay text as legacy
47
+ message content.
32
48
 
33
49
  ## Clicking buttons
34
50
 
@@ -100,6 +100,7 @@ Both `simcord.run(bot, **options)` and the underlying `Env` accept these keyword
100
100
  | `approved_intents` | `None` (all approved) | Simulates developer-portal privileged-intent toggles and REST Message Content access, independently of the Gateway request bit. Explicit `Intents.none()` denies approval. |
101
101
  | `shard_count` | client setting | Supplies the Get Gateway Bot recommendation when an `AutoShardedClient` does not configure `shard_count` itself. |
102
102
  | `settle_timeout` | `5.0` seconds | Default maximum time for joining runnable bot work; a direct settle `timeout=` overrides it. |
103
+ | `start_time` | `None` (2026-01-01 UTC) | Aware initial UTC instant, normalized from any offset before fixtures/startup; see [time control](time-control.md#choosing-the-initial-utc-instant) for supported bounds and application clock wiring. |
103
104
 
104
105
  `settle_timeout` does not change startup's fixed five-second READY settlement budget.
105
106
  Restart drains existing work using `settle_timeout`, then gets a fresh fixed five-second
@@ -181,6 +182,20 @@ async def test_command_logic_in_isolation(simcord_env):
181
182
  ...
182
183
  ```
183
184
 
185
+ The same marker accepts an initial clock origin:
186
+
187
+ ```python
188
+ from datetime import UTC, datetime
189
+
190
+ @pytest.mark.simcord(start_time=datetime(2030, 4, 5, tzinfo=UTC))
191
+ async def test_future_entitlement(simcord_env):
192
+ assert simcord_env.now() == datetime(2030, 4, 5, tzinfo=UTC)
193
+ ```
194
+
195
+ For application clock injection needed by `setup_hook`, construct `Env` and bind
196
+ `env.now` before manual startup in your fixture; the
197
+ [time-control example](time-control.md#choosing-the-initial-utc-instant) includes teardown.
198
+
184
199
  In 3.0, error snapshots with prefix acknowledgement and detached mutable result
185
200
  payloads are unconditional. Remove the temporary 2.3 `future_behavior` marker/run
186
201
  option; see [Migration](../stability.md#migrating-from-23-to-30).
@@ -90,13 +90,19 @@ uploaded = await alice.slash(channel, "upload", file=("photo.png", b"image bytes
90
90
 
91
91
  ## Bot-DM slash commands and visible command lists
92
92
 
93
- `UserHandle.slash(name, **options)` and `UserHandle.autocomplete(name, option, value, **filled)` invoke global commands whose contexts allow `BOT_DM`. Their interactions have `context == 1`:
93
+ `UserHandle.slash(name, **options)` and `UserHandle.autocomplete(name, option, value, **filled)` invoke global commands whose contexts allow `BOT_DM`. Their interactions have `context == 1`, and the SDK callback receives `interaction.guild is None`:
94
94
 
95
95
  ```python
96
96
  user = simcord_env.create_user("alice")
97
97
  await user.slash("help") # The bot's callback receives interaction.context == 1.
98
98
  ```
99
99
 
100
+ For an existing guild member, use `await member.user.slash("help")` or
101
+ `await member.user.autocomplete("tag", "name", "py")` for DM commands. Since 3.2.1,
102
+ passing `member.user.dm_channel` to `member.slash(...)` or `member.autocomplete(...)`
103
+ is rejected: `MemberActor` is guild-scoped. Keep `member.slash(channel, ...)` and
104
+ `member.autocomplete(channel, ...)` for its guild.
105
+
100
106
  Use `member.available_commands(channel)` or `user.available_commands()` to get a tuple of leaf invocation strings that are visible there and accepted by `slash()` (for example, `"config set"`):
101
107
 
102
108
  ```python
@@ -70,11 +70,32 @@ reply = alice.user.dm_channel.history() # what the bot DM'd back
70
70
  assert reply[-1].content.startswith("Hi")
71
71
  ```
72
72
 
73
+ `alice` above is a `MemberActor`; `alice.send_dm(...)` remains a supported convenience
74
+ that delegates to `alice.user.send_dm(...)`. Other guild-scoped actor methods do not
75
+ target DMs. Use the `UserHandle` (`alice.user`) for DM messages, commands, components,
76
+ and modals; keep `alice` for operations in its guild.
77
+
78
+ Since 3.2.1, migrate calls that passed a DM channel to a guild actor:
79
+
80
+ ```python
81
+ # Before 3.2.1: a guild actor was used to target a DM.
82
+ await alice.send(alice.user.dm_channel, "hello bot")
83
+
84
+ # Now: send through the user's DM-scoped handle.
85
+ await alice.user.send_dm("hello bot")
86
+ # This existing convenience also remains supported:
87
+ await alice.send_dm("hello bot")
88
+
89
+ # Guild operations still use the guild member.
90
+ await alice.send(channel, "hello guild")
91
+ ```
92
+
73
93
  `alice.user.dm_channel` is the user's DM channel with the bot; `.history()` returns the
74
94
  conversation as real `discord.Message` objects. A `UserHandle` can also invoke global slash
75
95
  commands and autocomplete in that DM when their command contexts allow `BOT_DM`; these
76
- interactions carry `context == 1`. Use `user.available_commands()` to list the slash invocations
77
- visible there. See [slash command visibility and DM commands](testing-slash-commands.md#bot-dm-slash-commands-and-visible-command-lists).
96
+ interactions carry `context == 1` and the SDK callback receives `interaction.guild is None`.
97
+ Use `user.available_commands()` to list the slash invocations visible there. See
98
+ [slash command visibility and DM commands](testing-slash-commands.md#bot-dm-slash-commands-and-visible-command-lists).
78
99
 
79
100
  Guild actors can edit/delete their messages and pin with `set_pinned`. In the user's bot DM,
80
101
  `UserHandle` supports their own message edits/deletes, desired reactions, and poll votes:
@@ -9,6 +9,47 @@ Time-dependent behaviour — `View` timeouts, command cooldowns, `asyncio.sleep`
9
9
  — is normally painful to test: you either wait in real time (slow, flaky) or mock the clock
10
10
  by hand (fiddly, leaky). SimCord gives you a **virtual clock** you fast-forward in one call.
11
11
 
12
+ ## Choosing the initial UTC instant
13
+
14
+ `Env(bot, start_time=...)` and `simcord.run(bot, start_time=...)` accept an aware
15
+ `datetime`. The default remains **2026-01-01 00:00:00 UTC**. Offsets normalize to UTC;
16
+ naive datetimes, non-datetimes and unsupported instants raise `SetupError` before backend
17
+ IDs or runtime hooks are created. The supported initial UTC range is
18
+ **2015-01-01 00:00:00 inclusive through 2154-05-15 07:35:11.104 exclusive**.
19
+ The origin is fixed at construction, already applies to pre-start fixtures and backend
20
+ timestamps, and survives bot restarts along with elapsed virtual time. There is no rewind
21
+ or `set_time` operation.
22
+
23
+ Bind your application's existing clock seam **before** startup so `setup_hook` and
24
+ fixture writes use the same clock. For example, a custom pytest fixture can use manual
25
+ lifecycle control:
26
+
27
+ ```python
28
+ from datetime import UTC, datetime
29
+
30
+ import pytest_asyncio
31
+ import simcord
32
+
33
+ @pytest_asyncio.fixture
34
+ async def world():
35
+ bot = create_bot()
36
+ env = simcord.Env(bot, start_time=datetime(2030, 4, 5, 12, tzinfo=UTC))
37
+ bot.service.now = env.now # application-owned callable used by startup and writes
38
+ await env.start()
39
+ try:
40
+ yield env
41
+ finally:
42
+ await env.shutdown()
43
+ ```
44
+
45
+ The standard pytest fixture also accepts
46
+ `@pytest.mark.simcord(start_time=datetime(2030, 4, 5, tzinfo=UTC))`.
47
+ Use a manual fixture when application wiring must happen before `env.start()`.
48
+ Selecting an initial origin does **not** synchronize Python wall time, SQL
49
+ `CURRENT_TIMESTAMP`/`now()`, database defaults or application clocks. Inject `env.now`
50
+ through existing application time seams, including relevant persisted writes, rather
51
+ than globally monkeypatching datetime, SQL or SDK functions.
52
+
12
53
  ## `env.advance_time(seconds)`
13
54
 
14
55
  ```python
@@ -35,6 +76,9 @@ Anything the bot scheduled against the loop's clock:
35
76
  - **Resource deadlines** — polls, scheduled events and explicitly scheduled entitlement ends.
36
77
  - **Supported SDK UTC helpers** — entitlement expiry, `discord.utils.sleep_until`, and
37
78
  relative/calendar `discord.ext.tasks` loops use the selected virtual clock.
79
+ - **Typing refresh intervals** — the native SDK typing task's direct five-second sleep
80
+ is virtual-only. Its initial HTTP request and every refresh still execute and join
81
+ normally; HTTP waits and retries are not treated as parked typing intervals.
38
82
 
39
83
  ```python
40
84
  async def test_cooldown_resets(simcord_env):
@@ -109,13 +153,17 @@ and retry shutdown.
109
153
 
110
154
  ## Determinism
111
155
 
112
- Snowflake IDs and UTC start from a fixed virtual epoch. ID allocation does not advance
113
- time: IDs use a sequence within the current millisecond and explicit IDs are reserved
114
- against collisions. Exhausting the sequence fails instead of silently moving UTC.
156
+ Snowflake IDs and UTC start from the configured origin (the fixed default is unchanged).
157
+ ID allocation does not advance time: IDs use a sequence within the current millisecond
158
+ and explicit IDs are reserved against collisions. Exhausting the sequence fails instead
159
+ of silently moving UTC.
115
160
  Shard-pinned guild fixtures can use an earlier timestamp so they route to the requested
116
161
  shard without time travel. Their `created_at` can precede arrangement time, and fixture
117
162
  creation order need not match numeric ID order. Normal snowflake timestamps have
118
163
  millisecond precision, while backend UTC retains microseconds.
164
+ At the Discord epoch, allocation skips ID zero. A shard request requiring a timestamp
165
+ before that epoch raises `SetupError` without reserving IDs. Allocation beyond the
166
+ 64-bit snowflake timestamp range also rejects rather than generating an invalid ID.
119
167
  Settlement joins runnable work before and after each timer step. Deadline timers behind
120
168
  recognized waits — `wait_for` timeouts on listener waits, View/Modal expiry, `ext.tasks`
121
169
  loop intervals and wake-up timers scheduled inside `env.external_wait` — never fire on the
@@ -124,6 +172,14 @@ recognized only when its live loop timer resolves the awaited future; unrelated
124
172
  timers do not make an unknown wait look parked. `advance_time()` is itself exclusive with
125
173
  other public operations and rejects invalid or negative durations.
126
174
 
175
+ Unrecognized waits can have real-due timers handled by the automatic coordinator. That
176
+ fallback advances through its due horizon even when an unrelated input remains blocked,
177
+ still processing resource deadlines before tied bot timers. It does not declare the
178
+ blocked input settled, relax executor ownership or suppress settlement failures.
179
+ Recognized intervals such as typing refresh sleeps remain virtual-only. Explicit
180
+ `advance_time()` still uses strict settlement and reports committed partial progress on
181
+ failure.
182
+
127
183
  If an advance times out, earlier transitions remain committed. The exception notes
128
184
  report requested and reached UTC instants. Resolve the outstanding work and advance
129
185
  only the remaining duration; do not repeat the original duration or replay an action.
@@ -67,7 +67,7 @@ The public surface is exactly what `simcord` exports from its top-level package
67
67
  - the world builders — `GuildHandle`, `ChannelHandle`, `UserHandle`,
68
68
  `RoleHandle` — and the `MemberActor` that drives simulated users;
69
69
  - `SKUHandle`, `EntitlementHandle`, the Env SKU/entitlement fixture methods,
70
- `Env.now()` and `Env.time_context()`;
70
+ `Env.now()`, `Env.time_context()` and the `Env`/`run` `start_time` option;
71
71
  - the result objects `ResponseMessage` and `InteractionResult`;
72
72
  - `Preview`, `PreviewCapture`, and the documented `window.simcordPreview` readiness fields for
73
73
  the optional local component preview;
@@ -110,6 +110,12 @@ use virtual time without replacing application/global datetime. See
110
110
  [simulator policies](guides/monetization.md#evidence-and-open-parity-checks), not captured
111
111
  Discord parity; future calibration may correct these policies.
112
112
 
113
+ `start_time=None` preserves the 2026-01-01 UTC origin and default generated IDs.
114
+ An aware custom origin takes effect before fixtures/startup and persists across restarts;
115
+ offsets normalize to UTC and unsupported origins fail with `SetupError` before backend
116
+ allocation or runtime hook installation. It does not virtualize application or SQL wall
117
+ clocks. The private `Clock` implementation is not a public clock-provider API.
118
+
113
119
 
114
120
  ## What is intentionally internal
115
121
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simcord"
7
- version = "3.2.2"
7
+ version = "3.3.0"
8
8
  description = "Offline discord.py bot testing with pytest. No network, token, or test server."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,35 @@
1
+ """A backend-owned clock, independent of event loops and environments."""
2
+
3
+ from datetime import UTC, datetime, timedelta
4
+
5
+ _DISCORD_EPOCH = datetime(2015, 1, 1, tzinfo=UTC)
6
+ _END_START = _DISCORD_EPOCH + timedelta(milliseconds=1 << 42)
7
+
8
+
9
+ class Clock:
10
+ def __init__(self, start_time: datetime | None = None) -> None:
11
+ from .backend.errors import SetupError
12
+
13
+ if start_time is None:
14
+ start_time = datetime(2026, 1, 1, tzinfo=UTC)
15
+ if not isinstance(start_time, datetime):
16
+ raise SetupError("start_time must be an aware datetime")
17
+ try:
18
+ if start_time.utcoffset() is None:
19
+ raise SetupError("start_time must be an aware datetime")
20
+ start_time = start_time.astimezone(UTC)
21
+ except (OverflowError, ValueError, TypeError) as exc:
22
+ raise SetupError("start_time must normalize to a supported UTC datetime") from exc
23
+ if not _DISCORD_EPOCH <= start_time < _END_START:
24
+ raise SetupError(
25
+ "start_time must be at least 2015-01-01T00:00:00+00:00 "
26
+ "and before 2154-05-15T07:35:11.104+00:00"
27
+ )
28
+ self.elapsed = 0.0
29
+ self._epoch = start_time
30
+
31
+ def now(self) -> datetime:
32
+ return self._epoch + timedelta(seconds=self.elapsed)
33
+
34
+ def advance(self, seconds: float) -> None:
35
+ self.elapsed += seconds
@@ -17,6 +17,7 @@ from types import ModuleType
17
17
  from typing import Any
18
18
 
19
19
  import discord
20
+ from discord.context_managers import Typing
20
21
  from discord.ext import tasks as _ext_tasks
21
22
  from discord.gateway import DiscordWebSocket
22
23
  from discord.http import HTTPClient
@@ -84,6 +85,7 @@ def verify() -> None:
84
85
  (_view.BaseView, "_BaseView__timeout_task_impl"),
85
86
  (_ext_tasks.Loop, "_loop"),
86
87
  (_ext_tasks.SleepHandle, "_wrapped_set_result"),
88
+ (Typing, "do_typing"),
87
89
  (asyncio.timeouts.Timeout, "_on_timeout"),
88
90
  ):
89
91
  if not hasattr(cls, attr): # pragma: no cover - fires only if discord.py drops an internal
@@ -132,6 +134,11 @@ def verify() -> None:
132
134
  or "self" not in impl_probe.__code__.co_varnames
133
135
  ): # pragma: no cover
134
136
  problems.append("BaseView._BaseView__timeout_task_impl coroutine")
137
+ typing_impl = getattr(Typing, "do_typing", None)
138
+ if (
139
+ not inspect.iscoroutinefunction(typing_impl) or "self" not in typing_impl.__code__.co_varnames
140
+ ): # pragma: no cover
141
+ problems.append("Typing.do_typing coroutine")
135
142
  if not hasattr(asyncio.timeouts.Timeout(0.0), "_task"): # pragma: no cover
136
143
  problems.append("Timeout._task")
137
144
 
@@ -311,20 +318,50 @@ def tasks_loop_sleep(task: asyncio.Task[Any], waiter: Any) -> bool:
311
318
  return getattr(handle, "future", None) is waiter
312
319
 
313
320
 
321
+ def typing_interval(task: asyncio.Task[Any], waiter: Any) -> bool:
322
+ """Recognize only the SDK task's direct sleep, including timer construction."""
323
+ if not isinstance(waiter, asyncio.Future) or waiter.done():
324
+ return False
325
+ coro = task.get_coro()
326
+ if getattr(coro, "cr_code", None) is not Typing.do_typing.__code__:
327
+ return False
328
+ frame = getattr(coro, "cr_frame", None)
329
+ owner = frame.f_locals.get("self") if frame is not None else None
330
+ if not isinstance(owner, Typing) or getattr(owner, "task", None) is not task:
331
+ return False
332
+ sleep = getattr(coro, "cr_await", None)
333
+ sleep_frame = getattr(sleep, "cr_frame", None)
334
+ if getattr(sleep, "cr_code", None) is asyncio.sleep.__code__:
335
+ return sleep_frame is not None and sleep_frame.f_locals.get("future") is waiter
336
+ # During call_later the coroutine is executing, not yet suspended.
337
+ # Inspect the exact direct caller, never an HTTP retry's nested sleep.
338
+ if task is not asyncio.current_task():
339
+ return False
340
+ current = inspect.currentframe()
341
+ try:
342
+ while current is not None:
343
+ if current.f_code is asyncio.sleep.__code__:
344
+ return current.f_locals.get("future") is waiter and current.f_back is frame
345
+ current = current.f_back
346
+ return False
347
+ finally:
348
+ del current
349
+
350
+
314
351
  def is_intentional_wait_wakeup(
315
352
  client: discord.Client, callback: Any, args: tuple[Any, ...], task: asyncio.Task[Any]
316
353
  ) -> bool:
317
354
  """True when a timer scheduled right now exists only to resume a wait on this task.
318
355
 
319
356
  Covers the shapes identifiable at schedule time, before the task suspends:
320
- a registered View/Modal expiry task's sleep, an ext.tasks iteration
321
- ``SleepHandle``, and a 3.11 ``wait_for`` listener's ``_release_waiter``.
357
+ a registered View/Modal expiry task's sleep, a native typing interval,
358
+ an ext.tasks ``SleepHandle``, and a 3.11 listener's ``_release_waiter``.
322
359
  Timer wake-ups not identifiable here are still caught at settle time and
323
360
  gated at fire time by the env.
324
361
  """
325
362
  name = getattr(callback, "__name__", "")
326
363
  if name == "_set_result_unless_cancelled":
327
- return _view_timeout_task_identity(client, task)
364
+ return _view_timeout_task_identity(client, task) or (bool(args) and typing_interval(task, args[0]))
328
365
  if name == "_wrapped_set_result":
329
366
  return is_tasks_loop_task(task)
330
367
  return name == "_release_waiter" and any(is_listener_future(client, arg) for arg in args)
@@ -153,6 +153,11 @@ def _recognized_wait(self: _SettlementState, task: asyncio.Task[Any], waiter: An
153
153
  or (waiter in self.views and not waiter.done())
154
154
  or _dpy_internals.view_expiry_task(self.bot, task, waiter, self.expiry_tasks)
155
155
  or _dpy_internals.tasks_loop_sleep(task, waiter)
156
+ or (
157
+ (record := self.work.tasks.get(task)) is not None
158
+ and record.generation == self.generation
159
+ and _dpy_internals.typing_interval(task, waiter)
160
+ )
156
161
  )
157
162
 
158
163
 
@@ -261,6 +266,14 @@ def _compute_park_reason(self: _SettlementState, task: asyncio.Task[Any], deadli
261
266
  return "discord View/Modal expiry timer"
262
267
  if _dpy_internals.tasks_loop_sleep(task, waiter) and _timer_records_waking(self, task, waiter):
263
268
  return "discord.ext.tasks loop interval"
269
+ record = self.work.tasks.get(task)
270
+ if (
271
+ record is not None
272
+ and record.generation == self.generation
273
+ and _dpy_internals.typing_interval(task, waiter)
274
+ and _timer_records_waking(self, task, waiter)
275
+ ):
276
+ return "discord typing interval"
264
277
  dependencies = _dpy_internals.composed_tasks(task, waiter)
265
278
  unresolved = [dependency for dependency in dependencies if not dependency.done()]
266
279
  if unresolved:
@@ -67,6 +67,8 @@ class MemberActor:
67
67
  return cached.get_member(self.id) if cached else None
68
68
 
69
69
  def _check(self, channel: ChannelHandle, *permissions: str) -> None:
70
+ _require_handle(self._env, channel)
71
+ _check_user_dm_channel(self, channel.id)
70
72
  _require_handle(self._env, channel, guild_id=self.guild.id)
71
73
  self._env.backend.require_permissions(self.guild.id, self.id, channel.id, *permissions)
72
74
 
@@ -626,18 +628,29 @@ _ENTITY_SELECT_HANDLES: dict[ComponentType, tuple[type, ...]] = {
626
628
 
627
629
  def _check_actor_channel(actor: Any, channel: ChannelHandle) -> None:
628
630
  guild = getattr(actor, "guild", None)
629
- _require_handle(actor._env, channel, guild_id=guild.id if guild is not None else None)
631
+ _require_handle(actor._env, channel)
630
632
  _check_user_dm_channel(actor, channel.id)
633
+ _require_handle(actor._env, channel, guild_id=guild.id if guild is not None else None)
631
634
  if not can_access_channel(actor._env, channel.id, actor):
632
635
  raise SetupError("That channel is not accessible to this user")
633
636
 
634
637
 
635
638
  def _check_user_dm_channel(actor: Any, channel_id: int) -> None:
636
- if isinstance(actor, UserHandle) and actor._env.backend.dm_channels.get(actor.id) != channel_id:
639
+ if isinstance(actor, MemberActor) and actor._env.backend.dm_channels.get(actor.id) == channel_id:
637
640
  raise SetupError(
638
- "UserHandle interaction operations are restricted to this user's DM channel — "
639
- "a real user could not interact with guild messages"
641
+ "MemberActor is scoped to its guild, not this user's bot DM — "
642
+ "use member.user.send_dm(...) for messages or member.user for "
643
+ "slash, autocomplete, click, select, and submit_modal. "
644
+ "member.send_dm(...) remains supported"
640
645
  )
646
+ if isinstance(actor, UserHandle) and actor._env.backend.dm_channels.get(actor.id) != channel_id:
647
+ channel = actor._env.backend.channels.get(channel_id)
648
+ if channel is not None and channel.guild_id is not None:
649
+ raise SetupError(
650
+ "UserHandle operations are DM-scoped — use a MemberActor belonging to "
651
+ "the target guild for guild messages and interactions"
652
+ )
653
+ raise SetupError("UserHandle operations are restricted to this user's DM channel")
641
654
 
642
655
 
643
656
  def _visible_message(actor: Any, message: MessageLike) -> Any:
@@ -40,7 +40,8 @@ from ..models import (
40
40
  )
41
41
 
42
42
  # Snowflakes embed milliseconds since Discord's epoch; allocation never moves UTC.
43
- _DISCORD_EPOCH_MS = 1420070400000
43
+ _DISCORD_EPOCH = datetime.datetime(2015, 1, 1, tzinfo=datetime.UTC)
44
+ _MILLISECOND = datetime.timedelta(milliseconds=1)
44
45
 
45
46
  EventListener = Callable[[str, Mapping[str, Any]], None]
46
47
 
@@ -106,7 +107,9 @@ class BackendBase:
106
107
  return value
107
108
 
108
109
  def _allocate_at(self, milliseconds: int) -> int:
109
- sequence = self._sequences.get(milliseconds, 0)
110
+ if not 0 <= milliseconds < 1 << 42:
111
+ raise errors.SetupError("generated snowflake timestamp is outside the 64-bit range")
112
+ sequence = self._sequences.get(milliseconds, 1 if milliseconds == 0 else 0)
110
113
  while sequence < 1 << 22:
111
114
  value = (milliseconds << 22) | sequence
112
115
  sequence += 1
@@ -118,12 +121,12 @@ class BackendBase:
118
121
 
119
122
  def snowflake(self) -> int:
120
123
  """Allocate at current UTC, using all 22 low bits without advancing time."""
121
- milliseconds = int(self.clock.now().timestamp() * 1000) - _DISCORD_EPOCH_MS
124
+ milliseconds = (self.clock.now() - _DISCORD_EPOCH) // _MILLISECOND
122
125
  return self._allocate_at(milliseconds)
123
126
 
124
127
  def snowflake_for_shard(self, shard_id: int, shard_count: int) -> int:
125
128
  """Allocate at the latest historical millisecond belonging to this shard."""
126
- milliseconds = int(self.clock.now().timestamp() * 1000) - _DISCORD_EPOCH_MS
129
+ milliseconds = (self.clock.now() - _DISCORD_EPOCH) // _MILLISECOND
127
130
  milliseconds -= (milliseconds - shard_id) % shard_count
128
131
  return self._allocate_at(milliseconds)
129
132