simcord 2.0.2__tar.gz → 2.1.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 (202) hide show
  1. {simcord-2.0.2 → simcord-2.1.0}/CHANGELOG.md +7 -0
  2. {simcord-2.0.2 → simcord-2.1.0}/PKG-INFO +6 -2
  3. {simcord-2.0.2 → simcord-2.1.0}/README.md +5 -1
  4. {simcord-2.0.2 → simcord-2.1.0}/docs/api.md +10 -0
  5. {simcord-2.0.2 → simcord-2.1.0}/docs/concepts.md +3 -3
  6. {simcord-2.0.2 → simcord-2.1.0}/docs/cookbook.md +5 -1
  7. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/diagnostics.md +19 -8
  8. {simcord-2.0.2 → simcord-2.1.0}/docs/parity-matrix.md +1 -1
  9. {simcord-2.0.2 → simcord-2.1.0}/docs/quickstart.md +3 -4
  10. {simcord-2.0.2 → simcord-2.1.0}/docs/stability.md +7 -0
  11. {simcord-2.0.2 → simcord-2.1.0}/pyproject.toml +1 -1
  12. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/__init__.py +2 -0
  13. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/base.py +2 -0
  14. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/env.py +13 -1
  15. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/router.py +3 -1
  16. simcord-2.1.0/src/simcord/types.py +17 -0
  17. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_diagnostics.py +14 -2
  18. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_misc_branches.py +1 -1
  19. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_moderation.py +23 -0
  20. simcord-2.1.0/tests/unit/test_router.py +80 -0
  21. {simcord-2.0.2 → simcord-2.1.0}/uv.lock +1 -1
  22. simcord-2.0.2/tests/unit/test_router.py +0 -39
  23. {simcord-2.0.2 → simcord-2.1.0}/.github/ISSUE_TEMPLATE/bug.md +0 -0
  24. {simcord-2.0.2 → simcord-2.1.0}/.github/ISSUE_TEMPLATE/feature.md +0 -0
  25. {simcord-2.0.2 → simcord-2.1.0}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
  26. {simcord-2.0.2 → simcord-2.1.0}/.github/dependabot.yml +0 -0
  27. {simcord-2.0.2 → simcord-2.1.0}/.github/workflows/ci.yml +0 -0
  28. {simcord-2.0.2 → simcord-2.1.0}/.github/workflows/dpy-master.yml +0 -0
  29. {simcord-2.0.2 → simcord-2.1.0}/.github/workflows/release.yml +0 -0
  30. {simcord-2.0.2 → simcord-2.1.0}/.gitignore +0 -0
  31. {simcord-2.0.2 → simcord-2.1.0}/.pre-commit-config.yaml +0 -0
  32. {simcord-2.0.2 → simcord-2.1.0}/.python-version +0 -0
  33. {simcord-2.0.2 → simcord-2.1.0}/.readthedocs.yaml +0 -0
  34. {simcord-2.0.2 → simcord-2.1.0}/CODE_OF_CONDUCT.md +0 -0
  35. {simcord-2.0.2 → simcord-2.1.0}/CONTRIBUTING.md +0 -0
  36. {simcord-2.0.2 → simcord-2.1.0}/LICENSE +0 -0
  37. {simcord-2.0.2 → simcord-2.1.0}/SECURITY.md +0 -0
  38. {simcord-2.0.2 → simcord-2.1.0}/benchmarks/_helpers.py +0 -0
  39. {simcord-2.0.2 → simcord-2.1.0}/benchmarks/test_dispatch_benchmarks.py +0 -0
  40. {simcord-2.0.2 → simcord-2.1.0}/benchmarks/test_perf_guards.py +0 -0
  41. {simcord-2.0.2 → simcord-2.1.0}/changes/README.md +0 -0
  42. {simcord-2.0.2 → simcord-2.1.0}/docs/architecture.md +0 -0
  43. {simcord-2.0.2 → simcord-2.1.0}/docs/assets/simcord-terminal-demo.gif +0 -0
  44. {simcord-2.0.2 → simcord-2.1.0}/docs/assets/simcord-transparent.webp +0 -0
  45. {simcord-2.0.2 → simcord-2.1.0}/docs/assets/social-card.png +0 -0
  46. {simcord-2.0.2 → simcord-2.1.0}/docs/changelog.md +0 -0
  47. {simcord-2.0.2 → simcord-2.1.0}/docs/faq.md +0 -0
  48. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/ai-coding-agents.md +0 -0
  49. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/components.md +0 -0
  50. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/fixtures.md +0 -0
  51. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/intents.md +0 -0
  52. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/interactions.md +0 -0
  53. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/messages.md +0 -0
  54. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/mocks-vs-simulation.md +0 -0
  55. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/moderation-and-guild.md +0 -0
  56. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/permissions.md +0 -0
  57. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/test-without-token.md +0 -0
  58. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/testing-discord-py-bots.md +0 -0
  59. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/testing-slash-commands.md +0 -0
  60. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/threads-reactions-dms.md +0 -0
  61. {simcord-2.0.2 → simcord-2.1.0}/docs/guides/time-control.md +0 -0
  62. {simcord-2.0.2 → simcord-2.1.0}/docs/index.md +0 -0
  63. {simcord-2.0.2 → simcord-2.1.0}/docs/installation.md +0 -0
  64. {simcord-2.0.2 → simcord-2.1.0}/docs/llms.txt +0 -0
  65. {simcord-2.0.2 → simcord-2.1.0}/docs/performance.md +0 -0
  66. {simcord-2.0.2 → simcord-2.1.0}/docs/robots.txt +0 -0
  67. {simcord-2.0.2 → simcord-2.1.0}/examples/README.md +0 -0
  68. {simcord-2.0.2 → simcord-2.1.0}/examples/__init__.py +0 -0
  69. {simcord-2.0.2 → simcord-2.1.0}/examples/bot.py +0 -0
  70. {simcord-2.0.2 → simcord-2.1.0}/examples/conftest.py +0 -0
  71. {simcord-2.0.2 → simcord-2.1.0}/examples/test_bot.py +0 -0
  72. {simcord-2.0.2 → simcord-2.1.0}/mkdocs.yml +0 -0
  73. {simcord-2.0.2 → simcord-2.1.0}/overrides/main.html +0 -0
  74. {simcord-2.0.2 → simcord-2.1.0}/scripts/check_discoverability.py +0 -0
  75. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/_dpy_internals.py +0 -0
  76. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/actors.py +0 -0
  77. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/asserts.py +0 -0
  78. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/__init__.py +0 -0
  79. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/cdn.py +0 -0
  80. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/errors.py +0 -0
  81. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/__init__.py +0 -0
  82. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/auditlog.py +0 -0
  83. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/automod.py +0 -0
  84. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/channel.py +0 -0
  85. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/expression.py +0 -0
  86. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/guild.py +0 -0
  87. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/interaction.py +0 -0
  88. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/invite.py +0 -0
  89. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/member.py +0 -0
  90. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/message.py +0 -0
  91. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/role.py +0 -0
  92. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/scheduled_event.py +0 -0
  93. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/stage_instance.py +0 -0
  94. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/user.py +0 -0
  95. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/voice.py +0 -0
  96. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/webhook.py +0 -0
  97. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/__init__.py +0 -0
  98. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/audit.py +0 -0
  99. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/channels.py +0 -0
  100. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/commands.py +0 -0
  101. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/expressions.py +0 -0
  102. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/guilds.py +0 -0
  103. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/invites.py +0 -0
  104. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/messages.py +0 -0
  105. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/permissions.py +0 -0
  106. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/polls.py +0 -0
  107. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/reactions.py +0 -0
  108. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/scheduled_events.py +0 -0
  109. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/voice.py +0 -0
  110. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/webhooks.py +0 -0
  111. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/permissions.py +0 -0
  112. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/serializers.py +0 -0
  113. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/state.py +0 -0
  114. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/builders.py +0 -0
  115. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/components.py +0 -0
  116. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/enums.py +0 -0
  117. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/gateway.py +0 -0
  118. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/__init__.py +0 -0
  119. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/_helpers.py +0 -0
  120. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/client.py +0 -0
  121. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/__init__.py +0 -0
  122. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/application.py +0 -0
  123. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/auditlogs.py +0 -0
  124. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/automod.py +0 -0
  125. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/channels.py +0 -0
  126. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/commands.py +0 -0
  127. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/expressions.py +0 -0
  128. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/guilds.py +0 -0
  129. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/interactions.py +0 -0
  130. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/invites.py +0 -0
  131. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/messages.py +0 -0
  132. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/reactions.py +0 -0
  133. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/scheduled_events.py +0 -0
  134. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/stage_instances.py +0 -0
  135. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/intents.py +0 -0
  136. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/interactions.py +0 -0
  137. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/parity.py +0 -0
  138. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/py.typed +0 -0
  139. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/pytest_plugin.py +0 -0
  140. {simcord-2.0.2 → simcord-2.1.0}/src/simcord/results.py +0 -0
  141. {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/_world.py +0 -0
  142. {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_field_honesty.py +0 -0
  143. {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_field_honesty_fuzz.py +0 -0
  144. {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_route_coverage.py +0 -0
  145. {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_serializers.py +0 -0
  146. {simcord-2.0.2 → simcord-2.1.0}/tests/conftest.py +0 -0
  147. {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/__init__.py +0 -0
  148. {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/__init__.py +0 -0
  149. {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/events.py +0 -0
  150. {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/general.py +0 -0
  151. {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/interactions.py +0 -0
  152. {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/moderation.py +0 -0
  153. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_app_commands.py +0 -0
  154. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_application_emojis.py +0 -0
  155. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_asserts.py +0 -0
  156. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_audit_logs.py +0 -0
  157. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_automod.py +0 -0
  158. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_bot_users_and_webhooks.py +0 -0
  159. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_channels.py +0 -0
  160. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_command_permissions.py +0 -0
  161. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_common_bot_sweep.py +0 -0
  162. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_components_v2_interactions.py +0 -0
  163. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_components_v2_messages.py +0 -0
  164. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_coverage_sweep.py +0 -0
  165. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_edge_cases.py +0 -0
  166. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_error_capture_and_edits.py +0 -0
  167. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_expressions.py +0 -0
  168. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_forum.py +0 -0
  169. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
  170. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_guilds.py +0 -0
  171. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_intents.py +0 -0
  172. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_interaction_followups.py +0 -0
  173. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_interactions.py +0 -0
  174. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_invites.py +0 -0
  175. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_lifecycle.py +0 -0
  176. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_members.py +0 -0
  177. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_message_routes.py +0 -0
  178. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_messages.py +0 -0
  179. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_polls.py +0 -0
  180. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_reactions.py +0 -0
  181. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_roles_and_ordering.py +0 -0
  182. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_scheduled_events.py +0 -0
  183. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_settlement_contract.py +0 -0
  184. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_setup_hook_suspension.py +0 -0
  185. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_sharding.py +0 -0
  186. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_stage_instances.py +0 -0
  187. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_threads.py +0 -0
  188. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_time.py +0 -0
  189. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_transport_edges.py +0 -0
  190. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_voice_state.py +0 -0
  191. {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_world_helpers.py +0 -0
  192. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_assert_helpers.py +0 -0
  193. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_backend_lookup_errors.py +0 -0
  194. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_components_v2_validation.py +0 -0
  195. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_errors.py +0 -0
  196. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_intent_rules.py +0 -0
  197. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_parity.py +0 -0
  198. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_parity_tooling.py +0 -0
  199. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_permission_validators.py +0 -0
  200. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_permissions.py +0 -0
  201. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_pytest_plugin.py +0 -0
  202. {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_version.py +0 -0
@@ -4,6 +4,13 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
4
4
 
5
5
  <!-- towncrier release notes start -->
6
6
 
7
+ ## 2.1.0 (2026-09-19)
8
+
9
+ ### Features
10
+
11
+ - Add `Env.http_requests` records with query parameters and audit reasons while preserving the deprecated live `Env.http_log` tuple list for 2.x compatibility.
12
+
13
+
7
14
  ## 2.0.2 (2026-09-18)
8
15
 
9
16
  ### Bug fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simcord
3
- Version: 2.0.2
3
+ Version: 2.1.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/
@@ -263,7 +263,11 @@ When something goes wrong, the `env` tells you what happened:
263
263
 
264
264
  - `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
265
265
  failing pytest tests.
266
- - `env.http_log`: every REST request the bot made, to assert on or inspect.
266
+ - `env.http_requests`: structured records for every REST request (`method`, `path`, `params`,
267
+ `json`, `reason`). Values are discord.py transport arguments, not wire-normalized strings or
268
+ headers; uploaded files are not captured.
269
+ - `env.http_log`: deprecated live `(method, path, json_body)` tuples for 2.x compatibility;
270
+ access emits `DeprecationWarning`.
267
271
  - `env.errors`: exceptions the bot swallowed.
268
272
  - `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
269
273
  fail, to test your bot's error handling.
@@ -216,7 +216,11 @@ When something goes wrong, the `env` tells you what happened:
216
216
 
217
217
  - `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
218
218
  failing pytest tests.
219
- - `env.http_log`: every REST request the bot made, to assert on or inspect.
219
+ - `env.http_requests`: structured records for every REST request (`method`, `path`, `params`,
220
+ `json`, `reason`). Values are discord.py transport arguments, not wire-normalized strings or
221
+ headers; uploaded files are not captured.
222
+ - `env.http_log`: deprecated live `(method, path, json_body)` tuples for 2.x compatibility;
223
+ access emits `DeprecationWarning`.
220
224
  - `env.errors`: exceptions the bot swallowed.
221
225
  - `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
222
226
  fail, to test your bot's error handling.
@@ -26,6 +26,16 @@ top-level `simcord` package.
26
26
  Use `await env.external_wait(awaitable, reason="...")` for one explicitly scoped external
27
27
  input wait. Unknown waits remain active and produce a diagnostic timeout.
28
28
 
29
+
30
+ ## Request observability
31
+
32
+ ::: simcord.HttpLogEntry
33
+
34
+ `Env.http_requests` exposes these records for transport-level assertions. The `params` and
35
+ `reason` fields contain discord.py transport arguments, not wire-normalized query strings or
36
+ encoded headers; uploaded files are not captured. `Env.http_log` is a deprecated compatibility
37
+ list of legacy tuples.
38
+
29
39
  ## Builders
30
40
 
31
41
  Synchronous, omnipotent handles for arranging the virtual Discord. Returned by `env`/`guild`
@@ -23,8 +23,8 @@ async def test_something(simcord_env):
23
23
  ```
24
24
 
25
25
  The `Env` owns the virtual backend, tracks the bot's background tasks, captures errors,
26
- holds the virtual clock, and exposes the diagnostics (`env.http_log`, `env.transcript()`).
27
- A fresh `Env` is created for each test, so tests never leak state into each other.
26
+ holds the virtual clock, and exposes the diagnostics (`env.http_requests`, `env.transcript()`).
27
+ `env.http_log` is retained as a deprecated compatibility view.
28
28
 
29
29
  !!! info "One environment per event loop"
30
30
  `Env` monkeypatches `loop.create_task` and `time.monotonic` while it's live so it can
@@ -132,7 +132,7 @@ channel.history(viewer=mod) # ephemeral-aware: what `mod` would see
132
132
  channel.pinned_messages() # list[discord.Message]
133
133
  guild.get_ban(target) # ban record | None
134
134
  env.errors # exceptions the bot swallowed
135
- env.http_log # every REST call the bot made
135
+ env.http_requests # structured REST request records
136
136
  ```
137
137
 
138
138
  Interaction verbs return a richer [`InteractionResult`](api.md#simcord.InteractionResult) capturing
@@ -246,7 +246,11 @@ async def test_no_double_post(simcord_env):
246
246
 
247
247
  await alice.send(channel, "!announce")
248
248
 
249
- posts = [c for c in simcord_env.http_log if c[0] == "POST" and "/messages" in c[1]]
249
+ posts = [
250
+ entry
251
+ for entry in simcord_env.http_requests
252
+ if entry.method == "POST" and "/messages" in entry.path
253
+ ]
250
254
  assert len(posts) == 1
251
255
  ```
252
256
 
@@ -107,19 +107,30 @@ The [pytest plugin attaches this to failing tests automatically](fixtures.md#the
107
107
  so a failure shows you exactly what the bot did, in order — usually enough to diagnose
108
108
  without a debugger.
109
109
 
110
- ## The HTTP log (`env.http_log`)
110
+ ## HTTP request records (`env.http_requests`)
111
111
 
112
- For fine-grained assertions, `env.http_log` is the list of every REST call the bot made, as
113
- `(method, path, json_body)` tuples:
112
+ For fine-grained assertions, `env.http_requests` is the list of every REST call the bot made,
113
+ as [`simcord.HttpLogEntry`](../api.md#simcord.HttpLogEntry) records:
114
114
 
115
115
  ```python
116
- posts = [c for c in simcord_env.http_log if c[0] == "POST" and "/messages" in c[1]]
117
- assert len(posts) == 1 # the bot sent exactly one message
118
- assert posts[0][2]["content"] == "Pong!"
116
+ posts = [
117
+ entry
118
+ for entry in simcord_env.http_requests
119
+ if entry.method == "POST" and "/messages" in entry.path
120
+ ]
121
+ assert len(posts) == 1
122
+ assert posts[0].json["content"] == "Pong!"
119
123
  ```
120
124
 
121
- This is how you assert the bot **didn't** do something (e.g. didn't double-post), or made
122
- exactly the calls you expect.
125
+ Each record exposes `method`, `path`, `params`, `json`, and `reason`. These values are the
126
+ arguments discord.py passed to its transport: query parameters are not wire-normalized
127
+ strings, and `reason` is not an encoded HTTP header value. Uploaded files are not captured.
128
+ The request record is appended before fault injection and route handling, so failed and
129
+ unimplemented requests remain observable.
130
+
131
+ `env.http_log` remains available as a deprecated live list of legacy
132
+ `(method, path, json_body)` tuples for 2.x compatibility. Access emits `DeprecationWarning`;
133
+ use `env.http_requests` for new assertions.
123
134
 
124
135
  ## Injecting API failures
125
136
 
@@ -46,7 +46,7 @@ serializer payloads are conformance-tested against discord.py's own model parser
46
46
  | Guilds (create/edit/delete) | ✅ | `Client.create_guild`, `Guild.edit`, `Guild.delete` (owner-only); `GUILD_UPDATE` audit; `Guild.leave`, `Client.fetch_guilds`, `ClientUser.edit` (bot username); `Guild.vanity_invite` (settable via `guild.set_vanity_url`) |
47
47
  | Channels (create/edit/delete, overwrites) | ✅ | Runtime create + list; text, voice, stage, category & forum kinds; reorder/move (`Channel.move`); announcement `TextChannel.follow` |
48
48
  | Webhooks | ✅ | Create, execute (with per-message `username` override), fetch/edit/delete (by id or token), guild listing |
49
- | Fault injection / HTTP log | ✅ | `env.inject_error`, `env.http_log` |
49
+ | Fault injection / HTTP log | ✅ | `env.inject_error`, structured `env.http_requests` records, deprecated `env.http_log` tuples |
50
50
  | Audit logs | ✅ | Recorded for ban/kick/role/member/channel/event actions; `guild.audit_logs()`, filtering |
51
51
  | Polls | ✅ | Message-level poll object; `actor.vote`, expiry (route + `advance_time`), vote events |
52
52
  | Scheduled events | ✅ | CRUD + subscribe/unsubscribe; auto status transitions via `advance_time` |
@@ -124,10 +124,9 @@ see [Core concepts](concepts.md) for the deep dive.
124
124
  `alice.click(...)`, `alice.select(...)`, `alice.react(...)`, `alice.submit_modal(...)`.
125
125
  Do only what a real user physically could. Each one **waits for your bot to finish
126
126
  reacting** before returning — so there's never an `asyncio.sleep` in your tests.
127
- - **Queries** — `channel.history()`, `channel.last_message`,
128
- [`env.errors`](guides/diagnostics.md) (errors your bot swallowed),
129
- [`env.http_log`](guides/diagnostics.md) (every REST call it made). Assert with plain
130
- Python against real `discord.Message` objects.
127
+ - **Queries** — `channel.history()`, [`env.errors`](guides/diagnostics.md) (errors your bot
128
+ swallowed), and [`env.http_requests`](guides/diagnostics.md) (structured REST request
129
+ records). Assert with plain Python against real `discord.Message` objects.
131
130
 
132
131
  ## Testing failure paths
133
132
 
@@ -68,11 +68,18 @@ The public surface is exactly what `simcord` exports from its top-level package
68
68
  - the world builders — `GuildHandle`, `ChannelHandle`, `UserHandle`,
69
69
  `RoleHandle` — and the `MemberActor` that drives simulated users;
70
70
  - the result objects `ResponseMessage` and `InteractionResult`;
71
+ - the request-observability type `HttpLogEntry` and `Env.http_requests`;
71
72
  - the assertion helpers (`assert_responded`, `assert_sent`, `assert_message`,
72
73
  `assert_error`, `assert_no_errors`);
73
74
  - the error and parity-signal types `BackendError`, `SetupError`,
74
75
  `RouteNotImplemented` and `UnsupportedField`.
75
76
 
77
+ `Env.http_requests` is the semver-covered request-observability API. Its
78
+ `HttpLogEntry` fields preserve discord.py transport arguments (`params`, `json`, and
79
+ `reason`); they are not wire-normalized query strings or encoded headers, and files are
80
+ not captured. `Env.http_log` remains the live 2.x tuple list and emits
81
+ `DeprecationWarning` on access.
82
+
76
83
  The `pytest` plugin (the `simcord_env` fixture) is part of the public surface
77
84
  too.
78
85
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simcord"
7
- version = "2.0.2"
7
+ version = "2.1.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"
@@ -46,6 +46,7 @@ from .builders import ( # noqa: E402
46
46
  from .env import Env, run # noqa: E402
47
47
  from .http import RouteNotImplemented, UnsupportedField # noqa: E402
48
48
  from .results import InteractionResult, ResponseMessage # noqa: E402
49
+ from .types import HttpLogEntry # noqa: E402
49
50
 
50
51
  try:
51
52
  __version__ = version("simcord")
@@ -57,6 +58,7 @@ __all__ = (
57
58
  "ChannelHandle",
58
59
  "Env",
59
60
  "GuildHandle",
61
+ "HttpLogEntry",
60
62
  "InteractionResult",
61
63
  "MemberActor",
62
64
  "ResponseMessage",
@@ -13,6 +13,7 @@ import datetime
13
13
  from collections.abc import Callable, Mapping, Sequence
14
14
  from typing import Any
15
15
 
16
+ from ...types import HttpLogEntry
16
17
  from .. import errors, permissions
17
18
  from ..cdn import CdnStore
18
19
  from ..models import (
@@ -59,6 +60,7 @@ class BackendBase:
59
60
  self.cdn = CdnStore()
60
61
  self.subscribers: list[EventListener] = []
61
62
  self.http_log: list[tuple[str, str, dict[str, Any] | None]] = []
63
+ self.http_requests: list[HttpLogEntry] = []
62
64
  #: Interleaved record of everything that crossed either seam, in order:
63
65
  #: ("HTTP", "METHOD /path", body) and ("GATEWAY", event, payload).
64
66
  self.transcript: list[tuple[str, str, Any]] = []
@@ -6,6 +6,7 @@ import asyncio
6
6
  import contextvars
7
7
  import math
8
8
  import time
9
+ import warnings
9
10
  import weakref
10
11
  from collections.abc import Iterator
11
12
  from contextlib import contextmanager
@@ -22,6 +23,7 @@ from .backend.errors import SetupError
22
23
  from .builders import GuildHandle, UserHandle
23
24
  from .gateway import ShardRouter
24
25
  from .http import FakeHTTPClient, FakeWebhookAdapter
26
+ from .types import HttpLogEntry
25
27
 
26
28
  _BOT_SCOPE: contextvars.ContextVar[tuple[Any, int] | None] = contextvars.ContextVar(
27
29
  "simcord_bot_scope", default=None
@@ -906,9 +908,19 @@ class Env:
906
908
 
907
909
  # ----------------------------------------------------------- diagnostics
908
910
 
911
+ @property
912
+ def http_requests(self) -> list[HttpLogEntry]:
913
+ """Every REST call the bot made as a structured request record."""
914
+ return self.backend.http_requests
915
+
909
916
  @property
910
917
  def http_log(self) -> list[tuple[str, str, dict[str, Any] | None]]:
911
- """Every REST call the bot made: (method, path, json body)."""
918
+ """Deprecated live ``(method, path, json)`` REST call tuples."""
919
+ warnings.warn(
920
+ "Env.http_log is deprecated; use Env.http_requests instead",
921
+ DeprecationWarning,
922
+ stacklevel=2,
923
+ )
912
924
  return self.backend.http_log
913
925
 
914
926
  def transcript(self) -> str:
@@ -3,7 +3,7 @@
3
3
  Unknown routes always fail loudly with :class:`RouteNotImplemented` — a
4
4
  testing tool must never silently fake success. The router also services
5
5
  test-injected faults (``env.inject_error``) and records every call to
6
- ``backend.http_log`` for advanced assertions.
6
+ ``backend.http_requests`` plus the deprecated ``backend.http_log`` tuple list.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -17,6 +17,7 @@ from typing import Any
17
17
  from ..backend import Backend
18
18
  from ..backend.errors import BackendError
19
19
  from ..backend.models import Channel
20
+ from ..types import HttpLogEntry
20
21
 
21
22
 
22
23
  class RouteNotImplemented(BackendError):
@@ -192,6 +193,7 @@ def dispatch(
192
193
  reason: str | None = None,
193
194
  ) -> Any:
194
195
  backend.http_log.append((method, path, json if isinstance(json, dict) else None))
196
+ backend.http_requests.append(HttpLogEntry(method, path, dict(params or {}), json, reason))
195
197
  backend.transcript.append(("HTTP", f"{method} {path}", json if isinstance(json, dict) else None))
196
198
  _check_faults(backend, method, path)
197
199
  segments = path.strip("/").split("/")
@@ -0,0 +1,17 @@
1
+ """Public observability value types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class HttpLogEntry:
11
+ """A transport-level REST request made by the bot."""
12
+
13
+ method: str
14
+ path: str
15
+ params: dict[str, Any]
16
+ json: Any | None
17
+ reason: str | None
@@ -77,8 +77,20 @@ async def test_unimplemented_route_attaches_parity_note(env, channel):
77
77
 
78
78
  async def test_http_log_records_bot_calls(env, channel, alice):
79
79
  await alice.send(channel, "!ping")
80
- sends = [(m, p) for (m, p, _body) in env.http_log if m == "POST" and p.endswith("/messages")]
81
- assert sends, env.http_log
80
+ sends = [
81
+ (entry.method, entry.path)
82
+ for entry in env.http_requests
83
+ if entry.method == "POST" and entry.path.endswith("/messages")
84
+ ]
85
+ assert sends, env.http_requests
86
+
87
+
88
+ async def test_http_log_legacy_tuples_warn_and_remain_live(env):
89
+ with pytest.warns(DeprecationWarning, match="use Env.http_requests"):
90
+ legacy = env.http_log
91
+ assert legacy is env.backend.http_log
92
+ legacy.append(("TEST", "/compatibility", None))
93
+ assert env.backend.http_log[-1] == ("TEST", "/compatibility", None)
82
94
 
83
95
 
84
96
  async def test_embed_limits_enforced(env, channel):
@@ -10,7 +10,7 @@ async def test_bot_typing_indicator(env, channel):
10
10
  pass
11
11
  await env.settle()
12
12
 
13
- assert any(method == "POST" and path.endswith("/typing") for method, path, _ in env.http_log)
13
+ assert any(entry.method == "POST" and entry.path.endswith("/typing") for entry in env.http_requests)
14
14
 
15
15
 
16
16
  async def test_everyone_mention_requires_permission(env, channel):
@@ -16,6 +16,29 @@ async def test_bulk_ban(env):
16
16
  assert alice.id not in env.backend.get_guild(env.guild.id).members
17
17
 
18
18
 
19
+ async def test_ban_request_records_query_params_and_reason(env):
20
+ target = env.guild.add_member(env.create_user("baddie"))
21
+ await env.settle()
22
+
23
+ guild = env.bot.get_guild(env.guild.id)
24
+ await guild.ban(
25
+ discord.Object(id=target.id),
26
+ delete_message_seconds=86400,
27
+ reason="cleanup needed",
28
+ )
29
+ await env.settle()
30
+
31
+ bans = [
32
+ entry
33
+ for entry in env.http_requests
34
+ if entry.method == "PUT" and entry.path.endswith(f"/bans/{target.id}")
35
+ ]
36
+ assert len(bans) == 1
37
+ assert bans[0].params == {"delete_message_seconds": 86400}
38
+ assert bans[0].reason == "cleanup needed"
39
+ assert bans[0].json is None
40
+
41
+
19
42
  async def test_bulk_ban_already_banned_reported_as_failed(env):
20
43
  # When nobody can be banned (all already banned), Discord still returns the
21
44
  # split result — not a 403 — with every id in `failed`.
@@ -0,0 +1,80 @@
1
+ import pytest
2
+
3
+ from simcord import HttpLogEntry
4
+ from simcord.backend import Backend
5
+ from simcord.backend.errors import BackendError
6
+ from simcord.http import router
7
+ from simcord.http import routes as _routes # noqa: F401 — registers handlers
8
+
9
+
10
+ def test_unknown_route_raises_with_route_name():
11
+ backend = Backend()
12
+ with pytest.raises(router.RouteNotImplemented, match="GET /made/up/route"):
13
+ router.dispatch(backend, "GET", "/made/up/route")
14
+ assert isinstance(backend.http_requests[-1], HttpLogEntry)
15
+
16
+
17
+ def test_literal_segments_beat_parameters():
18
+ backend = Backend()
19
+ guild = backend.create_guild("g")
20
+ channel = backend.create_channel(guild.id, "general")
21
+ message = backend.create_message(channel.id, backend.bot_user.id, "hi")
22
+ backend.set_pinned(channel.id, message.id, True)
23
+ # ".../messages/pins" must not be captured by ".../messages/{message_id}".
24
+ result = router.dispatch(backend, "GET", f"/channels/{channel.id}/messages/pins")
25
+ assert [int(i["message"]["id"]) for i in result["items"]] == [message.id]
26
+
27
+
28
+ def test_http_request_records_defaults_and_coexists_with_legacy_tuples():
29
+ backend = Backend()
30
+ guild = backend.create_guild("g")
31
+ path = f"/guilds/{guild.id}"
32
+ router.dispatch(backend, "GET", path)
33
+ entry = backend.http_requests[-1]
34
+ assert (entry.method, entry.path, entry.params, entry.json, entry.reason) == (
35
+ "GET",
36
+ path,
37
+ {},
38
+ None,
39
+ None,
40
+ )
41
+ assert backend.http_log[-1] == ("GET", path, None)
42
+
43
+ payload = {"name": "g"}
44
+ router.dispatch(backend, "GET", path, json=payload)
45
+ assert backend.http_requests[-1].json is payload
46
+ assert backend.http_log[-1] == ("GET", path, payload)
47
+
48
+
49
+ def test_http_request_records_snapshot_transport_values_before_fault():
50
+ backend = Backend()
51
+ guild = backend.create_guild("g")
52
+ path = f"/guilds/{guild.id}"
53
+ params = {"limit": 3}
54
+ payload = ["raw", 1]
55
+ backend.faults.append(
56
+ {
57
+ "method": "GET",
58
+ "path": path,
59
+ "status": 500,
60
+ "code": 0,
61
+ "message": "boom",
62
+ "times": 1,
63
+ }
64
+ )
65
+ with pytest.raises(BackendError):
66
+ router.dispatch(backend, "GET", path, params=params, json=payload, reason="cleanup needed")
67
+ entry = backend.http_requests[-1]
68
+ assert entry.params == params and entry.params is not params
69
+ assert entry.json is payload
70
+ assert entry.reason == "cleanup needed"
71
+
72
+
73
+ def test_snowflakes_are_monotonic_and_timestamped():
74
+ backend = Backend()
75
+ flakes = [backend.snowflake() for _ in range(100)]
76
+ assert flakes == sorted(flakes) and len(set(flakes)) == 100
77
+ import discord
78
+
79
+ # Embedded timestamps decode to the fixed virtual epoch (2026+).
80
+ assert discord.utils.snowflake_time(flakes[0]).year >= 2026
@@ -1402,7 +1402,7 @@ wheels = [
1402
1402
 
1403
1403
  [[package]]
1404
1404
  name = "simcord"
1405
- version = "2.0.2"
1405
+ version = "2.1.0"
1406
1406
  source = { editable = "." }
1407
1407
  dependencies = [
1408
1408
  { name = "discord-py" },
@@ -1,39 +0,0 @@
1
- import pytest
2
-
3
- from simcord.backend import Backend
4
- from simcord.http import router
5
- from simcord.http import routes as _routes # noqa: F401 — registers handlers
6
-
7
-
8
- def test_unknown_route_raises_with_route_name():
9
- backend = Backend()
10
- with pytest.raises(router.RouteNotImplemented, match="GET /made/up/route"):
11
- router.dispatch(backend, "GET", "/made/up/route")
12
-
13
-
14
- def test_literal_segments_beat_parameters():
15
- backend = Backend()
16
- guild = backend.create_guild("g")
17
- channel = backend.create_channel(guild.id, "general")
18
- message = backend.create_message(channel.id, backend.bot_user.id, "hi")
19
- backend.set_pinned(channel.id, message.id, True)
20
- # ".../messages/pins" must not be captured by ".../messages/{message_id}".
21
- result = router.dispatch(backend, "GET", f"/channels/{channel.id}/messages/pins")
22
- assert [int(i["message"]["id"]) for i in result["items"]] == [message.id]
23
-
24
-
25
- def test_http_log_records_calls():
26
- backend = Backend()
27
- guild = backend.create_guild("g")
28
- router.dispatch(backend, "GET", f"/guilds/{guild.id}")
29
- assert backend.http_log[-1][:2] == ("GET", f"/guilds/{guild.id}")
30
-
31
-
32
- def test_snowflakes_are_monotonic_and_timestamped():
33
- backend = Backend()
34
- flakes = [backend.snowflake() for _ in range(100)]
35
- assert flakes == sorted(flakes) and len(set(flakes)) == 100
36
- import discord
37
-
38
- # Embedded timestamps decode to the fixed virtual epoch (2026+).
39
- assert discord.utils.snowflake_time(flakes[0]).year >= 2026
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes