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.
- {simcord-3.2.2 → simcord-3.3.0}/CHANGELOG.md +16 -0
- {simcord-3.2.2 → simcord-3.3.0}/PKG-INFO +1 -1
- {simcord-3.2.2 → simcord-3.3.0}/docs/api.md +11 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/components.md +19 -3
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/fixtures.md +15 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/testing-slash-commands.md +7 -1
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/threads-reactions-dms.md +23 -2
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/time-control.md +59 -3
- {simcord-3.2.2 → simcord-3.3.0}/docs/stability.md +7 -1
- {simcord-3.2.2 → simcord-3.3.0}/pyproject.toml +1 -1
- simcord-3.3.0/src/simcord/_clock.py +35 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_dpy_internals.py +40 -3
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_settlement.py +13 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/actors.py +17 -4
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/base.py +7 -4
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/env.py +77 -26
- simcord-3.3.0/tests/integration/test_native_typing.py +200 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_settlement_contract.py +50 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_shared_clock.py +128 -3
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_time.py +97 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_dpy_internals.py +3 -3
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_pytest_clock.py +13 -1
- {simcord-3.2.2 → simcord-3.3.0}/uv.lock +1 -1
- simcord-3.2.2/src/simcord/_clock.py +0 -15
- {simcord-3.2.2 → simcord-3.3.0}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.github/dependabot.yml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/ci.yml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/dpy-master.yml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/release.yml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.github/workflows/verify.yml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.gitignore +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.pre-commit-config.yaml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.python-version +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/.readthedocs.yaml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/CODE_OF_CONDUCT.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/CONTRIBUTING.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/LICENSE +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/README.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/SECURITY.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/benchmarks/_helpers.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/benchmarks/simulation_attribution.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/benchmarks/test_dispatch_benchmarks.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/benchmarks/test_perf_guards.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/benchmarks/test_simulation_benchmarks.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/changes/README.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/architecture.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/adventure/alpine.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/adventure/canyon.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/attachments.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/buttons.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/embeds.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/entities.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/layouts.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/markdown.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/modals.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/polls.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/replies.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/selects.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/preview/slash-commands.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/simcord-showcase-poll-refresh.gif +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/assets/social-card.png +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/changelog.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/concepts.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/cookbook.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/faq.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/ai-coding-agents.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/diagnostics.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/discord-visual-calibration.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/intents.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/interactions.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/messages.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/monetization.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/permissions.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/preview.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/test-without-token.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/index.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/installation.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/llms.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/parity-matrix.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/performance.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/preview-attestation.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/quickstart.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/docs/robots.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/examples/README.md +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/examples/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/examples/bot.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/examples/conftest.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/examples/preview_example.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/examples/test_bot.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/jsconfig.preview.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/mkdocs.yml +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/overrides/main.html +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/package-lock.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/package.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_discord_reference.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_monetization_reference.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_remaining_monetization.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/capture_visual_reference.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/check_discoverability.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/check_package.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/check_reference_modal_offline.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/compare_visual_reference.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/discord_reference_bot.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_checkout.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_config.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_expiry.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_journal.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_lifecycle.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_transport.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/monetization_reference_upgrade.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/preview_dogfood.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/record_readme_demo.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/replay_visual_reference.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/validate_discord_reference_pack.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/scripts/validate_monetization_reference.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_modal.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_payload.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/_runtime.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/asserts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/_deadlines.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/access.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/cdn.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/command_access.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/errors.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/automod.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/channel.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/expression.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/guild.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/interaction.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/invite.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/member.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/message.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/monetization.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/role.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/user.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/voice.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/channels.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/guilds.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/messages.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/monetization.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/polls.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/scheduled_events.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/permissions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/serializers.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/backend/state.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/builders.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/components.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/enums.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/gateway.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/_helpers.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/client.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/router.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/application.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/automod.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/channels.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/commands.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/guilds.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/interactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/invites.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/messages.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/monetization.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/intents.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/interactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/monetization.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/parity.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_action_plans.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_actions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_assets.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_capture.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_commands.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_component_projection.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_dependencies.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_diagnostics.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_identity.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_markdown.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_media.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_media_worker.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_messages.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_pages.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_queries.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_server.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/_snapshot.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/protocol.schema.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/actions.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/app.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/assets.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/backend-diagnostics.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/command-catalog.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/command-drafts.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/commands.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/components.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/composer.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/connection.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/diagnostics.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/dom.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/focus.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Arabic.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-CJK.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Devanagari.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Emoji.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Hebrew.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto.txt +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/manifest.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-color-emoji-v2.051.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-arabic-v2.012.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-devanagari-v2.007.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-hebrew-v3.001.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-latin-italic-v2.015.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-latin-v2.015.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-mono-v2.014.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/fonts/noto-sans-sc-v2.004.ttf +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/geometry.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/highlight-types.d.ts +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/index.html +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/listbox.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/media.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/messages.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/modal.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/presentation.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/preview-types.d.ts +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/preview.css +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/select-drafts.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/selects.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/session.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/surfaces.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/text.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/timeline.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/transport.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/LICENSE +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/SHA256SUMS +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/core.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/bash.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/cpp.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/csharp.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/css.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/diff.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/dockerfile.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/go.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/ini.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/java.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/javascript.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/json.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/markdown.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/python.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/rust.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/sql.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/typescript.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/xml.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/highlight/es/languages/yaml.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/lottie/5.12.2/lottie_light_canvas.min.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/lottie/LICENSE +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/vendor/lottie/SHA256SUMS +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/preview/static/workbench.js +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/py.typed +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/pytest_plugin.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/results.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/src/simcord/types.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/_world.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/conformance/test_serializers.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/conftest.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/commands/option_cases.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/catalog.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/coverage.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/measurements.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/moving-square.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/profiles.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/states.json +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/video.mp4 +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/preview/voice.ogg +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/fixtures/sample_bot/picker.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/preview_helpers.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_access_boundaries.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_app_commands.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_application_emojis.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_asserts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_atomic_mutations.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_audit_logs.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_automod.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_bot_users_and_webhooks.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_callback_thread_safety.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_channels.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_command_permissions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_command_visibility.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_diagnostics.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_edge_cases.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_entitlement_journey.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_expressions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_forum.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_forwarding.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_guilds.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_intents.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_interaction_followups.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_interactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_invites.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_lifecycle.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_members.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_message_content_rest.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_message_routes.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_messages.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_misc_branches.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_moderation.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_observation_contracts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_polls.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_action_admission.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_additional.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_browser_ownership.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_capture.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_capture_dx.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_channel.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_command_picker.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_commands.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_components_v2.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_coverage.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_dx.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_edges.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_entity_contracts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_fixes.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_fonts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_identity.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_message_actions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_modal_contracts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_navigation_contracts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_observations.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_provenance.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_render_contracts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_security.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_support_report.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_viewport_ownership.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_preview_workbench_contracts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_reactions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_reference_replay.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_roles_and_ordering.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_runtime_executor_ownership.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_sdk_compatibility.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_setup_hook_suspension.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_sharding.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_stage_instances.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_threads.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_transport_edges.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_visual_reference_geometry.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_voice_state.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/integration/test_world_helpers.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_automod_preparation.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_backend_performance_contracts.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_command_access.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_components_v2_validation.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_errors.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_intent_rules.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_lazy_import.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_local_eliminations.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_backend.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_http.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_capture.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_checkout.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_expiry.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_lifecycle.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_upgrade.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_monetization_reference_validation.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_option_validation.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_parity.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_payload_ownership.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_permission_validators.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_permissions.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_preview_action_specs.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_pytest_plugin.py +0 -0
- {simcord-3.2.2 → simcord-3.3.0}/tests/unit/test_remaining_monetization_capture.py +0 -0
- {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.
|
|
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.
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
77
|
-
|
|
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
|
|
113
|
-
time: IDs use a sequence within the current millisecond
|
|
114
|
-
against collisions. Exhausting the sequence fails instead
|
|
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()
|
|
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
|
|
|
@@ -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,
|
|
321
|
-
``SleepHandle``, and a 3.11
|
|
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
|
|
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,
|
|
639
|
+
if isinstance(actor, MemberActor) and actor._env.backend.dm_channels.get(actor.id) == channel_id:
|
|
637
640
|
raise SetupError(
|
|
638
|
-
"
|
|
639
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|